# blender CAM strategy.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 strategy functionality of Blender CAM. The functions here are called with operators defined in ops.py. import bpy from bpy.props import * import time import math from math import * from bpy_extras import object_utils from cam import chunk from cam.chunk import * from cam import collision from cam.collision import * from cam import simple from cam.simple import * from cam import pattern from cam.pattern import * from cam import utils, bridges, ops from cam.utils import * from cam import polygon_utils_cam from cam.polygon_utils_cam import * from cam import image_utils from cam.image_utils import * from shapely.geometry import polygon as spolygon from shapely import geometry as sgeometry from shapely import affinity SHAPELY = True # cutout strategy is completely here: async def cutout(o): max_depth = checkminz(o) cutter_angle = math.radians(o.cutter_tip_angle / 2) c_offset = o.cutter_diameter / 2 # cutter ofset print("cuttertype:", o.cutter_type, "max_depth:", max_depth) if o.cutter_type == 'VCARVE': c_offset = -max_depth * math.tan(cutter_angle) elif o.cutter_type == 'CYLCONE': c_offset = -max_depth * math.tan(cutter_angle) + o.cylcone_diameter / 2 elif o.cutter_type == 'BALLCONE': c_offset = -max_depth * math.tan(cutter_angle) + o.ball_radius elif o.cutter_type == 'BALLNOSE': r = o.cutter_diameter / 2 print("cutter radius:", r," skin",o.skin) if -max_depth < r: c_offset = math.sqrt(r ** 2 - (r + max_depth) ** 2) print("offset:", c_offset) if c_offset > o.cutter_diameter / 2: c_offset = o.cutter_diameter / 2 c_offset += o.skin # add skin for profile if o.straight: join = 2 else: join = 1 print('operation: cutout') offset = True if o.cut_type == 'ONLINE' and o.onlycurves: # is separate to allow open curves :) print('separate') chunksFromCurve = [] for ob in o.objects: chunksFromCurve.extend(curveToChunks(ob, o.use_modifiers)) # chunks always have polys now # for ch in chunksFromCurve: # # print(ch.points) # if len(ch.points) > 2: # ch.poly = chunkToShapely(ch) # p.addContour(ch.poly) else: chunksFromCurve = [] if o.cut_type == 'ONLINE': p = utils.getObjectOutline(0, o, True) else: offset = True if o.cut_type == 'INSIDE': offset = False p = utils.getObjectOutline(c_offset, o, offset) if o.outlines_count > 1: for i in range(1, o.outlines_count): chunksFromCurve.extend(shapelyToChunks(p, -1)) path_distance = o.dist_between_paths if o.cut_type == "INSIDE": path_distance *= -1 p = p.buffer(distance = path_distance, resolution=o.optimisation.circle_detail, join_style=join, mitre_limit=2) chunksFromCurve.extend(shapelyToChunks(p, -1)) if o.outlines_count > 1 and o.movement.insideout == 'OUTSIDEIN': chunksFromCurve.reverse() # parentChildPoly(chunksFromCurve,chunksFromCurve,o) chunksFromCurve = limitChunks(chunksFromCurve, o) if not o.dont_merge: parentChildPoly(chunksFromCurve, chunksFromCurve, o) if o.outlines_count == 1: chunksFromCurve = await utils.sortChunks(chunksFromCurve, o) if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW') or ( o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW'): for ch in chunksFromCurve: ch.reverse() if o.cut_type == 'INSIDE': # there would bee too many conditions above, # so for now it gets reversed once again when inside cutting. for ch in chunksFromCurve: ch.reverse() layers = getLayers(o, o.maxz, checkminz(o)) extendorder = [] if o.first_down: # each shape gets either cut all the way to bottom, # or every shape gets cut 1 layer, then all again. has to create copies, # because same chunks are worked with on more layers usually for chunk in chunksFromCurve: dir_switch = False # needed to avoid unnecessary lifting of cutter with open chunks # and movement set to "MEANDER" for layer in layers: chunk_copy = chunk.copy() if dir_switch: chunk_copy.reverse() extendorder.append([chunk_copy, layer]) if (not chunk.closed) and o.movement.type == "MEANDER": dir_switch = not dir_switch else: for layer in layers: for chunk in chunksFromCurve: extendorder.append([chunk.copy(), layer]) for chl in extendorder: # Set Z for all chunks chunk = chl[0] layer = chl[1] print(layer[1]) chunk.setZ(layer[1]) chunks = [] if o.use_bridges: # add bridges to chunks print('using bridges') simple.remove_multiple(o.name+'_cut_bridges') print("old briddge cut removed") bridgeheight = min(o.max.z, o.min.z + abs(o.bridges_height)) for chl in extendorder: chunk = chl[0] layer = chl[1] if layer[1] < bridgeheight: bridges.useBridges(chunk, o) if o.profile_start > 0: print("cutout change profile start") for chl in extendorder: chunk = chl[0] if chunk.closed: chunk.changePathStart(o) # Lead in if o.lead_in > 0.0 or o.lead_out > 0: print("cutout leadin") for chl in extendorder: chunk = chl[0] if chunk.closed: chunk.breakPathForLeadinLeadout(o) chunk.leadContour(o) if o.movement.ramp: # add ramps or simply add chunks for chl in extendorder: chunk = chl[0] layer = chl[1] if chunk.closed: chunk.rampContour(layer[0], layer[1], o) chunks.append(chunk) else: chunk.rampZigZag(layer[0], layer[1], o) chunks.append(chunk) else: for chl in extendorder: chunks.append(chl[0]) chunksToMesh(chunks, o) async def curve(o): print('operation: curve') pathSamples = [] utils.getOperationSources(o) if not o.onlycurves: raise CamException("All objects must be curves for this operation.") for ob in o.objects: pathSamples.extend(curveToChunks(ob)) # make the chunks from curve here pathSamples = await utils.sortChunks(pathSamples, o) # sort before sampling pathSamples = chunksRefine(pathSamples, o) # simplify # layers here if o.use_layers: layers = getLayers(o, o.maxz, round(checkminz(o), 6)) # layers is a list of lists [[0.00,l1],[l1,l2],[l2,l3]] containg the start and end of each layer extendorder = [] chunks = [] for layer in layers: for ch in pathSamples: extendorder.append([ch.copy(), layer]) # include layer information to chunk list for chl in extendorder: # Set offset Z for all chunks according to the layer information, chunk = chl[0] layer = chl[1] print('layer: ' + str(layer[1])) chunk.offsetZ(o.maxz * 2 - o.minz + layer[1]) chunk.clampZ(o.minz) # safety to not cut lower than minz chunk.clampmaxZ(o.movement.free_height) # safety, not higher than free movement height for chl in extendorder: # strip layer information from extendorder and transfer them to chunks chunks.append(chl[0]) chunksToMesh(chunks, o) # finish by converting to mesh else: # no layers, old curve for ch in pathSamples: ch.clampZ(o.minz) # safety to not cut lower than minz ch.clampmaxZ(o.movement.free_height) # safety, not higher than free movement height chunksToMesh(pathSamples, o) async def proj_curve(s, o): print('operation: projected curve') pathSamples = [] chunks = [] ob = bpy.data.objects[o.curve_object] pathSamples.extend(curveToChunks(ob)) targetCurve = s.objects[o.curve_object1] from cam import chunk if targetCurve.type != 'CURVE': raise CamException('Projection target and source have to be curve objects!') if 1: extend_up = 0.1 extend_down = 0.04 tsamples = curveToChunks(targetCurve) for chi, ch in enumerate(pathSamples): cht = tsamples[chi].get_points() ch.depth = 0 ch_points=ch.get_points() for i, s in enumerate(ch_points): # move the points a bit ep = Vector(cht[i]) sp = Vector(ch_points[i]) # extend startpoint vecs = sp - ep vecs.normalize() vecs *= extend_up sp += vecs ch.startpoints.append(sp) # extend endpoint vece = sp - ep vece.normalize() vece *= extend_down ep -= vece ch.endpoints.append(ep) ch.rotations.append((0, 0, 0)) vec = sp - ep ch.depth = min(ch.depth, -vec.length) ch_points[i] = sp.copy() ch.set_points(ch_points) layers = getLayers(o, 0, ch.depth) chunks.extend(utils.sampleChunksNAxis(o, pathSamples, layers)) chunksToMesh(chunks, o) async def pocket(o): print('operation: pocket') scene = bpy.context.scene simple.remove_multiple("3D_poc") max_depth = checkminz(o) + o.skin cutter_angle = math.radians(o.cutter_tip_angle / 2) c_offset = o.cutter_diameter / 2 if o.cutter_type == 'VCARVE': c_offset = -max_depth * math.tan(cutter_angle) elif o.cutter_type == 'CYLCONE': c_offset = -max_depth * math.tan(cutter_angle) + o.cylcone_diameter / 2 elif o.cutter_type == 'BALLCONE': c_offset = -max_depth * math.tan(cutter_angle) + o.ball_radius if c_offset > o.cutter_diameter / 2: c_offset = o.cutter_diameter / 2 c_offset += o.skin # add skin print("cutter offset", c_offset) p = utils.getObjectOutline(c_offset, o, False) approxn = (min(o.max.x - o.min.x, o.max.y - o.min.y) / o.dist_between_paths) / 2 print("approximative:" + str(approxn)) print(o) i = 0 chunks = [] chunksFromCurve = [] lastchunks = [] centers = None firstoutline = p # for testing in the end. prest = p.buffer(-c_offset, o.optimisation.circle_detail) while not p.is_empty: if o.pocketToCurve: polygon_utils_cam.shapelyToCurve('3dpocket', p, 0.0) # make a curve starting with _3dpocket nchunks = shapelyToChunks(p, o.min.z) # print("nchunks") pnew = p.buffer(-o.dist_between_paths, o.optimisation.circle_detail) if pnew.is_empty: pt = p.buffer(-c_offset, o.optimisation.circle_detail) # test if the last curve will leave material if not pt.is_empty: pnew = pt # print("pnew") nchunks = limitChunks(nchunks, o) chunksFromCurve.extend(nchunks) parentChildDist(lastchunks, nchunks, o) lastchunks = nchunks percent = int(i / approxn * 100) progress('outlining polygons ', percent) p = pnew i += 1 # if (o.poc)#TODO inside outside! if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CW') or ( o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CCW'): for ch in chunksFromCurve: ch.reverse() chunksFromCurve = await utils.sortChunks(chunksFromCurve, o) chunks = [] layers = getLayers(o, o.maxz, checkminz(o)) for l in layers: lchunks = setChunksZ(chunksFromCurve, l[1]) if o.movement.ramp: for ch in lchunks: ch.zstart = l[0] ch.zend = l[1] # helix_enter first try here TODO: check if helix radius is not out of operation area. if o.movement.helix_enter: helix_radius = c_offset * o.movement.helix_diameter * 0.01 # 90 percent of cutter radius helix_circumference = helix_radius * pi * 2 revheight = helix_circumference * tan(o.movement.ramp_in_angle) for chi, ch in enumerate(lchunks): if not chunksFromCurve[chi].children: p = ch.get_point(0) # TODO:intercept closest next point when it should stay low # first thing to do is to check if helix enter can really enter. checkc = Circle(helix_radius + c_offset, o.optimisation.circle_detail) checkc = affinity.translate(checkc, p[0], p[1]) covers = False for poly in o.silhouete: if poly.contains(checkc): covers = True break if covers: revolutions = (l[0] - p[2]) / revheight # print(revolutions) h = Helix(helix_radius, o.optimisation.circle_detail, l[0], p, revolutions) # invert helix if not the typical direction if (o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW') or ( o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW'): nhelix = [] for v in h: nhelix.append((2 * p[0] - v[0], v[1], v[2])) h = nhelix ch.extend(h,at_index=0) # ch.points = h + ch.points else: o.info.warnings += 'Helix entry did not fit! \n ' ch.closed = True ch.rampZigZag(l[0], l[1], o) # Arc retract here first try: if o.movement.retract_tangential: # TODO: check for entry and exit point before actual computing... will be much better. # TODO: fix this for CW and CCW! for chi, ch in enumerate(lchunks): # print(chunksFromCurve[chi]) # print(chunksFromCurve[chi].parents) if chunksFromCurve[chi].parents == [] or len(chunksFromCurve[chi].parents) == 1: revolutions = 0.25 v1 = Vector(ch.get_point(-1)) i = -2 v2 = Vector(ch.get_point(i)) v = v1 - v2 while v.length == 0: i = i - 1 v2 = Vector(ch.get_point(i)) v = v1 - v2 v.normalize() rotangle = Vector((v.x, v.y)).angle_signed(Vector((1, 0))) e = Euler((0, 0, pi / 2.0)) # TODO:#CW CLIMB! v.rotate(e) p = v1 + v * o.movement.retract_radius center = p p = (p.x, p.y, p.z) # progress(str((v1,v,p))) h = Helix(o.movement.retract_radius, o.optimisation.circle_detail, p[2] + o.movement.retract_height, p, revolutions) e = Euler((0, 0, rotangle + pi)) # angle to rotate whole retract move rothelix = [] c = [] # polygon for outlining and checking collisions. for p in h: # rotate helix to go from tangent of vector v1 = Vector(p) v = v1 - center v.x = -v.x # flip it here first... v.rotate(e) p = center + v rothelix.append(p) c.append((p[0], p[1])) c = sgeometry.Polygon(c) # print('çoutline') # print(c) coutline = c.buffer(c_offset, o.optimisation.circle_detail) # print(h) # print('çoutline') # print(coutline) # polyToMesh(coutline,0) rothelix.reverse() covers = False for poly in o.silhouete: if poly.contains(coutline): covers = True break if covers: ch.extend(rothelix) chunks.extend(lchunks) if o.movement.ramp: for ch in chunks: ch.rampZigZag(ch.zstart, ch.get_point(0)[2], o) if o.first_down: if o.pocket_option == "OUTSIDE": chunks.reverse() chunks = await utils.sortChunks(chunks, o) if o.pocketToCurve: # make curve instead of a path simple.join_multiple("3dpocket") else: chunksToMesh(chunks, o) # make normal pocket path async def drill(o): print('operation: Drill') chunks = [] for ob in o.objects: activate(ob) bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'}, TRANSFORM_OT_translate={"value": (0, 0, 0), "constraint_axis": (False, False, False), "orient_type": 'GLOBAL', "mirror": False, "use_proportional_edit": False, "proportional_edit_falloff": 'SMOOTH', "proportional_size": 1, "snap": False, "snap_target": 'CLOSEST', "snap_point": (0, 0, 0), "snap_align": False, "snap_normal": (0, 0, 0), "texture_space": False, "release_confirm": False}) # bpy.ops.collection.objects_remove_all() bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') ob = bpy.context.active_object if ob.type == 'CURVE': ob.data.dimensions = '3D' try: bpy.ops.object.transform_apply(location=True, rotation=False, scale=False) bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) except: pass l = ob.location if ob.type == 'CURVE': for c in ob.data.splines: maxx, minx, maxy, miny, maxz, minz = -10000, 10000, -10000, 10000, -10000, 10000 for p in c.points: if o.drill_type == 'ALL_POINTS': chunks.append(camPathChunk([(p.co.x + l.x, p.co.y + l.y, p.co.z + l.z)])) minx = min(p.co.x, minx) maxx = max(p.co.x, maxx) miny = min(p.co.y, miny) maxy = max(p.co.y, maxy) minz = min(p.co.z, minz) maxz = max(p.co.z, maxz) for p in c.bezier_points: if o.drill_type == 'ALL_POINTS': chunks.append(camPathChunk([(p.co.x + l.x, p.co.y + l.y, p.co.z + l.z)])) minx = min(p.co.x, minx) maxx = max(p.co.x, maxx) miny = min(p.co.y, miny) maxy = max(p.co.y, maxy) minz = min(p.co.z, minz) maxz = max(p.co.z, maxz) cx = (maxx + minx) / 2 cy = (maxy + miny) / 2 cz = (maxz + minz) / 2 center = (cx, cy) aspect = (maxx - minx) / (maxy - miny) if (1.3 > aspect > 0.7 and o.drill_type == 'MIDDLE_SYMETRIC') or o.drill_type == 'MIDDLE_ALL': chunks.append(camPathChunk([(center[0] + l.x, center[1] + l.y, cz + l.z)])) elif ob.type == 'MESH': for v in ob.data.vertices: chunks.append(camPathChunk([(v.co.x + l.x, v.co.y + l.y, v.co.z + l.z)])) delob(ob) # delete temporary object with applied transforms layers = getLayers(o, o.maxz, checkminz(o)) chunklayers = [] for layer in layers: for chunk in chunks: # If using object for minz then use z from points in object if o.minz_from == 'OBJECT': z = chunk.get_point(0)[2] else: # using operation minz z = o.minz # only add a chunk layer if the chunk z point is in or lower than the layer if z <= layer[0]: if z <= layer[1]: z = layer[1] # perform peck drill newchunk = chunk.copy() newchunk.setZ(z) chunklayers.append(newchunk) # retract tool to maxz (operation depth start in ui) newchunk = chunk.copy() newchunk.setZ(o.maxz) chunklayers.append(newchunk) chunklayers = await utils.sortChunks(chunklayers, o) chunksToMesh(chunklayers, o) async def medial_axis(o): print('operation: Medial Axis') simple.remove_multiple("medialMesh") from cam.voronoi import Site, computeVoronoiDiagram chunks = [] gpoly = spolygon.Polygon() angle = o.cutter_tip_angle slope = math.tan(math.pi * (90 - angle / 2) / 180) # angle in degrees # slope = math.tan((math.pi-angle)/2) #angle in radian new_cutter_diameter = o.cutter_diameter m_o_ob = o.object_name if o.cutter_type == 'VCARVE': angle = o.cutter_tip_angle # start the max depth calc from the "start depth" of the operation. maxdepth = o.maxz - slope * o.cutter_diameter / 2 - o.skin # don't cut any deeper than the "end depth" of the operation. if maxdepth < o.minz: maxdepth = o.minz # the effective cutter diameter can be reduced from it's max # since we will be cutting shallower than the original maxdepth # without this, the curve is calculated as if the diameter was at the original maxdepth and we get the bit # pulling away from the desired cut surface new_cutter_diameter = (maxdepth - o.maxz) / (- slope) * 2 elif o.cutter_type == 'BALLNOSE': maxdepth = - new_cutter_diameter / 2 - o.skin else: raise CamException("Only Ballnose and V-carve cutters are supported for meial axis.") # remember resolutions of curves, to refine them, # otherwise medial axis computation yields too many branches in curved parts resolutions_before = [] for ob in o.objects: if ob.type == 'CURVE' or ob.type == 'FONT': resolutions_before.append(ob.data.resolution_u) if ob.data.resolution_u < 64: ob.data.resolution_u = 64 polys = utils.getOperationSilhouete(o) if isinstance(polys, list): if len(polys)==1 and isinstance(polys[0],shapely.MultiPolygon): mpoly=polys[0] else: mpoly=sgeometry.MultiPolygon(polys) elif isinstance(polys,shapely.MultiPolygon): # just a multipolygon mpoly=polys else: raise CamException("Failed getting object silhouette. Is input curve closed?") mpoly_boundary = mpoly.boundary ipol = 0 for poly in mpoly.geoms: ipol = ipol + 1 schunks = shapelyToChunks(poly, -1) schunks = chunksRefineThreshold(schunks, o.medial_axis_subdivision, o.medial_axis_threshold) # chunksRefine(schunks,o) verts = [] for ch in schunks: verts.extend(ch.get_points()) # for pt in ch.get_points(): # # pvoro = Site(pt[0], pt[1]) # verts.append(pt) # (pt[0], pt[1]), pt[2]) # verts= points#[[vert.x, vert.y, vert.z] for vert in vertsPts] nDupli, nZcolinear = unique(verts) nVerts = len(verts) print(str(nDupli) + " duplicates points ignored") print(str(nZcolinear) + " z colinear points excluded") if nVerts < 3: print("Not enough points") return {'FINISHED'} # Check colinear xValues = [pt[0] for pt in verts] yValues = [pt[1] for pt in verts] if checkEqual(xValues) or checkEqual(yValues): print("Points are colinear") return {'FINISHED'} # Create diagram print("Tesselation... (" + str(nVerts) + " points)") xbuff, ybuff = 5, 5 # % zPosition = 0 vertsPts = [Point(vert[0], vert[1], vert[2]) for vert in verts] # vertsPts= [Point(vert[0], vert[1]) for vert in verts] pts, edgesIdx = computeVoronoiDiagram(vertsPts, xbuff, ybuff, polygonsOutput=False, formatOutput=True) # pts=[[pt[0], pt[1], zPosition] for pt in pts] newIdx = 0 vertr = [] filteredPts = [] print('filter points') ipts = 0 for p in pts: ipts = ipts + 1 if ipts % 500 == 0: sys.stdout.write('\r') # the exact output you're looking for: prog_message = "points: " + str(ipts) + " / " + str(len(pts)) + " " + str( round(100 * ipts / len(pts))) + "%" sys.stdout.write(prog_message) sys.stdout.flush() if not poly.contains(sgeometry.Point(p)): vertr.append((True, -1)) else: vertr.append((False, newIdx)) if o.cutter_type == 'VCARVE': # start the z depth calc from the "start depth" of the operation. z = o.maxz - mpoly.boundary.distance(sgeometry.Point(p)) * slope if z < maxdepth: z = maxdepth elif o.cutter_type == 'BALL' or o.cutter_type == 'BALLNOSE': d = mpoly_boundary.distance(sgeometry.Point(p)) r = new_cutter_diameter / 2.0 if d >= r: z = -r else: # print(r, d) z = -r + sqrt(r * r - d * d) else: z = 0 # # print(mpoly.distance(sgeometry.Point(0,0))) # if(z!=0):print(z) filteredPts.append((p[0], p[1], z)) newIdx += 1 print('filter edges') filteredEdgs = [] ledges = [] for e in edgesIdx: do = True # p1 = pts[e[0]] # p2 = pts[e[1]] # print(p1,p2,len(vertr)) if vertr[e[0]][0]: # exclude edges with allready excluded points do = False elif vertr[e[1]][0]: do = False if do: filteredEdgs.append((vertr[e[0]][1], vertr[e[1]][1])) ledges.append(sgeometry.LineString((filteredPts[vertr[e[0]][1]], filteredPts[vertr[e[1]][1]]))) # print(ledges[-1].has_z) bufpoly = poly.buffer(-new_cutter_diameter / 2, resolution=64) lines = shapely.ops.linemerge(ledges) # print(lines.type) if bufpoly.type == 'Polygon' or bufpoly.type == 'MultiPolygon': lines = lines.difference(bufpoly) chunks.extend(shapelyToChunks(bufpoly, maxdepth)) chunks.extend(shapelyToChunks(lines, 0)) # generate a mesh from the medial calculations if o.add_mesh_for_medial: polygon_utils_cam.shapelyToCurve('medialMesh', lines, 0.0) bpy.ops.object.convert(target='MESH') oi = 0 for ob in o.objects: if ob.type == 'CURVE' or ob.type == 'FONT': ob.data.resolution_u = resolutions_before[oi] oi += 1 # bpy.ops.object.join() chunks = await utils.sortChunks(chunks, o) layers = getLayers(o, o.maxz, o.min.z) chunklayers = [] for layer in layers: for chunk in chunks: if chunk.isbelowZ(layer[0]): newchunk = chunk.copy() newchunk.clampZ(layer[1]) chunklayers.append(newchunk) if o.first_down: chunklayers = await utils.sortChunks(chunklayers, o) if o.add_mesh_for_medial: # make curve instead of a path simple.join_multiple("medialMesh") chunksToMesh(chunklayers, o) # add pocket operation for medial if add pocket checked if o.add_pocket_for_medial: # o.add_pocket_for_medial = False # export medial axis parameter to pocket op ops.Add_Pocket(None, maxdepth, m_o_ob, new_cutter_diameter) def getLayers(operation, startdepth, enddepth): """returns a list of layers bounded by startdepth and enddepth uses operation.stepdown to determine number of layers. """ if startdepth < enddepth: raise CamException("Start depth is lower than end depth. " "If you have set a custom depth end, it must be lower than depth start, " "and should usually be negative. Set this in the CAM Operation Area panel.") if operation.use_layers: layers = [] n = math.ceil((startdepth - enddepth) / operation.stepdown) print("start " + str(startdepth) + " end " + str(enddepth) + " n " + str(n)) layerstart = operation.maxz for x in range(0, n): layerend = round(max(startdepth - ((x + 1) * operation.stepdown), enddepth), 6) if int(layerstart * 10 ** 8) != int(layerend * 10 ** 8): # it was possible that with precise same end of operation, # last layer was done 2x on exactly same level... layers.append([layerstart, layerend]) layerstart = layerend else: layers = [[round(startdepth, 6), round(enddepth, 6)]] return layers def chunksToMesh(chunks, o): """convert sampled chunks to path, optimization of paths""" t = time.time() s = bpy.context.scene m = s.cam_machine verts = [] free_height = o.movement.free_height # o.max.z + if o.machine_axes == '3': if m.use_position_definitions: origin = (m.starting_position.x, m.starting_position.y, m.starting_position.z) # dhull else: origin = (0, 0, free_height) verts = [origin] if o.machine_axes != '3': verts_rotations = [] # (0,0,0) if (o.machine_axes == '5' and o.strategy5axis == 'INDEXED') or ( o.machine_axes == '4' and o.strategy4axis == 'INDEXED'): extendChunks5axis(chunks, o) if o.array: nchunks = [] for x in range(0, o.array_x_count): for y in range(0, o.array_y_count): print(x, y) for ch in chunks: ch = ch.copy() ch.shift(x * o.array_x_distance, y * o.array_y_distance, 0) nchunks.append(ch) chunks = nchunks progress('building paths from chunks') e = 0.0001 lifted = True for chi in range(0, len(chunks)): ch = chunks[chi] # print(chunks) # print (ch) if ch.count() > 0: # TODO: there is a case where parallel+layers+zigzag ramps send empty chunks here... # print(len(ch.points)) nverts = [] if o.optimisation.optimize: ch = optimizeChunk(ch, o) # lift and drop if lifted: # did the cutter lift before? if yes, put a new position above of the first point of next chunk. if o.machine_axes == '3' or (o.machine_axes == '5' and o.strategy5axis == 'INDEXED') or ( o.machine_axes == '4' and o.strategy4axis == 'INDEXED'): v = (ch.get_point(0)[0], ch.get_point(0)[1], free_height) else: # otherwise, continue with the next chunk without lifting/dropping v = ch.startpoints[0] # startpoints=retract points verts_rotations.append(ch.rotations[0]) verts.append(v) # add whole chunk verts.extend(ch.get_points()) # add rotations for n-axis if o.machine_axes != '3': verts_rotations.extend(ch.rotations) lift = True # check if lifting should happen if chi < len(chunks) - 1 and chunks[chi + 1].count() > 0: # TODO: remake this for n axis, and this check should be somewhere else... last = Vector(ch.get_point(-1)) first = Vector(chunks[chi + 1].get_point(0)) vect = first - last if (o.machine_axes == '3' and (o.strategy == 'PARALLEL' or o.strategy == 'CROSS') and vect.z == 0 and vect.length < o.dist_between_paths * 2.5) \ or (o.machine_axes == '4' and vect.length < o.dist_between_paths * 2.5): # case of neighbouring paths lift = False if abs(vect.x) < e and abs(vect.y) < e: # case of stepdown by cutting. lift = False if lift: if o.machine_axes == '3' or (o.machine_axes == '5' and o.strategy5axis == 'INDEXED') or ( o.machine_axes == '4' and o.strategy4axis == 'INDEXED'): v = (ch.get_point(-1)[0], ch.get_point(-1)[1], free_height) else: v = ch.startpoints[-1] verts_rotations.append(ch.rotations[-1]) verts.append(v) lifted = lift # print(verts_rotations) if o.optimisation.use_exact and not o.optimisation.use_opencamlib: cleanupBulletCollision(o) print(time.time() - t) t = time.time() # actual blender object generation starts here: edges = [] for a in range(0, len(verts) - 1): edges.append((a, a + 1)) oname = "cam_path_{}".format(o.name) mesh = bpy.data.meshes.new(oname) mesh.name = oname mesh.from_pydata(verts, edges, []) if oname in s.objects: s.objects[oname].data = mesh ob = s.objects[oname] else: ob = object_utils.object_data_add(bpy.context, mesh, operator=None) if o.machine_axes != '3': # store rotations into shape keys, only way to store large arrays with correct floating point precision # - object/mesh attributes can only store array up to 32000 intems. ob.shape_key_add() ob.shape_key_add() shapek = mesh.shape_keys.key_blocks[1] shapek.name = 'rotations' 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... shapek.data[i].co = co print(time.time() - t) ob.location = (0, 0, 0) o.path_object_name = oname # parent the path object to source object if object mode if (o.geometry_source == 'OBJECT') and o.parent_path_to_object: activate(o.objects[0]) ob.select_set(state=True, view_layer=None) bpy.ops.object.parent_set(type='OBJECT', keep_transform=True) else: ob.select_set(state=True, view_layer=None) def checkminz(o): if o.minz_from == 'MATERIAL': return o.min.z else: return o.minz