# blender CAM ops.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 ***** # blender operators definitions are in this file. They mostly call the functions from utils.py import bpy from bpy.props import * from bpy_extras.io_utils import ImportHelper import subprocess, os, threading from cam import utils, pack, polygon_utils_cam, simple, gcodepath, bridges, simulation from cam.async_op import AsyncOperatorMixin,AsyncCancelledException import shapely import mathutils import math import textwrap import traceback import cam from cam.exception import * class threadCom: # object passed to threads to read background process stdout info def __init__(self, o, proc): self.opname = o.name self.outtext = '' self.proc = proc self.lasttext = '' def threadread(tcom): """reads stdout of background process, done this way to have it non-blocking""" inline = tcom.proc.stdout.readline() inline = str(inline) s = inline.find('progress{') if s > -1: e = inline.find('}') tcom.outtext = inline[s + 9:e] @bpy.app.handlers.persistent def timer_update(context): """monitoring of background processes""" text = '' s = bpy.context.scene if hasattr(bpy.ops.object.calculate_cam_paths_background.__class__, 'cam_processes'): processes = bpy.ops.object.calculate_cam_paths_background.__class__.cam_processes for p in processes: # proc=p[1].proc readthread = p[0] tcom = p[1] if not readthread.is_alive(): readthread.join() # readthread. tcom.lasttext = tcom.outtext if tcom.outtext != '': print(tcom.opname, tcom.outtext) tcom.outtext = '' if 'finished' in tcom.lasttext: processes.remove(p) o = s.cam_operations[tcom.opname] o.computing = False utils.reload_paths(o) update_zbufferimage_tag = False update_offsetimage_tag = False else: readthread = threading.Thread(target=threadread, args=([tcom]), daemon=True) readthread.start() p[0] = readthread o = s.cam_operations[tcom.opname] # changes o.outtext = tcom.lasttext # changes class PathsBackground(bpy.types.Operator): """calculate CAM paths in background. File has to be saved before.""" bl_idname = "object.calculate_cam_paths_background" bl_label = "Calculate CAM paths in background" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): s = bpy.context.scene o = s.cam_operations[s.cam_active_operation] self.operation = o o.computing = True bpath = bpy.app.binary_path fpath = bpy.data.filepath for p in bpy.utils.script_paths(): scriptpath = p + os.sep + 'addons' + os.sep + 'cam' + os.sep + 'backgroundop.py' print(scriptpath) if os.path.isfile(scriptpath): break proc = subprocess.Popen([bpath, '-b', fpath, '-P', scriptpath, '--', '-o=' + str(s.cam_active_operation)], bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE) tcom = threadCom(o, proc) readthread = threading.Thread(target=threadread, args=([tcom]), daemon=True) readthread.start() # self.__class__.cam_processes=[] if not hasattr(bpy.ops.object.calculate_cam_paths_background.__class__, 'cam_processes'): bpy.ops.object.calculate_cam_paths_background.__class__.cam_processes = [] bpy.ops.object.calculate_cam_paths_background.__class__.cam_processes.append([readthread, tcom]) return {'FINISHED'} class KillPathsBackground(bpy.types.Operator): """Remove CAM path processes in background.""" bl_idname = "object.kill_calculate_cam_paths_background" bl_label = "Kill background computation of an operation" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): s = bpy.context.scene o = s.cam_operations[s.cam_active_operation] self.operation = o if hasattr(bpy.ops.object.calculate_cam_paths_background.__class__, 'cam_processes'): processes = bpy.ops.object.calculate_cam_paths_background.__class__.cam_processes for p in processes: tcom = p[1] if tcom.opname == o.name: processes.remove(p) tcom.proc.kill() o.computing = False return {'FINISHED'} async def _calc_path(operator,context): s = bpy.context.scene o = s.cam_operations[s.cam_active_operation] if o.geometry_source == 'OBJECT': ob = bpy.data.objects[o.object_name] ob.hide_set(False) if o.geometry_source == 'COLLECTION': obc = bpy.data.collections[o.collection_name] for ob in obc.objects: ob.hide_set(False) if o.strategy == "CARVE": curvob = bpy.data.objects[o.curve_object] curvob.hide_set(False) '''if o.strategy == 'WATERLINE': ob = bpy.data.objects[o.object_name] ob.select_set(True) bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)''' if bpy.context.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') # force object mode bpy.ops.object.select_all(action='DESELECT') path = bpy.data.objects.get('cam_path_{}'.format(o.name)) if path: path.select_set(state=True) bpy.ops.object.delete() if not o.valid: operator.report({'ERROR_INVALID_INPUT'}, "Operation can't be performed, see warnings for info") progress_async("Operation can't be performed, see warnings for info") return {'FINISHED',False} #check for free movement height < maxz and return with error if(o.movement.free_height < o.maxz): operator.report({'ERROR_INVALID_INPUT'}, "Free movement height is less than Operation depth start \n correct and try again.") progress_async("Operation can't be performed, see warnings for info") return {'FINISHED',False} if o.computing: return {'FINISHED',False} o.operator = operator if o.use_layers: o.movement.parallel_step_back = False try: await gcodepath.getPath(context, o) print("Got path okay") except CamException as e: traceback.print_tb(e.__traceback__) error_str="\n".join(textwrap.wrap(str(e),width=80)) operator.report({'ERROR'},error_str) return {'FINISHED',False} except AsyncCancelledException as e: return {'CANCELLED',False} except Exception as e: print("FAIL",e) traceback.print_tb(e.__traceback__) operator.report({'ERROR'},str(e)) return {'FINISHED',False} coll = bpy.data.collections.get('RigidBodyWorld') if coll: bpy.data.collections.remove(coll) return {'FINISHED',True} class CalculatePath(bpy.types.Operator,AsyncOperatorMixin): """calculate CAM paths""" bl_idname = "object.calculate_cam_path" bl_label = "Calculate CAM paths" bl_options = {'REGISTER', 'UNDO','BLOCKING'} @classmethod def poll(cls,context): s = context.scene o = s.cam_operations[s.cam_active_operation] if o is not None: if cam.isValid(o,context): return True return False async def execute_async(self, context): (retval,success) = await _calc_path(self,context) print(f"CALCULATED PATH (success={success},retval={retval}") return retval class PathsAll(bpy.types.Operator): """calculate all CAM paths""" bl_idname = "object.calculate_cam_paths_all" bl_label = "Calculate all CAM paths" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): i = 0 for o in bpy.context.scene.cam_operations: bpy.context.scene.cam_active_operation = i print('\nCalculating path :' + o.name) print('\n') bpy.ops.object.calculate_cam_paths_background() i += 1 return {'FINISHED'} def draw(self, context): layout = self.layout layout.prop_search(self, "operation", bpy.context.scene, "cam_operations") class CamPackObjects(bpy.types.Operator): """calculate all CAM paths""" bl_idname = "object.cam_pack_objects" bl_label = "Pack curves on sheet" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): bpy.ops.object.mode_set(mode='OBJECT') # force object mode obs = bpy.context.selected_objects pack.packCurves() # layout. return {'FINISHED'} def draw(self, context): layout = self.layout class CamSliceObjects(bpy.types.Operator): """Slice a mesh object horizontally""" # warning, this is a separate and neglected feature, it's a mess - by now it just slices up the object. bl_idname = "object.cam_slice_objects" bl_label = "Slice object - usefull for lasercut puzzles e.t.c." bl_options = {'REGISTER', 'UNDO'} def execute(self, context): from cam import slice ob = bpy.context.active_object slice.sliceObject(ob) return {'FINISHED'} def draw(self, context): layout = self.layout def getChainOperations(chain): """return chain operations, currently chain object can't store operations directly due to blender limitations""" chop = [] for cho in chain.operations: for so in bpy.context.scene.cam_operations: if so.name == cho.name: chop.append(so) return chop class PathsChain(bpy.types.Operator,AsyncOperatorMixin): """calculate a chain and export the gcode alltogether. """ bl_idname = "object.calculate_cam_paths_chain" bl_label = "Calculate CAM paths in current chain and export chain gcode" bl_options = {'REGISTER', 'UNDO','BLOCKING'} @classmethod def poll(cls, context): s = context.scene chain = s.cam_chains[s.cam_active_chain] return cam.isChainValid(chain,context)[0] async def execute_async(self, context): s = context.scene bpy.ops.object.mode_set(mode='OBJECT') # force object mode chain = s.cam_chains[s.cam_active_chain] chainops = getChainOperations(chain) meshes = [] try: for i in range(0, len(chainops)): s.cam_active_operation = s.cam_operations.find(chainops[i].name) self.report({'INFO'},f"Calculating path: {chainops[i].name}") result,success=await _calc_path(self,context) if not success and 'FINISHED' in result: self.report({'ERROR'},f"Couldn't calculate path: {chainops[i].name}") except Exception as e: print("FAIL",e) traceback.print_tb(e.__traceback__) operator.report({'ERROR'},str(e)) return {'FINISHED'} for o in chainops: meshes.append(bpy.data.objects["cam_path_{}".format(o.name)].data) gcodepath.exportGcodePath(chain.filename, meshes, chainops) return {'FINISHED'} class PathExportChain(bpy.types.Operator): """calculate a chain and export the gcode alltogether. """ bl_idname = "object.cam_export_paths_chain" bl_label = "Export CAM paths in current chain as gcode" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): s = context.scene chain = s.cam_chains[s.cam_active_chain] return cam.isChainValid(chain,context)[0] def execute(self, context): s = bpy.context.scene chain = s.cam_chains[s.cam_active_chain] chainops = getChainOperations(chain) meshes = [] # if len(chainops)<4: for o in chainops: # bpy.ops.object.calculate_cam_paths_background() meshes.append(bpy.data.objects["cam_path_{}".format(o.name)].data) gcodepath.exportGcodePath(chain.filename, meshes, chainops) return {'FINISHED'} class PathExport(bpy.types.Operator): """Export gcode. Can be used only when the path object is present""" bl_idname = "object.cam_export" bl_label = "Export operation gcode" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): s = bpy.context.scene operation = s.cam_operations[s.cam_active_operation] print("EXPORTING", operation.filename, bpy.data.objects["cam_path_{}".format(operation.name)].data, operation) gcodepath.exportGcodePath(operation.filename, [bpy.data.objects["cam_path_{}".format(operation.name)].data], [operation]) return {'FINISHED'} class CAMSimulate(bpy.types.Operator,AsyncOperatorMixin): """simulate CAM operation this is performed by: creating an image, painting Z depth of the brush substractively. Works only for some operations, can not be used for 4-5 axis.""" bl_idname = "object.cam_simulate" bl_label = "CAM simulation" bl_options = {'REGISTER', 'UNDO','BLOCKING'} operation: StringProperty(name="Operation", description="Specify the operation to calculate", default='Operation') async def execute_async(self, context): s = bpy.context.scene operation = s.cam_operations[s.cam_active_operation] operation_name = "cam_path_{}".format(operation.name) if operation_name in bpy.data.objects: try: await simulation.doSimulation(operation_name, [operation]) except AsyncCancelledException as e: return {'CANCELLED'} else: self.report({'ERROR'},'no computed path to simulate') return {'FINISHED'} return {'FINISHED'} def draw(self, context): layout = self.layout layout.prop_search(self, "operation", bpy.context.scene, "cam_operations") class CAMSimulateChain(bpy.types.Operator, AsyncOperatorMixin): """simulate CAM chain, compared to single op simulation just writes into one image and thus enables to see how ops work together.""" bl_idname = "object.cam_simulate_chain" bl_label = "CAM simulation" bl_options = {'REGISTER', 'UNDO','BLOCKING'} @classmethod def poll(cls, context): s = context.scene chain = s.cam_chains[s.cam_active_chain] return cam.isChainValid(chain,context)[0] operation: StringProperty(name="Operation", description="Specify the operation to calculate", default='Operation') async def execute_async(self, context): s = bpy.context.scene chain = s.cam_chains[s.cam_active_chain] chainops = getChainOperations(chain) canSimulate = True for operation in chainops: if operation.name not in bpy.data.objects: canSimulate = True # force true print("operation name " + str(operation.name)) if canSimulate: try: await simulation.doSimulation(chain.name, chainops) except AsyncCancelledException as e: return {'CANCELLED'} else: print('no computed path to simulate') return {'FINISHED'} return {'FINISHED'} def draw(self, context): layout = self.layout layout.prop_search(self, "operation", bpy.context.scene, "cam_operations") class CamChainAdd(bpy.types.Operator): """Add new CAM chain""" bl_idname = "scene.cam_chain_add" bl_label = "Add new CAM chain" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.scene is not None def execute(self, context): # main(context) s = bpy.context.scene s.cam_chains.add() chain = s.cam_chains[-1] s.cam_active_chain = len(s.cam_chains) - 1 chain.name = 'Chain_' + str(s.cam_active_chain + 1) chain.filename = chain.name chain.index = s.cam_active_chain return {'FINISHED'} class CamChainRemove(bpy.types.Operator): """Remove CAM chain""" bl_idname = "scene.cam_chain_remove" bl_label = "Remove CAM chain" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.scene is not None def execute(self, context): bpy.context.scene.cam_chains.remove(bpy.context.scene.cam_active_chain) if bpy.context.scene.cam_active_chain > 0: bpy.context.scene.cam_active_chain -= 1 return {'FINISHED'} class CamChainOperationAdd(bpy.types.Operator): """Add operation to chain""" bl_idname = "scene.cam_chain_operation_add" bl_label = "Add operation to chain" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.scene is not None def execute(self, context): s = bpy.context.scene chain = s.cam_chains[s.cam_active_chain] s = bpy.context.scene chain.operations.add() chain.active_operation += 1 chain.operations[-1].name = s.cam_operations[s.cam_active_operation].name return {'FINISHED'} class CamChainOperationUp(bpy.types.Operator): """Add operation to chain""" bl_idname = "scene.cam_chain_operation_up" bl_label = "Add operation to chain" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.scene is not None def execute(self, context): s = bpy.context.scene chain = s.cam_chains[s.cam_active_chain] a = chain.active_operation if a > 0: chain.operations.move(a, a - 1) chain.active_operation -= 1 return {'FINISHED'} class CamChainOperationDown(bpy.types.Operator): """Add operation to chain""" bl_idname = "scene.cam_chain_operation_down" bl_label = "Add operation to chain" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.scene is not None def execute(self, context): s = bpy.context.scene chain = s.cam_chains[s.cam_active_chain] a = chain.active_operation if a < len(chain.operations) - 1: chain.operations.move(a, a + 1) chain.active_operation += 1 return {'FINISHED'} class CamChainOperationRemove(bpy.types.Operator): """Remove operation from chain""" bl_idname = "scene.cam_chain_operation_remove" bl_label = "Remove operation from chain" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.scene is not None def execute(self, context): s = bpy.context.scene chain = s.cam_chains[s.cam_active_chain] chain.operations.remove(chain.active_operation) chain.active_operation -= 1 if chain.active_operation < 0: chain.active_operation = 0 return {'FINISHED'} def fixUnits(): """Sets up units for blender CAM""" s = bpy.context.scene s.unit_settings.system_rotation = 'DEGREES' s.unit_settings.scale_length = 1.0 # Blender CAM doesn't respect this property and there were users reporting problems, not seeing this was changed. # add pocket op for medial axis and profile cut inside to clean unremoved material def Add_Pocket(self, maxdepth, sname, new_cutter_diameter): bpy.ops.object.select_all(action='DESELECT') s = bpy.context.scene mpocket_exists = False for ob in s.objects: # delete old medial pocket if ob.name.startswith("medial_poc"): ob.select_set(True) bpy.ops.object.delete() for op in s.cam_operations: # verify medial pocket operation exists if op.name == "MedialPocket": mpocket_exists = True ob = bpy.data.objects[sname] ob.select_set(True) bpy.context.view_layer.objects.active = ob utils.silhoueteOffset(ob, -new_cutter_diameter/2, 1, 0.3) bpy.context.active_object.name = 'medial_pocket' if not mpocket_exists: # create a pocket operation if it does not exist already s.cam_operations.add() o = s.cam_operations[-1] o.object_name= 'medial_pocket' s.cam_active_operation = len(s.cam_operations) - 1 o.name = 'MedialPocket' o.filename = o.name o.strategy = 'POCKET' o.use_layers = False o.material.estimate_from_model = False o.material.size[2] = -maxdepth o.minz_from = 'MATERIAL' class CamOperationAdd(bpy.types.Operator): """Add new CAM operation""" bl_idname = "scene.cam_operation_add" bl_label = "Add new CAM operation" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.scene is not None def execute(self, context): s = bpy.context.scene fixUnits() ob = bpy.context.active_object if ob is None: self.report({'ERROR_INVALID_INPUT'}, "Please add an object to base the operation on.") return {'CANCELLED'} minx, miny, minz, maxx, maxy, maxz = utils.getBoundsWorldspace([ob]) s.cam_operations.add() o = s.cam_operations[-1] o.object_name = ob.name o.minz = minz s.cam_active_operation = len(s.cam_operations) - 1 o.name = f"Op_{ob.name}_{s.cam_active_operation + 1}" o.filename = o.name if s.objects.get('CAM_machine') is None: utils.addMachineAreaObject() return {'FINISHED'} class CamOperationCopy(bpy.types.Operator): """Copy CAM operation""" bl_idname = "scene.cam_operation_copy" bl_label = "Copy active CAM operation" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.scene is not None def execute(self, context): # main(context) scene = bpy.context.scene fixUnits() scene = bpy.context.scene if len(scene.cam_operations) == 0: return {'CANCELLED'} copyop = scene.cam_operations[scene.cam_active_operation] scene.cam_operations.add() scene.cam_active_operation += 1 l = len(scene.cam_operations) - 1 scene.cam_operations.move(l, scene.cam_active_operation) o = scene.cam_operations[scene.cam_active_operation] for k in copyop.keys(): o[k] = copyop[k] o.computing = False # ###get digits in the end isdigit = True numdigits = 0 num = 0 if o.name[-1].isdigit(): numdigits = 1 while isdigit: numdigits += 1 isdigit = o.name[-numdigits].isdigit() numdigits -= 1 o.name = o.name[:-numdigits] + str(int(o.name[-numdigits:]) + 1).zfill(numdigits) o.filename = o.name else: o.name = o.name + '_copy' o.filename = o.filename + '_copy' return {'FINISHED'} class CamOperationRemove(bpy.types.Operator): """Remove CAM operation""" bl_idname = "scene.cam_operation_remove" bl_label = "Remove CAM operation" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.scene is not None def execute(self, context): scene = context.scene try: if len(scene.cam_operations) == 0: return {'CANCELLED'} active_op = scene.cam_operations[scene.cam_active_operation] active_op_object = bpy.data.objects[active_op.name] scene.objects.active = active_op_object bpy.ops.object.delete(True) except: pass ao = scene.cam_operations[scene.cam_active_operation] print(cam.was_hidden_dict) if ao.name in cam.was_hidden_dict: del cam.was_hidden_dict[ao.name] scene.cam_operations.remove(scene.cam_active_operation) if scene.cam_active_operation > 0: scene.cam_active_operation -= 1 return {'FINISHED'} # move cam operation in the list up or down class CamOperationMove(bpy.types.Operator): """Move CAM operation""" bl_idname = "scene.cam_operation_move" bl_label = "Move CAM operation in list" bl_options = {'REGISTER', 'UNDO'} direction: EnumProperty(name='direction', items=(('UP', 'Up', ''), ('DOWN', 'Down', '')), description='direction', default='DOWN') @classmethod def poll(cls, context): return context.scene is not None def execute(self, context): # main(context) a = bpy.context.scene.cam_active_operation cops = bpy.context.scene.cam_operations if self.direction == 'UP': if a > 0: cops.move(a, a - 1) bpy.context.scene.cam_active_operation -= 1 else: if a < len(cops) - 1: cops.move(a, a + 1) bpy.context.scene.cam_active_operation += 1 return {'FINISHED'} class CamOrientationAdd(bpy.types.Operator): """Add orientation to cam operation, for multiaxis operations""" bl_idname = "scene.cam_orientation_add" bl_label = "Add orientation" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.scene is not None def execute(self, context): s = bpy.context.scene a = s.cam_active_operation o = s.cam_operations[a] gname = o.name + '_orientations' bpy.ops.object.empty_add(type='ARROWS') oriob = bpy.context.active_object oriob.empty_draw_size = 0.02 # 2 cm simple.addToGroup(oriob, gname) oriob.name = 'ori_' + o.name + '.' + str(len(bpy.data.collections[gname].objects)).zfill(3) return {'FINISHED'} class CamBridgesAdd(bpy.types.Operator): """Add bridge objects to curve""" bl_idname = "scene.cam_bridges_add" bl_label = "Add bridges" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.scene is not None def execute(self, context): s = bpy.context.scene a = s.cam_active_operation o = s.cam_operations[a] bridges.addAutoBridges(o) return {'FINISHED'}