blendercam/scripts/addons/cam/simulation.py

438 wiersze
16 KiB
Python

# blender CAM utils.py (c) 2012 Vilem Novak
#
# ***** BEGIN GPL LICENSE BLOCK *****
#
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ***** END GPL LICENCE BLOCK *****
# here is the main functionality of Blender CAM. The functions here are called with operators defined in ops.py.
import bpy
import mathutils
import math
import time
from bpy.props import *
from cam import utils
import numpy as np
from cam import simple
from cam import image_utils
def createSimulationObject(name, operations, i):
oname = 'csim_' + name
o = operations[0]
if oname in bpy.data.objects:
ob = bpy.data.objects[oname]
else:
bpy.ops.mesh.primitive_plane_add(align='WORLD', enter_editmode=False, location=(0, 0, 0), rotation=(0, 0, 0))
ob = bpy.context.active_object
ob.name = oname
bpy.ops.object.modifier_add(type='SUBSURF')
ss = ob.modifiers[-1]
ss.subdivision_type = 'SIMPLE'
ss.levels = 6
ss.render_levels = 6
bpy.ops.object.modifier_add(type='SUBSURF')
ss = ob.modifiers[-1]
ss.subdivision_type = 'SIMPLE'
ss.levels = 4
ss.render_levels = 3
bpy.ops.object.modifier_add(type='DISPLACE')
ob.location = ((o.max.x + o.min.x) / 2, (o.max.y + o.min.y) / 2, o.min.z)
ob.scale.x = (o.max.x - o.min.x) / 2
ob.scale.y = (o.max.y - o.min.y) / 2
print(o.max.x, o.min.x)
print(o.max.y, o.min.y)
print('bounds')
disp = ob.modifiers[-1]
disp.direction = 'Z'
disp.texture_coords = 'LOCAL'
disp.mid_level = 0
if oname in bpy.data.textures:
t = bpy.data.textures[oname]
t.type = 'IMAGE'
disp.texture = t
t.image = i
else:
bpy.ops.texture.new()
for t in bpy.data.textures:
if t.name == 'Texture':
t.type = 'IMAGE'
t.name = oname
t = t.type_recast()
t.type = 'IMAGE'
t.image = i
disp.texture = t
ob.hide_render = True
bpy.ops.object.shade_smooth()
def doSimulation(name, operations):
"""perform simulation of operations. Currently only for 3 axis"""
for o in operations:
utils.getOperationSources(o)
limits = utils.getBoundsMultiple(
operations) # this is here because some background computed operations still didn't have bounds data
i = generateSimulationImage(operations, limits)
# cp = simple.getCachePath(operations[0])[:-len(operations[0].name)] + name
cp = simple.getSimulationPath()+name
print('cp=',cp)
iname = cp + '_sim.exr'
image_utils.numpysave(i, iname)
i = bpy.data.images.load(iname)
createSimulationObject(name, operations, i)
def generateSimulationImage(operations, limits):
minx, miny, minz, maxx, maxy, maxz = limits
# print(minx,miny,minz,maxx,maxy,maxz)
sx = maxx - minx
sy = maxy - miny
t = time.time()
o = operations[0] # getting sim detail and others from first op.
simulation_detail = o.simulation_detail
borderwidth = o.borderwidth
resx = math.ceil(sx / simulation_detail) + 2 * borderwidth
resy = math.ceil(sy / simulation_detail) + 2 * borderwidth
# resx=ceil(sx/o.pixsize)+2*o.borderwidth
# resy=ceil(sy/o.pixsize)+2*o.borderwidth
# create array in which simulation happens, similar to an image to be painted in.
si = np.array((0.1), dtype=float)
si.resize(resx, resy)
si.fill(maxz)
for o in operations:
ob = bpy.data.objects["cam_path_{}".format(o.name)]
m = ob.data
verts = m.vertices
if o.do_simulation_feedrate:
kname = 'feedrates'
m.use_customdata_edge_crease = True
if m.shape_keys is None or m.shape_keys.key_blocks.find(kname) == -1:
ob.shape_key_add()
if len(m.shape_keys.key_blocks) == 1:
ob.shape_key_add()
shapek = m.shape_keys.key_blocks[-1]
shapek.name = kname
else:
shapek = m.shape_keys.key_blocks[kname]
shapek.data[0].co = (0.0, 0, 0)
# print(len(shapek.data))
# print(len(verts_rotations))
# for i,co in enumerate(verts_rotations):#TODO: optimize this. this is just rewritten too many times...
# print(r)
# shapek.data[i].co=co
totalvolume = 0.0
cutterArray = getCutterArray(o, simulation_detail)
# cb=cutterArray<-1
# cutterArray[cb]=1
cutterArray = -cutterArray
mid = int(cutterArray.shape[0] / 2)
size = cutterArray.shape[0]
# print(si.shape)
# for ch in chunks:
lasts = verts[1].co
perc = -1
vtotal = len(verts)
dropped = 0
xs = 0
ys = 0
for i, vert in enumerate(verts):
if perc != int(100 * i / vtotal):
perc = int(100 * i / vtotal)
simple.progress('simulation', perc)
# progress('simulation ',int(100*i/l))
if i > 0:
volume = 0
volume_partial = 0
s = vert.co
v = s - lasts
l = v.length
if (lasts.z < maxz or s.z < maxz) and not (
v.x == 0 and v.y == 0 and v.z > 0): # only simulate inside material, and exclude lift-ups
if (
v.x == 0 and v.y == 0 and v.z < 0): # if the cutter goes straight down, we don't have to interpolate.
pass;
elif v.length > simulation_detail: # and not :
v.length = simulation_detail
lastxs = xs
lastys = ys
while v.length < l:
xs = int((
lasts.x + v.x - minx) / simulation_detail + borderwidth + simulation_detail / 2) # -middle
ys = int((
lasts.y + v.y - miny) / simulation_detail + borderwidth + simulation_detail / 2) # -middle
z = lasts.z + v.z
# print(z)
if lastxs != xs or lastys != ys:
volume_partial = simCutterSpot(xs, ys, z, cutterArray, si, o.do_simulation_feedrate)
if o.do_simulation_feedrate:
totalvolume += volume
volume += volume_partial
lastxs = xs
lastys = ys
else:
dropped += 1
v.length += simulation_detail
xs = int((s.x - minx) / simulation_detail + borderwidth + simulation_detail / 2) # -middle
ys = int((s.y - miny) / simulation_detail + borderwidth + simulation_detail / 2) # -middle
volume_partial = simCutterSpot(xs, ys, s.z, cutterArray, si, o.do_simulation_feedrate)
if o.do_simulation_feedrate: # compute volumes and write data into shapekey.
volume += volume_partial
totalvolume += volume
if l > 0:
load = volume / l
else:
load = 0
# this will show the shapekey as debugging graph and will use same data to estimate parts with heavy load
if l != 0:
shapek.data[i].co.y = (load) * 0.000002
else:
shapek.data[i].co.y = shapek.data[i - 1].co.y
shapek.data[i].co.x = shapek.data[i - 1].co.x + l * 0.04
shapek.data[i].co.z = 0
lasts = s
# print('dropped '+str(dropped))
if o.do_simulation_feedrate: # smoothing ,but only backward!
xcoef = shapek.data[len(shapek.data) - 1].co.x / len(shapek.data)
for a in range(0, 10):
# print(shapek.data[-1].co)
nvals = []
val1 = 0 #
val2 = 0
w1 = 0 #
w2 = 0
for i, d in enumerate(shapek.data):
val = d.co.y
if i > 1:
d1 = shapek.data[i - 1].co
val1 = d1.y
if d1.x - d.co.x != 0:
w1 = 1 / (abs(d1.x - d.co.x) / xcoef)
if i < len(shapek.data) - 1:
d2 = shapek.data[i + 1].co
val2 = d2.y
if d2.x - d.co.x != 0:
w2 = 1 / (abs(d2.x - d.co.x) / xcoef)
# print(val,val1,val2,w1,w2)
val = (val + val1 * w1 + val2 * w2) / (1.0 + w1 + w2)
nvals.append(val)
for i, d in enumerate(shapek.data):
d.co.y = nvals[i]
# apply mapping - convert the values to actual feedrates.
total_load = 0
max_load = 0
for i, d in enumerate(shapek.data):
total_load += d.co.y
max_load = max(max_load, d.co.y)
normal_load = total_load / len(shapek.data)
thres = 0.5
scale_graph = 0.05 # warning this has to be same as in export in utils!!!!
totverts = len(shapek.data)
for i, d in enumerate(shapek.data):
if d.co.y > normal_load:
d.co.z = scale_graph * max(0.3,
normal_load / d.co.y) # original method was : max(0.4,1-2*(d.co.y-max_load*thres)/(max_load*(1-thres)))
else:
d.co.z = scale_graph * 1
if i < totverts - 1:
m.edges[i].crease = d.co.y / (normal_load * 4)
# d.co.z*=0.01#debug
o = operations[0]
si = si[borderwidth:-borderwidth, borderwidth:-borderwidth]
si += -minz
# print(si.shape[0],si.shape[1])
# print('simulation done in %f seconds' % (time.time()-t))
return si
def getCutterArray(operation, pixsize):
type = operation.cutter_type
# print('generating cutter')
r = operation.cutter_diameter / 2 + operation.skin # /operation.pixsize
res = math.ceil((r * 2) / pixsize)
# if res%2==0:#compensation for half-pixels issue, which wasn't an issue, so commented out
# res+=1
# m=res/2
m = res / 2.0
car = np.array((0), dtype=float)
car.resize(res, res)
car.fill(-10)
v = mathutils.Vector((0, 0, 0))
ps = pixsize
if type == 'END':
for a in range(0, res):
v.x = (a + 0.5 - m) * ps
for b in range(0, res):
v.y = (b + 0.5 - m) * ps
if (v.length <= r):
car.itemset((a, b), 0)
elif type == 'BALL' or type == 'BALLNOSE':
for a in range(0, res):
v.x = (a + 0.5 - m) * ps
for b in range(0, res):
v.y = (b + 0.5 - m) * ps
if (v.length <= r):
z = math.sin(math.acos(v.length / r)) * r - r
car.itemset((a, b), z) # [a,b]=z
elif type == 'VCARVE':
angle = operation.cutter_tip_angle
s = math.tan(math.pi * (90 - angle / 2) / 180) # angle in degrees
#s = math.tan((math.pi - angle) / 2) # angle in radians
for a in range(0, res):
v.x = (a + 0.5 - m) * ps
for b in range(0, res):
v.y = (b + 0.5 - m) * ps
if v.length <= r:
z = (-v.length * s)
car.itemset((a, b), z)
elif type == 'CYLCONE':
angle = operation.cutter_tip_angle
cyl_r = operation.cylcone_diameter/2
s = math.tan(math.pi * (90 - angle / 2) / 180) # angle in degrees
#s = math.tan((math.pi - angle) / 2) # angle in radians
for a in range(0, res):
v.x = (a + 0.5 - m) * ps
for b in range(0, res):
v.y = (b + 0.5 - m) * ps
if v.length <= r:
z = (-(v.length - cyl_r) * s)
if v.length <= cyl_r:
z =0
car.itemset((a, b), z)
elif type == 'BALLCONE':
angle =math.radians(operation.cutter_tip_angle)/2
ball_r = operation.ball_radius
cutter_r = operation.cutter_diameter / 2
conedepth = (cutter_r - ball_r)/math.tan(angle)
Ball_R = ball_r/math.cos(angle)
D_ofset = ball_r * math.tan(angle)
s = math.tan(math.pi/2-angle)
for a in range(0, res):
v.x = (a + 0.5 - m) * ps
for b in range(0, res):
v.y = (b + 0.5 - m) * ps
if v.length <= cutter_r:
z = -(v.length - ball_r ) * s -Ball_R + D_ofset
if v.length <= ball_r:
z = math.sin(math.acos(v.length / Ball_R)) * Ball_R - Ball_R
car.itemset((a, b), z)
elif type == 'CUSTOM':
cutob = bpy.data.objects[operation.cutter_object_name]
scale = ((cutob.dimensions.x / cutob.scale.x) / 2) / r #
# print(cutob.scale)
vstart = mathutils.Vector((0, 0, -10))
vend = mathutils.Vector((0, 0, 10))
print('sampling custom cutter')
maxz = -1
for a in range(0, res):
vstart.x = (a + 0.5 - m) * ps * scale
vend.x = vstart.x
for b in range(0, res):
vstart.y = (b + 0.5 - m) * ps * scale
vend.y = vstart.y
v = vend - vstart
c = cutob.ray_cast(vstart, v, distance=1.70141e+38)
if c[3] != -1:
z = -c[1][2] / scale
# print(c)
if z > -9:
# print(z)
if z > maxz:
maxz = z
car.itemset((a, b), z)
car -= maxz
return car
def simCutterSpot(xs, ys, z, cutterArray, si, getvolume=False):
"""simulates a cutter cutting into stock, taking away the volume, and optionally returning the volume that has been milled. This is now used for feedrate tweaking."""
# xs=int(xs)
# ys=int(ys)
m = int(cutterArray.shape[0] / 2)
size = cutterArray.shape[0]
if xs > m and xs < si.shape[0] - m and ys > m and ys < si.shape[1] - m: # whole cutter in image there
if getvolume:
volarray = si[xs - m:xs - m + size, ys - m:ys - m + size].copy()
si[xs - m:xs - m + size, ys - m:ys - m + size] = np.minimum(si[xs - m:xs - m + size, ys - m:ys - m + size],
cutterArray + z)
if getvolume:
volarray = si[xs - m:xs - m + size, ys - m:ys - m + size] - volarray
vsum = abs(volarray.sum())
# print(vsum)
return vsum
elif xs > -m and xs < si.shape[0] + m and ys > -m and ys < si.shape[
1] + m: # part of cutter in image, for extra large cutters
startx = max(0, xs - m)
starty = max(0, ys - m)
endx = min(si.shape[0], xs - m + size)
endy = min(si.shape[0], ys - m + size)
castartx = max(0, m - xs)
castarty = max(0, m - ys)
caendx = min(size, si.shape[0] - xs + m)
caendy = min(size, si.shape[1] - ys + m)
# print(startx,endx,starty,endy,castartx,caendx,castarty, caendy)
if getvolume:
volarray = si[startx:endx, starty:endy].copy()
si[startx:endx, starty:endy] = np.minimum(si[startx:endx, starty:endy],
cutterArray[castartx:caendx, castarty:caendy] + z)
if getvolume:
volarray = si[startx:endx, starty:endy] - volarray
vsum = abs(volarray.sum())
# print(vsum)
return vsum
return 0