diff --git a/scripts/addons/cam/__init__.py b/scripts/addons/cam/__init__.py index a201c69ad..f5c3cfe88 100644 --- a/scripts/addons/cam/__init__.py +++ b/scripts/addons/cam/__init__.py @@ -18,2394 +18,374 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENCE BLOCK **** + +# Python Standard Library import subprocess import sys + +# pip Packages try: import shapely except ModuleNotFoundError: # pip install required python stuff subprocess.check_call([sys.executable, "-m", "ensurepip"]) subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", " pip"]) - subprocess.check_call([sys.executable, "-m", "pip", "install", - "shapely", "Equation", "opencamlib"]) + subprocess.check_call( + [sys.executable, "-m", "pip", "install", "shapely", "Equation", "opencamlib"] + ) + # Numba Install temporarily disabled after crash report # install numba if available for this platform, ignore failure # subprocess.run([sys.executable, "-m", "pip", "install", "numba"]) -from shapely import geometry as sgeometry # noqa - -from .ui import * -from .version import __version__ -from . import ( - ui, - ops, - constants, - curvecamtools, - curvecamequation, - curvecamcreate, - utils, - simple, - polygon_utils_cam, - autoupdate, - basrelief, -) # , post_processors -from .utils import ( - updateMachine, - updateRest, - updateOperation, - updateOperationValid, - operationValid, - updateZbufferImage, - updateOffsetImage, - updateStrategy, - updateCutout, - updateChipload, - updateRotation, - update_operation, - getStrategyList, - updateBridges, -) -from .ui_panels.movement import CAM_MOVEMENT_Properties -import bl_operators -import blf +# Blender Library import bpy -from bpy.app.handlers import persistent from bpy.props import ( - BoolProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, + CollectionProperty, IntProperty, PointerProperty, StringProperty, - CollectionProperty, -) -from bpy.types import ( - Menu, - Operator, - UIList, - AddonPreferences, ) from bpy_extras.object_utils import object_data_add -import bpy.ops -import math -from mathutils import * -import numpy -import os -import pickle -import shutil -import threading -import time -from pathlib import Path +# Relative Imports - from 'cam' module +from . import basrelief +from .autoupdate import ( + UpdateChecker, + Updater, + UpdateSourceOperator, +) +from .cam_operation import camOperation +from .chain import ( + camChain, + opReference, +) +from .curvecamcreate import ( + CamCurveDrawer, + CamCurveFlatCone, + CamCurveGear, + CamCurveHatch, + CamCurveInterlock, + CamCurveMortise, + CamCurvePlate, + CamCurvePuzzle, +) +from .curvecamequation import ( + CamCustomCurve, + CamHypotrochoidCurve, + CamLissajousCurve, + CamSineCurve, +) +from .curvecamtools import ( + CamCurveBoolean, + CamCurveConvexHull, + CamCurveIntarsion, + CamCurveOvercuts, + CamCurveOvercutsB, + CamCurveRemoveDoubles, + CamMeshGetPockets, + CamOffsetSilhouete, + CamObjectSilhouete, +) +from .engine import ( + BLENDERCAM_ENGINE, + get_panels, +) +from .machine_settings import machineSettings +from .ops import ( + CalculatePath, + # bridges related + CamBridgesAdd, + CamChainAdd, + CamChainRemove, + CamChainOperationAdd, + CamChainOperationRemove, + CamChainOperationUp, + CamChainOperationDown, + CamOperationAdd, + CamOperationCopy, + CamOperationRemove, + CamOperationMove, + # 5 axis ops + CamOrientationAdd, + # shape packing + CamPackObjects, + CamSliceObjects, + CAMSimulate, + CAMSimulateChain, + KillPathsBackground, + PathsAll, + PathsBackground, + PathsChain, + PathExport, + PathExportChain, + timer_update, +) +from .pack import PackObjectsSettings +from .preferences import CamAddonPreferences +from .preset_managers import ( + AddPresetCamCutter, + AddPresetCamMachine, + AddPresetCamOperation, + CAM_CUTTER_MT_presets, + CAM_MACHINE_MT_presets, + CAM_OPERATION_MT_presets, +) +from .slice import SliceObjectsSettings +from .ui import ( + CustomPanel, + import_settings, + VIEW3D_PT_tools_curvetools, + VIEW3D_PT_tools_create, + WM_OT_gcode_import, +) +from .ui_panels.area import CAM_AREA_Panel +from .ui_panels.chains import ( + CAM_CHAINS_Panel, + CAM_UL_chains, + CAM_UL_operations, +) +from .ui_panels.cutter import CAM_CUTTER_Panel +from .ui_panels.feedrate import CAM_FEEDRATE_Panel +from .ui_panels.gcode import CAM_GCODE_Panel +from .ui_panels.info import ( + CAM_INFO_Panel, + CAM_INFO_Properties, +) +from .ui_panels.interface import ( + CAM_INTERFACE_Panel, + CAM_INTERFACE_Properties, +) +from .ui_panels.machine import CAM_MACHINE_Panel +from .ui_panels.material import ( + CAM_MATERIAL_Panel, + CAM_MATERIAL_PositionObject, + CAM_MATERIAL_Properties, +) +from .ui_panels.movement import ( + CAM_MOVEMENT_Panel, + CAM_MOVEMENT_Properties, +) +from .ui_panels.op_properties import CAM_OPERATION_PROPERTIES_Panel +from .ui_panels.operations import CAM_OPERATIONS_Panel +from .ui_panels.optimisation import ( + CAM_OPTIMISATION_Panel, + CAM_OPTIMISATION_Properties, +) +from .ui_panels.pack import CAM_PACK_Panel +from .ui_panels.slice import CAM_SLICE_Panel +from .utils import ( + check_operations_on_load, + updateOperation, +) + bl_info = { "name": "CAM - gcode generation tools", "author": "Vilem Novak & Contributors", - "version":(1,0,14), + "version": (1, 0, 14), "blender": (3, 6, 0), "location": "Properties > render", "description": "Generate machining paths for CNC", "warning": "", "doc_url": "https://blendercam.com/", "tracker_url": "", - "category": "Scene"} - - -class CamAddonPreferences(AddonPreferences): - # this must match the addon name, use '__package__' - # when defining this in a submodule of a python package. - bl_idname = __package__ - - op_preset_update: BoolProperty( - name="Have the Operation Presets been Updated", - default=False, - ) - - experimental: BoolProperty( - name="Show experimental features", - default=False, - ) - - update_source: StringProperty( - name="Source of updates for the addon", - description="This can be either a github repo link in which case " - "it will download the latest release on there, " - "or an api link like " - "https://api.github.com/repos//blendercam/commits" - " to get from a github repository", - default="https://github.com/pppalain/blendercam", - ) - - last_update_check: IntProperty( - name="Last update time", - default=0, - ) - - last_commit_hash: StringProperty( - name="Hash of last commit from updater", - default="", - ) - - just_updated: BoolProperty( - name="Set to true on update or initial install", - default=True, - ) - - new_version_available: StringProperty( - name="Set to new version name if one is found", - default="", - ) - - default_interface_level: EnumProperty( - name="Interface level in new file", - description="Choose visible options", - items=[ - ('0', "Basic", "Only show essential options"), - ('1', "Advanced", "Show advanced options"), - ('2', "Complete", "Show all options"), - ('3', "Experimental", "Show experimental options") - ], - default='3', - ) - - default_machine_preset: StringProperty( - name="Machine preset in new file", - description="So that machine preset choice persists between files", - default='', - ) - - def draw(self, context): - layout = self.layout - layout.label( - text="Use experimental features when you want to help development of Blender CAM:") - layout.prop(self, "experimental") - layout.prop(self, "update_source") - layout.label(text="Choose a preset update source") - - UPDATE_SOURCES = [("https://github.com/vilemduha/blendercam", "Stable", "Stable releases (github.com/vilemduja/blendercam)"), - ("https://github.com/pppalain/blendercam", "Unstable", - "Unstable releases (github.com/pppalain/blendercam)"), - # comments for searching in github actions release script to automatically set this repo - # if required - # REPO ON NEXT LINE - ("https://api.github.com/repos/pppalain/blendercam/commits", - "Direct from git (may not work)", "Get from git commits directly"), - # REPO ON PREV LINE - ("", "None", "Don't do auto update"), - ] - grid = layout.grid_flow(align=True) - for (url, short, long) in UPDATE_SOURCES: - op = grid.operator("render.cam_set_update_source", text=short) - op.new_source = url - - -class machineSettings(bpy.types.PropertyGroup): - """stores all data for machines""" - # name = StringProperty(name="Machine Name", default="Machine") - post_processor: EnumProperty( - name='Post processor', - items=( - ('ISO', 'Iso', 'exports standardized gcode ISO 6983 (RS-274)'), - ('MACH3', 'Mach3', 'default mach3'), - ('EMC', 'LinuxCNC - EMC2', - 'Linux based CNC control software - formally EMC2'), - ('FADAL', 'Fadal', 'Fadal VMC'), - ('GRBL', 'grbl', - 'optimized gcode for grbl firmware on Arduino with cnc shield'), - ('HEIDENHAIN', 'Heidenhain', 'heidenhain'), - ('HEIDENHAIN530', 'Heidenhain530', 'heidenhain530'), - ('TNC151', 'Heidenhain TNC151', - 'Post Processor for the Heidenhain TNC151 machine'), - ('SIEGKX1', 'Sieg KX1', 'Sieg KX1'), - ('HM50', 'Hafco HM-50', 'Hafco HM-50'), - ('CENTROID', 'Centroid M40', 'Centroid M40'), - ('ANILAM', 'Anilam Crusader M', 'Anilam Crusader M'), - ('GRAVOS', 'Gravos', 'Gravos'), - ('WIN-PC', 'WinPC-NC', 'German CNC by Burkhard Lewetz'), - ('SHOPBOT MTC', 'ShopBot MTC', 'ShopBot MTC'), - ('LYNX_OTTER_O', 'Lynx Otter o', 'Lynx Otter o') - ), - description='Post processor', - default='MACH3', - ) - # units = EnumProperty(name='Units', items = (('IMPERIAL', '')) - # position definitions: - use_position_definitions: BoolProperty( - name="Use position definitions", - description="Define own positions for op start, " - "toolchange, ending position", - default=False, - ) - starting_position: FloatVectorProperty( - name='Start position', - default=(0, 0, 0), - unit='LENGTH', - precision=constants.PRECISION, - subtype="XYZ", - update=updateMachine, - ) - mtc_position: FloatVectorProperty( - name='MTC position', - default=(0, 0, 0), - unit='LENGTH', - precision=constants.PRECISION, - subtype="XYZ", - update=updateMachine, - ) - ending_position: FloatVectorProperty( - name='End position', - default=(0, 0, 0), - unit='LENGTH', - precision=constants.PRECISION, - subtype="XYZ", - update=updateMachine, - ) - - working_area: FloatVectorProperty( - name='Work Area', - default=(0.500, 0.500, 0.100), - unit='LENGTH', - precision=constants.PRECISION, - subtype="XYZ", - update=updateMachine, - ) - feedrate_min: FloatProperty( - name="Feedrate minimum /min", - default=0.0, - min=0.00001, - max=320000, - precision=constants.PRECISION, - unit='LENGTH', - ) - feedrate_max: FloatProperty( - name="Feedrate maximum /min", - default=2, - min=0.00001, - max=320000, - precision=constants.PRECISION, - unit='LENGTH', - ) - feedrate_default: FloatProperty( - name="Feedrate default /min", - default=1.5, - min=0.00001, - max=320000, - precision=constants.PRECISION, - unit='LENGTH', - ) - hourly_rate: FloatProperty( - name="Price per hour", - default=100, - min=0.005, - precision=2, - ) - - # UNSUPPORTED: - - spindle_min: FloatProperty( - name="Spindle speed minimum RPM", - default=5000, - min=0.00001, - max=320000, - precision=1, - ) - spindle_max: FloatProperty( - name="Spindle speed maximum RPM", - default=30000, - min=0.00001, - max=320000, - precision=1, - ) - spindle_default: FloatProperty( - name="Spindle speed default RPM", - default=15000, - min=0.00001, - max=320000, - precision=1, - ) - spindle_start_time: FloatProperty( - name="Spindle start delay seconds", - description='Wait for the spindle to start spinning before starting ' - 'the feeds , in seconds', - default=0, - min=0.0000, - max=320000, - precision=1, - ) - - axis4: BoolProperty( - name="#4th axis", - description="Machine has 4th axis", - default=0, - ) - axis5: BoolProperty( - name="#5th axis", - description="Machine has 5th axis", - default=0, - ) - - eval_splitting: BoolProperty( - name="Split files", - description="split gcode file with large number of operations", - default=True, - ) # split large files - split_limit: IntProperty( - name="Operations per file", - description="Split files with larger number of operations than this", - min=1000, - max=20000000, - default=800000, - ) - - # rotary_axis1 = EnumProperty(name='Axis 1', - # items=( - # ('X', 'X', 'x'), - # ('Y', 'Y', 'y'), - # ('Z', 'Z', 'z')), - # description='Number 1 rotational axis', - # default='X', update = updateOffsetImage) - - collet_size: FloatProperty( - name="#Collet size", - description="Collet size for collision detection", - default=33, - min=0.00001, - max=320000, - precision=constants.PRECISION, - unit="LENGTH", - ) - # exporter_start = StringProperty(name="exporter start", default="%") - - # post processor options - - output_block_numbers: BoolProperty( - name="output block numbers", - description="output block numbers ie N10 at start of line", - default=False, - ) - - start_block_number: IntProperty( - name="start block number", - description="the starting block number ie 10", - default=10, - ) - - block_number_increment: IntProperty( - name="block number increment", - description="how much the block number should " - "increment for the next line", - default=10, - ) - - output_tool_definitions: BoolProperty( - name="output tool definitions", - description="output tool definitions", - default=True, - ) - - output_tool_change: BoolProperty( - name="output tool change commands", - description="output tool change commands ie: Tn M06", - default=True, - ) - - output_g43_on_tool_change: BoolProperty( - name="output G43 on tool change", - description="output G43 on tool change line", - default=False, - ) - - -class PackObjectsSettings(bpy.types.PropertyGroup): - """stores all data for machines""" - sheet_fill_direction: EnumProperty( - name='Fill direction', - items=( - ('X', 'X', 'Fills sheet in X axis direction'), - ('Y', 'Y', 'Fills sheet in Y axis direction') - ), - description='Fill direction of the packer algorithm', - default='Y', - ) - sheet_x: FloatProperty( - name="X size", - description="Sheet size", - min=0.001, - max=10, - default=0.5, - precision=constants.PRECISION, - unit="LENGTH", - ) - sheet_y: FloatProperty( - name="Y size", - description="Sheet size", - min=0.001, - max=10, - default=0.5, - precision=constants.PRECISION, - unit="LENGTH", - ) - distance: FloatProperty( - name="Minimum distance", - description="minimum distance between objects(should be " - "at least cutter diameter!)", - min=0.001, - max=10, - default=0.01, - precision=constants.PRECISION, - unit="LENGTH", - ) - tolerance: FloatProperty( - name="Placement Tolerance", - description="Tolerance for placement: smaller value slower placemant", - min=0.001, - max=0.02, - default=0.005, - precision=constants.PRECISION, - unit="LENGTH", - ) - rotate: BoolProperty( - name="enable rotation", - description="Enable rotation of elements", - default=True, - ) - rotate_angle: FloatProperty( - name="Placement Angle rotation step", - description="bigger rotation angle,faster placemant", - default=0.19635 * 4, - min=math.pi/180, - max=math.pi, - precision=5, - subtype="ANGLE", - unit="ROTATION", - ) - - -class SliceObjectsSettings(bpy.types.PropertyGroup): - """stores all data for machines""" - slice_distance: FloatProperty( - name="Slicing distance", - description="slices distance in z, should be most often " - "thickness of plywood sheet.", - min=0.001, - max=10, - default=0.005, - precision=constants.PRECISION, - unit="LENGTH", - ) - slice_above0: BoolProperty( - name="Slice above 0", - description="only slice model above 0", - default=False, - ) - slice_3d: BoolProperty( - name="3d slice", - description="for 3d carving", - default=False, - ) - indexes: BoolProperty( - name="add indexes", - description="adds index text of layer + index", - default=True, - ) - - -class import_settings(bpy.types.PropertyGroup): - split_layers: BoolProperty( - name="Split Layers", - description="Save every layer as single Objects in Collection", - default=False, - ) - subdivide: BoolProperty( - name="Subdivide", - description="Only Subdivide gcode segments that are " - "bigger than 'Segment length' ", - default=False, - ) - output: EnumProperty( - name="output type", - items=( - ('mesh', 'Mesh', 'Make a mesh output'), - ('curve', 'Curve', 'Make curve output') - ), - default='curve', - ) - max_segment_size: FloatProperty( - name="", - description="Only Segments bigger then this value get subdivided", - default=0.001, - min=0.0001, - max=1.0, - unit="LENGTH", - ) - - -class camOperation(bpy.types.PropertyGroup): - - material: PointerProperty( - type=CAM_MATERIAL_Properties - ) - info: PointerProperty( - type=CAM_INFO_Properties - ) - optimisation: PointerProperty( - type=CAM_OPTIMISATION_Properties - ) - movement: PointerProperty( - type=CAM_MOVEMENT_Properties - ) - - name: StringProperty( - name="Operation Name", - default="Operation", - update=updateRest, - ) - filename: StringProperty( - name="File name", - default="Operation", - update=updateRest, - ) - auto_export: BoolProperty( - name="Auto export", - description="export files immediately after path calculation", - default=True, - ) - remove_redundant_points: BoolProperty( - name="Symplify Gcode", - description="Remove redundant points sharing the same angle" - " as the start vector", - default=False, - ) - simplify_tol: IntProperty( - name="Tolerance", - description='lower number means more precise', - default=50, - min=1, - max=1000, - ) - hide_all_others: BoolProperty( - name="Hide all others", - description="Hide all other tool pathes except toolpath" - " assotiated with selected CAM operation", - default=False, - ) - parent_path_to_object: BoolProperty( - name="Parent path to object", - description="Parent generated CAM path to source object", - default=False, - ) - object_name: StringProperty( - name='Object', - description='object handled by this operation', - update=updateOperationValid, - ) - collection_name: StringProperty( - name='Collection', - description='Object collection handled by this operation', - update=updateOperationValid, - ) - curve_object: StringProperty( - name='Curve source', - description='curve which will be sampled along the 3d object', - update=operationValid, - ) - curve_object1: StringProperty( - name='Curve target', - description='curve which will serve as attractor for the ' - 'cutter when the cutter follows the curve', - update=operationValid, - ) - source_image_name: StringProperty( - name='image_source', - description='image source', - update=operationValid, - ) - geometry_source: EnumProperty( - name='Source of data', - items=( - ('OBJECT', 'object', 'a'), - ('COLLECTION', 'Collection of objects', 'a'), - ('IMAGE', 'Image', 'a') - ), - description='Geometry source', - default='OBJECT', - update=updateOperationValid, - ) - cutter_type: EnumProperty( - name='Cutter', - items=( - ('END', 'End', 'end - flat cutter'), - ('BALLNOSE', 'Ballnose', 'ballnose cutter'), - ('BULLNOSE', 'Bullnose', 'bullnose cutter ***placeholder **'), - ('VCARVE', 'V-carve', 'v carve cutter'), - ('BALLCONE', 'Ballcone', 'Ball with a Cone for Parallel - X'), - ('CYLCONE', 'Cylinder cone', - 'Cylinder end with a Cone for Parallel - X'), - ('LASER', 'Laser', 'Laser cutter'), - ('PLASMA', 'Plasma', 'Plasma cutter'), - ('CUSTOM', 'Custom-EXPERIMENTAL', - 'modelled cutter - not well tested yet.') - ), - description='Type of cutter used', - default='END', - update=updateZbufferImage, - ) - cutter_object_name: StringProperty( - name='Cutter object', - description='object used as custom cutter for this operation', - update=updateZbufferImage, - ) - - machine_axes: EnumProperty( - name='Number of axes', - items=( - ('3', '3 axis', 'a'), - ('4', '#4 axis - EXPERIMENTAL', 'a'), - ('5', '#5 axis - EXPERIMENTAL', 'a') - ), - description='How many axes will be used for the operation', - default='3', - update=updateStrategy, - ) - strategy: EnumProperty( - name='Strategy', - items=getStrategyList, - description='Strategy', - update=updateStrategy, - ) - - strategy4axis: EnumProperty( - name='4 axis Strategy', - items=( - ('PARALLELR', 'Parallel around 1st rotary axis', - 'Parallel lines around first rotary axis'), - ('PARALLEL', 'Parallel along 1st rotary axis', - 'Parallel lines along first rotary axis'), - ('HELIX', 'Helix around 1st rotary axis', - 'Helix around rotary axis'), - ('INDEXED', 'Indexed 3-axis', - 'all 3 axis strategies, just applied to the 4th axis'), - ('CROSS', 'Cross', 'Cross paths') - ), - description='#Strategy', - default='PARALLEL', - update=updateStrategy, - ) - strategy5axis: EnumProperty( - name='Strategy', - items=( - ('INDEXED', 'Indexed 3-axis', - 'all 3 axis strategies, just rotated by 4+5th axes'), - ), - description='5 axis Strategy', - default='INDEXED', - update=updateStrategy, - ) - - rotary_axis_1: EnumProperty( - name='Rotary axis', - items=( - ('X', 'X', ''), - ('Y', 'Y', ''), - ('Z', 'Z', ''), - ), - description='Around which axis rotates the first rotary axis', - default='X', - update=updateStrategy, - ) - rotary_axis_2: EnumProperty( - name='Rotary axis 2', - items=( - ('X', 'X', ''), - ('Y', 'Y', ''), - ('Z', 'Z', ''), - ), - description='Around which axis rotates the second rotary axis', - default='Z', - update=updateStrategy, - ) - - skin: FloatProperty( - name="Skin", - description="Material to leave when roughing ", - min=0.0, - max=1.0, - default=0.0, - precision=constants.PRECISION, - unit="LENGTH", - update=updateOffsetImage, - ) - inverse: BoolProperty( - name="Inverse milling", - description="Male to female model conversion", - default=False, - update=updateOffsetImage, - ) - array: BoolProperty( - name="Use array", - description="Create a repetitive array for producing the " - "same thing many times", - default=False, - update=updateRest, - ) - array_x_count: IntProperty( - name="X count", - description="X count", - default=1, - min=1, - max=32000, - update=updateRest, - ) - array_y_count: IntProperty( - name="Y count", - description="Y count", - default=1, - min=1, - max=32000, - update=updateRest, - ) - array_x_distance: FloatProperty( - name="X distance", - description="distance between operation origins", - min=0.00001, - max=1.0, - default=0.01, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) - array_y_distance: FloatProperty( - name="Y distance", - description="distance between operation origins", - min=0.00001, - max=1.0, - default=0.01, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) - - # pocket options - pocket_option: EnumProperty( - name='Start Position', - items=( - ('INSIDE', 'Inside', 'a'), - ('OUTSIDE', 'Outside', 'a') - ), - description='Pocket starting position', - default='INSIDE', - update=updateRest, - ) - pocketToCurve: BoolProperty( - name="Pocket to curve", - description="generates a curve instead of a path", - default=False, - update=updateRest, - ) - # Cutout - cut_type: EnumProperty( - name='Cut', - items=( - ('OUTSIDE', 'Outside', 'a'), - ('INSIDE', 'Inside', 'a'), - ('ONLINE', 'On line', 'a') - ), - description='Type of cutter used', - default='OUTSIDE', - update=updateRest, - ) - outlines_count: IntProperty( - name="Outlines count", - description="Outlines count", - default=1, - min=1, - max=32, - update=updateCutout, - ) - straight: BoolProperty( - name="Overshoot Style", - description="Use overshoot cutout instead of conventional rounded", - default=False, - update=updateRest, - ) - # cutter - cutter_id: IntProperty( - name="Tool number", - description="For machines which support tool change based on tool id", - min=0, - max=10000, - default=1, - update=updateRest, - ) - cutter_diameter: FloatProperty( - name="Cutter diameter", - description="Cutter diameter = 2x cutter radius", - min=0.000001, - max=10, - default=0.003, - precision=constants.PRECISION, - unit="LENGTH", - update=updateOffsetImage, - ) - cylcone_diameter: FloatProperty( - name="Bottom Diameter", - description="Bottom diameter", - min=0.000001, - max=10, - default=0.003, - precision=constants.PRECISION, - unit="LENGTH", - update=updateOffsetImage, - ) - cutter_length: FloatProperty( - name="#Cutter length", - description="#not supported#Cutter length", - min=0.0, - max=100.0, - default=25.0, - precision=constants.PRECISION, - unit="LENGTH", - update=updateOffsetImage, - ) - cutter_flutes: IntProperty( - name="Cutter flutes", - description="Cutter flutes", - min=1, - max=20, - default=2, - update=updateChipload, - ) - cutter_tip_angle: FloatProperty( - name="Cutter v-carve angle", - description="Cutter v-carve angle", - min=0.0, - max=180.0, - default=60.0, - precision=constants.PRECISION, - update=updateOffsetImage, - ) - ball_radius: FloatProperty( - name="Ball radius", - description="Radius of", - min=0.0, - max=0.035, - default=0.001, - unit="LENGTH", - precision=constants.PRECISION, - update=updateOffsetImage, - ) - # ball_cone_flute: FloatProperty(name="BallCone Flute Length", description="length of flute", min=0.0, - # max=0.1, default=0.017, unit="LENGTH", precision=constants.PRECISION, update=updateOffsetImage) - bull_corner_radius: FloatProperty( - name="Bull Corner Radius", - description="Radius tool bit corner", - min=0.0, - max=0.035, - default=0.005, - unit="LENGTH", - precision=constants.PRECISION, - update=updateOffsetImage, - ) - - cutter_description: StringProperty( - name="Tool Description", - default="", - update=updateOffsetImage, - ) - - Laser_on: StringProperty( - name="Laser ON string", - default="M68 E0 Q100", - ) - Laser_off: StringProperty( - name="Laser OFF string", - default="M68 E0 Q0", - ) - Laser_cmd: StringProperty( - name="Laser command", - default="M68 E0 Q", - ) - Laser_delay: FloatProperty( - name="Laser ON Delay", - description="time after fast move to turn on laser and " - "let machine stabilize", - default=0.2, - ) - Plasma_on: StringProperty( - name="Plasma ON string", - default="M03", - ) - Plasma_off: StringProperty( - name="Plasma OFF string", - default="M05", - ) - Plasma_delay: FloatProperty( - name="Plasma ON Delay", - description="time after fast move to turn on Plasma and " - "let machine stabilize", - default=0.1, - ) - Plasma_dwell: FloatProperty( - name="Plasma dwell time", - description="Time to dwell and warm up the torch", - default=0.0, - ) - - # steps - dist_between_paths: FloatProperty( - name="Distance between toolpaths", - default=0.001, - min=0.00001, - max=32, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) - dist_along_paths: FloatProperty( - name="Distance along toolpaths", - default=0.0002, - min=0.00001, - max=32, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) - parallel_angle: FloatProperty( - name="Angle of paths", - default=0, - min=-360, - max=360, - precision=0, - subtype="ANGLE", - unit="ROTATION", - update=updateRest, - ) - - old_rotation_A: FloatProperty( - name="A axis angle", - description="old value of Rotate A axis\nto specified angle", - default=0, - min=-360, - max=360, - precision=0, - subtype="ANGLE", - unit="ROTATION", - update=updateRest, - ) - - old_rotation_B: FloatProperty( - name="A axis angle", - description="old value of Rotate A axis\nto specified angle", - default=0, - min=-360, - max=360, - precision=0, - subtype="ANGLE", - unit="ROTATION", - update=updateRest, - ) - - rotation_A: FloatProperty( - name="A axis angle", - description="Rotate A axis\nto specified angle", - default=0, - min=-360, - max=360, - precision=0, - subtype="ANGLE", - unit="ROTATION", - update=updateRotation, - ) - enable_A: BoolProperty( - name="Enable A axis", - description="Rotate A axis", - default=False, - update=updateRotation, - ) - A_along_x: BoolProperty( - name="A Along X ", - description="A Parallel to X", - default=True, - update=updateRest, - ) - - rotation_B: FloatProperty( - name="B axis angle", - description="Rotate B axis\nto specified angle", - default=0, - min=-360, - max=360, - precision=0, - subtype="ANGLE", - unit="ROTATION", - update=updateRotation, - ) - enable_B: BoolProperty( - name="Enable B axis", - description="Rotate B axis", - default=False, - update=updateRotation, - ) - - # carve only - carve_depth: FloatProperty( - name="Carve depth", - default=0.001, - min=-.100, - max=32, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) - - # drill only - drill_type: EnumProperty( - name='Holes on', - items=( - ('MIDDLE_SYMETRIC', 'Middle of symetric curves', 'a'), - ('MIDDLE_ALL', 'Middle of all curve parts', 'a'), - ('ALL_POINTS', 'All points in curve', 'a') - ), - description='Strategy to detect holes to drill', - default='MIDDLE_SYMETRIC', - update=updateRest, - ) - # waterline only - slice_detail: FloatProperty( - name="Distance betwen slices", - default=0.001, - min=0.00001, - max=32, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) - waterline_fill: BoolProperty( - name="Fill areas between slices", - description="Fill areas between slices in waterline mode", - default=True, - update=updateRest, - ) - waterline_project: BoolProperty( - name="Project paths - not recomended", - description="Project paths in areas between slices", - default=True, - update=updateRest, - ) - - # movement and ramps - use_layers: BoolProperty( - name="Use Layers", - description="Use layers for roughing", - default=True, - update=updateRest, - ) - stepdown: FloatProperty( - name="", - description="Layer height", - default=0.01, - min=0.00001, - max=32, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) - lead_in: FloatProperty( - name="Lead in radius", - description="Lead out radius for torch or laser to turn off", - min=0.00, - max=1, - default=0.0, - precision=constants.PRECISION, - unit="LENGTH", - ) - lead_out: FloatProperty( - name="Lead out radius", - description="Lead out radius for torch or laser to turn off", - min=0.00, - max=1, - default=0.0, - precision=constants.PRECISION, - unit="LENGTH", - ) - profile_start: IntProperty( - name="Start point", - description="Start point offset", - min=0, - default=0, - update=updateRest, - ) - - # helix_angle: FloatProperty(name="Helix ramp angle", default=3*math.pi/180, min=0.00001, max=math.pi*0.4999,precision=1, subtype="ANGLE" , unit="ROTATION" , update = updateRest) - - minz: FloatProperty( - name="Operation depth end", - default=-0.01, - min=-3, - max=3, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) - - minz_from: EnumProperty( - name='Set max depth from', - description='Set maximum operation depth', - items=( - ('OBJECT', 'Object', 'Set max operation depth from Object'), - ('MATERIAL', 'Material', 'Set max operation depth from Material'), - ('CUSTOM', 'Custom', 'Custom max depth'), - ), - default='OBJECT', - update=updateRest, - ) - - start_type: EnumProperty( - name='Start type', - items=( - ('ZLEVEL', 'Z level', 'Starts on a given Z level'), - ('OPERATIONRESULT', 'Rest milling', - 'For rest milling, operations have to be ' - 'put in chain for this to work well.'), - ), - description='Starting depth', - default='ZLEVEL', - update=updateStrategy, - ) - - maxz: FloatProperty( - name="Operation depth start", - description='operation starting depth', - default=0, - min=-3, - max=10, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) # EXPERIMENTAL - - first_down: BoolProperty( - name="First down", - description="First go down on a contour, then go to the next one", - default=False, - update=update_operation, - ) - - ####################################################### - # Image related - #################################################### - - source_image_scale_z: FloatProperty( - name="Image source depth scale", - default=0.01, - min=-1, - max=1, - precision=constants.PRECISION, - unit="LENGTH", - update=updateZbufferImage, - ) - source_image_size_x: FloatProperty( - name="Image source x size", - default=0.1, - min=-10, - max=10, - precision=constants.PRECISION, - unit="LENGTH", - update=updateZbufferImage, - ) - source_image_offset: FloatVectorProperty( - name='Image offset', - default=(0, 0, 0), - unit='LENGTH', - precision=constants.PRECISION, - subtype="XYZ", - update=updateZbufferImage, - ) - - source_image_crop: BoolProperty( - name="Crop source image", - description="Crop source image - the position of the sub-rectangle " - "is relative to the whole image, so it can be used for e.g. " - "finishing just a part of an image", - default=False, - update=updateZbufferImage, - ) - source_image_crop_start_x: FloatProperty( - name='crop start x', - default=0, - min=0, - max=100, - precision=constants.PRECISION, - subtype='PERCENTAGE', - update=updateZbufferImage, - ) - source_image_crop_start_y: FloatProperty( - name='crop start y', - default=0, - min=0, - max=100, - precision=constants.PRECISION, - subtype='PERCENTAGE', - update=updateZbufferImage, - ) - source_image_crop_end_x: FloatProperty( - name='crop end x', - default=100, - min=0, - max=100, - precision=constants.PRECISION, - subtype='PERCENTAGE', - update=updateZbufferImage, - ) - source_image_crop_end_y: FloatProperty( - name='crop end y', - default=100, - min=0, - max=100, - precision=constants.PRECISION, - subtype='PERCENTAGE', - update=updateZbufferImage, - ) - - ######################################################### - # Toolpath and area related - ##################################################### - - ambient_behaviour: EnumProperty( - name='Ambient', - items=(('ALL', 'All', 'a'), ('AROUND', 'Around', 'a')), - description='handling ambient surfaces', - default='ALL', - update=updateZbufferImage, - ) - - ambient_radius: FloatProperty( - name="Ambient radius", - description="Radius around the part which will be milled if " - "ambient is set to Around", - min=0.0, - max=100.0, - default=0.01, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) - # ambient_cutter = EnumProperty(name='Borders',items=(('EXTRAFORCUTTER', 'Extra for cutter', "Extra space for cutter is cut around the segment"),('ONBORDER', "Cutter on edge", "Cutter goes exactly on edge of ambient with it's middle") ,('INSIDE', "Inside segment", 'Cutter stays within segment') ),description='handling of ambient and cutter size',default='INSIDE') - use_limit_curve: BoolProperty( - name="Use limit curve", - description="A curve limits the operation area", - default=False, - update=updateRest, - ) - ambient_cutter_restrict: BoolProperty( - name="Cutter stays in ambient limits", - description="Cutter doesn't get out from ambient limits otherwise " - "goes on the border exactly", - default=True, - update=updateRest, - ) # restricts cutter inside ambient only - limit_curve: StringProperty( - name='Limit curve', - description='curve used to limit the area of the operation', - update=updateRest, - ) - - # feeds - feedrate: FloatProperty( - name="Feedrate", - description="Feedrate in units per minute", - min=0.00005, - max=50.0, - default=1.0, - precision=constants.PRECISION, - unit="LENGTH", - update=updateChipload, - ) - plunge_feedrate: FloatProperty( - name="Plunge speed ", - description="% of feedrate", - min=0.1, - max=100.0, - default=50.0, - precision=1, - subtype='PERCENTAGE', - update=updateRest, - ) - plunge_angle: FloatProperty( - name="Plunge angle", - description="What angle is allready considered to plunge", - default=math.pi / 6, - min=0, - max=math.pi * 0.5, - precision=0, - subtype="ANGLE", - unit="ROTATION", - update=updateRest, - ) - spindle_rpm: FloatProperty( - name="Spindle rpm", - description="Spindle speed ", - min=0, - max=60000, - default=12000, - update=updateChipload, - ) - - # optimization and performance - - do_simulation_feedrate: BoolProperty( - name="Adjust feedrates with simulation EXPERIMENTAL", - description="Adjust feedrates with simulation", - default=False, - update=updateRest, - ) - - dont_merge: BoolProperty( - name="Dont merge outlines when cutting", - description="this is usefull when you want to cut around everything", - default=False, - update=updateRest, - ) - - pencil_threshold: FloatProperty( - name="Pencil threshold", - default=0.00002, - min=0.00000001, - max=1, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) - - crazy_threshold1: FloatProperty( - name="min engagement", - default=0.02, - min=0.00000001, - max=100, - precision=constants.PRECISION, - update=updateRest, - ) - crazy_threshold5: FloatProperty( - name="optimal engagement", - default=0.3, - min=0.00000001, - max=100, - precision=constants.PRECISION, - update=updateRest, - ) - crazy_threshold2: FloatProperty( - name="max engagement", - default=0.5, - min=0.00000001, - max=100, - precision=constants.PRECISION, - update=updateRest, - ) - crazy_threshold3: FloatProperty( - name="max angle", - default=2, - min=0.00000001, - max=100, - precision=constants.PRECISION, - update=updateRest, - ) - crazy_threshold4: FloatProperty( - name="test angle step", - default=0.05, - min=0.00000001, - max=100, - precision=constants.PRECISION, - update=updateRest, - ) - # Add pocket operation to medial axis - add_pocket_for_medial: BoolProperty( - name="Add pocket operation", - description="clean unremoved material after medial axis", - default=True, - update=updateRest, - ) - - add_mesh_for_medial: BoolProperty( - name="Add Medial mesh", - description="Medial operation returns mesh for editing and " - "further processing", - default=False, - update=updateRest, - ) - #### - medial_axis_threshold: FloatProperty( - name="Long vector threshold", - default=0.001, - min=0.00000001, - max=100, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) - medial_axis_subdivision: FloatProperty( - name="Fine subdivision", - default=0.0002, - min=0.00000001, - max=100, - precision=constants.PRECISION, - unit="LENGTH", - update=updateRest, - ) - # calculations - - # bridges - use_bridges: BoolProperty( - name="Use bridges", - description="use bridges in cutout", - default=False, - update=updateBridges, - ) - bridges_width: FloatProperty( - name='width of bridges', - default=0.002, - unit='LENGTH', - precision=constants.PRECISION, - update=updateBridges, - ) - bridges_height: FloatProperty( - name='height of bridges', - description="Height from the bottom of the cutting operation", - default=0.0005, - unit='LENGTH', - precision=constants.PRECISION, - update=updateBridges, - ) - bridges_collection_name: StringProperty( - name='Bridges Collection', - description='Collection of curves used as bridges', - update=operationValid, - ) - use_bridge_modifiers: BoolProperty( - name="use bridge modifiers", - description="include bridge curve modifiers using render level when " - "calculating operation, does not effect original bridge data", - default=True, - update=updateBridges, - ) - - # commented this - auto bridges will be generated, but not as a setting of the operation - # bridges_placement = EnumProperty(name='Bridge placement', - # items=( - # ('AUTO','Automatic', 'Automatic bridges with a set distance'), - # ('MANUAL','Manual', 'Manual placement of bridges'), - # ), - # description='Bridge placement', - # default='AUTO', - # update = updateStrategy) - # - # bridges_per_curve = IntProperty(name="minimum bridges per curve", description="", default=4, min=1, max=512, update = updateBridges) - # bridges_max_distance = FloatProperty(name = 'Maximum distance between bridges', default=0.08, unit='LENGTH', precision=constants.PRECISION, update = updateBridges) - - use_modifiers: BoolProperty( - name="use mesh modifiers", - description="include mesh modifiers using render level when " - "calculating operation, does not effect original mesh", - default=True, - update=operationValid, - ) - # optimisation panel - - # material settings - - -############################################################################## - # MATERIAL SETTINGS - - min: FloatVectorProperty( - name='Operation minimum', - default=(0, 0, 0), - unit='LENGTH', - precision=constants.PRECISION, - subtype="XYZ", - ) - max: FloatVectorProperty( - name='Operation maximum', - default=(0, 0, 0), - unit='LENGTH', - precision=constants.PRECISION, - subtype="XYZ", - ) - - # g-code options for operation - output_header: BoolProperty( - name="output g-code header", - description="output user defined g-code command header" - " at start of operation", - default=False, - ) - - gcode_header: StringProperty( - name="g-code header", - description="g-code commands at start of operation." - " Use ; for line breaks", - default="G53 G0", - ) - - enable_dust: BoolProperty( - name="Dust collector", - description="output user defined g-code command header" - " at start of operation", - default=False, - ) - - gcode_start_dust_cmd: StringProperty( - name="Start dust collector", - description="commands to start dust collection. Use ; for line breaks", - default="M100", - ) - - gcode_stop_dust_cmd: StringProperty( - name="Stop dust collector", - description="command to stop dust collection. Use ; for line breaks", - default="M101", - ) - - enable_hold: BoolProperty( - name="Hold down", - description="output hold down command at start of operation", - default=False, - ) - - gcode_start_hold_cmd: StringProperty( - name="g-code header", - description="g-code commands at start of operation." - " Use ; for line breaks", - default="M102", - ) - - gcode_stop_hold_cmd: StringProperty( - name="g-code header", - description="g-code commands at end operation. Use ; for line breaks", - default="M103", - ) - - enable_mist: BoolProperty( - name="Mist", - description="Mist command at start of operation", - default=False, - ) - - gcode_start_mist_cmd: StringProperty( - name="g-code header", - description="g-code commands at start of operation." - " Use ; for line breaks", - default="M104", - ) - - gcode_stop_mist_cmd: StringProperty( - name="g-code header", - description="g-code commands at end operation. Use ; for line breaks", - default="M105", - ) - - output_trailer: BoolProperty( - name="output g-code trailer", - description="output user defined g-code command trailer" - " at end of operation", - default=False, - ) - - gcode_trailer: StringProperty( - name="g-code trailer", - description="g-code commands at end of operation." - " Use ; for line breaks", - default="M02", - ) - - # internal properties - ########################################### - - # testing = IntProperty(name="developer testing ", description="This is just for script authors for help in coding, keep 0", default=0, min=0, max=512) - offset_image = numpy.array([], dtype=float) - zbuffer_image = numpy.array([], dtype=float) - - silhouete = sgeometry.Polygon() - ambient = sgeometry.Polygon() - operation_limit = sgeometry.Polygon() - borderwidth = 50 - object = None - path_object_name: StringProperty( - name='Path object', - description='actual cnc path' - ) - - # update and tags and related - - changed: BoolProperty( - name="True if any of the operation settings has changed", - description="mark for update", - default=False, - ) - update_zbufferimage_tag: BoolProperty( - name="mark zbuffer image for update", - description="mark for update", - default=True, - ) - update_offsetimage_tag: BoolProperty( - name="mark offset image for update", - description="mark for update", - default=True, - ) - update_silhouete_tag: BoolProperty( - name="mark silhouete image for update", - description="mark for update", - default=True, - ) - update_ambient_tag: BoolProperty( - name="mark ambient polygon for update", - description="mark for update", - default=True, - ) - update_bullet_collision_tag: BoolProperty( - name="mark bullet collisionworld for update", - description="mark for update", - default=True, - ) - - valid: BoolProperty( - name="Valid", - description="True if operation is ok for calculation", - default=True, - ) - changedata: StringProperty( - name='changedata', - description='change data for checking if stuff changed.', - ) - - # process related data - - computing: BoolProperty( - name="Computing right now", - description="", - default=False, - ) - pid: IntProperty( - name="process id", - description="Background process id", - default=-1, - ) - outtext: StringProperty( - name='outtext', - description='outtext', - default='', - ) - - -# this type is defined just to hold reference to operations for chains -class opReference(bpy.types.PropertyGroup): - name: StringProperty( - name="Operation name", - default="Operation", - ) - computing = False # for UiList display - - -# chain is just a set of operations which get connected on export into 1 file. -class camChain(bpy.types.PropertyGroup): - index: IntProperty( - name="index", - description="index in the hard-defined camChains", - default=-1, - ) - active_operation: IntProperty( - name="active operation", - description="active operation in chain", - default=-1, - ) - name: StringProperty( - name="Chain Name", - default="Chain", - ) - filename: StringProperty( - name="File name", - default="Chain", - ) # filename of - valid: BoolProperty( - name="Valid", - description="True if whole chain is ok for calculation", - default=True, - ) - computing: BoolProperty( - name="Computing right now", - description="", - default=False, - ) - # this is to hold just operation names. - operations: CollectionProperty( - type=opReference, - ) - + "category": "Scene", +} -class CAM_CUTTER_MT_presets(Menu): - bl_label = "Cutter presets" - preset_subdir = "cam_cutters" - preset_operator = "script.execute_preset" - draw = Menu.draw_preset +classes = [ + # CamBackgroundMonitor -class CAM_MACHINE_MT_presets(Menu): - bl_label = "Machine presets" - preset_subdir = "cam_machines" - preset_operator = "script.execute_preset" - draw = Menu.draw_preset - - @classmethod - def post_cb(cls, context): - name = cls.bl_label - filepath = bpy.utils.preset_find(name, - cls.preset_subdir, - display_name=True, - ext=".py") - context.preferences.addons['cam'].preferences.default_machine_preset = filepath - bpy.ops.wm.save_userpref() - - -class AddPresetCamCutter(bl_operators.presets.AddPresetBase, Operator): - """Add a Cutter Preset""" - bl_idname = "render.cam_preset_cutter_add" - bl_label = "Add Cutter Preset" - preset_menu = "CAM_CUTTER_MT_presets" - - preset_defines = [ - "d = bpy.context.scene.cam_operations[bpy.context.scene.cam_active_operation]" - ] - - preset_values = [ - "d.cutter_id", - "d.cutter_type", - "d.cutter_diameter", - "d.cutter_length", - "d.cutter_flutes", - "d.cutter_tip_angle", - "d.cutter_description", - ] - - preset_subdir = "cam_cutters" - - -class CAM_OPERATION_MT_presets(Menu): - bl_label = "Operation presets" - preset_subdir = "cam_operations" - preset_operator = "script.execute_preset" - draw = Menu.draw_preset - - -class AddPresetCamOperation(bl_operators.presets.AddPresetBase, Operator): - """Add an Operation Preset""" - bl_idname = "render.cam_preset_operation_add" - bl_label = "Add Operation Preset" - preset_menu = "CAM_OPERATION_MT_presets" - - preset_defines = [ - 'import cam', - 'o = cam.utils.setup_operation_preset()', - ] - - preset_values = [ - 'o.info.duration', - 'o.info.chipload', - 'o.info.warnings', - - 'o.material.estimate_from_model', - 'o.material.size', - 'o.material.radius_around_model', - 'o.material.origin', - - 'o.movement.stay_low', - 'o.movement.free_height', - 'o.movement.insideout', - 'o.movement.spindle_rotation', - 'o.movement.type', - 'o.movement.useG64', - 'o.movement.G64', - 'o.movement.parallel_step_back', - 'o.movement.protect_vertical', - - 'o.source_image_name', - 'o.source_image_offset', - 'o.source_image_size_x', - 'o.source_image_crop', - 'o.source_image_crop_start_x', - 'o.source_image_crop_start_y', - 'o.source_image_crop_end_x', - 'o.source_image_crop_end_y', - 'o.source_image_scale_z', - - 'o.optimisation.optimize', - 'o.optimisation.optimize_threshold', - 'o.optimisation.use_exact', - 'o.optimisation.exact_subdivide_edges', - 'o.optimisation.simulation_detail', - 'o.optimisation.pixsize', - 'o.optimisation.circle_detail', - - 'o.cut_type', - 'o.cutter_tip_angle', - 'o.cutter_id', - 'o.cutter_diameter', - 'o.cutter_type', - 'o.cutter_flutes', - 'o.cutter_length', - - 'o.ambient_behaviour', - 'o.ambient_radius', - - 'o.curve_object', - 'o.curve_object1', - 'o.limit_curve', - 'o.use_limit_curve', - - 'o.feedrate', - 'o.plunge_feedrate', - - 'o.dist_along_paths', - 'o.dist_between_paths', - - 'o.max', - 'o.min', - 'o.minz_from', - 'o.minz', - - 'o.skin', - 'o.spindle_rpm', - 'o.use_layers', - 'o.carve_depth', - - 'o.update_offsetimage_tag', - 'o.slice_detail', - 'o.drill_type', - 'o.dont_merge', - 'o.update_silhouete_tag', - 'o.inverse', - 'o.waterline_fill', - 'o.strategy', - 'o.update_zbufferimage_tag', - 'o.stepdown', - 'o.path_object_name', - 'o.pencil_threshold', - 'o.geometry_source', - 'o.object_name', - 'o.parallel_angle', - - 'o.output_header', - 'o.gcode_header', - 'o.output_trailer', - 'o.gcode_trailer', - 'o.use_modifiers', - - 'o.enable_A', - 'o.enable_B', - 'o.A_along_x', - 'o.rotation_A', - 'o.rotation_B', - 'o.straight' - ] - - preset_subdir = "cam_operations" - - -class AddPresetCamMachine(bl_operators.presets.AddPresetBase, Operator): - """Add a Cam Machine Preset""" - bl_idname = "render.cam_preset_machine_add" - bl_label = "Add Machine Preset" - preset_menu = "CAM_MACHINE_MT_presets" - - preset_defines = [ - "d = bpy.context.scene.cam_machine", - "s = bpy.context.scene.unit_settings" - ] - preset_values = [ - "d.post_processor", - "s.system", - "d.use_position_definitions", - "d.starting_position", - "d.mtc_position", - "d.ending_position", - "d.working_area", - "d.feedrate_min", - "d.feedrate_max", - "d.feedrate_default", - "d.spindle_min", - "d.spindle_max", - "d.spindle_default", - "d.axis4", - "d.axis5", - "d.collet_size", - "d.output_tool_change", - "d.output_block_numbers", - "d.output_tool_definitions", - "d.output_g43_on_tool_change", - ] - - preset_subdir = "cam_machines" - - -class BLENDERCAM_ENGINE(bpy.types.RenderEngine): - bl_idname = 'BLENDERCAM_RENDER' - bl_label = "Cam" - bl_use_eevee_viewport = True - - -_IS_LOADING_DEFAULTS = False - - -@bpy.app.handlers.persistent -def check_operations_on_load(context): - global _IS_LOADING_DEFAULTS - """checks any broken computations on load and reset them.""" - s = bpy.context.scene - for o in s.cam_operations: - if o.computing: - o.computing = False - # set interface level to previously used level for a new file - if not bpy.data.filepath: - _IS_LOADING_DEFAULTS = True - s.interface.level = bpy.context.preferences.addons['cam'].preferences.default_interface_level - machine_preset = bpy.context.preferences.addons[ - 'cam'].preferences.machine_preset = bpy.context.preferences.addons['cam'].preferences.default_machine_preset - if len(machine_preset) > 0: - print("Loading preset:", machine_preset) - # load last used machine preset - bpy.ops.script.execute_preset(filepath=machine_preset, - menu_idname="CAM_MACHINE_MT_presets") - _IS_LOADING_DEFAULTS = False - # check for updated version of the plugin - bpy.ops.render.cam_check_updates() - # copy presets if not there yet - if bpy.context.preferences.addons['cam'].preferences.just_updated: - preset_source_path = Path(__file__).parent / 'presets' - preset_target_path = Path(bpy.utils.script_path_user()) / 'presets' - - def copy_if_not_exists(src, dst): - if Path(dst).exists() == False: - shutil.copy2(src, dst) - shutil.copytree(preset_source_path, preset_target_path, - copy_function=copy_if_not_exists, dirs_exist_ok=True) - - bpy.context.preferences.addons['cam'].preferences.just_updated = False - bpy.ops.wm.save_userpref() - - if not bpy.context.preferences.addons['cam'].preferences.op_preset_update: - # Update the Operation presets - op_presets_source = os.path.join(preset_source_path, 'cam_operations') - op_presets_target = os.path.join(preset_target_path, 'cam_operations') - shutil.copytree(preset_source_path, preset_target_path, dirs_exist_ok=True) - bpy.context.preferences.addons['cam'].preferences.op_preset_update = True - - -def get_panels(): # convenience function for bot register and unregister functions - # types = bpy.types - return ( - ui.CAM_UL_operations, - # ui.CAM_UL_orientations, - ui.CAM_UL_chains, - opReference, - camChain, - machineSettings, - CamAddonPreferences, - - ui.CAM_INTERFACE_Panel, - ui.CAM_CHAINS_Panel, - ui.CAM_OPERATIONS_Panel, - ui.CAM_INFO_Panel, - ui.CAM_MATERIAL_Panel, - ui.CAM_OPERATION_PROPERTIES_Panel, - ui.CAM_OPTIMISATION_Panel, - ui.CAM_AREA_Panel, - ui.CAM_MOVEMENT_Panel, - ui.CAM_FEEDRATE_Panel, - ui.CAM_CUTTER_Panel, - ui.CAM_GCODE_Panel, - ui.CAM_MACHINE_Panel, - ui.CAM_PACK_Panel, - ui.CAM_SLICE_Panel, - ui.VIEW3D_PT_tools_curvetools, - ui.CustomPanel, - - ops.PathsBackground, - ops.KillPathsBackground, - ops.CalculatePath, - ops.PathsChain, - ops.PathExportChain, - ops.PathsAll, - ops.PathExport, - ops.CAMPositionObject, - ops.CAMSimulate, - ops.CAMSimulateChain, - ops.CamChainAdd, - ops.CamChainRemove, - ops.CamChainOperationAdd, - ops.CamChainOperationRemove, - ops.CamChainOperationUp, - ops.CamChainOperationDown, - - ops.CamOperationAdd, - ops.CamOperationCopy, - ops.CamOperationRemove, - ops.CamOperationMove, - # bridges related - ops.CamBridgesAdd, - # 5 axis ops - ops.CamOrientationAdd, - # shape packing - ops.CamPackObjects, - ops.CamSliceObjects, - # other tools - curvecamtools.CamCurveBoolean, - curvecamtools.CamCurveConvexHull, - curvecamtools.CamCurveHatch, - curvecamtools.CamCurvePlate, - curvecamtools.CamCurveDrawer, - curvecamcreate.CamCurveFlatCone, - curvecamtools.CamCurveMortise, - curvecamtools.CamOffsetSilhouete, - curvecamtools.CamObjectSilhouete, - curvecamtools.CamCurveIntarsion, - curvecamtools.CamCurveOvercuts, - curvecamtools.CamCurveOvercutsB, - curvecamtools.CamCurveRemoveDoubles, - curvecamtools.CamMeshGetPockets, - curvecamequation.CamSineCurve, - curvecamequation.CamLissajousCurve, - curvecamequation.CamHypotrochoidCurve, - curvecamequation.CamCustomCurve, - - CAM_CUTTER_MT_presets, - CAM_OPERATION_MT_presets, - CAM_MACHINE_MT_presets, - AddPresetCamCutter, - AddPresetCamOperation, - AddPresetCamMachine, - BLENDERCAM_ENGINE, - # CamBackgroundMonitor - # pack module: - PackObjectsSettings, - SliceObjectsSettings, - camOperation, - - ) - - -def compatible_panels(): - """gets panels that are for blender internal, but are compatible with blender CAM""" - t = bpy.types - return ( - # textures - t.TEXTURE_PT_context_texture, - t.TEXTURE_PT_preview, - t.TEXTURE_PT_colors, - t.TEXTURE_PT_clouds, - t.TEXTURE_PT_wood, - t.TEXTURE_PT_marble, - t.TEXTURE_PT_magic, - t.TEXTURE_PT_blend, - t.TEXTURE_PT_stucci, - t.TEXTURE_PT_image, - t.TEXTURE_PT_image_sampling, - t.TEXTURE_PT_image_mapping, - t.TEXTURE_PT_envmap, - t.TEXTURE_PT_envmap_sampling, - t.TEXTURE_PT_musgrave, - t.TEXTURE_PT_voronoi, - t.TEXTURE_PT_distortednoise, - t.TEXTURE_PT_voxeldata, - t.TEXTURE_PT_pointdensity, - t.TEXTURE_PT_pointdensity_turbulence, - t.TEXTURE_PT_ocean, - t.TEXTURE_PT_mapping, - t.TEXTURE_PT_influence, - t.TEXTURE_PT_custom_props, - - # meshes - t.DATA_PT_context_mesh, - t.DATA_PT_normals, - t.DATA_PT_texture_space, - t.DATA_PT_shape_keys, - t.DATA_PT_uv_texture, - t.DATA_PT_vertex_colors, - t.DATA_PT_vertex_groups, - t.DATA_PT_customdata, - t.DATA_PT_custom_props_mesh, - - # materials - t.MATERIAL_PT_context_material, - t.MATERIAL_PT_preview, - t.MATERIAL_PT_pipeline, - t.MATERIAL_PT_diffuse, - t.MATERIAL_PT_specular, - t.MATERIAL_PT_shading, - t.MATERIAL_PT_transp, - t.MATERIAL_PT_mirror, - t.MATERIAL_PT_sss, - t.MATERIAL_PT_halo, - t.MATERIAL_PT_flare, - t.MATERIAL_PT_game_settings, - t.MATERIAL_PT_physics, - t.MATERIAL_PT_strand, - t.MATERIAL_PT_options, - t.MATERIAL_PT_shadow, - - t.MATERIAL_PT_transp_game, - t.MATERIAL_PT_volume_density, - t.MATERIAL_PT_volume_shading, - t.MATERIAL_PT_volume_lighting, - t.MATERIAL_PT_volume_transp, - t.MATERIAL_PT_volume_integration, - t.MATERIAL_PT_volume_options, - t.MATERIAL_PT_custom_props, - - # particles - t.PARTICLE_PT_context_particles, - t.PARTICLE_PT_emission, - t.PARTICLE_PT_hair_dynamics, - t.PARTICLE_PT_cache, - t.PARTICLE_PT_velocity, - t.PARTICLE_PT_rotation, - t.PARTICLE_PT_physics, - t.PARTICLE_PT_boidbrain, - t.PARTICLE_PT_render, - t.PARTICLE_PT_draw, - t.PARTICLE_PT_children, - t.PARTICLE_PT_field_weights, - t.PARTICLE_PT_force_fields, - t.PARTICLE_PT_vertexgroups, - - # scene - t.SCENE_PT_scene, - t.SCENE_PT_unit, - t.SCENE_PT_keying_sets, - t.SCENE_PT_keying_set_paths, - t.SCENE_PT_color_management, - - t.SCENE_PT_audio, - t.SCENE_PT_physics, - t.SCENE_PT_rigid_body_world, - t.SCENE_PT_rigid_body_cache, - t.SCENE_PT_rigid_body_field_weights, - t.SCENE_PT_simplify, - t.SCENE_PT_custom_props, - - # world - t.WORLD_PT_context_world, - t.WORLD_PT_preview, - t.WORLD_PT_world, - t.WORLD_PT_ambient_occlusion, - t.WORLD_PT_environment_lighting, - t.WORLD_PT_indirect_lighting, - t.WORLD_PT_gather, - t.WORLD_PT_mist, - t.WORLD_PT_custom_props - - ) - + # .autoupdate + UpdateSourceOperator, + Updater, + UpdateChecker, -classes = [ - autoupdate.UpdateSourceOperator, - autoupdate.Updater, - autoupdate.UpdateChecker, - ui.CAM_UL_operations, - ui.CAM_UL_chains, + # .chain opReference, camChain, - machineSettings, - CamAddonPreferences, - import_settings, - ui.CAM_INTERFACE_Panel, - ui.CAM_INTERFACE_Properties, - ui.CAM_CHAINS_Panel, - ui.CAM_OPERATIONS_Panel, - ui.CAM_INFO_Properties, - ui.CAM_INFO_Panel, - ui.CAM_MATERIAL_Panel, - ui.CAM_MATERIAL_Properties, - ui.CAM_MATERIAL_PositionObject, - ui.CAM_OPERATION_PROPERTIES_Panel, - ui.CAM_OPTIMISATION_Panel, - ui.CAM_OPTIMISATION_Properties, - ui.CAM_AREA_Panel, - ui.CAM_MOVEMENT_Panel, - CAM_MOVEMENT_Properties, - ui.CAM_FEEDRATE_Panel, - ui.CAM_CUTTER_Panel, - ui.CAM_GCODE_Panel, - ui.CAM_MACHINE_Panel, - ui.CAM_PACK_Panel, - ui.CAM_SLICE_Panel, - ui.VIEW3D_PT_tools_curvetools, - ui.VIEW3D_PT_tools_create, - ui.CustomPanel, - ui.WM_OT_gcode_import, - ops.PathsBackground, - ops.KillPathsBackground, - ops.CalculatePath, - ops.PathsChain, - ops.PathExportChain, - ops.PathsAll, - ops.PathExport, - ops.CAMSimulate, - ops.CAMSimulateChain, - ops.CamChainAdd, - ops.CamChainRemove, - ops.CamChainOperationAdd, - ops.CamChainOperationRemove, - ops.CamChainOperationUp, - ops.CamChainOperationDown, + # .curvecamcreate + CamCurveDrawer, + CamCurveFlatCone, + CamCurveGear, + CamCurveHatch, + CamCurveInterlock, + CamCurveMortise, + CamCurvePlate, + CamCurvePuzzle, + + # .curvecamequation + CamCustomCurve, + CamHypotrochoidCurve, + CamLissajousCurve, + CamSineCurve, + + # .curvecamtools + CamCurveBoolean, + CamCurveConvexHull, + CamCurveIntarsion, + CamCurveOvercuts, + CamCurveOvercutsB, + CamCurveRemoveDoubles, + CamMeshGetPockets, + CamOffsetSilhouete, + CamObjectSilhouete, + + # .engine + BLENDERCAM_ENGINE, - ops.CamOperationAdd, - ops.CamOperationCopy, - ops.CamOperationRemove, - ops.CamOperationMove, + # .machine_settings + machineSettings, + + # .ops + CalculatePath, # bridges related - ops.CamBridgesAdd, + CamBridgesAdd, + CamChainAdd, + CamChainRemove, + CamChainOperationAdd, + CamChainOperationRemove, + CamChainOperationUp, + CamChainOperationDown, + CamOperationAdd, + CamOperationCopy, + CamOperationRemove, + CamOperationMove, # 5 axis ops - ops.CamOrientationAdd, + CamOrientationAdd, # shape packing - ops.CamPackObjects, - ops.CamSliceObjects, - # other tools - curvecamtools.CamCurveBoolean, - curvecamtools.CamCurveConvexHull, - curvecamtools.CamOffsetSilhouete, - curvecamtools.CamObjectSilhouete, - curvecamtools.CamCurveIntarsion, - curvecamtools.CamCurveOvercuts, - curvecamtools.CamCurveOvercutsB, - curvecamtools.CamCurveRemoveDoubles, - curvecamtools.CamMeshGetPockets, - - curvecamequation.CamSineCurve, - curvecamequation.CamLissajousCurve, - curvecamequation.CamHypotrochoidCurve, - curvecamequation.CamCustomCurve, + CamPackObjects, + CamSliceObjects, + CAMSimulate, + CAMSimulateChain, + KillPathsBackground, + PathsAll, + PathsBackground, + PathsChain, + PathExport, + PathExportChain, + + # .pack + PackObjectsSettings, - curvecamcreate.CamCurveHatch, - curvecamcreate.CamCurvePlate, - curvecamcreate.CamCurveDrawer, - curvecamcreate.CamCurveGear, - curvecamcreate.CamCurveFlatCone, - curvecamcreate.CamCurveMortise, - curvecamcreate.CamCurveInterlock, - curvecamcreate.CamCurvePuzzle, + # .preferences + CamAddonPreferences, + # .preset_managers CAM_CUTTER_MT_presets, CAM_OPERATION_MT_presets, CAM_MACHINE_MT_presets, AddPresetCamCutter, AddPresetCamOperation, AddPresetCamMachine, - BLENDERCAM_ENGINE, - # CamBackgroundMonitor - # pack module: - PackObjectsSettings, + + # .slice SliceObjectsSettings, - camOperation, + # .ui and .ui_panels - the order will affect the layout + import_settings, + CAM_UL_operations, + CAM_UL_chains, + CAM_INTERFACE_Panel, + CAM_INTERFACE_Properties, + CAM_CHAINS_Panel, + CAM_OPERATIONS_Panel, + CAM_INFO_Properties, + CAM_INFO_Panel, + CAM_MATERIAL_Panel, + CAM_MATERIAL_Properties, + CAM_MATERIAL_PositionObject, + CAM_OPERATION_PROPERTIES_Panel, + CAM_OPTIMISATION_Panel, + CAM_OPTIMISATION_Properties, + CAM_AREA_Panel, + CAM_MOVEMENT_Panel, + CAM_MOVEMENT_Properties, + CAM_FEEDRATE_Panel, + CAM_CUTTER_Panel, + CAM_GCODE_Panel, + CAM_MACHINE_Panel, + CAM_PACK_Panel, + CAM_SLICE_Panel, + VIEW3D_PT_tools_curvetools, + VIEW3D_PT_tools_create, + CustomPanel, + WM_OT_gcode_import, + + # .cam_operation - last to allow dependencies to register before it + camOperation, ] def register() -> None: - for p in classes: - bpy.utils.register_class(p) + for cls in classes: + bpy.utils.register_class(cls) - s = bpy.types.Scene + basrelief.register() - s.cam_chains = CollectionProperty( - type=camChain, - ) - s.cam_active_chain = IntProperty( + bpy.app.handlers.frame_change_pre.append(timer_update) + bpy.app.handlers.load_post.append(check_operations_on_load) + # bpy.types.INFO_HT_header.append(header_info) + + scene = bpy.types.Scene + + scene.cam_active_chain = IntProperty( name="CAM Active Chain", description="The selected chain", ) - - s.cam_operations = CollectionProperty( - type=camOperation, - ) - - s.cam_active_operation = IntProperty( + scene.cam_active_operation = IntProperty( name="CAM Active Operation", description="The selected operation", update=updateOperation, ) - s.cam_machine = PointerProperty( - type=machineSettings, + scene.cam_chains = CollectionProperty( + type=camChain, ) - - s.cam_import_gcode = PointerProperty( + scene.cam_import_gcode = PointerProperty( type=import_settings, ) - - s.cam_text = StringProperty() - bpy.app.handlers.frame_change_pre.append(ops.timer_update) - bpy.app.handlers.load_post.append(check_operations_on_load) - # bpy.types.INFO_HT_header.append(header_info) - - s.cam_pack = PointerProperty( + scene.cam_machine = PointerProperty( + type=machineSettings, + ) + scene.cam_operations = CollectionProperty( + type=camOperation, + ) + scene.cam_pack = PointerProperty( type=PackObjectsSettings, ) - - s.cam_slice = PointerProperty( + scene.cam_slice = PointerProperty( type=SliceObjectsSettings, ) - - bpy.types.Scene.interface = PointerProperty( + scene.cam_text = StringProperty() + scene.interface = PointerProperty( type=CAM_INTERFACE_Properties, ) - basrelief.register() - - from bl_ui.properties_material import ( - EEVEE_MATERIAL_PT_context_material, - EEVEE_MATERIAL_PT_surface, - EEVEE_MATERIAL_PT_settings, - ) - - panels = [ - EEVEE_MATERIAL_PT_context_material, - EEVEE_MATERIAL_PT_surface, - EEVEE_MATERIAL_PT_settings, - ] - - for panel in panels: - bpy.utils.unregister_class(panel) - panel.COMPAT_ENGINES.add('BLENDERCAM_RENDER') - bpy.utils.register_class(panel) + for panel in get_panels(): + panel.COMPAT_ENGINES.add("BLENDERCAM_RENDER") def unregister() -> None: - for p in classes: - bpy.utils.unregister_class(p) - s = bpy.types.Scene + for cls in classes: + bpy.utils.unregister_class(cls) - # cam chains are defined hardly now. - del s.cam_chains - del s.cam_active_chain - del s.cam_operations - del s.cam_active_operation - del s.cam_machine - del s.cam_import_gcode - del s.cam_text - del s.cam_pack - del s.cam_slice basrelief.unregister() - from bl_ui.properties_material import ( - EEVEE_MATERIAL_PT_context_material, - EEVEE_MATERIAL_PT_surface, - EEVEE_MATERIAL_PT_settings, - ) - - panels = [ - EEVEE_MATERIAL_PT_context_material, - EEVEE_MATERIAL_PT_surface, - EEVEE_MATERIAL_PT_settings, - ] + scene = bpy.types.Scene - for panel in panels: - panel.COMPAT_ENGINES.remove('BLENDERCAM_RENDER') + # cam chains are defined hardly now. + del scene.cam_chains + del scene.cam_active_chain + del scene.cam_operations + del scene.cam_active_operation + del scene.cam_machine + del scene.cam_import_gcode + del scene.cam_text + del scene.cam_pack + del scene.cam_slice + + for panel in get_panels(): + if 'BLENDERCAM_RENDER' in panel.COMPAT_ENGINES: + panel.COMPAT_ENGINES.remove('BLENDERCAM_RENDER') diff --git a/scripts/addons/cam/async_op.py b/scripts/addons/cam/async_op.py index 6e212fa51..bb298a626 100644 --- a/scripts/addons/cam/async_op.py +++ b/scripts/addons/cam/async_op.py @@ -1,8 +1,8 @@ -import bpy import sys - import types +import bpy + @types.coroutine def progress_async(text, n=None, value_type='%'): diff --git a/scripts/addons/cam/autoupdate.py b/scripts/addons/cam/autoupdate.py index cd522e430..70856e9c4 100644 --- a/scripts/addons/cam/autoupdate.py +++ b/scripts/addons/cam/autoupdate.py @@ -1,17 +1,18 @@ +import calendar from datetime import date -from .version import __version__ as current_version -from urllib.request import urlopen +import io import json +import os import pathlib +import re +import sys +from urllib.request import urlopen import zipfile + import bpy from bpy.props import StringProperty -import re -import io -import os -import sys -import calendar +from .version import __version__ as current_version class UpdateChecker(bpy.types.Operator): diff --git a/scripts/addons/cam/basrelief.py b/scripts/addons/cam/basrelief.py index 86035f4ca..f1c4ad195 100644 --- a/scripts/addons/cam/basrelief.py +++ b/scripts/addons/cam/basrelief.py @@ -1,9 +1,14 @@ -import bpy +from math import ( + ceil, + floor, + sqrt +) +import re import time + import numpy -import math -import re -from math import * + +import bpy from bpy.props import ( BoolProperty, EnumProperty, diff --git a/scripts/addons/cam/bridges.py b/scripts/addons/cam/bridges.py index 224d7ff38..f45f729bc 100644 --- a/scripts/addons/cam/bridges.py +++ b/scripts/addons/cam/bridges.py @@ -19,21 +19,22 @@ # # ***** END GPL LICENCE BLOCK ***** # here is the bridges functionality of Blender CAM. The functions here are called with operators defined from ops.py. +from math import ( + hypot, + pi, +) + +from shapely import ops as sops +from shapely import geometry as sgeometry +from shapely import prepared import bpy -from bpy_extras.object_utils import AddObjectHelper, object_data_add +from bpy_extras.object_utils import object_data_add +from mathutils import Vector from . import utils from . import simple -import mathutils -import math - - -from shapely import ops as sops -from shapely import geometry as sgeometry -from shapely import affinity, prepared - def addBridge(x, y, rot, sizex, sizey): bpy.ops.mesh.primitive_plane_add(size=sizey*2, calc_uvs=True, enter_editmode=False, align='WORLD', @@ -77,12 +78,12 @@ def addAutoBridges(o): minx, miny, maxx, maxy = c.bounds d1 = c.project(sgeometry.Point(maxx + 1000, (maxy + miny) / 2.0)) p = c.interpolate(d1) - bo = addBridge(p.x, p.y, -math.pi / 2, o.bridges_width, o.cutter_diameter * 1) + bo = addBridge(p.x, p.y, -pi / 2, o.bridges_width, o.cutter_diameter * 1) g.objects.link(bo) bpy.context.collection.objects.unlink(bo) d1 = c.project(sgeometry.Point(minx - 1000, (maxy + miny) / 2.0)) p = c.interpolate(d1) - bo = addBridge(p.x, p.y, math.pi / 2, o.bridges_width, o.cutter_diameter * 1) + bo = addBridge(p.x, p.y, pi / 2, o.bridges_width, o.cutter_diameter * 1) g.objects.link(bo) bpy.context.collection.objects.unlink(bo) d1 = c.project(sgeometry.Point((minx + maxx) / 2.0, maxy + 1000)) @@ -92,7 +93,7 @@ def addAutoBridges(o): bpy.context.collection.objects.unlink(bo) d1 = c.project(sgeometry.Point((minx + maxx) / 2.0, miny - 1000)) p = c.interpolate(d1) - bo = addBridge(p.x, p.y, math.pi, o.bridges_width, o.cutter_diameter * 1) + bo = addBridge(p.x, p.y, pi, o.bridges_width, o.cutter_diameter * 1) g.objects.link(bo) bpy.context.collection.objects.unlink(bo) @@ -153,8 +154,8 @@ def useBridges(ch, o): if vi + 1 < len(ch_points): i2 = vi + 1 chp2 = ch_points[vi + 1] # Vector(ch_points[vi+1]) - v1 = mathutils.Vector(chp1) - v2 = mathutils.Vector(chp2) + v1 = Vector(chp1) + v2 = Vector(chp2) if v1.z < bridgeheight or v2.z < bridgeheight: v = v2 - v1 p2 = sgeometry.Point(chp2) @@ -181,13 +182,13 @@ def useBridges(ch, o): newpoints.append((chp1[0], chp1[1], max(chp1[2], bridgeheight))) cpoints = [] if itpoint: - pt = mathutils.Vector((intersections.x, intersections.y, intersections.z)) + pt = Vector((intersections.x, intersections.y, intersections.z)) cpoints = [pt] elif itmpoint: cpoints = [] for p in intersections.geoms: - pt = mathutils.Vector((p.x, p.y, p.z)) + pt = Vector((p.x, p.y, p.z)) cpoints.append(pt) # ####sort collisions here :( ncpoints = [] @@ -243,7 +244,7 @@ def useBridges(ch, o): if z == bridgeheight: # find all points with z = bridge height count += 1 if isedge == 1: # This is to subdivide edges which are longer than the width of the bridge - edgelength = math.hypot(x - x2, y - y2) + edgelength = hypot(x - x2, y - y2) if edgelength > o.bridges_width: # make new vertex verts.append(((x + x2)/2, (y + y2)/2, o.minz)) diff --git a/scripts/addons/cam/cam_chunk.py b/scripts/addons/cam/cam_chunk.py new file mode 100644 index 000000000..cd5baedb0 --- /dev/null +++ b/scripts/addons/cam/cam_chunk.py @@ -0,0 +1,1271 @@ +# blender CAM chunk.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 ***** + +from math import ( + ceil, + cos, + hypot, + pi, + sin, + sqrt, + tan, +) + +import numpy as np +from shapely.geometry import polygon as spolygon +from shapely import geometry as sgeometry + +import bpy +from mathutils import Vector + +from . import polygon_utils_cam +from .simple import ( + activate, + dist2d, + progress, +) +from .exception import CamException +from .numba_wrapper import jit + + +def Rotate_pbyp(originp, p, ang): # rotate point around another point with angle + ox, oy, oz = originp + px, py, oz = p + + if ang == abs(pi / 2): + d = ang / abs(ang) + qx = ox + d * (oy - py) + qy = oy + d * (px - ox) + else: + qx = ox + cos(ang) * (px - ox) - sin(ang) * (py - oy) + qy = oy + sin(ang) * (px - ox) + cos(ang) * (py - oy) + rot_p = [qx, qy, oz] + return rot_p + + +@jit(nopython=True, parallel=True, fastmath=True, cache=True) +def _internalXyDistanceTo(ourpoints, theirpoints, cutoff): + v1 = ourpoints[0] + v2 = theirpoints[0] + minDistSq = (v1[0]-v2[0])**2 + (v1[1]-v2[1])**2 + cutoffSq = cutoff**2 + for v1 in ourpoints: + for v2 in theirpoints: + distSq = (v1[0]-v2[0])**2 + (v1[1]-v2[1])**2 + if distSq < cutoffSq: + return sqrt(distSq) + minDistSq = min(distSq, minDistSq) + return sqrt(minDistSq) + + +# for building points - stores points as lists for easy insert /append behaviour +class camPathChunkBuilder: + def __init__(self, inpoints=None, startpoints=None, endpoints=None, rotations=None): + if inpoints is None: + inpoints = [] + self.points = inpoints + self.startpoints = startpoints or [] + self.endpoints = endpoints or [] + self.rotations = rotations or [] + self.depth = None + + def to_chunk(self): + chunk = camPathChunk(self.points, self.startpoints, self.endpoints, self.rotations) + if len(self.points) > 2 and np.array_equal(self.points[0], self.points[-1]): + chunk.closed = True + if self.depth is not None: + chunk.depth = self.depth + + return chunk + +# an actual chunk - stores points as numpy arrays + + +class camPathChunk: + # parents=[] + # children=[] + # sorted=False + + # progressIndex=-1# for e.g. parallel strategy, when trying to save time.. + def __init__(self, inpoints, startpoints=None, endpoints=None, rotations=None): + # name this as _points so nothing external accesses it directly + # for 3 axes, this is only storage of points. For N axes, here go the sampled points + if len(inpoints) == 0: + self.points = np.empty(shape=(0, 3)) + else: + self.points = np.array(inpoints) + self.poly = None # get polygon just in time + self.simppoly = None + if startpoints: + # from where the sweep test begins, but also retract point for given path + self.startpoints = startpoints + else: + self.startpoints = [] + if endpoints: + self.endpoints = endpoints + else: + self.endpoints = [] # where sweep test ends + if rotations: + self.rotations = rotations + else: + self.rotations = [] # rotation of the machine axes + self.closed = False + self.children = [] + self.parents = [] + # self.unsortedchildren=False + self.sorted = False # if the chunk has allready been milled in the simulation + self.length = 0 # this is total length of this chunk. + self.zstart = 0 # this is stored for ramps mainly, + # because they are added afterwards, but have to use layer info + self.zend = 0 # + + def update_poly(self): + if len(self.points) > 2: + self.poly = sgeometry.Polygon(self.points[:, 0:2]) + else: + self.poly = sgeometry.Polygon() + + def get_point(self, n): + return self.points[n].tolist() + + def get_points(self): + return self.points.tolist() + + def get_points_np(self): + return self.points + + def set_points(self, points): + self.points = np.array(points) + + def count(self): + return len(self.points) + + def copy(self): + nchunk = camPathChunk(inpoints=self.points.copy(), startpoints=self.startpoints, + endpoints=self.endpoints, rotations=self.rotations) + nchunk.closed = self.closed + nchunk.children = self.children + nchunk.parents = self.parents + nchunk.sorted = self.sorted + nchunk.length = self.length + return nchunk + + def shift(self, x, y, z): + self.points = self.points + np.array([x, y, z]) + for i, p in enumerate(self.startpoints): + self.startpoints[i] = (p[0] + x, p[1] + y, p[2] + z) + for i, p in enumerate(self.endpoints): + self.endpoints[i] = (p[0] + x, p[1] + y, p[2] + z) + + def setZ(self, z, if_bigger=False): + if if_bigger: + self.points[:, 2] = z if z > self.points[:, 2] else self.points[:, 2] + else: + self.points[:, 2] = z + + def offsetZ(self, z): + self.points[:, 2] += z + + def flipX(self, x_centre): + self.points[:, 0] = x_centre - self.points[:, 0] + + def isbelowZ(self, z): + return np.any(self.points[:, 2] < z) + + def clampZ(self, z): + np.clip(self.points[:, 2], z, None, self.points[:, 2]) + + def clampmaxZ(self, z): + np.clip(self.points[:, 2], None, z, self.points[:, 2]) + + def dist(self, pos, o): + if self.closed: + dist_sq = (pos[0]-self.points[:, 0])**2 + (pos[1]-self.points[:, 1])**2 + return sqrt(np.min(dist_sq)) + else: + if o.movement.type == 'MEANDER': + d1 = dist2d(pos, self.points[0]) + d2 = dist2d(pos, self.points[-1]) + # if d22 points + # simplify them if they aren't already, to speed up distance finding + if self.simppoly is None: + self.simppoly = self.poly.simplify(0.0003).boundary + if other.simppoly is None: + other.simppoly = other.poly.simplify(0.0003).boundary + return self.simppoly.distance(other.simppoly) + else: # this is the old method, preferably should be replaced in most cases except parallel + # where this method works probably faster. + # print('warning, sorting will be slow due to bad parenting in parentChildDist') + return _internalXyDistanceTo(self.points, other.points, cutoff) + + def adaptdist(self, pos, o): + # reorders chunk so that it starts at the closest point to pos. + if self.closed: + dist_sq = (pos[0]-self.points[:, 0])**2 + (pos[1]-self.points[:, 1])**2 + point_idx = np.argmin(dist_sq) + new_points = np.concatenate((self.points[point_idx:], self.points[:point_idx+1])) + self.points = new_points + else: + if o.movement.type == 'MEANDER': + d1 = dist2d(pos, self.points[0]) + d2 = dist2d(pos, self.points[-1]) + if d2 < d1: + self.points = np.flip(self.points, axis=0) + + def getNextClosest(self, o, pos): + # finds closest chunk that can be milled, when inside sorting hierarchy. + mind = 100000000000 + + self.cango = False + closest = None + testlist = [] + testlist.extend(self.children) + tested = [] + tested.extend(self.children) + ch = None + while len(testlist) > 0: + chtest = testlist.pop() + if not chtest.sorted: + self.cango = False + cango = True + + for child in chtest.children: + if not child.sorted: + if child not in tested: + testlist.append(child) + tested.append(child) + cango = False + + if cango: + d = chtest.dist(pos, o) + if d < mind: + ch = chtest + mind = d + if ch is not None: + # print('found some') + return ch + # print('returning none') + return None + + def getLength(self): + # computes length of the chunk - in 3d + + point_differences = self.points[0:-1, :] - self.points[1:, :] + distances = np.linalg.norm(point_differences, axis=1) + self.length = np.sum(distances) + + def reverse(self): + self.points = np.flip(self.points, axis=0) + self.startpoints.reverse() + self.endpoints.reverse() + self.rotations.reverse() + + def pop(self, index): + print("WARNING: Popping from chunk is slow", self, index) + self.points = np.concatenate((self.points[0:index], self.points[index+1:]), axis=0) + if len(self.startpoints) > 0: + self.startpoints.pop(index) + self.endpoints.pop(index) + self.rotations.pop(index) + + def dedupePoints(self): + if len(self.points) > 1: + keep_points = np.empty(self.points.shape[0], dtype=bool) + keep_points[0] = True + diff_points = np.sum((self.points[1:]-self.points[:1])**2, axis=1) + keep_points[1:] = diff_points > 0.000000001 + self.points = self.points[keep_points, :] + + def insert(self, at_index, point, startpoint=None, endpoint=None, rotation=None): + self.append(point, startpoint=startpoint, endpoint=endpoint, + rotation=rotation, at_index=at_index) + + def append(self, point, startpoint=None, endpoint=None, rotation=None, at_index=None): + if at_index is None: + self.points = np.concatenate((self.points, np.array([point]))) + if startpoint is not None: + self.startpoints.append(startpoint) + if endpoint is not None: + self.endpoints.append(endpoint) + if rotation is not None: + self.rotations.append(rotation) + else: + self.points = np.concatenate( + (self.points[0:at_index], np.array([point]), self.points[at_index:])) + if startpoint is not None: + self.startpoints[at_index:at_index] = [startpoint] + if endpoint is not None: + self.endpoints[at_index:at_index] = [endpoint] + if rotation is not None: + self.rotations[at_index:at_index] = [rotation] + + def extend(self, points, startpoints=None, endpoints=None, rotations=None, at_index=None): + if len(points) == 0: + return + if at_index is None: + self.points = np.concatenate((self.points, np.array(points))) + if startpoints is not None: + self.startpoints.extend(startpoints) + if endpoints is not None: + self.endpoints.extend(endpoints) + if rotations is not None: + self.rotations.extend(rotations) + else: + self.points = np.concatenate( + (self.points[0:at_index], np.array(points), self.points[at_index:])) + if startpoints is not None: + self.startpoints[at_index:at_index] = startpoints + if endpoints is not None: + self.endpoints[at_index:at_index] = endpoints + if rotations is not None: + self.rotations[at_index:at_index] = rotations + + def clip_points(self, minx, maxx, miny, maxy): + """ remove any points outside this range """ + included_values = (self.points[:, 0] >= minx) and ((self.points[:, 0] <= maxx) + and (self.points[:, 1] >= maxy) and (self.points[:, 1] <= maxy)) + self.points = self.points[included_values] + + def rampContour(self, zstart, zend, o): + + stepdown = zstart - zend + chunk_points = [] + estlength = (zstart - zend) / tan(o.movement.ramp_in_angle) + self.getLength() + ramplength = estlength # min(ch.length,estlength) + ltraveled = 0 + endpoint = None + i = 0 + # z=zstart + znew = 10 + rounds = 0 # for counting if ramping makes more layers + while endpoint is None and not (znew == zend and i == 0): # + # for i,s in enumerate(ch.points): + # print(i, znew, zend, len(ch.points)) + s = self.points[i] + + if i > 0: + s2 = self.points[i - 1] + ltraveled += dist2d(s, s2) + ratio = ltraveled / ramplength + elif rounds > 0 and i == 0: + s2 = self.points[-1] + ltraveled += dist2d(s, s2) + ratio = ltraveled / ramplength + else: + ratio = 0 + znew = zstart - stepdown * ratio + if znew <= zend: + + ratio = ((z - zend) / (z - znew)) + v1 = Vector(chunk_points[-1]) + v2 = Vector((s[0], s[1], znew)) + v = v1 + ratio * (v2 - v1) + chunk_points.append((v.x, v.y, max(s[2], v.z))) + + if zend == o.min.z and endpoint is None and self.closed: + endpoint = i + 1 + if endpoint == len(self.points): + endpoint = 0 + # print(endpoint,len(ch.points)) + # else: + znew = max(znew, zend, s[2]) + chunk_points.append((s[0], s[1], znew)) + z = znew + if endpoint is not None: + break + i += 1 + if i >= len(self.points): + i = 0 + rounds += 1 + # if not o.use_layers: + # endpoint=0 + if endpoint is not None: # append final contour on the bottom z level + i = endpoint + started = False + # print('finaliz') + if i == len(self.points): + i = 0 + while i != endpoint or not started: + started = True + s = self.points[i] + chunk_points.append((s[0], s[1], s[2])) + # print(i,endpoint) + i += 1 + if i == len(self.points): + i = 0 + # ramp out + if o.movement.ramp_out and (not o.use_layers or not o.first_down or (o.first_down and endpoint is not None)): + z = zend + # i=endpoint + + while z < o.maxz: + if i == len(self.points): + i = 0 + s1 = self.points[i] + i2 = i - 1 + if i2 < 0: + i2 = len(self.points) - 1 + s2 = self.points[i2] + l = dist2d(s1, s2) + znew = z + tan(o.movement.ramp_out_angle) * l + if znew > o.maxz: + ratio = ((z - o.maxz) / (z - znew)) + v1 = Vector(chunk_points[-1]) + v2 = Vector((s1[0], s1[1], znew)) + v = v1 + ratio * (v2 - v1) + chunk_points.append((v.x, v.y, v.z)) + + else: + chunk_points.append((s1[0], s1[1], znew)) + z = znew + i += 1 + + # TODO: convert to numpy properly + self.points = np.array(chunk_points) + + def rampZigZag(self, zstart, zend, o): + # TODO: convert to numpy properly + if zend == None: + zend = self.points[0][2] + chunk_points = [] + # print(zstart,zend) + if zend < zstart: # this check here is only for stupid setup, + # when the chunks lie actually above operation start z. + + stepdown = zstart - zend + + estlength = (zstart - zend) / tan(o.movement.ramp_in_angle) + self.getLength() + if self.length > 0: # for single point chunks.. + ramplength = estlength + zigzaglength = ramplength / 2.000 + turns = 1 + print('turns %i' % turns) + if zigzaglength > self.length: + turns = ceil(zigzaglength / self.length) + ramplength = turns * self.length * 2.0 + zigzaglength = self.length + ramppoints = self.points.tolist() + + else: + zigzagtraveled = 0.0 + haspoints = False + ramppoints = [(self.points[0][0], self.points[0][1], self.points[0][2])] + i = 1 + while not haspoints: + # print(i,zigzaglength,zigzagtraveled) + p1 = ramppoints[-1] + p2 = self.points[i] + d = dist2d(p1, p2) + zigzagtraveled += d + if zigzagtraveled >= zigzaglength or i + 1 == len(self.points): + ratio = 1 - (zigzagtraveled - zigzaglength) / d + if (i + 1 == len( + self.points)): # this condition is for a rare case of combined layers+bridges+ramps.. + ratio = 1 + v = p1 + ratio * (p2 - p1) + ramppoints.append(v.tolist()) + haspoints = True + else: + ramppoints.append(p2) + i += 1 + negramppoints = ramppoints.copy() + negramppoints.reverse() + ramppoints.extend(negramppoints[1:]) + + traveled = 0.0 + chunk_points.append( + (self.points[0][0], self.points[0][1], max(self.points[0][2], zstart))) + for r in range(turns): + for p in range(0, len(ramppoints)): + p1 = chunk_points[-1] + p2 = ramppoints[p] + d = dist2d(p1, p2) + traveled += d + ratio = traveled / ramplength + znew = zstart - stepdown * ratio + # max value here is so that it doesn't go + chunk_points.append((p2[0], p2[1], max(p2[2], znew))) + # below surface in the case of 3d paths + + # chunks = setChunksZ([ch],zend) + chunk_points.extend(self.points.tolist()) + + ###################################### + # ramp out - this is the same thing, just on the other side.. + if o.movement.ramp_out: + zstart = o.maxz + zend = self.points[-1][2] + # again, sometimes a chunk could theoretically end above the starting level. + if zend < zstart: + stepdown = zstart - zend + + estlength = (zstart - zend) / tan(o.movement.ramp_out_angle) + self.getLength() + if self.length > 0: + ramplength = estlength + zigzaglength = ramplength / 2.000 + turns = 1 + print('turns %i' % turns) + if zigzaglength > self.length: + turns = ceil(zigzaglength / self.length) + ramplength = turns * self.length * 2.0 + zigzaglength = self.length + ramppoints = self.points.tolist() + # revert points here, we go the other way. + ramppoints.reverse() + + else: + zigzagtraveled = 0.0 + haspoints = False + ramppoints = [(self.points[-1][0], self.points[-1] + [1], self.points[-1][2])] + i = len(self.points) - 2 + while not haspoints: + # print(i,zigzaglength,zigzagtraveled) + p1 = ramppoints[-1] + p2 = self.points[i] + d = dist2d(p1, p2) + zigzagtraveled += d + if zigzagtraveled >= zigzaglength or i + 1 == len(self.points): + ratio = 1 - (zigzagtraveled - zigzaglength) / d + if (i + 1 == len( + self.points)): # this condition is for a rare case of + # combined layers+bridges+ramps... + ratio = 1 + # print((ratio,zigzaglength)) + v = p1 + ratio * (p2 - p1) + ramppoints.append(v.tolist()) + haspoints = True + # elif : + + else: + ramppoints.append(p2) + i -= 1 + negramppoints = ramppoints.copy() + negramppoints.reverse() + ramppoints.extend(negramppoints[1:]) + + traveled = 0.0 + for r in range(turns): + for p in range(0, len(ramppoints)): + p1 = chunk_points[-1] + p2 = ramppoints[p] + d = dist2d(p1, p2) + traveled += d + ratio = 1 - (traveled / ramplength) + znew = zstart - stepdown * ratio + chunk_points.append((p2[0], p2[1], max(p2[2], znew))) + # max value here is so that it doesn't go below surface in the case of 3d paths + self.points = np.array(chunk_points) + + # modify existing path start point + def changePathStart(self, o): + if o.profile_start > 0: + newstart = o.profile_start + chunkamt = len(self.points) + newstart = newstart % chunkamt + self.points = np.concatenate((self.points[newstart:], self.points[:newstart])) + + def breakPathForLeadinLeadout(self, o): + iradius = o.lead_in + oradius = o.lead_out + if iradius + oradius > 0: + chunkamt = len(self.points) + + for i in range(chunkamt - 1): + apoint = self.points[i] + bpoint = self.points[i + 1] + bmax = bpoint[0] - apoint[0] + bmay = bpoint[1] - apoint[1] + segmentLength = hypot(bmax, bmay) # find segment length + + if segmentLength > 2 * max(iradius, + oradius): # Be certain there is enough room for the leadin and leadiout + # add point on the line here + # average of the two x points to find center + newpointx = (bpoint[0] + apoint[0]) / 2 + # average of the two y points to find center + newpointy = (bpoint[1] + apoint[1]) / 2 + self.points = np.concatenate( + (self.points[:i+1], np.array([[newpointx, newpointy, apoint[2]]]), self.points[i+1:])) + + def leadContour(self, o): + perimeterDirection = 1 # 1 is clockwise, 0 is CCW + if o.movement.spindle_rotation == 'CW': + if o.movement.type == 'CONVENTIONAL': + perimeterDirection = 0 + + if self.parents: # if it is inside another parent + perimeterDirection ^= 1 # toggle with a bitwise XOR + print("has parent") + + if perimeterDirection == 1: + print("path direction is Clockwise") + else: + print("path direction is counterclockwise") + iradius = o.lead_in + oradius = o.lead_out + start = self.points[0] + nextp = self.points[1] + rpoint = Rotate_pbyp(start, nextp, pi / 2) + dx = rpoint[0] - start[0] + dy = rpoint[1] - start[1] + la = hypot(dx, dy) + pvx = (iradius * dx) / la + start[0] # arc center(x) + pvy = (iradius * dy) / la + start[1] # arc center(y) + arc_c = [pvx, pvy, start[2]] + + # TODO: this could easily be numpy + chunk_points = [] # create a new cutting path + + # add lead in arc in the begining + if round(o.lead_in, 6) > 0.0: + for i in range(15): + iangle = -i * (pi / 2) / 15 + arc_p = Rotate_pbyp(arc_c, start, iangle) + chunk_points.insert(0, arc_p) + + # glue rest of the path to the arc + chunk_points.extend(self.points.tolist()) + # for i in range(len(self.points)): + # chunk_points.append(self.points[i]) + + # add lead out arc to the end + if round(o.lead_in, 6) > 0.0: + for i in range(15): + iangle = i * (pi / 2) / 15 + arc_p = Rotate_pbyp(arc_c, start, iangle) + chunk_points.append(arc_p) + + self.points = np.array(chunk_points) + + +def chunksCoherency(chunks): + # checks chunks for their stability, for pencil path. + # it checks if the vectors direction doesn't jump too much too quickly, + # if this happens it splits the chunk on such places, + # too much jumps = deletion of the chunk. this is because otherwise the router has to slow down too often, + # but also means that some parts detected by cavity algorithm won't be milled + nchunks = [] + for chunk in chunks: + if len(chunk.points) > 2: + nchunk = camPathChunkBuilder() + + # doesn't check for 1 point chunks here, they shouldn't get here at all. + lastvec = Vector(chunk.points[1]) - Vector(chunk.points[0]) + for i in range(0, len(chunk.points) - 1): + nchunk.points.append(chunk.points[i]) + vec = Vector(chunk.points[i + 1]) - Vector(chunk.points[i]) + angle = vec.angle(lastvec, vec) + # print(angle,i) + if angle > 1.07: # 60 degrees is maximum toleration for pencil paths. + if len(nchunk.points) > 4: # this is a testing threshold + nchunks.append(nchunk.to_chunk()) + nchunk = camPathChunkBuilder() + lastvec = vec + if len(nchunk.points) > 4: # this is a testing threshold + nchunk.points = np.array(nchunk.points) + nchunks.append(nchunk) + return nchunks + + +def setChunksZ(chunks, z): + newchunks = [] + for ch in chunks: + chunk = ch.copy() + chunk.setZ(z) + newchunks.append(chunk) + return newchunks + +# don't make this @jit parallel, because it sometimes gets called with small N +# and the overhead of threading is too much. + + +@jit(nopython=True, fastmath=True, cache=True) +def _optimize_internal(points, keep_points, e, protect_vertical, protect_vertical_limit): + # inlined so that numba can optimize it nicely + def _mag_sq(v1): + return v1[0]**2 + v1[1]**2 + v1[2]**2 + + def _dot_pr(v1, v2): + return v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2] + + def _applyVerticalLimit(v1, v2, cos_limit): + """test path segment on verticality threshold, for protect_vertical option""" + z = abs(v1[2] - v2[2]) + if z > 0: + # don't use this vector because dot product of 0,0,1 is trivially just v2[2] + # vec_up = np.array([0, 0, 1]) + vec_diff = v1-v2 + vec_diff2 = v2-v1 + vec_diff_mag = np.sqrt(_mag_sq(vec_diff)) + # dot product = cos(angle) * mag1 * mag2 + cos1_times_mag = vec_diff[2] + cos2_times_mag = vec_diff2[2] + if cos1_times_mag > cos_limit*vec_diff_mag: + # vertical, moving down + v1[0] = v2[0] + v1[1] = v2[1] + elif cos2_times_mag > cos_limit*vec_diff_mag: + # vertical, moving up + v2[0] = v1[0] + v2[1] = v1[1] + + cos_limit = cos(protect_vertical_limit) + prev_i = 0 + for i in range(1, points.shape[0]-1): + v1 = points[prev_i] + v2 = points[i+1] + vmiddle = points[i] + + line_direction = v2-v1 + line_length = sqrt(_mag_sq(line_direction)) + if line_length == 0: + # don't keep duplicate points + keep_points[i] = False + continue + # normalize line direction + line_direction *= (1.0/line_length) # N in formula below + # X = A + tN (line formula) Distance to point P + # A = v1, N = line_direction, P = vmiddle + # distance = || (P - A) - ((P-A).N)N || + point_offset = vmiddle - v1 + distance_sq = _mag_sq(point_offset - (line_direction * + _dot_pr(point_offset, line_direction))) + # compare on squared distance to save a sqrt + if distance_sq < e*e: + keep_points[i] = False + else: + keep_points[i] = True + if protect_vertical: + _applyVerticalLimit(points[prev_i], points[i], cos_limit) + prev_i = i + + +def optimizeChunk(chunk, operation): + if len(chunk.points) > 2: + points = chunk.points + naxispoints = False + if len(chunk.startpoints) > 0: + startpoints = chunk.startpoints + endpoints = chunk.endpoints + naxispoints = True + + protect_vertical = operation.movement.protect_vertical and operation.machine_axes == '3' + keep_points = np.full(points.shape[0], True) + # shape points need to be on line, + # but we need to protect vertical - which + # means changing point values + # bits of this are moved from simple.py so that + # numba can optimize as a whole + _optimize_internal(points, keep_points, operation.optimisation.optimize_threshold * + 0.000001, protect_vertical, operation.movement.protect_vertical_limit) + + # now do numpy select by boolean array + chunk.points = points[keep_points] + if naxispoints: + # list comprehension so we don't have to do tons of appends + chunk.startpoints = [chunk.startpoints[i] + for i, b in enumerate(keep_points) if b == True] + chunk.endpoints = [chunk.endpoints[i] for i, b in enumerate(keep_points) if b == True] + chunk.rotations = [chunk.rotations[i] for i, b in enumerate(keep_points) if b == True] + return chunk + + +def limitChunks(chunks, o, + force=False): # TODO: this should at least add point on area border... + # but shouldn't be needed at all at the first place... + if o.use_limit_curve or force: + nchunks = [] + for ch in chunks: + prevsampled = True + nch = camPathChunkBuilder() + nch1 = None + closed = True + for s in ch.points: + sampled = o.ambient.contains(sgeometry.Point(s[0], s[1])) + if not sampled and len(nch.points) > 0: + nch.closed = False + closed = False + nchunks.append(nch.to_chunk()) + if nch1 is None: + nch1 = nchunks[-1] + nch = camPathChunkBuilder() + elif sampled: + nch.points.append(s) + prevsampled = sampled + if len(nch.points) > 2 and closed and ch.closed and np.array_equal(ch.points[0], ch.points[-1]): + nch.closed = True + elif ch.closed and nch1 is not None and len(nch.points) > 1 and np.array_equal(nch.points[-1], nch1.points[0]): + # here adds beginning of closed chunk to the end, if the chunks were split during limiting + nch.points.extend(nch1.points.tolist()) + nchunks.remove(nch1) + print('joining stuff') + if len(nch.points) > 0: + nchunks.append(nch.to_chunk()) + return nchunks + else: + return chunks + + +def parentChildPoly(parents, children, o): + # hierarchy based on polygons - a polygon inside another is his child. + # hierarchy works like this: - children get milled first. + + for parent in parents: + if parent.poly is None: + parent.update_poly() + for child in children: + if child.poly is None: + child.update_poly() + if child != parent: # and len(child.poly)>0 + if parent.poly.contains(sgeometry.Point(child.poly.boundary.coords[0])): + parent.children.append(child) + child.parents.append(parent) + + +def parentChildDist(parents, children, o, distance=None): + # parenting based on x,y distance between chunks + # hierarchy works like this: - children get milled first. + + if distance is None: + dlim = o.dist_between_paths * 2 + if (o.strategy == 'PARALLEL' or o.strategy == 'CROSS') and o.movement.parallel_step_back: + dlim = dlim * 2 + else: + dlim = distance + + for child in children: + for parent in parents: + isrelation = False + if parent != child: + if parent.xyDistanceWithin(child, cutoff=dlim): + parent.children.append(child) + child.parents.append(parent) + + +def parentChild(parents, children, o): + # connect all children to all parents. Useful for any type of defining hierarchy. + # hierarchy works like this: - children get milled first. + + for child in children: + for parent in parents: + if parent != child: + parent.children.append(child) + child.parents.append(parent) + + +# this does more cleve chunks to Poly with hierarchies... ;) +def chunksToShapely(chunks): + # print ('analyzing paths') + for ch in chunks: # first convert chunk to poly + if len(ch.points) > 2: + # pchunk=[] + ch.poly = sgeometry.Polygon(ch.points[:, 0:2]) + if not ch.poly.is_valid: + ch.poly = sgeometry.Polygon() + else: + ch.poly = sgeometry.Polygon() + + for ppart in chunks: # then add hierarchy relations + for ptest in chunks: + if ppart != ptest: + if not ppart.poly.is_empty and not ptest.poly.is_empty: + if ptest.poly.contains(ppart.poly): + # hierarchy works like this: - children get milled first. + ppart.parents.append(ptest) + + for ch in chunks: # now make only simple polygons with holes, not more polys inside others + found = False + if len(ch.parents) % 2 == 1: + + for parent in ch.parents: + if len(parent.parents) + 1 == len(ch.parents): + # nparents serves as temporary storage for parents, + ch.nparents = [parent] + # not to get mixed with the first parenting during the check + found = True + break + + if not found: + ch.nparents = [] + + for ch in chunks: # then subtract the 1st level holes + ch.parents = ch.nparents + ch.nparents = None + if len(ch.parents) > 0: + + try: + ch.parents[0].poly = ch.parents[0].poly.difference( + ch.poly) # sgeometry.Polygon( ch.parents[0].poly, ch.poly) + except: + + print('chunksToShapely oops!') + + lastPt = None + tolerance = 0.0000003 + newPoints = [] + + for pt in ch.points: + toleranceXok = True + toleranceYok = True + if lastPt is not None: + if abs(pt[0] - lastPt[0]) < tolerance: + toleranceXok = False + if abs(pt[1] - lastPt[1]) < tolerance: + toleranceYok = False + + if toleranceXok or toleranceYok: + newPoints.append(pt) + lastPt = pt + else: + newPoints.append(pt) + lastPt = pt + + toleranceXok = True + toleranceYok = True + if abs(newPoints[0][0] - lastPt[0]) < tolerance: + toleranceXok = False + if abs(newPoints[0][1] - lastPt[1]) < tolerance: + toleranceYok = False + + if not toleranceXok and not toleranceYok: + newPoints.pop() + + ch.points = np.array(newPoints) + ch.poly = sgeometry.Polygon(ch.points) + + try: + ch.parents[0].poly = ch.parents[0].poly.difference(ch.poly) + except: + + # print('chunksToShapely double oops!') + + lastPt = None + tolerance = 0.0000003 + newPoints = [] + + for pt in ch.parents[0].points: + toleranceXok = True + toleranceYok = True + # print( '{0:.9f}, {0:.9f}, {0:.9f}'.format(pt[0], pt[1], pt[2]) ) + # print(pt) + if lastPt is not None: + if abs(pt[0] - lastPt[0]) < tolerance: + toleranceXok = False + if abs(pt[1] - lastPt[1]) < tolerance: + toleranceYok = False + + if toleranceXok or toleranceYok: + newPoints.append(pt) + lastPt = pt + else: + newPoints.append(pt) + lastPt = pt + + toleranceXok = True + toleranceYok = True + if abs(newPoints[0][0] - lastPt[0]) < tolerance: + toleranceXok = False + if abs(newPoints[0][1] - lastPt[1]) < tolerance: + toleranceYok = False + + if not toleranceXok and not toleranceYok: + newPoints.pop() + # print('starting and ending points too close, removing ending point') + + ch.parents[0].points = np.array(newPoints) + ch.parents[0].poly = sgeometry.Polygon(ch.parents[0].points) + + ch.parents[0].poly = ch.parents[0].poly.difference( + ch.poly) # sgeometry.Polygon( ch.parents[0].poly, ch.poly) + + returnpolys = [] + + for polyi in range(0, len(chunks)): # export only the booleaned polygons + ch = chunks[polyi] + if not ch.poly.is_empty: + if len(ch.parents) == 0: + returnpolys.append(ch.poly) + from shapely.geometry import MultiPolygon + polys = MultiPolygon(returnpolys) + return polys + + +def meshFromCurveToChunk(object): + mesh = object.data + # print('detecting contours from curve') + chunks = [] + chunk = camPathChunkBuilder() + ek = mesh.edge_keys + d = {} + for e in ek: + d[e] = 1 # + dk = d.keys() + x = object.location.x + y = object.location.y + z = object.location.z + lastvi = 0 + vtotal = len(mesh.vertices) + perc = 0 + progress('processing curve - START - Vertices: ' + str(vtotal)) + for vi in range(0, len(mesh.vertices) - 1): + co = (mesh.vertices[vi].co + object.location).to_tuple() + if not dk.isdisjoint([(vi, vi + 1)]) and d[(vi, vi + 1)] == 1: + chunk.points.append(co) + else: + chunk.points.append(co) + if len(chunk.points) > 2 and (not (dk.isdisjoint([(vi, lastvi)])) or not ( + dk.isdisjoint([(lastvi, vi)]))): # this was looping chunks of length of only 2 points... + # print('itis') + + chunk.closed = True + chunk.points.append((mesh.vertices[lastvi].co + object.location).to_tuple()) + # add first point to end#originally the z was mesh.vertices[lastvi].co.z+z + lastvi = vi + 1 + chunk = chunk.to_chunk() + chunk.dedupePoints() + if chunk.count() >= 1: + # dump single point chunks + chunks.append(chunk) + chunk = camPathChunkBuilder() + + progress('processing curve - FINISHED') + + vi = len(mesh.vertices) - 1 + chunk.points.append((mesh.vertices[vi].co.x + x, + mesh.vertices[vi].co.y + y, mesh.vertices[vi].co.z + z)) + if not (dk.isdisjoint([(vi, lastvi)])) or not (dk.isdisjoint([(lastvi, vi)])): + chunk.closed = True + chunk.points.append( + (mesh.vertices[lastvi].co.x + x, mesh.vertices[lastvi].co.y + y, mesh.vertices[lastvi].co.z + z)) + chunk = chunk.to_chunk() + chunk.dedupePoints() + if chunk.count() >= 1: + # dump single point chunks + chunks.append(chunk) + return chunks + + +def makeVisible(o): + storage = [True, []] + + if not o.visible_get(): + storage[0] = False + + cam_collection = bpy.data.collections.new("cam") + bpy.context.scene.collection.children.link(cam_collection) + cam_collection.objects.link(bpy.context.object) + + for i in range(0, 20): + storage[1].append(o.layers[i]) + + o.layers[i] = bpy.context.scene.layers[i] + + return storage + + +def restoreVisibility(o, storage): + o.hide_viewport = storage[0] + # print(storage) + for i in range(0, 20): + o.layers[i] = storage[1][i] + + +def meshFromCurve(o, use_modifiers=False): + activate(o) + bpy.ops.object.duplicate() + + bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') + + co = bpy.context.active_object + + # support for text objects is only and only here, just convert them to curves. + if co.type == 'FONT': + bpy.ops.object.convert(target='CURVE', keep_original=False) + elif co.type != 'CURVE': # curve must be a curve... + bpy.ops.object.delete() # delete temporary object + raise CamException("Source curve object must be of type CURVE") + co.data.dimensions = '3D' + co.data.bevel_depth = 0 + co.data.extrude = 0 + + # first, convert to mesh to avoid parenting issues with hooks, then apply locrotscale. + bpy.ops.object.convert(target='MESH', keep_original=False) + + if use_modifiers: + eval_object = co.evaluated_get(bpy.context.evaluated_depsgraph_get()) + newmesh = bpy.data.meshes.new_from_object(eval_object) + oldmesh = co.data + co.modifiers.clear() + co.data = newmesh + bpy.data.meshes.remove(oldmesh) + + 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 + + return bpy.context.active_object + + +def curveToChunks(o, use_modifiers=False): + co = meshFromCurve(o, use_modifiers) + chunks = meshFromCurveToChunk(co) + + co = bpy.context.active_object + + bpy.ops.object.select_all(action='DESELECT') + bpy.data.objects[co.name].select_set(True) + bpy.ops.object.delete() + + return chunks + + +def shapelyToChunks(p, zlevel): # + chunk_builders = [] + # p=sortContours(p) + seq = polygon_utils_cam.shapelyToCoords(p) + i = 0 + for s in seq: + # progress(p[i]) + if len(s) > 1: + chunk = camPathChunkBuilder([]) + for v in s: + if p.has_z: + chunk.points.append((v[0], v[1], v[2])) + else: + chunk.points.append((v[0], v[1], zlevel)) + + chunk_builders.append(chunk) + i += 1 + chunk_builders.reverse() # this is for smaller shapes first. + return [c.to_chunk() for c in chunk_builders] + + +def chunkToShapely(chunk): + p = spolygon.Polygon(chunk.points) + return p + + +def chunksRefine(chunks, o): + """add extra points in between for chunks""" + for ch in chunks: + # print('before',len(ch)) + newchunk = [] + v2 = Vector(ch.points[0]) + # print(ch.points) + for s in ch.points: + + v1 = Vector(s) + v = v1 - v2 + + if v.length > o.dist_along_paths: + d = v.length + v.normalize() + i = 0 + vref = Vector((0, 0, 0)) + + while vref.length < d: + i += 1 + vref = v * o.dist_along_paths * i + if vref.length < d: + p = v2 + vref + + newchunk.append((p.x, p.y, p.z)) + + newchunk.append(s) + v2 = v1 + ch.points = np.array(newchunk) + + return chunks + + +def chunksRefineThreshold(chunks, distance, limitdistance): + """add extra points in between for chunks. For medial axis strategy only !""" + for ch in chunks: + newchunk = [] + v2 = Vector(ch.points[0]) + + for s in ch.points: + + v1 = Vector(s) + v = v1 - v2 + + if v.length > limitdistance: + d = v.length + v.normalize() + i = 1 + vref = Vector((0, 0, 0)) + while vref.length < d / 2: + + vref = v * distance * i + if vref.length < d: + p = v2 + vref + + newchunk.append((p.x, p.y, p.z)) + i += 1 + # because of the condition, so it doesn't run again. + vref = v * distance * i + while i > 0: + vref = v * distance * i + if vref.length < d: + p = v1 - vref + + newchunk.append((p.x, p.y, p.z)) + i -= 1 + + newchunk.append(s) + v2 = v1 + ch.points = np.array(newchunk) + + return chunks diff --git a/scripts/addons/cam/cam_operation.py b/scripts/addons/cam/cam_operation.py new file mode 100644 index 000000000..60dcfbde9 --- /dev/null +++ b/scripts/addons/cam/cam_operation.py @@ -0,0 +1,1192 @@ +from math import pi + +import numpy +from shapely import geometry as sgeometry + +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, + FloatVectorProperty, + IntProperty, + PointerProperty, + StringProperty, +) +from bpy.types import ( + PropertyGroup, +) +from . import constants +from .utils import ( + getStrategyList, + operationValid, + update_operation, + updateBridges, + updateChipload, + updateCutout, + updateOffsetImage, + updateOperationValid, + updateRest, + updateRotation, + updateStrategy, + updateZbufferImage, +) +from .ui_panels.info import CAM_INFO_Properties +from .ui_panels.material import CAM_MATERIAL_Properties +from .ui_panels.movement import CAM_MOVEMENT_Properties +from .ui_panels.optimisation import CAM_OPTIMISATION_Properties + + +class camOperation(PropertyGroup): + + material: PointerProperty( + type=CAM_MATERIAL_Properties + ) + info: PointerProperty( + type=CAM_INFO_Properties + ) + optimisation: PointerProperty( + type=CAM_OPTIMISATION_Properties + ) + movement: PointerProperty( + type=CAM_MOVEMENT_Properties + ) + + name: StringProperty( + name="Operation Name", + default="Operation", + update=updateRest, + ) + filename: StringProperty( + name="File name", + default="Operation", + update=updateRest, + ) + auto_export: BoolProperty( + name="Auto export", + description="export files immediately after path calculation", + default=True, + ) + remove_redundant_points: BoolProperty( + name="Symplify Gcode", + description="Remove redundant points sharing the same angle" + " as the start vector", + default=False, + ) + simplify_tol: IntProperty( + name="Tolerance", + description='lower number means more precise', + default=50, + min=1, + max=1000, + ) + hide_all_others: BoolProperty( + name="Hide all others", + description="Hide all other tool pathes except toolpath" + " assotiated with selected CAM operation", + default=False, + ) + parent_path_to_object: BoolProperty( + name="Parent path to object", + description="Parent generated CAM path to source object", + default=False, + ) + object_name: StringProperty( + name='Object', + description='object handled by this operation', + update=updateOperationValid, + ) + collection_name: StringProperty( + name='Collection', + description='Object collection handled by this operation', + update=updateOperationValid, + ) + curve_object: StringProperty( + name='Curve source', + description='curve which will be sampled along the 3d object', + update=operationValid, + ) + curve_object1: StringProperty( + name='Curve target', + description='curve which will serve as attractor for the ' + 'cutter when the cutter follows the curve', + update=operationValid, + ) + source_image_name: StringProperty( + name='image_source', + description='image source', + update=operationValid, + ) + geometry_source: EnumProperty( + name='Source of data', + items=( + ('OBJECT', 'object', 'a'), + ('COLLECTION', 'Collection of objects', 'a'), + ('IMAGE', 'Image', 'a') + ), + description='Geometry source', + default='OBJECT', + update=updateOperationValid, + ) + cutter_type: EnumProperty( + name='Cutter', + items=( + ('END', 'End', 'end - flat cutter'), + ('BALLNOSE', 'Ballnose', 'ballnose cutter'), + ('BULLNOSE', 'Bullnose', 'bullnose cutter ***placeholder **'), + ('VCARVE', 'V-carve', 'v carve cutter'), + ('BALLCONE', 'Ballcone', 'Ball with a Cone for Parallel - X'), + ('CYLCONE', 'Cylinder cone', + 'Cylinder end with a Cone for Parallel - X'), + ('LASER', 'Laser', 'Laser cutter'), + ('PLASMA', 'Plasma', 'Plasma cutter'), + ('CUSTOM', 'Custom-EXPERIMENTAL', + 'modelled cutter - not well tested yet.') + ), + description='Type of cutter used', + default='END', + update=updateZbufferImage, + ) + cutter_object_name: StringProperty( + name='Cutter object', + description='object used as custom cutter for this operation', + update=updateZbufferImage, + ) + + machine_axes: EnumProperty( + name='Number of axes', + items=( + ('3', '3 axis', 'a'), + ('4', '#4 axis - EXPERIMENTAL', 'a'), + ('5', '#5 axis - EXPERIMENTAL', 'a') + ), + description='How many axes will be used for the operation', + default='3', + update=updateStrategy, + ) + strategy: EnumProperty( + name='Strategy', + items=getStrategyList, + description='Strategy', + update=updateStrategy, + ) + + strategy4axis: EnumProperty( + name='4 axis Strategy', + items=( + ('PARALLELR', 'Parallel around 1st rotary axis', + 'Parallel lines around first rotary axis'), + ('PARALLEL', 'Parallel along 1st rotary axis', + 'Parallel lines along first rotary axis'), + ('HELIX', 'Helix around 1st rotary axis', + 'Helix around rotary axis'), + ('INDEXED', 'Indexed 3-axis', + 'all 3 axis strategies, just applied to the 4th axis'), + ('CROSS', 'Cross', 'Cross paths') + ), + description='#Strategy', + default='PARALLEL', + update=updateStrategy, + ) + strategy5axis: EnumProperty( + name='Strategy', + items=( + ('INDEXED', 'Indexed 3-axis', + 'all 3 axis strategies, just rotated by 4+5th axes'), + ), + description='5 axis Strategy', + default='INDEXED', + update=updateStrategy, + ) + + rotary_axis_1: EnumProperty( + name='Rotary axis', + items=( + ('X', 'X', ''), + ('Y', 'Y', ''), + ('Z', 'Z', ''), + ), + description='Around which axis rotates the first rotary axis', + default='X', + update=updateStrategy, + ) + rotary_axis_2: EnumProperty( + name='Rotary axis 2', + items=( + ('X', 'X', ''), + ('Y', 'Y', ''), + ('Z', 'Z', ''), + ), + description='Around which axis rotates the second rotary axis', + default='Z', + update=updateStrategy, + ) + + skin: FloatProperty( + name="Skin", + description="Material to leave when roughing ", + min=0.0, + max=1.0, + default=0.0, + precision=constants.PRECISION, + unit="LENGTH", + update=updateOffsetImage, + ) + inverse: BoolProperty( + name="Inverse milling", + description="Male to female model conversion", + default=False, + update=updateOffsetImage, + ) + array: BoolProperty( + name="Use array", + description="Create a repetitive array for producing the " + "same thing many times", + default=False, + update=updateRest, + ) + array_x_count: IntProperty( + name="X count", + description="X count", + default=1, + min=1, + max=32000, + update=updateRest, + ) + array_y_count: IntProperty( + name="Y count", + description="Y count", + default=1, + min=1, + max=32000, + update=updateRest, + ) + array_x_distance: FloatProperty( + name="X distance", + description="distance between operation origins", + min=0.00001, + max=1.0, + default=0.01, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) + array_y_distance: FloatProperty( + name="Y distance", + description="distance between operation origins", + min=0.00001, + max=1.0, + default=0.01, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) + + # pocket options + pocket_option: EnumProperty( + name='Start Position', + items=( + ('INSIDE', 'Inside', 'a'), + ('OUTSIDE', 'Outside', 'a') + ), + description='Pocket starting position', + default='INSIDE', + update=updateRest, + ) + pocketToCurve: BoolProperty( + name="Pocket to curve", + description="generates a curve instead of a path", + default=False, + update=updateRest, + ) + # Cutout + cut_type: EnumProperty( + name='Cut', + items=( + ('OUTSIDE', 'Outside', 'a'), + ('INSIDE', 'Inside', 'a'), + ('ONLINE', 'On line', 'a') + ), + description='Type of cutter used', + default='OUTSIDE', + update=updateRest, + ) + outlines_count: IntProperty( + name="Outlines count", + description="Outlines count", + default=1, + min=1, + max=32, + update=updateCutout, + ) + straight: BoolProperty( + name="Overshoot Style", + description="Use overshoot cutout instead of conventional rounded", + default=False, + update=updateRest, + ) + # cutter + cutter_id: IntProperty( + name="Tool number", + description="For machines which support tool change based on tool id", + min=0, + max=10000, + default=1, + update=updateRest, + ) + cutter_diameter: FloatProperty( + name="Cutter diameter", + description="Cutter diameter = 2x cutter radius", + min=0.000001, + max=10, + default=0.003, + precision=constants.PRECISION, + unit="LENGTH", + update=updateOffsetImage, + ) + cylcone_diameter: FloatProperty( + name="Bottom Diameter", + description="Bottom diameter", + min=0.000001, + max=10, + default=0.003, + precision=constants.PRECISION, + unit="LENGTH", + update=updateOffsetImage, + ) + cutter_length: FloatProperty( + name="#Cutter length", + description="#not supported#Cutter length", + min=0.0, + max=100.0, + default=25.0, + precision=constants.PRECISION, + unit="LENGTH", + update=updateOffsetImage, + ) + cutter_flutes: IntProperty( + name="Cutter flutes", + description="Cutter flutes", + min=1, + max=20, + default=2, + update=updateChipload, + ) + cutter_tip_angle: FloatProperty( + name="Cutter v-carve angle", + description="Cutter v-carve angle", + min=0.0, + max=180.0, + default=60.0, + precision=constants.PRECISION, + update=updateOffsetImage, + ) + ball_radius: FloatProperty( + name="Ball radius", + description="Radius of", + min=0.0, + max=0.035, + default=0.001, + unit="LENGTH", + precision=constants.PRECISION, + update=updateOffsetImage, + ) + # ball_cone_flute: FloatProperty(name="BallCone Flute Length", description="length of flute", min=0.0, + # max=0.1, default=0.017, unit="LENGTH", precision=constants.PRECISION, update=updateOffsetImage) + bull_corner_radius: FloatProperty( + name="Bull Corner Radius", + description="Radius tool bit corner", + min=0.0, + max=0.035, + default=0.005, + unit="LENGTH", + precision=constants.PRECISION, + update=updateOffsetImage, + ) + + cutter_description: StringProperty( + name="Tool Description", + default="", + update=updateOffsetImage, + ) + + Laser_on: StringProperty( + name="Laser ON string", + default="M68 E0 Q100", + ) + Laser_off: StringProperty( + name="Laser OFF string", + default="M68 E0 Q0", + ) + Laser_cmd: StringProperty( + name="Laser command", + default="M68 E0 Q", + ) + Laser_delay: FloatProperty( + name="Laser ON Delay", + description="time after fast move to turn on laser and " + "let machine stabilize", + default=0.2, + ) + Plasma_on: StringProperty( + name="Plasma ON string", + default="M03", + ) + Plasma_off: StringProperty( + name="Plasma OFF string", + default="M05", + ) + Plasma_delay: FloatProperty( + name="Plasma ON Delay", + description="time after fast move to turn on Plasma and " + "let machine stabilize", + default=0.1, + ) + Plasma_dwell: FloatProperty( + name="Plasma dwell time", + description="Time to dwell and warm up the torch", + default=0.0, + ) + + # steps + dist_between_paths: FloatProperty( + name="Distance between toolpaths", + default=0.001, + min=0.00001, + max=32, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) + dist_along_paths: FloatProperty( + name="Distance along toolpaths", + default=0.0002, + min=0.00001, + max=32, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) + parallel_angle: FloatProperty( + name="Angle of paths", + default=0, + min=-360, + max=360, + precision=0, + subtype="ANGLE", + unit="ROTATION", + update=updateRest, + ) + + old_rotation_A: FloatProperty( + name="A axis angle", + description="old value of Rotate A axis\nto specified angle", + default=0, + min=-360, + max=360, + precision=0, + subtype="ANGLE", + unit="ROTATION", + update=updateRest, + ) + + old_rotation_B: FloatProperty( + name="A axis angle", + description="old value of Rotate A axis\nto specified angle", + default=0, + min=-360, + max=360, + precision=0, + subtype="ANGLE", + unit="ROTATION", + update=updateRest, + ) + + rotation_A: FloatProperty( + name="A axis angle", + description="Rotate A axis\nto specified angle", + default=0, + min=-360, + max=360, + precision=0, + subtype="ANGLE", + unit="ROTATION", + update=updateRotation, + ) + enable_A: BoolProperty( + name="Enable A axis", + description="Rotate A axis", + default=False, + update=updateRotation, + ) + A_along_x: BoolProperty( + name="A Along X ", + description="A Parallel to X", + default=True, + update=updateRest, + ) + + rotation_B: FloatProperty( + name="B axis angle", + description="Rotate B axis\nto specified angle", + default=0, + min=-360, + max=360, + precision=0, + subtype="ANGLE", + unit="ROTATION", + update=updateRotation, + ) + enable_B: BoolProperty( + name="Enable B axis", + description="Rotate B axis", + default=False, + update=updateRotation, + ) + + # carve only + carve_depth: FloatProperty( + name="Carve depth", + default=0.001, + min=-.100, + max=32, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) + + # drill only + drill_type: EnumProperty( + name='Holes on', + items=( + ('MIDDLE_SYMETRIC', 'Middle of symetric curves', 'a'), + ('MIDDLE_ALL', 'Middle of all curve parts', 'a'), + ('ALL_POINTS', 'All points in curve', 'a') + ), + description='Strategy to detect holes to drill', + default='MIDDLE_SYMETRIC', + update=updateRest, + ) + # waterline only + slice_detail: FloatProperty( + name="Distance betwen slices", + default=0.001, + min=0.00001, + max=32, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) + waterline_fill: BoolProperty( + name="Fill areas between slices", + description="Fill areas between slices in waterline mode", + default=True, + update=updateRest, + ) + waterline_project: BoolProperty( + name="Project paths - not recomended", + description="Project paths in areas between slices", + default=True, + update=updateRest, + ) + + # movement and ramps + use_layers: BoolProperty( + name="Use Layers", + description="Use layers for roughing", + default=True, + update=updateRest, + ) + stepdown: FloatProperty( + name="", + description="Layer height", + default=0.01, + min=0.00001, + max=32, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) + lead_in: FloatProperty( + name="Lead in radius", + description="Lead out radius for torch or laser to turn off", + min=0.00, + max=1, + default=0.0, + precision=constants.PRECISION, + unit="LENGTH", + ) + lead_out: FloatProperty( + name="Lead out radius", + description="Lead out radius for torch or laser to turn off", + min=0.00, + max=1, + default=0.0, + precision=constants.PRECISION, + unit="LENGTH", + ) + profile_start: IntProperty( + name="Start point", + description="Start point offset", + min=0, + default=0, + update=updateRest, + ) + + # helix_angle: FloatProperty(name="Helix ramp angle", default=3*pi/180, min=0.00001, max=pi*0.4999,precision=1, subtype="ANGLE" , unit="ROTATION" , update = updateRest) + + minz: FloatProperty( + name="Operation depth end", + default=-0.01, + min=-3, + max=3, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) + + minz_from: EnumProperty( + name='Set max depth from', + description='Set maximum operation depth', + items=( + ('OBJECT', 'Object', 'Set max operation depth from Object'), + ('MATERIAL', 'Material', 'Set max operation depth from Material'), + ('CUSTOM', 'Custom', 'Custom max depth'), + ), + default='OBJECT', + update=updateRest, + ) + + start_type: EnumProperty( + name='Start type', + items=( + ('ZLEVEL', 'Z level', 'Starts on a given Z level'), + ('OPERATIONRESULT', 'Rest milling', + 'For rest milling, operations have to be ' + 'put in chain for this to work well.'), + ), + description='Starting depth', + default='ZLEVEL', + update=updateStrategy, + ) + + maxz: FloatProperty( + name="Operation depth start", + description='operation starting depth', + default=0, + min=-3, + max=10, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) # EXPERIMENTAL + + first_down: BoolProperty( + name="First down", + description="First go down on a contour, then go to the next one", + default=False, + update=update_operation, + ) + + ####################################################### + # Image related + #################################################### + + source_image_scale_z: FloatProperty( + name="Image source depth scale", + default=0.01, + min=-1, + max=1, + precision=constants.PRECISION, + unit="LENGTH", + update=updateZbufferImage, + ) + source_image_size_x: FloatProperty( + name="Image source x size", + default=0.1, + min=-10, + max=10, + precision=constants.PRECISION, + unit="LENGTH", + update=updateZbufferImage, + ) + source_image_offset: FloatVectorProperty( + name='Image offset', + default=(0, 0, 0), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + update=updateZbufferImage, + ) + + source_image_crop: BoolProperty( + name="Crop source image", + description="Crop source image - the position of the sub-rectangle " + "is relative to the whole image, so it can be used for e.g. " + "finishing just a part of an image", + default=False, + update=updateZbufferImage, + ) + source_image_crop_start_x: FloatProperty( + name='crop start x', + default=0, + min=0, + max=100, + precision=constants.PRECISION, + subtype='PERCENTAGE', + update=updateZbufferImage, + ) + source_image_crop_start_y: FloatProperty( + name='crop start y', + default=0, + min=0, + max=100, + precision=constants.PRECISION, + subtype='PERCENTAGE', + update=updateZbufferImage, + ) + source_image_crop_end_x: FloatProperty( + name='crop end x', + default=100, + min=0, + max=100, + precision=constants.PRECISION, + subtype='PERCENTAGE', + update=updateZbufferImage, + ) + source_image_crop_end_y: FloatProperty( + name='crop end y', + default=100, + min=0, + max=100, + precision=constants.PRECISION, + subtype='PERCENTAGE', + update=updateZbufferImage, + ) + + ######################################################### + # Toolpath and area related + ##################################################### + + ambient_behaviour: EnumProperty( + name='Ambient', + items=(('ALL', 'All', 'a'), ('AROUND', 'Around', 'a')), + description='handling ambient surfaces', + default='ALL', + update=updateZbufferImage, + ) + + ambient_radius: FloatProperty( + name="Ambient radius", + description="Radius around the part which will be milled if " + "ambient is set to Around", + min=0.0, + max=100.0, + default=0.01, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) + # ambient_cutter = EnumProperty(name='Borders',items=(('EXTRAFORCUTTER', 'Extra for cutter', "Extra space for cutter is cut around the segment"),('ONBORDER', "Cutter on edge", "Cutter goes exactly on edge of ambient with it's middle") ,('INSIDE', "Inside segment", 'Cutter stays within segment') ),description='handling of ambient and cutter size',default='INSIDE') + use_limit_curve: BoolProperty( + name="Use limit curve", + description="A curve limits the operation area", + default=False, + update=updateRest, + ) + ambient_cutter_restrict: BoolProperty( + name="Cutter stays in ambient limits", + description="Cutter doesn't get out from ambient limits otherwise " + "goes on the border exactly", + default=True, + update=updateRest, + ) # restricts cutter inside ambient only + limit_curve: StringProperty( + name='Limit curve', + description='curve used to limit the area of the operation', + update=updateRest, + ) + + # feeds + feedrate: FloatProperty( + name="Feedrate", + description="Feedrate in units per minute", + min=0.00005, + max=50.0, + default=1.0, + precision=constants.PRECISION, + unit="LENGTH", + update=updateChipload, + ) + plunge_feedrate: FloatProperty( + name="Plunge speed ", + description="% of feedrate", + min=0.1, + max=100.0, + default=50.0, + precision=1, + subtype='PERCENTAGE', + update=updateRest, + ) + plunge_angle: FloatProperty( + name="Plunge angle", + description="What angle is allready considered to plunge", + default=pi / 6, + min=0, + max=pi * 0.5, + precision=0, + subtype="ANGLE", + unit="ROTATION", + update=updateRest, + ) + spindle_rpm: FloatProperty( + name="Spindle rpm", + description="Spindle speed ", + min=0, + max=60000, + default=12000, + update=updateChipload, + ) + + # optimization and performance + + do_simulation_feedrate: BoolProperty( + name="Adjust feedrates with simulation EXPERIMENTAL", + description="Adjust feedrates with simulation", + default=False, + update=updateRest, + ) + + dont_merge: BoolProperty( + name="Dont merge outlines when cutting", + description="this is usefull when you want to cut around everything", + default=False, + update=updateRest, + ) + + pencil_threshold: FloatProperty( + name="Pencil threshold", + default=0.00002, + min=0.00000001, + max=1, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) + + crazy_threshold1: FloatProperty( + name="min engagement", + default=0.02, + min=0.00000001, + max=100, + precision=constants.PRECISION, + update=updateRest, + ) + crazy_threshold5: FloatProperty( + name="optimal engagement", + default=0.3, + min=0.00000001, + max=100, + precision=constants.PRECISION, + update=updateRest, + ) + crazy_threshold2: FloatProperty( + name="max engagement", + default=0.5, + min=0.00000001, + max=100, + precision=constants.PRECISION, + update=updateRest, + ) + crazy_threshold3: FloatProperty( + name="max angle", + default=2, + min=0.00000001, + max=100, + precision=constants.PRECISION, + update=updateRest, + ) + crazy_threshold4: FloatProperty( + name="test angle step", + default=0.05, + min=0.00000001, + max=100, + precision=constants.PRECISION, + update=updateRest, + ) + # Add pocket operation to medial axis + add_pocket_for_medial: BoolProperty( + name="Add pocket operation", + description="clean unremoved material after medial axis", + default=True, + update=updateRest, + ) + + add_mesh_for_medial: BoolProperty( + name="Add Medial mesh", + description="Medial operation returns mesh for editing and " + "further processing", + default=False, + update=updateRest, + ) + #### + medial_axis_threshold: FloatProperty( + name="Long vector threshold", + default=0.001, + min=0.00000001, + max=100, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) + medial_axis_subdivision: FloatProperty( + name="Fine subdivision", + default=0.0002, + min=0.00000001, + max=100, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) + # calculations + + # bridges + use_bridges: BoolProperty( + name="Use bridges", + description="use bridges in cutout", + default=False, + update=updateBridges, + ) + bridges_width: FloatProperty( + name='width of bridges', + default=0.002, + unit='LENGTH', + precision=constants.PRECISION, + update=updateBridges, + ) + bridges_height: FloatProperty( + name='height of bridges', + description="Height from the bottom of the cutting operation", + default=0.0005, + unit='LENGTH', + precision=constants.PRECISION, + update=updateBridges, + ) + bridges_collection_name: StringProperty( + name='Bridges Collection', + description='Collection of curves used as bridges', + update=operationValid, + ) + use_bridge_modifiers: BoolProperty( + name="use bridge modifiers", + description="include bridge curve modifiers using render level when " + "calculating operation, does not effect original bridge data", + default=True, + update=updateBridges, + ) + + # commented this - auto bridges will be generated, but not as a setting of the operation + # bridges_placement = EnumProperty(name='Bridge placement', + # items=( + # ('AUTO','Automatic', 'Automatic bridges with a set distance'), + # ('MANUAL','Manual', 'Manual placement of bridges'), + # ), + # description='Bridge placement', + # default='AUTO', + # update = updateStrategy) + # + # bridges_per_curve = IntProperty(name="minimum bridges per curve", description="", default=4, min=1, max=512, update = updateBridges) + # bridges_max_distance = FloatProperty(name = 'Maximum distance between bridges', default=0.08, unit='LENGTH', precision=constants.PRECISION, update = updateBridges) + + use_modifiers: BoolProperty( + name="use mesh modifiers", + description="include mesh modifiers using render level when " + "calculating operation, does not effect original mesh", + default=True, + update=operationValid, + ) + # optimisation panel + + # material settings + + +############################################################################## + # MATERIAL SETTINGS + + min: FloatVectorProperty( + name='Operation minimum', + default=(0, 0, 0), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + ) + max: FloatVectorProperty( + name='Operation maximum', + default=(0, 0, 0), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + ) + + # g-code options for operation + output_header: BoolProperty( + name="output g-code header", + description="output user defined g-code command header" + " at start of operation", + default=False, + ) + + gcode_header: StringProperty( + name="g-code header", + description="g-code commands at start of operation." + " Use ; for line breaks", + default="G53 G0", + ) + + enable_dust: BoolProperty( + name="Dust collector", + description="output user defined g-code command header" + " at start of operation", + default=False, + ) + + gcode_start_dust_cmd: StringProperty( + name="Start dust collector", + description="commands to start dust collection. Use ; for line breaks", + default="M100", + ) + + gcode_stop_dust_cmd: StringProperty( + name="Stop dust collector", + description="command to stop dust collection. Use ; for line breaks", + default="M101", + ) + + enable_hold: BoolProperty( + name="Hold down", + description="output hold down command at start of operation", + default=False, + ) + + gcode_start_hold_cmd: StringProperty( + name="g-code header", + description="g-code commands at start of operation." + " Use ; for line breaks", + default="M102", + ) + + gcode_stop_hold_cmd: StringProperty( + name="g-code header", + description="g-code commands at end operation. Use ; for line breaks", + default="M103", + ) + + enable_mist: BoolProperty( + name="Mist", + description="Mist command at start of operation", + default=False, + ) + + gcode_start_mist_cmd: StringProperty( + name="g-code header", + description="g-code commands at start of operation." + " Use ; for line breaks", + default="M104", + ) + + gcode_stop_mist_cmd: StringProperty( + name="g-code header", + description="g-code commands at end operation. Use ; for line breaks", + default="M105", + ) + + output_trailer: BoolProperty( + name="output g-code trailer", + description="output user defined g-code command trailer" + " at end of operation", + default=False, + ) + + gcode_trailer: StringProperty( + name="g-code trailer", + description="g-code commands at end of operation." + " Use ; for line breaks", + default="M02", + ) + + # internal properties + ########################################### + + # testing = IntProperty(name="developer testing ", description="This is just for script authors for help in coding, keep 0", default=0, min=0, max=512) + offset_image = numpy.array([], dtype=float) + zbuffer_image = numpy.array([], dtype=float) + + silhouete = sgeometry.Polygon() + ambient = sgeometry.Polygon() + operation_limit = sgeometry.Polygon() + borderwidth = 50 + object = None + path_object_name: StringProperty( + name='Path object', + description='actual cnc path' + ) + + # update and tags and related + + changed: BoolProperty( + name="True if any of the operation settings has changed", + description="mark for update", + default=False, + ) + update_zbufferimage_tag: BoolProperty( + name="mark zbuffer image for update", + description="mark for update", + default=True, + ) + update_offsetimage_tag: BoolProperty( + name="mark offset image for update", + description="mark for update", + default=True, + ) + update_silhouete_tag: BoolProperty( + name="mark silhouete image for update", + description="mark for update", + default=True, + ) + update_ambient_tag: BoolProperty( + name="mark ambient polygon for update", + description="mark for update", + default=True, + ) + update_bullet_collision_tag: BoolProperty( + name="mark bullet collisionworld for update", + description="mark for update", + default=True, + ) + + valid: BoolProperty( + name="Valid", + description="True if operation is ok for calculation", + default=True, + ) + changedata: StringProperty( + name='changedata', + description='change data for checking if stuff changed.', + ) + + # process related data + + computing: BoolProperty( + name="Computing right now", + description="", + default=False, + ) + pid: IntProperty( + name="process id", + description="Background process id", + default=-1, + ) + outtext: StringProperty( + name='outtext', + description='outtext', + default='', + ) diff --git a/scripts/addons/cam/chain.py b/scripts/addons/cam/chain.py new file mode 100644 index 000000000..44b60b213 --- /dev/null +++ b/scripts/addons/cam/chain.py @@ -0,0 +1,52 @@ +from bpy.props import ( + BoolProperty, + CollectionProperty, + IntProperty, + StringProperty, +) +from bpy.types import PropertyGroup + + +# this type is defined just to hold reference to operations for chains +class opReference(PropertyGroup): + name: StringProperty( + name="Operation name", + default="Operation", + ) + computing = False # for UiList display + + +# chain is just a set of operations which get connected on export into 1 file. +class camChain(PropertyGroup): + index: IntProperty( + name="index", + description="index in the hard-defined camChains", + default=-1, + ) + active_operation: IntProperty( + name="active operation", + description="active operation in chain", + default=-1, + ) + name: StringProperty( + name="Chain Name", + default="Chain", + ) + filename: StringProperty( + name="File name", + default="Chain", + ) # filename of + valid: BoolProperty( + name="Valid", + description="True if whole chain is ok for calculation", + default=True, + ) + computing: BoolProperty( + name="Computing right now", + description="", + default=False, + ) + # this is to hold just operation names. + operations: CollectionProperty( + type=opReference, + ) diff --git a/scripts/addons/cam/collision.py b/scripts/addons/cam/collision.py index 48a41986a..8c7200fa9 100644 --- a/scripts/addons/cam/collision.py +++ b/scripts/addons/cam/collision.py @@ -18,19 +18,30 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENCE BLOCK ***** - -import bpy +from math import ( + cos, + pi, + radians, + sin, + tan, +) import time -from . import simple -from .simple import * - - -BULLET_SCALE = 10000 -# this is a constant for scaling the rigidbody collision world for higher precision from bullet library -CUTTER_OFFSET = (-5 * BULLET_SCALE, -5 * BULLET_SCALE, -5 * BULLET_SCALE) -# the cutter object has to be present in the scene , so we need to put it aside for sweep collisions, -# otherwise it collides itself. +import bpy +from mathutils import ( + Euler, + Vector, +) + +from .constants import ( + BULLET_SCALE, + CUTTER_OFFSET, +) +from .simple import ( + activate, + delob, + progress, +) def getCutterBullet(o): @@ -75,12 +86,12 @@ def getCutterBullet(o): elif type == 'VCARVE': angle = o.cutter_tip_angle - s = math.tan(math.pi * (90 - angle / 2) / 180) / 2 # angles in degrees + s = tan(pi * (90 - angle / 2) / 180) / 2 # angles in degrees cone_d = o.cutter_diameter * s bpy.ops.mesh.primitive_cone_add(vertices=32, radius1=o.cutter_diameter / 2, radius2=0, depth=cone_d, end_fill_type='NGON', align='WORLD', enter_editmode=False, location=CUTTER_OFFSET, - rotation=(math.pi, 0, 0)) + rotation=(pi, 0, 0)) cutter = bpy.context.active_object cutter.scale *= BULLET_SCALE bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) @@ -91,13 +102,13 @@ def getCutterBullet(o): elif type == 'CYLCONE': angle = o.cutter_tip_angle - s = math.tan(math.pi * (90 - angle / 2) / 180) / 2 # angles in degrees + s = tan(pi * (90 - angle / 2) / 180) / 2 # angles in degrees cylcone_d = (o.cutter_diameter - o.cylcone_diameter) * s bpy.ops.mesh.primitive_cone_add(vertices=32, radius1=o.cutter_diameter / 2, radius2=o.cylcone_diameter / 2, depth=cylcone_d, end_fill_type='NGON', align='WORLD', enter_editmode=False, - location=CUTTER_OFFSET, rotation=(math.pi, 0, 0)) + location=CUTTER_OFFSET, rotation=(pi, 0, 0)) cutter = bpy.context.active_object cutter.scale *= BULLET_SCALE bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) @@ -107,29 +118,29 @@ def getCutterBullet(o): cutter.rigid_body.collision_shape = 'CONVEX_HULL' cutter.location = CUTTER_OFFSET elif type == 'BALLCONE': - angle = math.radians(o.cutter_tip_angle)/2 + angle = radians(o.cutter_tip_angle)/2 cutter_R = o.cutter_diameter/2 - Ball_R = o.ball_radius/math.cos(angle) - conedepth = (cutter_R - o.ball_radius)/math.tan(angle) + Ball_R = o.ball_radius/cos(angle) + conedepth = (cutter_R - o.ball_radius)/tan(angle) bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Point', use_cyclic_u=False) oy = Ball_R for i in range(1, 10): - ang = -i * (math.pi/2-angle) / 9 - qx = math.sin(ang) * oy - qy = oy - math.cos(ang) * oy + ang = -i * (pi/2-angle) / 9 + qx = sin(ang) * oy + qy = oy - cos(ang) * oy bpy.ops.curve.vertex_add(location=(qx, qy, 0)) conedepth += qy bpy.ops.curve.vertex_add(location=(-cutter_R, conedepth, 0)) #bpy.ops.curve.vertex_add(location=(0 , conedepth , 0)) bpy.ops.object.editmode_toggle() bpy.ops.object.convert(target='MESH') - bpy.ops.transform.rotate(value=-math.pi / 2, orient_axis='X') + bpy.ops.transform.rotate(value=-pi / 2, orient_axis='X') bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) ob = bpy.context.active_object ob.name = "BallConeTool" ob_scr = ob.modifiers.new(type='SCREW', name='scr') - ob_scr.angle = math.radians(-360) + ob_scr.angle = radians(-360) ob_scr.steps = 32 ob_scr.merge_threshold = 0 ob_scr.use_merge_vertices = True diff --git a/scripts/addons/cam/constants.py b/scripts/addons/cam/constants.py index 595cab16f..34d2f7d4b 100644 --- a/scripts/addons/cam/constants.py +++ b/scripts/addons/cam/constants.py @@ -9,3 +9,9 @@ MAX_OPERATION_TIME = 3200000000 # seconds G64_INCOMPATIBLE_MACHINES = ['GRBL'] + +BULLET_SCALE = 10000 +# this is a constant for scaling the rigidbody collision world for higher precision from bullet library +CUTTER_OFFSET = (-5 * BULLET_SCALE, -5 * BULLET_SCALE, -5 * BULLET_SCALE) +# the cutter object has to be present in the scene , so we need to put it aside for sweep collisions, +# otherwise it collides itself. diff --git a/scripts/addons/cam/curvecamcreate.py b/scripts/addons/cam/curvecamcreate.py index 1c2f82645..0387caadc 100644 --- a/scripts/addons/cam/curvecamcreate.py +++ b/scripts/addons/cam/curvecamcreate.py @@ -18,7 +18,17 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENCE BLOCK ***** +from math import ( + degrees, + hypot, + pi, + radians +) +from shapely.geometry import ( + LineString, + MultiLineString, +) import bpy from bpy.props import ( @@ -28,35 +38,17 @@ IntProperty, ) from bpy.types import Operator -from bpy_extras.io_utils import ImportHelper + from . import ( - utils, - pack, - polygon_utils_cam, - simple, - gcodepath, - bridges, - parametric, + involute_gear, joinery, - curvecamtools, puzzle_joinery, - involute_gear -) -import shapely -from shapely.geometry import ( - Point, - LineString, - Polygon, - MultiLineString, - MultiPoint + simple, + utils, ) -import mathutils -import math -from Equation import Expression -import numpy as np -class CamCurveHatch(bpy.types.Operator): +class CamCurveHatch(Operator): """perform hatch operation on single or multiple curves""" # by Alain Pelletier September 2021 bl_idname = "object.curve_hatch" bl_label = "CrossHatch curve" @@ -64,9 +56,9 @@ class CamCurveHatch(bpy.types.Operator): angle: FloatProperty( name="angle", - default=0, min=- - math.pi/2, - max=math.pi/2, + default=0, + min=-pi/2, + max=pi/2, precision=4, subtype="ANGLE", ) @@ -167,7 +159,7 @@ def execute(self, context): height = maxy - miny width = maxx - minx centerx = (minx+maxx) / 2 - diagonal = math.hypot(width, height) + diagonal = hypot(width, height) simple.add_bound_rectangle( minx, miny, maxx, maxy, 'crosshatch_bound') amount = int(2*diagonal/self.distance) + 1 @@ -224,7 +216,7 @@ def execute(self, context): return {'FINISHED'} -class CamCurvePlate(bpy.types.Operator): +class CamCurvePlate(Operator): """perform generates rounded plate with mounting holes""" # by Alain Pelletier Sept 2021 bl_idname = "object.curve_plate" bl_label = "Sign plate" @@ -499,7 +491,7 @@ def execute(self, context): return {'FINISHED'} -class CamCurveFlatCone(bpy.types.Operator): +class CamCurveFlatCone(Operator): """perform generates rounded plate with mounting holes""" # by Alain Pelletier Sept 2021 bl_idname = "object.curve_flat_cone" bl_label = "Cone flat calculator" @@ -563,13 +555,13 @@ def execute(self, context): z = self.large_d / 2 x = self.height h = x * y / (z - y) - a = math.hypot(h, y) - ab = math.hypot(x+h, z) + a = hypot(h, y) + ab = hypot(x+h, z) b = ab - a - angle = math.pi * 2 * y / a + angle = pi * 2 * y / a # create base - bpy.ops.curve.simple(Simple_Type='Segment', Simple_a=ab, Simple_b=a, Simple_endangle=math.degrees(angle), + bpy.ops.curve.simple(Simple_Type='Segment', Simple_a=ab, Simple_b=a, Simple_endangle=degrees(angle), use_cyclic_u=True, edit_mode=False) simple.active_name("_segment") @@ -596,7 +588,7 @@ def execute(self, context): return {'FINISHED'} -class CamCurveMortise(bpy.types.Operator): +class CamCurveMortise(Operator): """Generates mortise along a curve""" # by Alain Pelletier December 2021 bl_idname = "object.curve_mortise" bl_label = "Mortise" @@ -739,7 +731,7 @@ def execute(self, context): return {'FINISHED'} -class CamCurveInterlock(bpy.types.Operator): +class CamCurveInterlock(Operator): """Generates interlock along a curve""" # by Alain Pelletier December 2021 bl_idname = "object.curve_interlock" bl_label = "Interlock" @@ -859,7 +851,7 @@ def execute(self, context): return {'FINISHED'} -class CamCurveDrawer(bpy.types.Operator): +class CamCurveDrawer(Operator): """Generates drawers""" # by Alain Pelletier December 2021 inspired by The Drawinator bl_idname = "object.curve_drawer" bl_label = "Drawer" @@ -1045,7 +1037,7 @@ def execute(self, context): simple.active_name('_bot_fingers') simple.difference('_bot', '_bottom') - simple.rotate(math.pi/2) + simple.rotate(pi/2) joinery.finger_pair("_wfb0", 0, self.width - self.drawer_plate_thickness - self.finger_inset * 2) @@ -1073,7 +1065,7 @@ def execute(self, context): return {'FINISHED'} -class CamCurvePuzzle(bpy.types.Operator): +class CamCurvePuzzle(Operator): """Generates Puzzle joints and interlocks""" # by Alain Pelletier December 2021 bl_idname = "object.curve_puzzle" bl_label = "Puzzle joints" @@ -1126,7 +1118,7 @@ class CamCurvePuzzle(bpy.types.Operator): angle: FloatProperty( name="angle A", - default=math.pi/4, + default=pi/4, min=-10, max=10, subtype="ANGLE", @@ -1134,7 +1126,7 @@ class CamCurvePuzzle(bpy.types.Operator): ) angleb: FloatProperty( name="angle B", - default=math.pi/4, + default=pi/4, min=-10, max=10, subtype="ANGLE", @@ -1420,7 +1412,7 @@ def execute(self, context): which=self.gender) elif self.interlock_type == 'MULTIANGLE': - puzzle_joinery.multiangle(self.radius, self.height, math.pi/3, self.diameter, self.finger_tolerance, + puzzle_joinery.multiangle(self.radius, self.height, pi/3, self.diameter, self.finger_tolerance, self.finger_amount, stem=self.stem_size, twist=self.twist_lock, tneck=self.twist_percent, tthick=self.twist_thick, @@ -1466,7 +1458,7 @@ def execute(self, context): return {'FINISHED'} -class CamCurveGear(bpy.types.Operator): +class CamCurveGear(Operator): """Generates involute Gears // version 1.1 by Leemon Baird, 2011, Leemon@Leemon.com http://www.thingiverse.com/thing:5505""" # ported by Alain Pelletier January 2022 @@ -1520,9 +1512,9 @@ class CamCurveGear(bpy.types.Operator): ) pressure_angle: FloatProperty( name="Pressure Angle", - default=math.radians(20), + default=radians(20), min=0.001, - max=math.pi/2, + max=pi/2, precision=4, subtype="ANGLE", unit="ROTATION", diff --git a/scripts/addons/cam/curvecamequation.py b/scripts/addons/cam/curvecamequation.py index 1819ffbb9..bba928244 100644 --- a/scripts/addons/cam/curvecamequation.py +++ b/scripts/addons/cam/curvecamequation.py @@ -18,6 +18,10 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENCE BLOCK ***** +from math import pi + +from Equation import Expression +import numpy as np import bpy from bpy.props import ( @@ -27,10 +31,7 @@ StringProperty, ) -from . import utils, parametric -import math -from Equation import Expression -import numpy as np +from . import parametric class CamSineCurve(bpy.types.Operator): @@ -133,9 +134,9 @@ class CamSineCurve(bpy.types.Operator): ) wave_angle_offset: FloatProperty( name="angle offset for multiple waves", - default=math.pi/2, - min=-200*math.pi, - max=200*math.pi, + default=pi/2, + min=-200*pi, + max=200*pi, precision=4, unit="ROTATION", ) @@ -184,7 +185,7 @@ def f(t, offset: float = 0.0, angle_offset: float = 0.0): return c for i in range(self.wave_amount): - angle_off = self.wave_angle_offset*self.period*i/(2*math.pi) + angle_off = self.wave_angle_offset*self.period*i/(2*pi) parametric.create_parametric_curve(f, offset=self.wave_distance*i, min=self.mint, max=self.maxt, use_cubic=True, iterations=self.iteration, angle_offset=angle_off) @@ -381,7 +382,7 @@ def execute(self, context): Rpr = round(R + r, 6) # R +r Rpror = round(Rpr / r, 6) # (R+r)/r Rmror = round(Rmr / r, 6) # (R-r)/r - maxangle = 2 * math.pi * \ + maxangle = 2 * pi * \ ((np.lcm(round(self.R * 1000), round(self.r * 1000)) / (R * 1000))) if self.typecurve == "hypo": @@ -487,12 +488,12 @@ def f(t, offset: float = 0.0): def triangle(i, T, A): - s = str(A*8/(math.pi**2))+'*(' + s = str(A*8/(pi**2))+'*(' for n in range(i): if n % 2 != 0: e = (n-1)/2 a = round(((-1)**e)/(n**2), 8) - b = round(n*math.pi/(T/2), 8) + b = round(n*pi/(T/2), 8) if n > 1: s += '+' s += str(a) + "*sin("+str(b)+"*t) " diff --git a/scripts/addons/cam/curvecamtools.py b/scripts/addons/cam/curvecamtools.py index f18fef6e3..96c7ec669 100644 --- a/scripts/addons/cam/curvecamtools.py +++ b/scripts/addons/cam/curvecamtools.py @@ -20,7 +20,13 @@ # ***** END GPL LICENCE BLOCK ***** # blender operators definitions are in this file. They mostly call the functions from utils.py +from math import ( + pi, + tan +) +import shapely +from shapely.geometry import LineString import bpy from bpy.props import ( @@ -29,29 +35,17 @@ FloatProperty, ) from bpy.types import Operator -from bpy_extras.io_utils import ImportHelper +from mathutils import Vector from . import ( - utils, - pack, polygon_utils_cam, simple, - gcodepath, - bridges, - parametric, - gcodeimportparser, - joinery + utils, ) -import shapely -from shapely.geometry import Point, LineString, Polygon -import mathutils -import math -from Equation import Expression -import numpy as np # boolean operations for curve objects -class CamCurveBoolean(bpy.types.Operator): +class CamCurveBoolean(Operator): """perform Boolean operation on two or more curves""" bl_idname = "object.curve_boolean" bl_label = "Curve Boolean" @@ -81,7 +75,7 @@ def execute(self, context): return {'CANCELLED'} -class CamCurveConvexHull(bpy.types.Operator): +class CamCurveConvexHull(Operator): """perform hull operation on single or multiple curves""" # by Alain Pelletier april 2021 bl_idname = "object.convex_hull" bl_label = "Convex Hull" @@ -97,7 +91,7 @@ def execute(self, context): # intarsion or joints -class CamCurveIntarsion(bpy.types.Operator): +class CamCurveIntarsion(Operator): """makes curve cuttable both inside and outside, for intarsion and joints""" bl_idname = "object.curve_intarsion" bl_label = "Intarsion" @@ -219,7 +213,7 @@ def execute(self, context): # intarsion or joints -class CamCurveOvercuts(bpy.types.Operator): +class CamCurveOvercuts(Operator): """Adds overcuts for slots""" bl_idname = "object.curve_overcuts" bl_label = "Add Overcuts" @@ -235,7 +229,7 @@ class CamCurveOvercuts(bpy.types.Operator): ) threshold: FloatProperty( name="threshold", - default=math.pi / 2 * .99, + default=pi / 2 * .99, min=-3.14, max=3.14, precision=4, @@ -280,11 +274,11 @@ def execute(self, context): if i2 == len(c.coords): i2 = 0 - v1 = mathutils.Vector( - co) - mathutils.Vector(c.coords[i1]) + v1 = Vector( + co) - Vector(c.coords[i1]) v1 = v1.xy # Vector((v1.x,v1.y,0)) - v2 = mathutils.Vector( - c.coords[i2]) - mathutils.Vector(co) + v2 = Vector( + c.coords[i2]) - Vector(co) v2 = v2.xy # v2 = Vector((v2.x,v2.y,0)) if not v1.length == 0 and not v2.length == 0: a = v1.angle_signed(v2) @@ -293,18 +287,18 @@ def execute(self, context): if self.invert: # and ci>0: sign *= -1 if (sign < 0 and a < -self.threshold) or (sign > 0 and a > self.threshold): - p = mathutils.Vector((co[0], co[1])) + p = Vector((co[0], co[1])) v1.normalize() v2.normalize() v = v1 - v2 v.normalize() p = p - v * diameter / 2 - if abs(a) < math.pi / 2: + if abs(a) < pi / 2: shape = utils.Circle(diameter / 2, 64) shape = shapely.affinity.translate( shape, p.x, p.y) else: - l = math.tan(a / 2) * diameter / 2 + l = tan(a / 2) * diameter / 2 p1 = p - sign * v * l l = shapely.geometry.LineString((p, p1)) shape = l.buffer( @@ -327,7 +321,7 @@ def execute(self, context): # Overcut type B -class CamCurveOvercutsB(bpy.types.Operator): +class CamCurveOvercutsB(Operator): """Adds overcuts for slots""" bl_idname = "object.curve_overcuts_b" bl_label = "Add Overcuts-B" @@ -356,7 +350,7 @@ class CamCurveOvercutsB(bpy.types.Operator): ) threshold: FloatProperty( name="Max Inside Angle", - default=math.pi / 2, + default=pi / 2, min=-3.14, max=3.14, description='The maximum angle to be considered as an inside corner', @@ -397,10 +391,10 @@ def execute(self, context): insideCorners = [] diameter = self.diameter * 1.002 # make bit size slightly larger to allow cutter radius = diameter / 2 - anglethreshold = math.pi - self.threshold - centerv = mathutils.Vector((0, 0)) - extendedv = mathutils.Vector((0, 0)) - pos = mathutils.Vector((0, 0)) + anglethreshold = pi - self.threshold + centerv = Vector((0, 0)) + extendedv = Vector((0, 0)) + pos = Vector((0, 0)) sign = -1 if self.do_invert else 1 isTBone = self.style == 'TBONE' # indexes in insideCorner tuple @@ -411,7 +405,7 @@ def addOvercut(a): # move the overcut shape center position 1 radius in direction v pos -= centerv * radius print("abs(a)", abs(a)) - if abs(a) <= math.pi / 2 + 0.0001: + if abs(a) <= pi / 2 + 0.0001: print("<=pi/2") shape = utils.Circle(radius, 64) shape = shapely.affinity.translate(shape, pos.x, pos.y) @@ -440,7 +434,7 @@ def setCenterOffset(a): nonlocal centerv, extendedv, sign centerv = v1 - v2 centerv.normalize() - extendedv = centerv * math.tan(a / 2) * -sign + extendedv = centerv * tan(a / 2) * -sign addOvercut(a) def getCorner(idx, offset): @@ -482,10 +476,10 @@ def getCornerDelta(curidx, nextidx): if i2 == len(c.coords): i2 = 0 - v1 = mathutils.Vector( - co).xy - mathutils.Vector(c.coords[i1]).xy - v2 = mathutils.Vector( - c.coords[i2]).xy - mathutils.Vector(co).xy + v1 = Vector( + co).xy - Vector(c.coords[i1]).xy + v2 = Vector( + c.coords[i2]).xy - Vector(co).xy if not v1.length == 0 and not v2.length == 0: a = v1.angle_signed(v2) @@ -505,7 +499,7 @@ def getCornerDelta(curidx, nextidx): if insideCornerFound: # an inside corner with an overcut has been found # which means a new side has been found - pos = mathutils.Vector((co[0], co[1])) + pos = Vector((co[0], co[1])) v1.normalize() v2.normalize() # figure out which direction vector to use @@ -567,7 +561,7 @@ def getCornerDelta(curidx, nextidx): if getCornerDelta(prevCorner[IDX], idx) == 3: # check if they share the same edge a1 = v1.angle_signed( - prevCorner[V2]) * 180.0 / math.pi + prevCorner[V2]) * 180.0 / pi print('third won', a1) if a1 < -135 or a1 > 135: setOtherEdge(-v2, -v1, a) @@ -577,7 +571,7 @@ def getCornerDelta(curidx, nextidx): if getCornerDelta(idx, nextCorner[IDX]) == 3: # check if they share the same edge a1 = v2.angle_signed( - nextCorner[V1]) * 180.0 / math.pi + nextCorner[V1]) * 180.0 / pi print('fourth won', a1) if a1 < -135 or a1 > 135: setOtherEdge(v1, v2, a) @@ -597,7 +591,7 @@ def getCornerDelta(curidx, nextidx): return {'FINISHED'} -class CamCurveRemoveDoubles(bpy.types.Operator): +class CamCurveRemoveDoubles(Operator): """curve remove doubles - warning, removes beziers!""" bl_idname = "object.curve_remove_doubles" bl_label = "C-Remove doubles" @@ -629,7 +623,7 @@ def execute(self, context): return {'FINISHED'} -class CamMeshGetPockets(bpy.types.Operator): +class CamMeshGetPockets(Operator): """Detect pockets in a mesh and extract them as curves""" bl_idname = "object.mesh_get_pockets" bl_label = "Get pocket surfaces" @@ -740,7 +734,7 @@ def execute(self, context): # this operator finds the silhouette of objects(meshes, curves just get converted) and offsets it. -class CamOffsetSilhouete(bpy.types.Operator): +class CamOffsetSilhouete(Operator): """Curve offset operation """ bl_idname = "object.silhouete_offset" bl_label = "Silhouete offset" @@ -815,7 +809,7 @@ def execute(self, context): # Finds object silhouette, usefull for meshes, since with curves it's not needed. -class CamObjectSilhouete(bpy.types.Operator): +class CamObjectSilhouete(Operator): """Object silhouete """ bl_idname = "object.silhouete" bl_label = "Object silhouete" diff --git a/scripts/addons/cam/engine.py b/scripts/addons/cam/engine.py new file mode 100644 index 000000000..8e426006b --- /dev/null +++ b/scripts/addons/cam/engine.py @@ -0,0 +1,74 @@ +from bl_ui.properties_material import ( + EEVEE_MATERIAL_PT_context_material, + EEVEE_MATERIAL_PT_settings, + EEVEE_MATERIAL_PT_surface, +) +import bpy +from bpy.types import RenderEngine + +from .ui_panels.area import CAM_AREA_Panel +from .ui_panels.chains import CAM_CHAINS_Panel +from .ui_panels.cutter import CAM_CUTTER_Panel +from .ui_panels.feedrate import CAM_FEEDRATE_Panel +from .ui_panels.gcode import CAM_GCODE_Panel +from .ui_panels.info import CAM_INFO_Panel +from .ui_panels.interface import CAM_INTERFACE_Panel +from .ui_panels.machine import CAM_MACHINE_Panel +from .ui_panels.material import CAM_MATERIAL_Panel +from .ui_panels.movement import CAM_MOVEMENT_Panel +from .ui_panels.op_properties import CAM_OPERATION_PROPERTIES_Panel +from .ui_panels.operations import CAM_OPERATIONS_Panel +from .ui_panels.optimisation import CAM_OPTIMISATION_Panel +from .ui_panels.pack import CAM_PACK_Panel +from .ui_panels.slice import CAM_SLICE_Panel + + +class BLENDERCAM_ENGINE(RenderEngine): + bl_idname = "BLENDERCAM_RENDER" + bl_label = "Cam" + bl_use_eevee_viewport = True + + +def get_panels(): + exclude_panels = { + 'RENDER_PT_eevee_performance', + 'RENDER_PT_opengl_sampling', + 'RENDER_PT_opengl_lighting', + 'RENDER_PT_opengl_color', + 'RENDER_PT_opengl_options', + 'RENDER_PT_simplify', + 'RENDER_PT_gpencil', + 'RENDER_PT_freestyle', + 'RENDER_PT_color_management', + 'MATERIAL_PT_viewport', + 'MATERIAL_PT_lineart', + } + + panels = [ + EEVEE_MATERIAL_PT_context_material, + EEVEE_MATERIAL_PT_surface, + EEVEE_MATERIAL_PT_settings, + + CAM_INTERFACE_Panel, + CAM_CHAINS_Panel, + CAM_OPERATIONS_Panel, + CAM_INFO_Panel, + CAM_MATERIAL_Panel, + CAM_OPERATION_PROPERTIES_Panel, + CAM_OPTIMISATION_Panel, + CAM_AREA_Panel, + CAM_MOVEMENT_Panel, + CAM_FEEDRATE_Panel, + CAM_CUTTER_Panel, + CAM_GCODE_Panel, + CAM_MACHINE_Panel, + CAM_PACK_Panel, + CAM_SLICE_Panel, + ] + + for panel in bpy.types.Panel.__subclasses__(): + if hasattr(panel, 'COMPAT_ENGINES') and 'BLENDER_RENDER' in panel.COMPAT_ENGINES: + if panel.__name__ not in exclude_panels: + panels.append(panel) + + return panels diff --git a/scripts/addons/cam/gcodeimportparser.py b/scripts/addons/cam/gcodeimportparser.py index 81dd9beca..ee09d06ac 100644 --- a/scripts/addons/cam/gcodeimportparser.py +++ b/scripts/addons/cam/gcodeimportparser.py @@ -17,14 +17,12 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENCE BLOCK ***** - -import bpy -import bmesh import math -import re import numpy as np +import bpy + np.set_printoptions(suppress=True) # suppress scientific notation in subdivide functions linspace diff --git a/scripts/addons/cam/gcodepath.py b/scripts/addons/cam/gcodepath.py index 5630c63d3..19b0f0c9d 100644 --- a/scripts/addons/cam/gcodepath.py +++ b/scripts/addons/cam/gcodepath.py @@ -20,44 +20,61 @@ # ***** END GPL LICENCE BLOCK ***** # here is the Gcode generaton - -import bpy +from math import ( + ceil, + floor, + pi, + sqrt +) import time -import mathutils -import math -from math import * -from mathutils import * import numpy +from shapely.geometry import polygon as spolygon -from . import chunk -from .chunk import * -from .utils import USE_PROFILER - -from . import collision -from .collision import * - -from . import simple -from .simple import * - -from .async_op import progress_async - -from . import bridges -from .bridges import * +import bpy +from mathutils import Euler, Vector -from . import utils from . import strategy - -from . import pattern -from .pattern import * - -from . import polygon_utils_cam -from .polygon_utils_cam import * - -from . import image_utils -from .image_utils import * -from .opencamlib.opencamlib import * +from .async_op import progress_async +from .bridges import useBridges +from .cam_chunk import ( + curveToChunks, + chunksRefine, + limitChunks, + chunksCoherency, + shapelyToChunks, + parentChildDist, +) +from .image_utils import ( + crazyStrokeImageBinary, + getOffsetImageCavities, + imageToShapely, + prepareArea, +) from .nc import iso +from .opencamlib.opencamlib import oclGetWaterline +from .pattern import ( + getPathPattern, + getPathPattern4axis +) +from .simple import ( + progress, + safeFileName, + strInUnits +) +from .utils import ( + cleanupIndexed, + connectChunksLow, + getAmbient, + getBounds, + getOperationSilhouete, + getOperationSources, + prepareIndexed, + sampleChunks, + sampleChunksNAxis, + sortChunks, + USE_PROFILER, +) def pointonline(a, b, c, tolerence): @@ -278,12 +295,12 @@ def startNewFile(): if o.enable_A: if o.rotation_A == 0: o.rotation_A = 0.0001 - c.rapid(a=o.rotation_A * 180 / math.pi) + c.rapid(a=o.rotation_A * 180 / pi) if o.enable_B: if o.rotation_B == 0: o.rotation_B = 0.0001 - c.rapid(a=o.rotation_B * 180 / math.pi) + c.rapid(a=o.rotation_B * 180 / pi) c.write('\n') c.flush_nc() @@ -538,7 +555,7 @@ async def getPath(context, operation): # should do all path calculations. operation.update_ambient_tag = True operation.update_bullet_collision_tag = True - utils.getOperationSources(operation) + getOperationSources(operation) operation.info.warnings = '' checkMemoryLimit(operation) @@ -600,7 +617,7 @@ def getChangeData(o): def checkMemoryLimit(o): - # utils.getBounds(o) + # getBounds(o) sx = o.max.x - o.min.x sy = o.max.y - o.min.y resx = sx / o.optimisation.pixsize @@ -610,7 +627,7 @@ def checkMemoryLimit(o): # print('co se to deje') if res > limit: ratio = (res / limit) - o.optimisation.pixsize = o.optimisation.pixsize * math.sqrt(ratio) + o.optimisation.pixsize = o.optimisation.pixsize * sqrt(ratio) o.info.warnings += f"Memory limit: sampling resolution reduced to {o.optimisation.pixsize:.2e}\n" print('changing sampling resolution to %f' % o.optimisation.pixsize) @@ -620,7 +637,7 @@ def checkMemoryLimit(o): async def getPath3axis(context, operation): s = bpy.context.scene o = operation - utils.getBounds(o) + getBounds(o) tw = time.time() if o.strategy == 'CUTOUT': @@ -642,15 +659,15 @@ async def getPath3axis(context, operation): ob = bpy.data.objects[o.curve_object] pathSamples.extend(curveToChunks(ob)) # sort before sampling - pathSamples = await utils.sortChunks(pathSamples, o) + pathSamples = await sortChunks(pathSamples, o) pathSamples = chunksRefine(pathSamples, o) elif o.strategy == 'PENCIL': await prepareArea(o) - utils.getAmbient(o) + getAmbient(o) pathSamples = getOffsetImageCavities(o, o.offset_image) pathSamples = limitChunks(pathSamples, o) # sort before sampling - pathSamples = await utils.sortChunks(pathSamples, o) + pathSamples = await sortChunks(pathSamples, o) elif o.strategy == 'CRAZY': await prepareArea(o) # pathSamples = crazyStrokeImage(o) @@ -660,21 +677,21 @@ async def getPath3axis(context, operation): pathSamples = crazyStrokeImageBinary(o, millarea, avoidarea) ##### - pathSamples = await utils.sortChunks(pathSamples, o) + pathSamples = await sortChunks(pathSamples, o) pathSamples = chunksRefine(pathSamples, o) else: if o.strategy == 'OUTLINEFILL': - utils.getOperationSilhouete(o) + getOperationSilhouete(o) pathSamples = getPathPattern(o) if o.strategy == 'OUTLINEFILL': - pathSamples = await utils.sortChunks(pathSamples, o) + pathSamples = await sortChunks(pathSamples, o) # have to be sorted once before, because of the parenting inside of samplechunks if o.strategy in ['BLOCK', 'SPIRAL', 'CIRCLES']: - pathSamples = await utils.connectChunksLow(pathSamples, o) + pathSamples = await connectChunksLow(pathSamples, o) # print (minz) @@ -682,7 +699,7 @@ async def getPath3axis(context, operation): layers = strategy.getLayers(o, o.maxz, o.min.z) print("SAMPLE", o.name) - chunks.extend(await utils.sampleChunks(o, pathSamples, layers)) + chunks.extend(await sampleChunks(o, pathSamples, layers)) print("SAMPLE OK") if o.strategy == 'PENCIL': # and bpy.app.debug_value==-3: chunks = chunksCoherency(chunks) @@ -691,9 +708,9 @@ async def getPath3axis(context, operation): # and not o.movement.parallel_step_back: if o.strategy in ['PARALLEL', 'CROSS', 'PENCIL', 'OUTLINEFILL']: print('sorting') - chunks = await utils.sortChunks(chunks, o) + chunks = await sortChunks(chunks, o) if o.strategy == 'OUTLINEFILL': - chunks = await utils.connectChunksLow(chunks, o) + chunks = await connectChunksLow(chunks, o) if o.movement.ramp: for ch in chunks: ch.rampZigZag(ch.zstart, None, o) @@ -711,7 +728,7 @@ async def getPath3axis(context, operation): strategy.chunksToMesh(chunks, o) elif o.strategy == 'WATERLINE' and o.optimisation.use_opencamlib: - utils.getAmbient(o) + getAmbient(o) chunks = [] await oclGetWaterline(o, chunks) chunks = limitChunks(chunks, o) @@ -729,7 +746,7 @@ async def getPath3axis(context, operation): await prepareArea(o) layerstep = 1000000000 if o.use_layers: - layerstep = math.floor(o.stepdown / o.slice_detail) + layerstep = floor(o.stepdown / o.slice_detail) if layerstep == 0: layerstep = 1 @@ -743,7 +760,7 @@ async def getPath3axis(context, operation): layerstepinc = 0 slicesfilled = 0 - utils.getAmbient(o) + getAmbient(o) for h in range(0, nslices): layerstepinc += 1 @@ -798,7 +815,7 @@ async def getPath3axis(context, operation): # project paths TODO: path projection during waterline is not working if o.waterline_project: nchunks = chunksRefine(nchunks, o) - nchunks = await utils.sampleChunks(o, nchunks, layers) + nchunks = await sampleChunks(o, nchunks, layers) nchunks = limitChunks(nchunks, o, force=True) ######################### @@ -850,7 +867,7 @@ async def getPath3axis(context, operation): o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CW'): for chunk in slicechunks: chunk.reverse() - slicechunks = await utils.sortChunks(slicechunks, o) + slicechunks = await sortChunks(slicechunks, o) if topdown: slicechunks.reverse() # project chunks in between @@ -870,7 +887,7 @@ async def getPath3axis(context, operation): async def getPath4axis(context, operation): o = operation - utils.getBounds(o) + getBounds(o) if o.strategy4axis in ['PARALLELR', 'PARALLEL', 'HELIX', 'CROSS']: path_samples = getPathPattern4axis(o) @@ -879,5 +896,5 @@ async def getPath4axis(context, operation): layers = strategy.getLayers(o, 0, depth) - chunks.extend(await utils.sampleChunksNAxis(o, path_samples, layers)) + chunks.extend(await sampleChunksNAxis(o, path_samples, layers)) strategy.chunksToMesh(chunks, o) diff --git a/scripts/addons/cam/image_utils.py b/scripts/addons/cam/image_utils.py index 34a83b2bf..074f1f5d0 100644 --- a/scripts/addons/cam/image_utils.py +++ b/scripts/addons/cam/image_utils.py @@ -19,31 +19,65 @@ # # ***** END GPL LICENCE BLOCK ***** -import math -import numpy +from math import ( + acos, + ceil, + cos, + floor, + pi, + radians, + sin, + tan, +) import os import random import time +import numpy + +import bpy import curve_simplify -import mathutils -from mathutils import * - -from . import simple -from .simple import * -from . import chunk -from .chunk import * -from . import simulation +from mathutils import ( + Euler, + Vector, +) + +from .simple import ( + progress, + getCachePath, +) +from .cam_chunk import ( + parentChildDist, + camPathChunkBuilder, + camPathChunk, + chunksToShapely, +) from .async_op import progress_async +from .numba_wrapper import ( + jit, + prange, +) -from .numba_wrapper import jit, prange + +def numpysave(a, iname): + inamebase = bpy.path.basename(iname) + + i = numpytoimage(a, inamebase) + + r = bpy.context.scene.render + + r.image_settings.file_format = 'OPEN_EXR' + r.image_settings.color_mode = 'BW' + r.image_settings.color_depth = '32' + + i.save_render(iname) def getCircle(r, z): car = numpy.full(shape=(r*2, r*2), fill_value=-10, dtype=numpy.double) res = 2 * r m = r - v = mathutils.Vector((0, 0, 0)) + v = Vector((0, 0, 0)) for a in range(0, res): v.x = (a + 0.5 - m) for b in range(0, res): @@ -57,7 +91,7 @@ def getCircleBinary(r): car = numpy.full(shape=(r*2, r*2), fill_value=False, dtype=bool) res = 2 * r m = r - v = mathutils.Vector((0, 0, 0)) + v = Vector((0, 0, 0)) for a in range(0, res): v.x = (a + 0.5 - m) for b in range(0, res): @@ -70,20 +104,6 @@ def getCircleBinary(r): # get cutters for the z-buffer image method -def numpysave(a, iname): - inamebase = bpy.path.basename(iname) - - i = numpytoimage(a, inamebase) - - r = bpy.context.scene.render - - r.image_settings.file_format = 'OPEN_EXR' - r.image_settings.color_mode = 'BW' - r.image_settings.color_depth = '32' - - i.save_render(iname) - - def numpytoimage(a, iname): print('numpy to image', iname) t = time.time() @@ -145,7 +165,7 @@ async def offsetArea(o, samples): minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z sourceArray = samples - cutterArray = simulation.getCutterArray(o, o.optimisation.pixsize) + cutterArray = getCutterArray(o, o.optimisation.pixsize) # progress('image size', sourceArray.shape) @@ -162,8 +182,8 @@ async def offsetArea(o, samples): sourceArray = -sourceArray + minz comparearea = o.offset_image[m: width - cwidth + m, m:height - cwidth + m] # i=0 - cutterArrayNan = np.where(cutterArray > -10, cutterArray, - np.full(cutterArray.shape, np.nan)) + cutterArrayNan = numpy.where(cutterArray > -10, cutterArray, + numpy.full(cutterArray.shape, numpy.nan)) for y in range(0, 10): y1 = (y * comparearea.shape[1])//10 y2 = ((y+1) * comparearea.shape[1])//10 @@ -187,7 +207,7 @@ def dilateAr(ar, cycles): def getOffsetImageCavities(o, i): # for pencil operation mainly """detects areas in the offset image which are 'cavities' - the curvature changes.""" # i=numpy.logical_xor(lastislice , islice) - simple.progress('detect corners in the offset image') + progress('detect corners in the offset image') vertical = i[:-2, 1:-1] - i[1:-1, 1:-1] - o.pencil_threshold > i[1:-1, 1:-1] - i[2:, 1:-1] horizontal = i[1:-1, :-2] - i[1:-1, 1:-1] - o.pencil_threshold > i[1:-1, 1:-1] - i[1:-1, 2:] # if bpy.app.debug_value==2: @@ -196,19 +216,19 @@ def getOffsetImageCavities(o, i): # for pencil operation mainly if 1: # this is newer strategy, finds edges nicely, but pff.going exacty on edge, # it has tons of spikes and simply is not better than the old one - iname = simple.getCachePath(o) + '_pencilthres.exr' + iname = getCachePath(o) + '_pencilthres.exr' # numpysave(ar,iname)#save for comparison before chunks = imageEdgeSearch_online(o, ar, i) - iname = simple.getCachePath(o) + '_pencilthres_comp.exr' + iname = getCachePath(o) + '_pencilthres_comp.exr' print("new pencil strategy") # ##crop pixels that are on outer borders for chi in range(len(chunks) - 1, -1, -1): chunk = chunks[chi] chunk.clip_points(o.min.x, o.max.x, o.min.y, o.max.y) - # for si in range(len(chunk.points) - 1, -1, -1): - # if not (o.min.x < chunk.points[si][0] < o.max.x and o.min.y < chunk.points[si][1] < o.max.y): - # chunk.points.pop(si) + # for si in range(len(points) - 1, -1, -1): + # if not (o.min.x < points[si][0] < o.max.x and o.min.y < points[si][1] < o.max.y): + # points.pop(si) if chunk.count() < 2: chunks.pop(chi) @@ -247,7 +267,7 @@ def imageEdgeSearch_online(o, ar, zimage): if perc != int(100 - 100 * totpix / startpix): perc = int(100 - 100 * totpix / startpix) - simple.progress('pencil path searching', perc) + progress('pencil path searching', perc) # progress('simulation ',int(100*i/l)) success = False testangulardistance = 0 # distance from initial direction in the list of direction @@ -272,7 +292,7 @@ def imageEdgeSearch_online(o, ar, zimage): print(testvect) print(itests) else: - # nchunk.append([xs,ys])#for debugging purpose + # nappend([xs,ys])#for debugging purpose # ar.shape[0] test_direction = last_direction if testleftright: @@ -353,7 +373,7 @@ async def crazyPath(o): o.millimage = numpy.full(shape=(resx, resy), fill_value=0., dtype=numpy.float) # getting inverted cutter - o.cutterArray = -simulation.getCutterArray(o, o.optimisation.simulation_detail) + o.cutterArray = -getCutterArray(o, o.optimisation.simulation_detail) def buildStroke(start, end, cutterArray): @@ -768,7 +788,7 @@ def crazyStrokeImageBinary(o, ar, avoidar): andar = numpy.logical_and(ar, numpy.logical_not(avoidar)) indices = andar.nonzero() if len(nchunk.points) > 1: - chunk.parentChildDist([nchunk], chunks, o, distance=r) + parentChildDist([nchunk], chunks, o, distance=r) chunk_builders.append(nchunk) if totpix > startpix * 0.001: @@ -824,7 +844,7 @@ def crazyStrokeImageBinary(o, ar, avoidar): print(totaltests) i = 0 if len(nchunk.points) > 1: - chunk.parentChildDist([nchunk], chunks, o, distance=r) + parentChildDist([nchunk], chunks, o, distance=r) chunk_builders.append(nchunk) for ch in chunk_builders: @@ -1080,7 +1100,7 @@ def _restore_render_settings(pairs, properties): def renderSampleImage(o): t = time.time() - simple.progress('getting zbuffer') + progress('getting zbuffer') # print(o.zbuffer_image) o.update_offsetimage_tag = True if o.geometry_source == 'OBJECT' or o.geometry_source == 'COLLECTION': @@ -1089,8 +1109,8 @@ def renderSampleImage(o): sx = o.max.x - o.min.x sy = o.max.y - o.min.y - resx = math.ceil(sx / o.optimisation.pixsize) + 2 * o.borderwidth - resy = math.ceil(sy / o.optimisation.pixsize) + 2 * o.borderwidth + resx = ceil(sx / o.optimisation.pixsize) + 2 * o.borderwidth + resy = ceil(sy / o.optimisation.pixsize) + 2 * o.borderwidth if not o.update_zbufferimage_tag and len(o.zbuffer_image) == resx and len(o.zbuffer_image[0]) == resy: # if we call this accidentally in more functions, which currently happens... @@ -1228,7 +1248,7 @@ def renderSampleImage(o): #o.offset_image.resize(ex - sx + 2 * o.borderwidth, ey - sy + 2 * o.borderwidth) o.optimisation.pixsize = o.source_image_size_x / i.size[0] - simple.progress('pixel size in the image source', o.optimisation.pixsize) + progress('pixel size in the image source', o.optimisation.pixsize) rawimage = imagetonumpy(i) maxa = numpy.max(rawimage) @@ -1267,7 +1287,7 @@ def renderSampleImage(o): print('min image ', numpy.min(a)) o.zbuffer_image = a # progress('got z buffer also with conversion in:') - simple.progress(time.time() - t) + progress(time.time() - t) # progress(a) o.update_zbufferimage_tag = False @@ -1281,7 +1301,7 @@ async def prepareArea(o): renderSampleImage(o) samples = o.zbuffer_image - iname = simple.getCachePath(o) + '_off.exr' + iname = getCachePath(o) + '_off.exr' if not o.update_offsetimage_tag: progress('loading offset image') @@ -1296,3 +1316,98 @@ async def prepareArea(o): samples = numpy.maximum(samples, o.min.z - 0.00001) await offsetArea(o, samples) numpysave(o.offset_image, iname) + + +def getCutterArray(operation, pixsize): + type = operation.cutter_type + # print('generating cutter') + r = operation.cutter_diameter / 2 + operation.skin # /operation.pixsize + res = ceil((r * 2) / pixsize) + m = res / 2.0 + car = numpy.full(shape=(res, res), fill_value=-10.0, dtype=float) + + v = 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 = sin(acos(v.length / r)) * r - r + car.itemset((a, b), z) # [a,b]=z + + elif type == 'VCARVE': + angle = operation.cutter_tip_angle + s = tan(pi * (90 - angle / 2) / 180) # angle in degrees + 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 = tan(pi * (90 - angle / 2) / 180) # angle in degrees + 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 = radians(operation.cutter_tip_angle)/2 + ball_r = operation.ball_radius + cutter_r = operation.cutter_diameter / 2 + conedepth = (cutter_r - ball_r)/tan(angle) + Ball_R = ball_r/cos(angle) + D_ofset = ball_r * tan(angle) + s = tan(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 = sin(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 = Vector((0, 0, -10)) + vend = 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 diff --git a/scripts/addons/cam/involute_gear.py b/scripts/addons/cam/involute_gear.py index c823d6cc2..bca9579c0 100644 --- a/scripts/addons/cam/involute_gear.py +++ b/scripts/addons/cam/involute_gear.py @@ -62,35 +62,34 @@ //and be separated by the sum of their pitch radii, which can be found with pitch_radius(). """ # ported to Blendercam by Alain Pelletier Jan 2022 +from math import ( + acos, + cos, + degrees, + pi, + sin, + sqrt +) +from shapely.geometry import Polygon import bpy -from bpy.props import * -from bpy.types import Operator from . import ( - utils, - polygon_utils_cam, simple, + utils, ) -import shapely -from shapely.geometry import ( - Point, - LineString, - Polygon -) -import mathutils -import math - # convert gear_polar to cartesian coordinates + + def gear_polar(r, theta): - return r * math.sin(theta), r * math.cos(theta) + return r * sin(theta), r * cos(theta) # unwind a string this many degrees to go from radius r1 to radius r2 def gear_iang(r1, r2): - return math.sqrt((r2 / r1) * (r2 / r1) - 1) - math.acos(r1 / r2) + return sqrt((r2 / r1) * (r2 / r1) - 1) - acos(r1 / r2) # radius a fraction f up the curved side of the tooth @@ -113,10 +112,9 @@ def gear_q6(b, s, t, d): def gear(mm_per_tooth=0.003, number_of_teeth=5, hole_diameter=0.003175, pressure_angle=0.3488, clearance=0.0, backlash=0.0, rim_size=0.0005, hub_diameter=0.006, spokes=4): simple.deselect() - pi = math.pi p = mm_per_tooth * number_of_teeth / pi / 2 # radius of pitch circle c = p + mm_per_tooth / pi - clearance # radius of outer circle - b = p * math.cos(pressure_angle) # radius of base circle + b = p * cos(pressure_angle) # radius of base circle r = p-(c-p)-clearance # radius of root circle t = mm_per_tooth / 2 - backlash / 2 # tooth thickness at pitch circle # angle to where involute meets base circle on each side of tooth @@ -152,7 +150,7 @@ def gear(mm_per_tooth=0.003, number_of_teeth=5, hole_diameter=0.003175, i = number_of_teeth while i > 1: simple.duplicate() - simple.rotate(2 * math.pi / number_of_teeth) + simple.rotate(2 * pi / number_of_teeth) i -= 1 simple.join_multiple('tooth') simple.active_name('_teeth') @@ -200,18 +198,17 @@ def gear(mm_per_tooth=0.003, number_of_teeth=5, hole_diameter=0.003175, name = 'gear-' + str(round(mm_per_tooth*1000, 1)) name += 'mm-pitch-' + str(number_of_teeth) - name += 'teeth-PA-' + str(round(math.degrees(pressure_angle), 1)) + name += 'teeth-PA-' + str(round(degrees(pressure_angle), 1)) simple.active_name(name) def rack(mm_per_tooth=0.01, number_of_teeth=11, height=0.012, pressure_angle=0.3488, backlash=0.0, hole_diameter=0.003175, tooth_per_hole=4): simple.deselect() - pi = math.pi mm_per_tooth *= 1000 a = mm_per_tooth / pi # addendum # tooth side is tilted so top/bottom corners move this amount - t = (a * math.sin(pressure_angle)) + t = (a * sin(pressure_angle)) a /= 1000 mm_per_tooth /= 1000 t /= 1000 @@ -245,5 +242,5 @@ def rack(mm_per_tooth=0.01, number_of_teeth=11, height=0.012, pressure_angle=0.3 simple.difference('_', '_tooth') name = 'rack-' + str(round(mm_per_tooth * 1000, 1)) - name += '-PA-' + str(round(math.degrees(pressure_angle), 1)) + name += '-PA-' + str(round(degrees(pressure_angle), 1)) simple.active_name(name) diff --git a/scripts/addons/cam/joinery.py b/scripts/addons/cam/joinery.py index 85944c097..2077461aa 100644 --- a/scripts/addons/cam/joinery.py +++ b/scripts/addons/cam/joinery.py @@ -20,26 +20,26 @@ # ***** END GPL LICENCE BLOCK ***** # blender operators definitions are in this file. They mostly call the functions from utils.py +from math import ( + asin, + atan2, + degrees, + hypot, + pi, +) +from shapely.geometry import ( + LineString, + Point, +) import bpy -from bpy.types import Operator -from bpy_extras.io_utils import ImportHelper from . import ( - utils, - pack, - polygon_utils_cam, + puzzle_joinery, simple, - gcodepath, - bridges, - parametric, - puzzle_joinery + utils, ) -import shapely -from shapely.geometry import Point, LineString, Polygon -import mathutils -import math # boolean operations for curve objects @@ -76,10 +76,10 @@ def interlock_groove(length, thickness, finger_play, cx=0, cy=0, rotation=0): def interlock_twist(length, thickness, finger_play, cx=0, cy=0, rotation=0, percentage=0.5): mortise(length, thickness, finger_play, 0, 0, 0) simple.active_name("_tmp") - mortise(length * percentage, thickness, finger_play, 0, 0, math.pi / 2) + mortise(length * percentage, thickness, finger_play, 0, 0, pi / 2) simple.active_name("_tmp") - h = math.hypot(thickness, length * percentage) - oangle = math.degrees(math.asin(length * percentage / h)) + h = hypot(thickness, length * percentage) + oangle = degrees(asin(length * percentage / h)) bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Sector', Simple_startangle=90 + oangle, Simple_endangle=180 - oangle, Simple_radius=h / 2, use_cyclic_u=True, edit_mode=False) @@ -180,7 +180,7 @@ def vertical_finger(length, thickness, finger_play, amount): for i in range(amount): mortise(length, thickness, finger_play, 0, i * 2 * - length + length / 2, rotation=math.pi / 2) + length + length / 2, rotation=pi / 2) simple.active_name("_height_finger") simple.join_multiple("_height_finger") @@ -237,7 +237,7 @@ def make_flex_pocket(length, height, finger_thick, finger_width, pocket_width): # creates pockets pocket using mortise function for kerf bending dist = 3 * finger_width / 2 while dist < length: - mortise(height - 2 * finger_thick, pocket_width, 0, dist, 0, math.pi / 2) + mortise(height - 2 * finger_thick, pocket_width, 0, dist, 0, pi / 2) simple.active_name("_flex_pocket") dist += finger_width * 2 @@ -248,7 +248,7 @@ def make_flex_pocket(length, height, finger_thick, finger_width, pocket_width): def make_variable_flex_pocket(height, finger_thick, pocket_width, locations): # creates pockets pocket using mortise function for kerf bending for dist in locations: - mortise(height + 2 * finger_thick, pocket_width, 0, dist, 0, math.pi / 2) + mortise(height + 2 * finger_thick, pocket_width, 0, dist, 0, pi / 2) simple.active_name("_flex_pocket") simple.join_multiple("_flex_pocket") @@ -292,7 +292,7 @@ def create_flex_side(length, height, finger_thick, top_bottom=False): def angle(a, b): - return math.atan2(b[1] - a[1], b[0] - a[0]) + return atan2(b[1] - a[1], b[0] - a[0]) def angle_difference(a, b, c): @@ -326,8 +326,8 @@ def fixed_finger(loop, loop_length, finger_size, finger_thick, finger_tolerance, while distance <= pd: mortise_angle = angle(oldp, p) mortise_angle_difference = abs(mortise_angle - old_mortise_angle) - mad = (1 + 6 * min(mortise_angle_difference, math.pi / 4) / ( - math.pi / 4)) # factor for tolerance for the finger + mad = (1 + 6 * min(mortise_angle_difference, pi / 4) / ( + pi / 4)) # factor for tolerance for the finger if base: mortise(finger_size, finger_thick, finger_tolerance * mad, distance, 0, 0) @@ -440,8 +440,8 @@ def variable_finger(loop, loop_length, min_finger, finger_size, finger_thick, fi while distance <= pd: mortise_angle = angle(oldp, p) mortise_angle_difference = abs(mortise_angle - old_mortise_angle) - mad = (1 + 6 * min(mortise_angle_difference, math.pi / 4) / ( - math.pi / 4)) # factor for tolerance for the finger + mad = (1 + 6 * min(mortise_angle_difference, pi / 4) / ( + pi / 4)) # factor for tolerance for the finger # move finger by the factor mad greater with larger angle difference distance += mad * finger_tolerance mortise_point = loop.interpolate(distance) @@ -469,7 +469,7 @@ def variable_finger(loop, loop_length, min_finger, finger_size, finger_thick, fi old_distance = distance old_mortise_point = mortise_point finger_sz = finger_size - next_angle_difference = math.pi + next_angle_difference = pi # adaptive finger length start while finger_sz > min_finger and next_angle_difference > adaptive: @@ -543,13 +543,13 @@ def distributed_interlock(loop, loop_length, finger_depth, finger_thick, finger_ if not_start: while distance <= pd and end_distance >= distance: if fixed_angle == 0: - groove_angle = angle(oldp, p) + math.pi / 2 + tangent + groove_angle = angle(oldp, p) + pi / 2 + tangent else: groove_angle = fixed_angle groove_point = loop.interpolate(distance) - print(j, "groove_angle", round(180 * groove_angle / math.pi), + print(j, "groove_angle", round(180 * groove_angle / pi), "distance", round(distance * 1000), "mm") single_interlock(finger_depth, finger_thick, finger_tolerance, groove_point.x, groove_point.y, groove_angle, type, twist_percentage=twist_percentage) diff --git a/scripts/addons/cam/machine_settings.py b/scripts/addons/cam/machine_settings.py new file mode 100644 index 000000000..b466dfc20 --- /dev/null +++ b/scripts/addons/cam/machine_settings.py @@ -0,0 +1,228 @@ +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, + FloatVectorProperty, + IntProperty, +) +from bpy.types import PropertyGroup + +from . import constants +from .utils import updateMachine + + +class machineSettings(PropertyGroup): + """stores all data for machines""" + # name = StringProperty(name="Machine Name", default="Machine") + post_processor: EnumProperty( + name='Post processor', + items=( + ('ISO', 'Iso', 'exports standardized gcode ISO 6983 (RS-274)'), + ('MACH3', 'Mach3', 'default mach3'), + ('EMC', 'LinuxCNC - EMC2', + 'Linux based CNC control software - formally EMC2'), + ('FADAL', 'Fadal', 'Fadal VMC'), + ('GRBL', 'grbl', + 'optimized gcode for grbl firmware on Arduino with cnc shield'), + ('HEIDENHAIN', 'Heidenhain', 'heidenhain'), + ('HEIDENHAIN530', 'Heidenhain530', 'heidenhain530'), + ('TNC151', 'Heidenhain TNC151', + 'Post Processor for the Heidenhain TNC151 machine'), + ('SIEGKX1', 'Sieg KX1', 'Sieg KX1'), + ('HM50', 'Hafco HM-50', 'Hafco HM-50'), + ('CENTROID', 'Centroid M40', 'Centroid M40'), + ('ANILAM', 'Anilam Crusader M', 'Anilam Crusader M'), + ('GRAVOS', 'Gravos', 'Gravos'), + ('WIN-PC', 'WinPC-NC', 'German CNC by Burkhard Lewetz'), + ('SHOPBOT MTC', 'ShopBot MTC', 'ShopBot MTC'), + ('LYNX_OTTER_O', 'Lynx Otter o', 'Lynx Otter o') + ), + description='Post processor', + default='MACH3', + ) + # units = EnumProperty(name='Units', items = (('IMPERIAL', '')) + # position definitions: + use_position_definitions: BoolProperty( + name="Use position definitions", + description="Define own positions for op start, " + "toolchange, ending position", + default=False, + ) + starting_position: FloatVectorProperty( + name='Start position', + default=(0, 0, 0), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + update=updateMachine, + ) + mtc_position: FloatVectorProperty( + name='MTC position', + default=(0, 0, 0), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + update=updateMachine, + ) + ending_position: FloatVectorProperty( + name='End position', + default=(0, 0, 0), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + update=updateMachine, + ) + + working_area: FloatVectorProperty( + name='Work Area', + default=(0.500, 0.500, 0.100), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + update=updateMachine, + ) + feedrate_min: FloatProperty( + name="Feedrate minimum /min", + default=0.0, + min=0.00001, + max=320000, + precision=constants.PRECISION, + unit='LENGTH', + ) + feedrate_max: FloatProperty( + name="Feedrate maximum /min", + default=2, + min=0.00001, + max=320000, + precision=constants.PRECISION, + unit='LENGTH', + ) + feedrate_default: FloatProperty( + name="Feedrate default /min", + default=1.5, + min=0.00001, + max=320000, + precision=constants.PRECISION, + unit='LENGTH', + ) + hourly_rate: FloatProperty( + name="Price per hour", + default=100, + min=0.005, + precision=2, + ) + + # UNSUPPORTED: + + spindle_min: FloatProperty( + name="Spindle speed minimum RPM", + default=5000, + min=0.00001, + max=320000, + precision=1, + ) + spindle_max: FloatProperty( + name="Spindle speed maximum RPM", + default=30000, + min=0.00001, + max=320000, + precision=1, + ) + spindle_default: FloatProperty( + name="Spindle speed default RPM", + default=15000, + min=0.00001, + max=320000, + precision=1, + ) + spindle_start_time: FloatProperty( + name="Spindle start delay seconds", + description='Wait for the spindle to start spinning before starting ' + 'the feeds , in seconds', + default=0, + min=0.0000, + max=320000, + precision=1, + ) + + axis4: BoolProperty( + name="#4th axis", + description="Machine has 4th axis", + default=0, + ) + axis5: BoolProperty( + name="#5th axis", + description="Machine has 5th axis", + default=0, + ) + + eval_splitting: BoolProperty( + name="Split files", + description="split gcode file with large number of operations", + default=True, + ) # split large files + split_limit: IntProperty( + name="Operations per file", + description="Split files with larger number of operations than this", + min=1000, + max=20000000, + default=800000, + ) + + # rotary_axis1 = EnumProperty(name='Axis 1', + # items=( + # ('X', 'X', 'x'), + # ('Y', 'Y', 'y'), + # ('Z', 'Z', 'z')), + # description='Number 1 rotational axis', + # default='X', update = updateOffsetImage) + + collet_size: FloatProperty( + name="#Collet size", + description="Collet size for collision detection", + default=33, + min=0.00001, + max=320000, + precision=constants.PRECISION, + unit="LENGTH", + ) + # exporter_start = StringProperty(name="exporter start", default="%") + + # post processor options + + output_block_numbers: BoolProperty( + name="output block numbers", + description="output block numbers ie N10 at start of line", + default=False, + ) + + start_block_number: IntProperty( + name="start block number", + description="the starting block number ie 10", + default=10, + ) + + block_number_increment: IntProperty( + name="block number increment", + description="how much the block number should " + "increment for the next line", + default=10, + ) + + output_tool_definitions: BoolProperty( + name="output tool definitions", + description="output tool definitions", + default=True, + ) + + output_tool_change: BoolProperty( + name="output tool change commands", + description="output tool change commands ie: Tn M06", + default=True, + ) + + output_g43_on_tool_change: BoolProperty( + name="output G43 on tool change", + description="output G43 on tool change line", + default=False, + ) diff --git a/scripts/addons/cam/ops.py b/scripts/addons/cam/ops.py index 39c3009cc..f63740ac5 100644 --- a/scripts/addons/cam/ops.py +++ b/scripts/addons/cam/ops.py @@ -20,48 +20,43 @@ # ***** END GPL LICENCE BLOCK ***** # blender operators definitions are in this file. They mostly call the functions from utils.py - +import os +import subprocess +import textwrap +import threading +import traceback import bpy from bpy.props import ( EnumProperty, StringProperty, - ) -from bpy_extras.io_utils import ImportHelper +from bpy.types import ( + Operator, +) -import subprocess -import os -import threading from . import ( - utils, + bridges, + gcodepath, pack, - polygon_utils_cam, simple, - gcodepath, - bridges, simulation, ) +from .async_op import ( + AsyncCancelledException, + AsyncOperatorMixin, + progress_async, +) +from .exception import CamException from .utils import ( - was_hidden_dict, - reload_paths, - isValid, + addMachineAreaObject, + getBoundsWorldspace, isChainValid, + isValid, + reload_paths, silhoueteOffset, - getBoundsWorldspace, - addMachineAreaObject, -) -from .async_op import ( - AsyncOperatorMixin, - AsyncCancelledException, + was_hidden_dict, ) -import shapely -import mathutils -import math -import textwrap -import traceback - -from .exception import * class threadCom: # object passed to threads to read background process stdout info @@ -118,7 +113,7 @@ def timer_update(context): o.outtext = tcom.lasttext # changes -class PathsBackground(bpy.types.Operator): +class PathsBackground(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" @@ -153,7 +148,7 @@ def execute(self, context): return {'FINISHED'} -class KillPathsBackground(bpy.types.Operator): +class KillPathsBackground(Operator): """Remove CAM path processes in background.""" bl_idname = "object.kill_calculate_cam_paths_background" bl_label = "Kill background computation of an operation" @@ -239,7 +234,7 @@ async def _calc_path(operator, context): return {'FINISHED', True} -class CalculatePath(bpy.types.Operator, AsyncOperatorMixin): +class CalculatePath(Operator, AsyncOperatorMixin): """calculate CAM paths""" bl_idname = "object.calculate_cam_path" bl_label = "Calculate CAM paths" @@ -260,7 +255,7 @@ async def execute_async(self, context): return retval -class PathsAll(bpy.types.Operator): +class PathsAll(Operator): """calculate all CAM paths""" bl_idname = "object.calculate_cam_paths_all" bl_label = "Calculate all CAM paths" @@ -283,7 +278,7 @@ def draw(self, context): bpy.context.scene, "cam_operations") -class CamPackObjects(bpy.types.Operator): +class CamPackObjects(Operator): """calculate all CAM paths""" bl_idname = "object.cam_pack_objects" bl_label = "Pack curves on sheet" @@ -300,7 +295,7 @@ def draw(self, context): layout = self.layout -class CamSliceObjects(bpy.types.Operator): +class CamSliceObjects(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" @@ -327,7 +322,7 @@ def getChainOperations(chain): return chop -class PathsChain(bpy.types.Operator, AsyncOperatorMixin): +class PathsChain(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" @@ -357,7 +352,7 @@ async def execute_async(self, context): except Exception as e: print("FAIL", e) traceback.print_tb(e.__traceback__) - operator.report({'ERROR'}, str(e)) + self.report({'ERROR'}, str(e)) return {'FINISHED'} for o in chainops: @@ -366,7 +361,7 @@ async def execute_async(self, context): return {'FINISHED'} -class PathExportChain(bpy.types.Operator): +class PathExportChain(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" @@ -394,7 +389,7 @@ def execute(self, context): return {'FINISHED'} -class PathExport(bpy.types.Operator): +class PathExport(Operator): """Export gcode. Can be used only when the path object is present""" bl_idname = "object.cam_export" bl_label = "Export operation gcode" @@ -413,7 +408,7 @@ def execute(self, context): return {'FINISHED'} -class CAMSimulate(bpy.types.Operator, AsyncOperatorMixin): +class CAMSimulate(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.""" @@ -449,7 +444,7 @@ def draw(self, context): bpy.context.scene, "cam_operations") -class CAMSimulateChain(bpy.types.Operator, AsyncOperatorMixin): +class CAMSimulateChain(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" @@ -494,7 +489,7 @@ def draw(self, context): bpy.context.scene, "cam_operations") -class CamChainAdd(bpy.types.Operator): +class CamChainAdd(Operator): """Add new CAM chain""" bl_idname = "scene.cam_chain_add" bl_label = "Add new CAM chain" @@ -517,7 +512,7 @@ def execute(self, context): return {'FINISHED'} -class CamChainRemove(bpy.types.Operator): +class CamChainRemove(Operator): """Remove CAM chain""" bl_idname = "scene.cam_chain_remove" bl_label = "Remove CAM chain" @@ -535,7 +530,7 @@ def execute(self, context): return {'FINISHED'} -class CamChainOperationAdd(bpy.types.Operator): +class CamChainOperationAdd(Operator): """Add operation to chain""" bl_idname = "scene.cam_chain_operation_add" bl_label = "Add operation to chain" @@ -555,7 +550,7 @@ def execute(self, context): return {'FINISHED'} -class CamChainOperationUp(bpy.types.Operator): +class CamChainOperationUp(Operator): """Add operation to chain""" bl_idname = "scene.cam_chain_operation_up" bl_label = "Add operation to chain" @@ -575,7 +570,7 @@ def execute(self, context): return {'FINISHED'} -class CamChainOperationDown(bpy.types.Operator): +class CamChainOperationDown(Operator): """Add operation to chain""" bl_idname = "scene.cam_chain_operation_down" bl_label = "Add operation to chain" @@ -595,7 +590,7 @@ def execute(self, context): return {'FINISHED'} -class CamChainOperationRemove(bpy.types.Operator): +class CamChainOperationRemove(Operator): """Remove operation from chain""" bl_idname = "scene.cam_chain_operation_remove" bl_label = "Remove operation from chain" @@ -625,41 +620,7 @@ def fixUnits(): # 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 - 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): +class CamOperationAdd(Operator): """Add new CAM operation""" bl_idname = "scene.cam_operation_add" bl_label = "Add new CAM operation" @@ -696,7 +657,7 @@ def execute(self, context): return {'FINISHED'} -class CamOperationCopy(bpy.types.Operator): +class CamOperationCopy(Operator): """Copy CAM operation""" bl_idname = "scene.cam_operation_copy" bl_label = "Copy active CAM operation" @@ -747,7 +708,7 @@ def execute(self, context): return {'FINISHED'} -class CamOperationRemove(bpy.types.Operator): +class CamOperationRemove(Operator): """Remove CAM operation""" bl_idname = "scene.cam_operation_remove" bl_label = "Remove CAM operation" @@ -782,7 +743,7 @@ def execute(self, context): # move cam operation in the list up or down -class CamOperationMove(bpy.types.Operator): +class CamOperationMove(Operator): """Move CAM operation""" bl_idname = "scene.cam_operation_move" bl_label = "Move CAM operation in list" @@ -819,7 +780,7 @@ def execute(self, context): return {'FINISHED'} -class CamOrientationAdd(bpy.types.Operator): +class CamOrientationAdd(Operator): """Add orientation to cam operation, for multiaxis operations""" bl_idname = "scene.cam_orientation_add" bl_label = "Add orientation" @@ -846,7 +807,7 @@ def execute(self, context): return {'FINISHED'} -class CamBridgesAdd(bpy.types.Operator): +class CamBridgesAdd(Operator): """Add bridge objects to curve""" bl_idname = "scene.cam_bridges_add" bl_label = "Add bridges" diff --git a/scripts/addons/cam/pack.py b/scripts/addons/cam/pack.py index 64d6bfe98..3e99dbe3c 100644 --- a/scripts/addons/cam/pack.py +++ b/scripts/addons/cam/pack.py @@ -18,21 +18,36 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENCE BLOCK ***** +from math import pi +import random +import time + +import shapely +from shapely import geometry as sgeometry +from shapely import ( + affinity, + prepared, + speedups +) import bpy +from bpy.types import PropertyGroup +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, +) +from mathutils import ( + Euler, + Vector +) + from . import ( - utils, + constants, + polygon_utils_cam, simple, - polygon_utils_cam + utils, ) -import shapely -from shapely import geometry as sgeometry -from shapely import affinity, prepared -from shapely import speedups -import random -import time -import mathutils -from mathutils import Vector # this algorithm takes all selected curves, @@ -46,7 +61,7 @@ def srotate(s, r, x, y): ncoords = [] - e = mathutils.Euler((0, 0, r)) + e = Euler((0, 0, r)) for p in s.exterior.coords: v1 = Vector((p[0], p[1], 0)) v2 = Vector((x, y, 0)) @@ -197,3 +212,69 @@ def packCurves(): polygon_utils_cam.shapelyToCurve('test', sgeometry.MultiPolygon(placedpolys), 0) print(t) + + +class PackObjectsSettings(PropertyGroup): + """stores all data for machines""" + + sheet_fill_direction: EnumProperty( + name="Fill direction", + items=( + ("X", "X", "Fills sheet in X axis direction"), + ("Y", "Y", "Fills sheet in Y axis direction"), + ), + description="Fill direction of the packer algorithm", + default="Y", + ) + sheet_x: FloatProperty( + name="X size", + description="Sheet size", + min=0.001, + max=10, + default=0.5, + precision=constants.PRECISION, + unit="LENGTH", + ) + sheet_y: FloatProperty( + name="Y size", + description="Sheet size", + min=0.001, + max=10, + default=0.5, + precision=constants.PRECISION, + unit="LENGTH", + ) + distance: FloatProperty( + name="Minimum distance", + description="minimum distance between objects(should be " + "at least cutter diameter!)", + min=0.001, + max=10, + default=0.01, + precision=constants.PRECISION, + unit="LENGTH", + ) + tolerance: FloatProperty( + name="Placement Tolerance", + description="Tolerance for placement: smaller value slower placemant", + min=0.001, + max=0.02, + default=0.005, + precision=constants.PRECISION, + unit="LENGTH", + ) + rotate: BoolProperty( + name="enable rotation", + description="Enable rotation of elements", + default=True, + ) + rotate_angle: FloatProperty( + name="Placement Angle rotation step", + description="bigger rotation angle,faster placemant", + default=0.19635 * 4, + min=pi / 180, + max=pi, + precision=5, + subtype="ANGLE", + unit="ROTATION", + ) diff --git a/scripts/addons/cam/parametric.py b/scripts/addons/cam/parametric.py index 0de7f67d2..b2d60048e 100644 --- a/scripts/addons/cam/parametric.py +++ b/scripts/addons/cam/parametric.py @@ -29,10 +29,8 @@ # the iteration count to your liking. # # This code has been checked to work on Blender 2.92. +from math import pow -import math -from math import sin, cos, pi -import bmesh import bpy from mathutils import Vector @@ -60,17 +58,17 @@ def derive_bezier_handles(a, b, c, d, tb, tc): """ # Calculate matrix coefficients - matrix_a = 3 * math.pow(1 - tb, 2) * tb - matrix_b = 3 * (1 - tb) * math.pow(tb, 2) - matrix_c = 3 * math.pow(1 - tc, 2) * tc - matrix_d = 3 * (1 - tc) * math.pow(tc, 2) + matrix_a = 3 * pow(1 - tb, 2) * tb + matrix_b = 3 * (1 - tb) * pow(tb, 2) + matrix_c = 3 * pow(1 - tc, 2) * tc + matrix_d = 3 * (1 - tc) * pow(tc, 2) # Calculate the matrix determinant matrix_determinant = 1 / ((matrix_a * matrix_d) - (matrix_b * matrix_c)) # Calculate the components of the target position vector - final_b = b - (math.pow(1 - tb, 3) * a) - (math.pow(tb, 3) * d) - final_c = c - (math.pow(1 - tc, 3) * a) - (math.pow(tc, 3) * d) + final_b = b - (pow(1 - tb, 3) * a) - (pow(tb, 3) * d) + final_c = c - (pow(1 - tc, 3) * a) - (pow(tc, 3) * d) # Multiply the inversed matrix with the position vector to get the handle points bezier_b = matrix_determinant * ((matrix_d * final_b) + (-matrix_b * final_c)) @@ -191,6 +189,8 @@ def make_edge_loops(*objects): :param *objects: Positional arguments for each object to be converted and merged. """ + context = bpy.context + scene = context.scene mesh_objects = [] vertex_groups = [] diff --git a/scripts/addons/cam/pattern.py b/scripts/addons/cam/pattern.py index b02b978a2..3e84a0049 100644 --- a/scripts/addons/cam/pattern.py +++ b/scripts/addons/cam/pattern.py @@ -18,24 +18,31 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENCE BLOCK ***** - -import time -import mathutils -from mathutils import * - -from . import ( - simple, - chunk, - utils, - polygon_utils_cam +from math import ( + ceil, + floor, + pi, + sqrt ) -from .simple import * -from .chunk import * -from .polygon_utils_cam import * -import shapely -from shapely import geometry as sgeometry +import time + import numpy +import bpy +from mathutils import ( + Euler, + Vector +) + +from .cam_chunk import ( + camPathChunk, + camPathChunkBuilder, + chunksRefine, + parentChildDist, + shapelyToChunks, +) +from .simple import progress + def getPathPatternParallel(o, angle): zlevel = 1 @@ -93,7 +100,7 @@ def getPathPatternParallel(o, angle): v = Vector((0, 1, 0)) v.rotate(e) - e1 = Euler((0, 0, -math.pi / 2)) + e1 = Euler((0, 0, -pi / 2)) v1 = v.copy() v1.rotate(e1) @@ -154,7 +161,7 @@ def getPathPattern(operation): elif o.strategy == 'CROSS': pathchunks.extend(getPathPatternParallel(o, o.parallel_angle)) - pathchunks.extend(getPathPatternParallel(o, o.parallel_angle - math.pi / 2.0)) + pathchunks.extend(getPathPatternParallel(o, o.parallel_angle - pi / 2.0)) elif o.strategy == 'BLOCK': @@ -228,7 +235,7 @@ def getPathPattern(operation): # progress(x,y,midx,midy) e = Euler((0, 0, 0)) - pi = math.pi + # pi = pi chunk.points.append((midx + v.x, midy + v.y, zlevel)) while midx + v.x > o.min.x or midy + v.y > o.min.y: # v.x=x-midx @@ -268,11 +275,11 @@ def getPathPattern(operation): midy = (o.max.y + o.min.y) / 2 rx = o.max.x - o.min.x ry = o.max.y - o.min.y - maxr = math.sqrt(rx * rx + ry * ry) + maxr = sqrt(rx * rx + ry * ry) # progress(x,y,midx,midy) e = Euler((0, 0, 0)) - pi = math.pi + # pi = pi chunk = camPathChunkBuilder([]) chunk.points.append((midx, midy, zlevel)) pathchunks.append(chunk.to_chunk()) diff --git a/scripts/addons/cam/polygon_utils_cam.py b/scripts/addons/cam/polygon_utils_cam.py index 362911f4c..acd31bc46 100644 --- a/scripts/addons/cam/polygon_utils_cam.py +++ b/scripts/addons/cam/polygon_utils_cam.py @@ -19,21 +19,22 @@ # # ***** END GPL LICENCE BLOCK ***** -import math -import mathutils -import curve_simplify +from math import pi import shapely from shapely.geometry import polygon as spolygon from shapely import geometry as sgeometry +from mathutils import Euler, Vector +import curve_simplify + SHAPELY = True def Circle(r, np): c = [] - v = mathutils.Vector((r, 0, 0)) - e = mathutils.Euler((0, 0, 2.0 * math.pi / np)) + v = Vector((r, 0, 0)) + e = Euler((0, 0, 2.0 * pi / np)) for a in range(0, np): c.append((v.x, v.y)) v.rotate(e) @@ -50,7 +51,7 @@ def shapelyRemoveDoubles(p, optimize_threshold): veclist = [] for v in c: - veclist.append(mathutils.Vector((v[0], v[1]))) + veclist.append(Vector((v[0], v[1]))) s = curve_simplify.simplify_RDP(veclist, soptions) nc = [] for i in range(0, len(s)): diff --git a/scripts/addons/cam/preferences.py b/scripts/addons/cam/preferences.py new file mode 100644 index 000000000..71d83514f --- /dev/null +++ b/scripts/addons/cam/preferences.py @@ -0,0 +1,109 @@ +from bpy.props import ( + BoolProperty, + EnumProperty, + IntProperty, + StringProperty, +) +from bpy.types import ( + AddonPreferences, +) + + +class CamAddonPreferences(AddonPreferences): + # this must match the addon name, use '__package__' + # when defining this in a submodule of a python package. + bl_idname = __package__ + + op_preset_update: BoolProperty( + name="Have the Operation Presets been Updated", + default=False, + ) + + experimental: BoolProperty( + name="Show experimental features", + default=False, + ) + + update_source: StringProperty( + name="Source of updates for the addon", + description="This can be either a github repo link in which case " + "it will download the latest release on there, " + "or an api link like " + "https://api.github.com/repos//blendercam/commits" + " to get from a github repository", + default="https://github.com/pppalain/blendercam", + ) + + last_update_check: IntProperty( + name="Last update time", + default=0, + ) + + last_commit_hash: StringProperty( + name="Hash of last commit from updater", + default="", + ) + + just_updated: BoolProperty( + name="Set to true on update or initial install", + default=True, + ) + + new_version_available: StringProperty( + name="Set to new version name if one is found", + default="", + ) + + default_interface_level: EnumProperty( + name="Interface level in new file", + description="Choose visible options", + items=[ + ("0", "Basic", "Only show essential options"), + ("1", "Advanced", "Show advanced options"), + ("2", "Complete", "Show all options"), + ("3", "Experimental", "Show experimental options"), + ], + default="3", + ) + + default_machine_preset: StringProperty( + name="Machine preset in new file", + description="So that machine preset choice persists between files", + default="", + ) + + def draw(self, context): + layout = self.layout + layout.label( + text="Use experimental features when you want to help development of Blender CAM:" + ) + layout.prop(self, "experimental") + layout.prop(self, "update_source") + layout.label(text="Choose a preset update source") + + UPDATE_SOURCES = [ + ( + "https://github.com/vilemduha/blendercam", + "Stable", + "Stable releases (github.com/vilemduja/blendercam)", + ), + ( + "https://github.com/pppalain/blendercam", + "Unstable", + "Unstable releases (github.com/pppalain/blendercam)", + ), + # comments for searching in github actions release script to + # automatically set this repo if required + # REPO ON NEXT LINE + ( + "https://api.github.com/repos/pppalain/blendercam/commits", + "Direct from git (may not work)", + "Get from git commits directly", + ), + # REPO ON PREV LINE + ("", "None", "Don't do auto update"), + ] + grid = layout.grid_flow(align=True) + for url, short, long in UPDATE_SOURCES: + op = grid.operator("render.cam_set_update_source", text=short) + op.new_source = url diff --git a/scripts/addons/cam/preset_managers.py b/scripts/addons/cam/preset_managers.py new file mode 100644 index 000000000..01ef2c981 --- /dev/null +++ b/scripts/addons/cam/preset_managers.py @@ -0,0 +1,210 @@ +import bpy +from bl_operators.presets import AddPresetBase +from bpy.types import ( + Menu, + Operator, +) + + +class CAM_CUTTER_MT_presets(Menu): + bl_label = "Cutter presets" + preset_subdir = "cam_cutters" + preset_operator = "script.execute_preset" + draw = Menu.draw_preset + + +class CAM_MACHINE_MT_presets(Menu): + bl_label = "Machine presets" + preset_subdir = "cam_machines" + preset_operator = "script.execute_preset" + draw = Menu.draw_preset + + @classmethod + def post_cb(cls, context): + name = cls.bl_label + filepath = bpy.utils.preset_find(name, + cls.preset_subdir, + display_name=True, + ext=".py") + context.preferences.addons['cam'].preferences.default_machine_preset = filepath + bpy.ops.wm.save_userpref() + + +class AddPresetCamCutter(AddPresetBase, Operator): + """Add a Cutter Preset""" + bl_idname = "render.cam_preset_cutter_add" + bl_label = "Add Cutter Preset" + preset_menu = "CAM_CUTTER_MT_presets" + + preset_defines = [ + "d = bpy.context.scene.cam_operations[bpy.context.scene.cam_active_operation]" + ] + + preset_values = [ + "d.cutter_id", + "d.cutter_type", + "d.cutter_diameter", + "d.cutter_length", + "d.cutter_flutes", + "d.cutter_tip_angle", + "d.cutter_description", + ] + + preset_subdir = "cam_cutters" + + +class CAM_OPERATION_MT_presets(Menu): + bl_label = "Operation presets" + preset_subdir = "cam_operations" + preset_operator = "script.execute_preset" + draw = Menu.draw_preset + + +class AddPresetCamOperation(AddPresetBase, Operator): + """Add an Operation Preset""" + bl_idname = "render.cam_preset_operation_add" + bl_label = "Add Operation Preset" + preset_menu = "CAM_OPERATION_MT_presets" + + preset_defines = [ + 'import cam', + 'o = cam.utils.setup_operation_preset()', + ] + + preset_values = [ + 'o.info.duration', + 'o.info.chipload', + 'o.info.warnings', + + 'o.material.estimate_from_model', + 'o.material.size', + 'o.material.radius_around_model', + 'o.material.origin', + + 'o.movement.stay_low', + 'o.movement.free_height', + 'o.movement.insideout', + 'o.movement.spindle_rotation', + 'o.movement.type', + 'o.movement.useG64', + 'o.movement.G64', + 'o.movement.parallel_step_back', + 'o.movement.protect_vertical', + + 'o.source_image_name', + 'o.source_image_offset', + 'o.source_image_size_x', + 'o.source_image_crop', + 'o.source_image_crop_start_x', + 'o.source_image_crop_start_y', + 'o.source_image_crop_end_x', + 'o.source_image_crop_end_y', + 'o.source_image_scale_z', + + 'o.optimisation.optimize', + 'o.optimisation.optimize_threshold', + 'o.optimisation.use_exact', + 'o.optimisation.exact_subdivide_edges', + 'o.optimisation.simulation_detail', + 'o.optimisation.pixsize', + 'o.optimisation.circle_detail', + + 'o.cut_type', + 'o.cutter_tip_angle', + 'o.cutter_id', + 'o.cutter_diameter', + 'o.cutter_type', + 'o.cutter_flutes', + 'o.cutter_length', + + 'o.ambient_behaviour', + 'o.ambient_radius', + + 'o.curve_object', + 'o.curve_object1', + 'o.limit_curve', + 'o.use_limit_curve', + + 'o.feedrate', + 'o.plunge_feedrate', + + 'o.dist_along_paths', + 'o.dist_between_paths', + + 'o.max', + 'o.min', + 'o.minz_from', + 'o.minz', + + 'o.skin', + 'o.spindle_rpm', + 'o.use_layers', + 'o.carve_depth', + + 'o.update_offsetimage_tag', + 'o.slice_detail', + 'o.drill_type', + 'o.dont_merge', + 'o.update_silhouete_tag', + 'o.inverse', + 'o.waterline_fill', + 'o.strategy', + 'o.update_zbufferimage_tag', + 'o.stepdown', + 'o.path_object_name', + 'o.pencil_threshold', + 'o.geometry_source', + 'o.object_name', + 'o.parallel_angle', + + 'o.output_header', + 'o.gcode_header', + 'o.output_trailer', + 'o.gcode_trailer', + 'o.use_modifiers', + + 'o.enable_A', + 'o.enable_B', + 'o.A_along_x', + 'o.rotation_A', + 'o.rotation_B', + 'o.straight' + ] + + preset_subdir = "cam_operations" + + +class AddPresetCamMachine(AddPresetBase, Operator): + """Add a Cam Machine Preset""" + bl_idname = "render.cam_preset_machine_add" + bl_label = "Add Machine Preset" + preset_menu = "CAM_MACHINE_MT_presets" + + preset_defines = [ + "d = bpy.context.scene.cam_machine", + "s = bpy.context.scene.unit_settings" + ] + preset_values = [ + "d.post_processor", + "s.system", + "d.use_position_definitions", + "d.starting_position", + "d.mtc_position", + "d.ending_position", + "d.working_area", + "d.feedrate_min", + "d.feedrate_max", + "d.feedrate_default", + "d.spindle_min", + "d.spindle_max", + "d.spindle_default", + "d.axis4", + "d.axis5", + "d.collet_size", + "d.output_tool_change", + "d.output_block_numbers", + "d.output_tool_definitions", + "d.output_g43_on_tool_change", + ] + + preset_subdir = "cam_machines" diff --git a/scripts/addons/cam/puzzle_joinery.py b/scripts/addons/cam/puzzle_joinery.py index 7a554a4b6..f547b19c3 100644 --- a/scripts/addons/cam/puzzle_joinery.py +++ b/scripts/addons/cam/puzzle_joinery.py @@ -20,29 +20,22 @@ # ***** END GPL LICENCE BLOCK ***** # blender operators definitions are in this file. They mostly call the functions from curvecamcreate.py -from typing import Any +from math import ( + cos, + degrees, + pi, + sin, + sqrt, + tan, +) import bpy -from bpy.types import Operator from . import ( - utils, - pack, - polygon_utils_cam, - simple, - gcodepath, - bridges, - parametric, joinery, + simple, + utils, ) -import shapely -from shapely.geometry import ( - Point, - LineString, - Polygon, -) -import mathutils -import math DT = 1.025 @@ -134,7 +127,7 @@ def twistf(name, length, diameter, tolerance, twist, tneck, tthick, twist_keep=F # add twist lock to receptacle if twist: joinery.interlock_twist(length, tthick, tolerance, cx=0, cy=0, rotation=0, percentage=tneck) - simple.rotate(math.pi / 2) + simple.rotate(pi / 2) simple.move(y=-tthick / 2 + 2 * diameter + 2 * tolerance) simple.active_name('xtemptwist') if twist_keep: @@ -151,7 +144,7 @@ def twistm(name, length, diameter, tolerance, twist, tneck, tthick, angle, twist global DT if twist: joinery.interlock_twist(length, tthick, tolerance, cx=0, cy=0, rotation=0, percentage=tneck) - simple.rotate(math.pi / 2) + simple.rotate(pi / 2) simple.move(y=-tthick / 2 + 2 * diameter * DT) simple.rotate(angle) simple.move(x=x, y=y) @@ -190,25 +183,25 @@ def bar(width, thick, diameter, tolerance, amount=0, stem=1, twist=False, tneck= if which == 'MM' or which == 'M' or which == 'MF': simple.rename('fingers', '_tmpfingers') - simple.rotate(-math.pi / 2) + simple.rotate(-pi / 2) simple.move(x=width / 2) simple.rename('tmprect', '_tmprect') simple.union('_tmp') simple.active_name("tmprect") - twistm('tmprect', thick, diameter, tolerance, twist, tneck, tthick, -math.pi / 2, + twistm('tmprect', thick, diameter, tolerance, twist, tneck, tthick, -pi / 2, x=width / 2, twist_keep=twist_keep) twistf('receptacle', thick, diameter, tolerance, twist, tneck, tthick, twist_keep=twist_keep) simple.rename('receptacle', '_tmpreceptacle') if which == 'FF' or which == 'F' or which == 'MF': - simple.rotate(-math.pi / 2) + simple.rotate(-pi / 2) simple.move(x=-width / 2) simple.rename('tmprect', '_tmprect') simple.difference('_tmp', '_tmprect') simple.active_name("tmprect") if twist_keep: simple.make_active('twist_keep_f') - simple.rotate(-math.pi / 2) + simple.rotate(-pi / 2) simple.move(x=-width / 2) simple.remove_multiple("_") # Remove temporary base and holes @@ -259,13 +252,13 @@ def arc(radius, thick, angle, diameter, tolerance, amount=0, stem=1, twist=False # generate arc bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Segment', Simple_a=radius - thick / 2, - Simple_b=radius + thick / 2, Simple_startangle=-0.0001, Simple_endangle=math.degrees(angle), + Simple_b=radius + thick / 2, Simple_startangle=-0.0001, Simple_endangle=degrees(angle), Simple_radius=radius, use_cyclic_u=False, edit_mode=False) bpy.context.active_object.name = "tmparc" simple.rename('fingers', '_tmpfingers') - simple.rotate(math.pi) + simple.rotate(pi) simple.move(x=radius) bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN') @@ -273,7 +266,7 @@ def arc(radius, thick, angle, diameter, tolerance, amount=0, stem=1, twist=False if which == 'MF' or which == 'M': simple.union('_tmp') simple.active_name("base") - twistm('base', thick, diameter, tolerance, twist, tneck, tthick, math.pi, x=radius) + twistm('base', thick, diameter, tolerance, twist, tneck, tthick, pi, x=radius) simple.rename('base', '_tmparc') simple.rename('receptacle', '_tmpreceptacle') @@ -293,13 +286,13 @@ def arc(radius, thick, angle, diameter, tolerance, amount=0, stem=1, twist=False simple.rotate(-angle) simple.mirrory() bpy.ops.object.transform_apply(location=True, rotation=True, scale=False) - simple.rotate(-math.pi / 2) + simple.rotate(-pi / 2) simple.move(y=radius) simple.rename('PUZZLE_arc', 'PUZZLE_arc_male') elif which == 'F': simple.mirrorx() simple.move(x=radius) - simple.rotate(math.pi / 2) + simple.rotate(pi / 2) simple.rename('PUZZLE_arc', 'PUZZLE_arc_receptacle') else: simple.move(x=-radius) @@ -398,7 +391,7 @@ def arcbar(length, radius, thick, angle, diameter, tolerance, amount=0, stem=1, if which == 'FF' or which == 'FM': bar(length, thick, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, tthick=tthick, which='F', twist_keep=twist_keep, twist_line=twist_line, twist_line_amount=twist_line_amount) - simple.rotate(math.pi) + simple.rotate(pi) simple.active_name('tmprect') # Generate female section and join to base @@ -443,7 +436,7 @@ def multiangle(radius, thick, angle, diameter, tolerance, amount=0, stem=1, twis r_exterior = radius + thick / 2 r_interior = radius - thick / 2 - height = math.sqrt(r_exterior * r_exterior - radius * radius) + r_interior / 4 + height = sqrt(r_exterior * r_exterior - radius * radius) + r_interior / 4 bpy.ops.curve.simple(align='WORLD', location=(0, height, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', @@ -453,7 +446,7 @@ def multiangle(radius, thick, angle, diameter, tolerance, amount=0, stem=1, twis bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Circle', Simple_sides=4, Simple_radius=r_interior, shape='3D', use_cyclic_u=True, edit_mode=False) - simple.move(y=radius * math.tan(angle)) + simple.move(y=radius * tan(angle)) simple.active_name('tmpCircle') arc(radius, thick, angle, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, tthick=tthick, @@ -468,7 +461,7 @@ def multiangle(radius, thick, angle, diameter, tolerance, amount=0, stem=1, twis which='M') simple.active_name('tmp_arc') simple.mirrory() - simple.rotate(math.pi / 2) + simple.rotate(pi / 2) simple.union("tmp_") simple.difference('tmp', 'tmp_') simple.active_name('multiAngle60') @@ -535,11 +528,11 @@ def curved_t(length, thick, radius, diameter, tolerance, amount=0, stem=1, twist simple.active_name("tmp_rect") if base_gender == 'MF': - arc(radius, thick, math.pi / 2, diameter, tolerance, + arc(radius, thick, pi / 2, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, tthick=tthick, which='M') simple.move(-radius) simple.active_name('tmp_arc') - arc(radius, thick, math.pi / 2, diameter, tolerance, + arc(radius, thick, pi / 2, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, tthick=tthick, which='F') simple.move(radius) simple.mirrory() @@ -550,7 +543,7 @@ def curved_t(length, thick, radius, diameter, tolerance, amount=0, stem=1, twist simple.union('tmp_arc') simple.difference('tmp_', 'tmp_arc') else: - arc(radius, thick, math.pi / 2, diameter, tolerance, + arc(radius, thick, pi / 2, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, tthick=tthick, which=base_gender) simple.active_name('tmp_arc') simple.difference('tmp_', 'tmp_arc') @@ -615,16 +608,16 @@ def mitre(length, thick, angle, angleb, diameter, tolerance, amount=0, stem=1, t simple.make_active('fingers') simple.duplicate() simple.active_name('tmpfingers') - simple.rotate(angle - math.pi / 2) - h = thick / math.cos(angle) + simple.rotate(angle - pi / 2) + h = thick / cos(angle) h /= 2 - simple.move(x=length / 2 + h * math.sin(angle), y=-thick / 2) + simple.move(x=length / 2 + h * sin(angle), y=-thick / 2) if which == 'M': simple.rename('fingers', 'tmpfingers') - simple.rotate(angleb - math.pi / 2) - h = thick / math.cos(angleb) + simple.rotate(angleb - pi / 2) + h = thick / cos(angleb) h /= 2 - simple.move(x=length / 2 + h * math.sin(angleb), y=-thick / 2) + simple.move(x=length / 2 + h * sin(angleb), y=-thick / 2) simple.mirrorx() simple.union('tmp') @@ -636,17 +629,17 @@ def mitre(length, thick, angle, angleb, diameter, tolerance, amount=0, stem=1, t simple.mirrory() simple.duplicate() simple.active_name('tmpreceptacle') - simple.rotate(angleb - math.pi / 2) - h = thick / math.cos(angleb) + simple.rotate(angleb - pi / 2) + h = thick / cos(angleb) h /= 2 - simple.move(x=length / 2 + h * math.sin(angleb), y=-thick / 2) + simple.move(x=length / 2 + h * sin(angleb), y=-thick / 2) simple.mirrorx() if which == 'F': simple.rename('receptacle', 'tmpreceptacle2') - simple.rotate(angle - math.pi / 2) - h = thick / math.cos(angle) + simple.rotate(angle - pi / 2) + h = thick / cos(angle) h /= 2 - simple.move(x=length / 2 + h * math.sin(angle), y=-thick / 2) + simple.move(x=length / 2 + h * sin(angle), y=-thick / 2) simple.difference('tmp', 'tmprect') simple.remove_multiple('receptacle') @@ -673,8 +666,8 @@ def open_curve(line, thick, diameter, tolerance, amount=0, stem=1, twist=False, coords = list(line.coords) - start_angle = joinery.angle(coords[0], coords[1]) + math.pi/2 - end_angle = joinery.angle(coords[-1], coords[-2]) + math.pi/2 + start_angle = joinery.angle(coords[0], coords[1]) + pi/2 + end_angle = joinery.angle(coords[-1], coords[-2]) + pi/2 p_start = coords[0] p_end = coords[-1] @@ -709,18 +702,18 @@ def open_curve(line, thick, diameter, tolerance, amount=0, stem=1, twist=False, twistf('receptacle', thick, diameter, tolerance, twist, t_neck, t_thick, twist_keep=twist_keep) simple.rename('receptacle', 'tmp') - simple.rotate(start_angle+math.pi) + simple.rotate(start_angle+pi) simple.move(x=p_start[0], y=p_start[1]) simple.difference('tmp', 'tmp_curve') if twist_keep: simple.make_active('twist_keep_f') - simple.rotate(start_angle + math.pi) + simple.rotate(start_angle + pi) simple.move(x=p_start[0], y=p_start[1]) if twist_amount > 0 and twist: twist_start = line.length / (twist_amount+1) joinery.distributed_interlock(line, line.length, thick, t_thick, tolerance, twist_amount, - tangent=math.pi/2, fixed_angle=0, start=twist_start, end=twist_start, + tangent=pi/2, fixed_angle=0, start=twist_start, end=twist_start, closed=False, type='TWIST', twist_percentage=t_neck) if twist_keep: simple.duplicate() @@ -761,12 +754,12 @@ def tile(diameter, tolerance, tile_x_amount, tile_y_amount, stem=1): simple.rename('base', '_base') simple.remove_doubles() simple.rename('fingers', '_fingers') - simple.rotate(math.pi/2) + simple.rotate(pi/2) simple.move(x=-width/2) simple.union('_') simple.active_name('_base') simple.rename('receptacle', '_receptacle') - simple.rotate(math.pi/2) + simple.rotate(pi/2) simple.move(x=width/2) simple.difference('_', '_base') simple.active_name('tile_ ' + str(tile_x_amount) + '_' + str(tile_y_amount)) diff --git a/scripts/addons/cam/simple.py b/scripts/addons/cam/simple.py index c05cf96be..8d52cfe52 100644 --- a/scripts/addons/cam/simple.py +++ b/scripts/addons/cam/simple.py @@ -18,25 +18,21 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENCE BLOCK ***** - -# Solves: No module named 'shapely' (even if it is installed) -# help('modules') - -import math -import sys +from math import ( + hypot, + pi, +) import os import string +import sys import time + +from shapely.geometry import Polygon + import bpy -import mathutils -from mathutils import * -from math import * -from shapely.geometry import ( - Point, - LineString, - Polygon, - multilinestring -) +from mathutils import Vector + +from .constants import BULLET_SCALE def tuple_add(t, t1): # add two tuples as Vectors @@ -96,7 +92,7 @@ def activate(o): def dist2d(v1, v2): """distance between two points in 2d""" - return math.hypot((v1[0] - v2[0]), (v1[1] - v2[1])) + return hypot((v1[0] - v2[0]), (v1[1] - v2[1])) def delob(ob): @@ -325,7 +321,7 @@ def remove_doubles(): def add_overcut(diametre, overcut=True): if overcut: name = bpy.context.active_object.name - bpy.ops.object.curve_overcuts(diameter=diametre, threshold=math.pi/2.05) + bpy.ops.object.curve_overcuts(diameter=diametre, threshold=pi/2.05) overcut_name = bpy.context.active_object.name make_active(name) bpy.ops.object.delete() diff --git a/scripts/addons/cam/simulation.py b/scripts/addons/cam/simulation.py index 3682813af..398c13498 100644 --- a/scripts/addons/cam/simulation.py +++ b/scripts/addons/cam/simulation.py @@ -20,17 +20,24 @@ # ***** 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 . import utils + import numpy as np -from . import simple -from . import image_utils +import bpy +from mathutils import Vector + from .async_op import progress_async +from .image_utils import ( + getCutterArray, + numpysave, +) +from .simple import getSimulationPath +from .utils import ( + getBoundsMultiple, + getOperationSources, +) def createSimulationObject(name, operations, i): @@ -93,16 +100,16 @@ def createSimulationObject(name, operations, i): async def doSimulation(name, operations): """perform simulation of operations. Currently only for 3 axis""" for o in operations: - utils.getOperationSources(o) - limits = utils.getBoundsMultiple( + getOperationSources(o) + limits = getBoundsMultiple( operations) # this is here because some background computed operations still didn't have bounds data i = await generateSimulationImage(operations, limits) -# cp = simple.getCachePath(operations[0])[:-len(operations[0].name)] + name - cp = simple.getSimulationPath()+name +# cp = getCachePath(operations[0])[:-len(operations[0].name)] + name + cp = getSimulationPath()+name print('cp=', cp) iname = cp + '_sim.exr' - image_utils.numpysave(i, iname) + numpysave(i, iname) i = bpy.data.images.load(iname) createSimulationObject(name, operations, i) @@ -288,101 +295,6 @@ async def generateSimulationImage(operations, limits): 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) - m = res / 2.0 - car = np.full(shape=(res, res), fill_value=-10.0, dtype=float) - - 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 - 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 - 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.""" diff --git a/scripts/addons/cam/slice.py b/scripts/addons/cam/slice.py index f0364be44..8dcbeaaf8 100644 --- a/scripts/addons/cam/slice.py +++ b/scripts/addons/cam/slice.py @@ -23,8 +23,16 @@ # completely rewritten April 2021 import bpy +from bpy.props import ( + BoolProperty, + FloatProperty, +) +from bpy.types import PropertyGroup -from . import utils +from . import ( + constants, + utils, +) def slicing2d(ob, height): # April 2020 Alain Pelletier @@ -134,3 +142,33 @@ def sliceObject(ob): # April 2020 Alain Pelletier # select all slices for obj in bpy.data.collections['Slices'].all_objects: obj.select_set(True) + + +class SliceObjectsSettings(PropertyGroup): + """stores all data for machines""" + + slice_distance: FloatProperty( + name="Slicing distance", + description="slices distance in z, should be most often " + "thickness of plywood sheet.", + min=0.001, + max=10, + default=0.005, + precision=constants.PRECISION, + unit="LENGTH", + ) + slice_above0: BoolProperty( + name="Slice above 0", + description="only slice model above 0", + default=False, + ) + slice_3d: BoolProperty( + name="3d slice", + description="for 3d carving", + default=False, + ) + indexes: BoolProperty( + name="add indexes", + description="adds index text of layer + index", + default=True, + ) diff --git a/scripts/addons/cam/strategy.py b/scripts/addons/cam/strategy.py index d4fa6ec5e..98ebc0781 100644 --- a/scripts/addons/cam/strategy.py +++ b/scripts/addons/cam/strategy.py @@ -20,55 +20,88 @@ # ***** 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 -import time -import math -from math import * -from bpy_extras import object_utils -from . import ( - chunk, - collision, - simple, - pattern, - utils, - bridges, - ops, - polygon_utils_cam, - image_utils +from math import ( + ceil, + pi, + radians, + sqrt, + tan, ) -from .chunk import * -from .collision import * -from .simple import * -from .pattern import * -from .utils import * -from .polygon_utils_cam import * -from .image_utils import * +import sys +import time +import shapely from shapely.geometry import polygon as spolygon +from shapely.geometry import Point # Double check this import! from shapely import geometry as sgeometry from shapely import affinity +import bpy +from bpy_extras import object_utils +from mathutils import ( + Euler, + Vector +) + +from .bridges import useBridges +from .cam_chunk import ( + camPathChunk, + chunksRefine, + chunksRefineThreshold, + curveToChunks, + limitChunks, + optimizeChunk, + parentChildDist, + parentChildPoly, + setChunksZ, + shapelyToChunks, +) +from .collision import cleanupBulletCollision +from .exception import CamException +from .polygon_utils_cam import Circle, shapelyToCurve +from .simple import ( + activate, + delob, + join_multiple, + progress, + remove_multiple, +) +from .utils import ( + Add_Pocket, + checkEqual, + extendChunks5axis, + getObjectOutline, + getObjectSilhouete, + getOperationSilhouete, + getOperationSources, + Helix, + # Point, + sampleChunksNAxis, + sortChunks, + unique, +) + + SHAPELY = True # cutout strategy is completely here: async def cutout(o): max_depth = checkminz(o) - cutter_angle = math.radians(o.cutter_tip_angle / 2) + cutter_angle = 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) + c_offset = -max_depth * tan(cutter_angle) elif o.cutter_type == 'CYLCONE': - c_offset = -max_depth * math.tan(cutter_angle) + o.cylcone_diameter / 2 + c_offset = -max_depth * tan(cutter_angle) + o.cylcone_diameter / 2 elif o.cutter_type == 'BALLCONE': - c_offset = -max_depth * math.tan(cutter_angle) + o.ball_radius + c_offset = -max_depth * 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) + c_offset = sqrt(r ** 2 - (r + max_depth) ** 2) print("offset:", c_offset) if c_offset > o.cutter_diameter / 2: c_offset = o.cutter_diameter / 2 @@ -95,14 +128,14 @@ async def cutout(o): else: chunksFromCurve = [] if o.cut_type == 'ONLINE': - p = utils.getObjectOutline(0, o, True) + p = getObjectOutline(0, o, True) else: offset = True if o.cut_type == 'INSIDE': offset = False - p = utils.getObjectOutline(c_offset, o, offset) + p = getObjectOutline(c_offset, o, offset) if o.outlines_count > 1: for i in range(1, o.outlines_count): chunksFromCurve.extend(shapelyToChunks(p, -1)) @@ -121,7 +154,7 @@ async def cutout(o): if not o.dont_merge: parentChildPoly(chunksFromCurve, chunksFromCurve, o) if o.outlines_count == 1: - chunksFromCurve = await utils.sortChunks(chunksFromCurve, o) + chunksFromCurve = await 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'): @@ -164,7 +197,7 @@ async def cutout(o): if o.use_bridges: # add bridges to chunks print('using bridges') - simple.remove_multiple(o.name+'_cut_bridges') + remove_multiple(o.name+'_cut_bridges') print("old briddge cut removed") bridgeheight = min(o.max.z, o.min.z + abs(o.bridges_height)) @@ -173,7 +206,7 @@ async def cutout(o): chunk = chl[0] layer = chl[1] if layer[1] < bridgeheight: - bridges.useBridges(chunk, o) + useBridges(chunk, o) if o.profile_start > 0: print("cutout change profile start") @@ -211,7 +244,7 @@ async def cutout(o): async def curve(o): print('operation: curve') pathSamples = [] - utils.getOperationSources(o) + getOperationSources(o) if not o.onlycurves: raise CamException("All objects must be curves for this operation.") @@ -219,7 +252,7 @@ async def curve(o): # make the chunks from curve here pathSamples.extend(curveToChunks(ob)) # sort before sampling - pathSamples = await utils.sortChunks(pathSamples, o) + pathSamples = await sortChunks(pathSamples, o) pathSamples = chunksRefine(pathSamples, o) # simplify # layers here @@ -264,7 +297,7 @@ async def proj_curve(s, o): targetCurve = s.objects[o.curve_object1] - from cam import chunk + from cam import cam_chunk if targetCurve.type != 'CURVE': raise CamException('Projection target and source have to be curve objects!') @@ -302,7 +335,7 @@ async def proj_curve(s, o): ch.set_points(ch_points) layers = getLayers(o, 0, ch.depth) - chunks.extend(utils.sampleChunksNAxis(o, pathSamples, layers)) + chunks.extend(sampleChunksNAxis(o, pathSamples, layers)) chunksToMesh(chunks, o) @@ -310,24 +343,24 @@ async def pocket(o): print('operation: pocket') scene = bpy.context.scene - simple.remove_multiple("3D_poc") + remove_multiple("3D_poc") max_depth = checkminz(o) + o.skin - cutter_angle = math.radians(o.cutter_tip_angle / 2) + cutter_angle = 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) + c_offset = -max_depth * tan(cutter_angle) elif o.cutter_type == 'CYLCONE': - c_offset = -max_depth * math.tan(cutter_angle) + o.cylcone_diameter / 2 + c_offset = -max_depth * tan(cutter_angle) + o.cylcone_diameter / 2 elif o.cutter_type == 'BALLCONE': - c_offset = -max_depth * math.tan(cutter_angle) + o.ball_radius + c_offset = -max_depth * 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) + p = 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) @@ -342,7 +375,7 @@ async def pocket(o): while not p.is_empty: if o.pocketToCurve: # make a curve starting with _3dpocket - polygon_utils_cam.shapelyToCurve('3dpocket', p, 0.0) + shapelyToCurve('3dpocket', p, 0.0) nchunks = shapelyToChunks(p, o.min.z) # print("nchunks") @@ -372,7 +405,7 @@ async def pocket(o): for ch in chunksFromCurve: ch.reverse() - chunksFromCurve = await utils.sortChunks(chunksFromCurve, o) + chunksFromCurve = await sortChunks(chunksFromCurve, o) chunks = [] layers = getLayers(o, o.maxz, checkminz(o)) @@ -494,10 +527,10 @@ async def pocket(o): if o.first_down: if o.pocket_option == "OUTSIDE": chunks.reverse() - chunks = await utils.sortChunks(chunks, o) + chunks = await sortChunks(chunks, o) if o.pocketToCurve: # make curve instead of a path - simple.join_multiple("3dpocket") + join_multiple("3dpocket") else: chunksToMesh(chunks, o) # make normal pocket path @@ -593,14 +626,14 @@ async def drill(o): newchunk.setZ(o.maxz) chunklayers.append(newchunk) - chunklayers = await utils.sortChunks(chunklayers, o) + chunklayers = await sortChunks(chunklayers, o) chunksToMesh(chunklayers, o) async def medial_axis(o): print('operation: Medial Axis') - simple.remove_multiple("medialMesh") + remove_multiple("medialMesh") from .voronoi import Site, computeVoronoiDiagram @@ -608,8 +641,8 @@ async def medial_axis(o): 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 + slope = tan(pi * (90 - angle / 2) / 180) # angle in degrees + # slope = tan((pi-angle)/2) #angle in radian new_cutter_diameter = o.cutter_diameter m_o_ob = o.object_name if o.cutter_type == 'VCARVE': @@ -638,7 +671,7 @@ async def medial_axis(o): if ob.data.resolution_u < 64: ob.data.resolution_u = 64 - polys = utils.getOperationSilhouete(o) + polys = getOperationSilhouete(o) if isinstance(polys, list): if len(polys) == 1 and isinstance(polys[0], shapely.MultiPolygon): mpoly = polys[0] @@ -758,7 +791,7 @@ async def medial_axis(o): # generate a mesh from the medial calculations if o.add_mesh_for_medial: - polygon_utils_cam.shapelyToCurve('medialMesh', lines, 0.0) + shapelyToCurve('medialMesh', lines, 0.0) bpy.ops.object.convert(target='MESH') oi = 0 @@ -768,7 +801,7 @@ async def medial_axis(o): oi += 1 # bpy.ops.object.join() - chunks = await utils.sortChunks(chunks, o) + chunks = await sortChunks(chunks, o) layers = getLayers(o, o.maxz, o.min.z) @@ -782,17 +815,17 @@ async def medial_axis(o): chunklayers.append(newchunk) if o.first_down: - chunklayers = await utils.sortChunks(chunklayers, o) + chunklayers = await sortChunks(chunklayers, o) if o.add_mesh_for_medial: # make curve instead of a path - simple.join_multiple("medialMesh") + 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) + Add_Pocket(None, maxdepth, m_o_ob, new_cutter_diameter) def getLayers(operation, startdepth, enddepth): @@ -805,7 +838,7 @@ def getLayers(operation, startdepth, enddepth): "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) + n = ceil((startdepth - enddepth) / operation.stepdown) print("start " + str(startdepth) + " end " + str(enddepth) + " n " + str(n)) layerstart = operation.maxz diff --git a/scripts/addons/cam/testing.py b/scripts/addons/cam/testing.py index 830b1ceff..4decdd0c7 100644 --- a/scripts/addons/cam/testing.py +++ b/scripts/addons/cam/testing.py @@ -18,12 +18,10 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENCE BLOCK ***** - -import sys import bpy -from . import simple, utils -from .simple import * +from .gcodepath import getPath +from .simple import activate def addTestCurve(loc): @@ -138,7 +136,7 @@ def testOperation(i): report = '' report += 'testing operation ' + o.name + '\n' - utils.getPath(bpy.context, o) + getPath(bpy.context, o) newresult = bpy.data.objects[o.path_object_name] origname = "test_cam_path_" + o.name diff --git a/scripts/addons/cam/ui.py b/scripts/addons/cam/ui.py index 8034cbd57..fe56733ae 100644 --- a/scripts/addons/cam/ui.py +++ b/scripts/addons/cam/ui.py @@ -18,42 +18,21 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENCE BLOCK ***** - -import sys -import bpy from bpy_extras.io_utils import ImportHelper from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, StringProperty, ) from bpy.types import ( Panel, - Menu, Operator, - UIList + UIList, + PropertyGroup, ) -from . import ( - gcodeimportparser, - simple -) -from .simple import * - -from .ui_panels.buttons_panel import CAMButtonsPanel -from .ui_panels.interface import * -from .ui_panels.info import * -from .ui_panels.operations import * -from .ui_panels.cutter import * -from .ui_panels.machine import * -from .ui_panels.material import * -from .ui_panels.chains import * -from .ui_panels.op_properties import * -from .ui_panels.movement import * -from .ui_panels.feedrate import * -from .ui_panels.optimisation import * -from .ui_panels.area import * -from .ui_panels.gcode import * -from .ui_panels.pack import * -from .ui_panels.slice import * +from .gcodeimportparser import import_gcode class CAM_UL_orientations(UIList): @@ -68,7 +47,7 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn # panel containing all tools -class VIEW3D_PT_tools_curvetools(bpy.types.Panel): +class VIEW3D_PT_tools_curvetools(Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'TOOLS' bl_context = "objectmode" @@ -87,7 +66,7 @@ def draw(self, context): layout.operator("object.mesh_get_pockets") -class VIEW3D_PT_tools_create(bpy.types.Panel): +class VIEW3D_PT_tools_create(Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'TOOLS' bl_context = "objectmode" @@ -115,7 +94,7 @@ def draw(self, context): # ------------------------------------------------------------------------ -class CustomPanel(bpy.types.Panel): +class CustomPanel(Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'TOOLS' bl_context = "objectmode" @@ -167,4 +146,34 @@ class WM_OT_gcode_import(Operator, ImportHelper): def execute(self, context): print(self.filepath) - return gcodeimportparser.import_gcode(context, self.filepath) + return import_gcode(context, self.filepath) + + +class import_settings(PropertyGroup): + split_layers: BoolProperty( + name="Split Layers", + description="Save every layer as single Objects in Collection", + default=False, + ) + subdivide: BoolProperty( + name="Subdivide", + description="Only Subdivide gcode segments that are " + "bigger than 'Segment length' ", + default=False, + ) + output: EnumProperty( + name="output type", + items=( + ("mesh", "Mesh", "Make a mesh output"), + ("curve", "Curve", "Make curve output"), + ), + default="curve", + ) + max_segment_size: FloatProperty( + name="", + description="Only Segments bigger then this value get subdivided", + default=0.001, + min=0.0001, + max=1.0, + unit="LENGTH", + ) diff --git a/scripts/addons/cam/ui_panels/area.py b/scripts/addons/cam/ui_panels/area.py index 9423bd83c..c07a0b68f 100644 --- a/scripts/addons/cam/ui_panels/area.py +++ b/scripts/addons/cam/ui_panels/area.py @@ -1,9 +1,11 @@ - import bpy +from bpy.types import Panel + from .buttons_panel import CAMButtonsPanel +from ..simple import strInUnits -class CAM_AREA_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_AREA_Panel(CAMButtonsPanel, Panel): """CAM operation area panel""" bl_label = "CAM operation area " bl_idname = "WORLD_PT_CAM_OPERATION_AREA" @@ -15,8 +17,7 @@ class CAM_AREA_Panel(CAMButtonsPanel, bpy.types.Panel): 'draw_minz': 1, 'draw_ambient': 1, 'draw_limit_curve': 1, - 'draw_first_down': 1 - + 'draw_first_down': 1, } def draw_use_layers(self): diff --git a/scripts/addons/cam/ui_panels/buttons_panel.py b/scripts/addons/cam/ui_panels/buttons_panel.py index c3a118acf..07dfc7973 100644 --- a/scripts/addons/cam/ui_panels/buttons_panel.py +++ b/scripts/addons/cam/ui_panels/buttons_panel.py @@ -1,9 +1,9 @@ -import bpy import inspect -# Panel definitions +import bpy +# Panel definitions class CAMButtonsPanel: bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' diff --git a/scripts/addons/cam/ui_panels/chains.py b/scripts/addons/cam/ui_panels/chains.py index bf8b7c0cc..15c112d1b 100644 --- a/scripts/addons/cam/ui_panels/chains.py +++ b/scripts/addons/cam/ui_panels/chains.py @@ -1,8 +1,7 @@ - import bpy -from bpy.types import UIList -from .buttons_panel import CAMButtonsPanel +from bpy.types import UIList, Panel +from .buttons_panel import CAMButtonsPanel from ..utils import isChainValid @@ -34,7 +33,7 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn layout.label(text="", icon_value=icon) -class CAM_CHAINS_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_CHAINS_Panel(CAMButtonsPanel, Panel): """CAM chains panel""" bl_label = "CAM chains" bl_idname = "WORLD_PT_CAM_CHAINS" diff --git a/scripts/addons/cam/ui_panels/cutter.py b/scripts/addons/cam/ui_panels/cutter.py index 08afeb270..31973e170 100644 --- a/scripts/addons/cam/ui_panels/cutter.py +++ b/scripts/addons/cam/ui_panels/cutter.py @@ -1,9 +1,10 @@ - import bpy +from bpy.types import Panel + from .buttons_panel import CAMButtonsPanel -class CAM_CUTTER_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_CUTTER_Panel(CAMButtonsPanel, Panel): """CAM cutter panel""" bl_label = "CAM Cutter" bl_idname = "WORLD_PT_CAM_CUTTER" diff --git a/scripts/addons/cam/ui_panels/feedrate.py b/scripts/addons/cam/ui_panels/feedrate.py index a1a410d82..e66f041d9 100644 --- a/scripts/addons/cam/ui_panels/feedrate.py +++ b/scripts/addons/cam/ui_panels/feedrate.py @@ -1,8 +1,10 @@ import bpy +from bpy.types import Panel + from .buttons_panel import CAMButtonsPanel -class CAM_FEEDRATE_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_FEEDRATE_Panel(CAMButtonsPanel, Panel): """CAM feedrate panel""" bl_label = "CAM feedrate" bl_idname = "WORLD_PT_CAM_FEEDRATE" diff --git a/scripts/addons/cam/ui_panels/gcode.py b/scripts/addons/cam/ui_panels/gcode.py index d802f63e0..934eaefba 100644 --- a/scripts/addons/cam/ui_panels/gcode.py +++ b/scripts/addons/cam/ui_panels/gcode.py @@ -1,9 +1,10 @@ - import bpy +from bpy.types import Panel + from .buttons_panel import CAMButtonsPanel -class CAM_GCODE_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_GCODE_Panel(CAMButtonsPanel, Panel): """CAM operation g-code options panel""" bl_label = "CAM g-code options " bl_idname = "WORLD_PT_CAM_GCODE" diff --git a/scripts/addons/cam/ui_panels/info.py b/scripts/addons/cam/ui_panels/info.py index c96efb769..4acbd0578 100644 --- a/scripts/addons/cam/ui_panels/info.py +++ b/scripts/addons/cam/ui_panels/info.py @@ -1,13 +1,17 @@ import bpy from bpy.props import ( StringProperty, - FloatProperty + FloatProperty, +) +from bpy.types import ( + Panel, + PropertyGroup, ) from .buttons_panel import CAMButtonsPanel from ..utils import ( + opencamlib_version, update_operation, - opencamlib_version ) from ..constants import ( PRECISION, @@ -21,7 +25,7 @@ # This panel gives general information about the current operation -class CAM_INFO_Properties(bpy.types.PropertyGroup): +class CAM_INFO_Properties(PropertyGroup): warnings: StringProperty( name='warnings', @@ -44,7 +48,7 @@ class CAM_INFO_Properties(bpy.types.PropertyGroup): ) -class CAM_INFO_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_INFO_Panel(CAMButtonsPanel, Panel): bl_label = "CAM info & warnings" bl_idname = "WORLD_PT_CAM_INFO" panel_interface_level = 0 diff --git a/scripts/addons/cam/ui_panels/interface.py b/scripts/addons/cam/ui_panels/interface.py index 6c158929e..61ba69410 100644 --- a/scripts/addons/cam/ui_panels/interface.py +++ b/scripts/addons/cam/ui_panels/interface.py @@ -1,7 +1,10 @@ import bpy from bpy.props import EnumProperty +from bpy.types import ( + Panel, + PropertyGroup +) -import math from .buttons_panel import CAMButtonsPanel @@ -11,7 +14,7 @@ def update_interface(self, context): bpy.ops.wm.save_userpref() -class CAM_INTERFACE_Properties(bpy.types.PropertyGroup): +class CAM_INTERFACE_Properties(PropertyGroup): level: EnumProperty( name="Interface", description="Choose visible options", @@ -26,7 +29,7 @@ class CAM_INTERFACE_Properties(bpy.types.PropertyGroup): ) -class CAM_INTERFACE_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_INTERFACE_Panel(CAMButtonsPanel, Panel): bl_label = "Interface" bl_idname = "WORLD_PT_CAM_INTERFACE" always_show_panel = True diff --git a/scripts/addons/cam/ui_panels/machine.py b/scripts/addons/cam/ui_panels/machine.py index e54b829ba..8e2b18689 100644 --- a/scripts/addons/cam/ui_panels/machine.py +++ b/scripts/addons/cam/ui_panels/machine.py @@ -1,9 +1,10 @@ - import bpy +from bpy.types import Panel + from .buttons_panel import CAMButtonsPanel -class CAM_MACHINE_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_MACHINE_Panel(CAMButtonsPanel, Panel): """CAM machine panel""" bl_label = "CAM Machine" bl_idname = "WORLD_PT_CAM_MACHINE" diff --git a/scripts/addons/cam/ui_panels/material.py b/scripts/addons/cam/ui_panels/material.py index 7f218fc71..28ee78cfd 100644 --- a/scripts/addons/cam/ui_panels/material.py +++ b/scripts/addons/cam/ui_panels/material.py @@ -5,16 +5,21 @@ FloatProperty, FloatVectorProperty, ) +from bpy.types import ( + Operator, + Panel, + PropertyGroup, +) from .buttons_panel import CAMButtonsPanel from ..utils import ( + positionObject, update_material, - positionObject ) from ..constants import PRECISION -class CAM_MATERIAL_Properties(bpy.types.PropertyGroup): +class CAM_MATERIAL_Properties(PropertyGroup): estimate_from_model: BoolProperty( name="Estimate cut area from model", @@ -84,7 +89,7 @@ class CAM_MATERIAL_Properties(bpy.types.PropertyGroup): # Position object for CAM operation. Tests object bounds and places them so the object # is aligned to be positive from x and y and negative from z.""" -class CAM_MATERIAL_PositionObject(bpy.types.Operator): +class CAM_MATERIAL_PositionObject(Operator): bl_idname = "object.material_cam_position" bl_label = "position object for CAM operation" @@ -107,7 +112,7 @@ def draw(self, context): self, "operation", bpy.context.scene, "cam_operations") -class CAM_MATERIAL_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_MATERIAL_Panel(CAMButtonsPanel, Panel): bl_label = "CAM Material size and position" bl_idname = "WORLD_PT_CAM_MATERIAL" panel_interface_level = 0 diff --git a/scripts/addons/cam/ui_panels/movement.py b/scripts/addons/cam/ui_panels/movement.py index b66db7228..a29885528 100644 --- a/scripts/addons/cam/ui_panels/movement.py +++ b/scripts/addons/cam/ui_panels/movement.py @@ -1,20 +1,25 @@ +from math import pi + import bpy from bpy.props import ( BoolProperty, EnumProperty, FloatProperty, ) +from bpy.types import ( + Panel, + PropertyGroup +) -import math from .buttons_panel import CAMButtonsPanel from ..utils import update_operation from ..constants import ( PRECISION, - G64_INCOMPATIBLE_MACHINES + G64_INCOMPATIBLE_MACHINES, ) -class CAM_MOVEMENT_Properties(bpy.types.PropertyGroup): +class CAM_MOVEMENT_Properties(PropertyGroup): # movement parallel_step_back type: EnumProperty( name='Movement type', @@ -100,9 +105,9 @@ class CAM_MOVEMENT_Properties(bpy.types.PropertyGroup): ramp_in_angle: FloatProperty( name="Ramp in angle", - default=math.pi / 6, + default=pi / 6, min=0, - max=math.pi * 0.4999, + max=pi * 0.4999, precision=1, subtype="ANGLE", unit="ROTATION", @@ -136,9 +141,9 @@ class CAM_MOVEMENT_Properties(bpy.types.PropertyGroup): ramp_out_angle: FloatProperty( name="Ramp out angle", - default=math.pi / 6, + default=pi / 6, min=0, - max=math.pi * 0.4999, + max=pi * 0.4999, precision=1, subtype="ANGLE", unit="ROTATION", @@ -198,9 +203,9 @@ class CAM_MOVEMENT_Properties(bpy.types.PropertyGroup): protect_vertical_limit: FloatProperty( name="Verticality limit", description="What angle is allready considered vertical", - default=math.pi / 45, + default=pi / 45, min=0, - max=math.pi * 0.5, + max=pi * 0.5, precision=0, subtype="ANGLE", unit="ROTATION", @@ -208,7 +213,7 @@ class CAM_MOVEMENT_Properties(bpy.types.PropertyGroup): ) -class CAM_MOVEMENT_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_MOVEMENT_Panel(CAMButtonsPanel, Panel): """CAM movement panel""" bl_label = "CAM movement" bl_idname = "WORLD_PT_CAM_MOVEMENT" diff --git a/scripts/addons/cam/ui_panels/op_properties.py b/scripts/addons/cam/ui_panels/op_properties.py index e9e0d2eff..dc52da053 100644 --- a/scripts/addons/cam/ui_panels/op_properties.py +++ b/scripts/addons/cam/ui_panels/op_properties.py @@ -1,9 +1,10 @@ - import bpy +from bpy.types import Panel + from .buttons_panel import CAMButtonsPanel -class CAM_OPERATION_PROPERTIES_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_OPERATION_PROPERTIES_Panel(CAMButtonsPanel, Panel): """CAM operation properties panel""" bl_label = "CAM operation setup" bl_idname = "WORLD_PT_CAM_OPERATION" diff --git a/scripts/addons/cam/ui_panels/operations.py b/scripts/addons/cam/ui_panels/operations.py index e7d3973b0..7fa38ef27 100644 --- a/scripts/addons/cam/ui_panels/operations.py +++ b/scripts/addons/cam/ui_panels/operations.py @@ -1,5 +1,6 @@ - import bpy +from bpy.types import Panel + from .buttons_panel import CAMButtonsPanel # Operations panel @@ -12,7 +13,7 @@ # For each operation, generate the corresponding gcode and export the gcode file -class CAM_OPERATIONS_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_OPERATIONS_Panel(CAMButtonsPanel, Panel): """CAM operations panel""" bl_label = "CAM operations" bl_idname = "WORLD_PT_CAM_OPERATIONS" diff --git a/scripts/addons/cam/ui_panels/optimisation.py b/scripts/addons/cam/ui_panels/optimisation.py index ae1ac28e9..4b6490119 100644 --- a/scripts/addons/cam/ui_panels/optimisation.py +++ b/scripts/addons/cam/ui_panels/optimisation.py @@ -4,19 +4,23 @@ FloatProperty, IntProperty, ) +from bpy.types import ( + Panel, + PropertyGroup, +) from .buttons_panel import CAMButtonsPanel from ..utils import ( - update_operation, + opencamlib_version, update_exact_mode, - update_zbuffer_image, update_opencamlib, - opencamlib_version, + update_operation, + update_zbuffer_image, ) from ..constants import PRECISION -class CAM_OPTIMISATION_Properties(bpy.types.PropertyGroup): +class CAM_OPTIMISATION_Properties(PropertyGroup): optimize: BoolProperty( name="Reduce path points", @@ -96,7 +100,7 @@ class CAM_OPTIMISATION_Properties(bpy.types.PropertyGroup): ) -class CAM_OPTIMISATION_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_OPTIMISATION_Panel(CAMButtonsPanel, Panel): """CAM optimisation panel""" bl_label = "CAM optimisation" bl_idname = "WORLD_PT_CAM_OPTIMISATION" diff --git a/scripts/addons/cam/ui_panels/pack.py b/scripts/addons/cam/ui_panels/pack.py index acda772f1..6d2f3767e 100644 --- a/scripts/addons/cam/ui_panels/pack.py +++ b/scripts/addons/cam/ui_panels/pack.py @@ -1,9 +1,10 @@ - import bpy +from bpy.types import Panel + from .buttons_panel import CAMButtonsPanel -class CAM_PACK_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_PACK_Panel(CAMButtonsPanel, Panel): """CAM material panel""" bl_label = "Pack curves on sheet" bl_idname = "WORLD_PT_CAM_PACK" diff --git a/scripts/addons/cam/ui_panels/slice.py b/scripts/addons/cam/ui_panels/slice.py index 6f42868d3..10ef67029 100644 --- a/scripts/addons/cam/ui_panels/slice.py +++ b/scripts/addons/cam/ui_panels/slice.py @@ -1,9 +1,10 @@ - import bpy +from bpy.types import Panel + from .buttons_panel import CAMButtonsPanel -class CAM_SLICE_Panel(CAMButtonsPanel, bpy.types.Panel): +class CAM_SLICE_Panel(CAMButtonsPanel, Panel): """CAM slicer panel""" bl_label = "Slice model to plywood sheets" bl_idname = "WORLD_PT_CAM_SLICE" diff --git a/scripts/addons/cam/utils.py b/scripts/addons/cam/utils.py index e2ed68344..0b76428d7 100644 --- a/scripts/addons/cam/utils.py +++ b/scripts/addons/cam/utils.py @@ -21,39 +21,68 @@ # ***** 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 +from math import ( + ceil, + pi +) +from pathlib import Path +import pickle +import shutil +import sys import time -import mathutils -import math -from math import * -from mathutils import * -from bpy_extras import object_utils -import sys import numpy -import pickle +import shapely +from shapely import ops as sops +from shapely import geometry as sgeometry +from shapely.geometry import polygon as spolygon +from shapely.geometry import MultiPolygon -from .chunk import * -from .collision import * -from .simple import * -from .pattern import * -from .polygon_utils_cam import * -from .image_utils import * -from .exception import * +import bpy +from bpy.app.handlers import persistent +from bpy_extras import object_utils +from mathutils import Euler, Vector from .async_op import progress_async +from .cam_chunk import ( + curveToChunks, + parentChild, + camPathChunk, + camPathChunkBuilder, + parentChildDist, + chunksToShapely +) +from .collision import ( + getSampleBullet, + getSampleBulletNAxis, + prepareBulletCollision +) +from .exception import CamException +from .image_utils import ( + imageToChunks, + getSampleImage, + renderSampleImage, + prepareArea, +) from .opencamlib.opencamlib import ( oclSample, - oclSamplePoints, oclResampleChunks, - oclGetWaterline ) - -from shapely.geometry import polygon as spolygon -from shapely.geometry import MultiPolygon -from shapely import ops as sops -from shapely import geometry as sgeometry +from .polygon_utils_cam import shapelyToCurve +from .simple import ( + activate, + progress, + select_multiple, + delob, + timingadd, + timinginit, + timingstart, + tuple_add, + tuple_mul, + tuple_sub, + isVerticalLimit, + getCachePath +) # from shapely.geometry import * not possible until Polygon libs gets out finally.. SHAPELY = True @@ -959,8 +988,8 @@ def polygonConvexHull(context): c = (v.co.x, v.co.y) coords.append(c) - simple.select_multiple('_tmp') # delete temporary mesh - simple.select_multiple('ConvexHull') # delete old hull + select_multiple('_tmp') # delete temporary mesh + select_multiple('ConvexHull') # delete old hull # convert coordinates to shapely MultiPoint datastructure points = sgeometry.MultiPoint(coords) @@ -973,9 +1002,8 @@ def polygonConvexHull(context): def Helix(r, np, zstart, pend, rev): c = [] - pi = math.pi - v = mathutils.Vector((r, 0, zstart)) - e = mathutils.Euler((0, 0, 2.0 * pi / np)) + v = Vector((r, 0, zstart)) + e = Euler((0, 0, 2.0 * pi / np)) zstep = (zstart - pend[2]) / (np * rev) for a in range(0, int(np * rev)): c.append((v.x + pend[0], v.y + pend[1], zstart - (a * zstep))) @@ -1707,7 +1735,7 @@ def rotTo2axes(e, axescombination): # print(v) # print(bangle) - return (angle1, angle2) + # return (angle1, angle2) def reload_paths(o): @@ -1786,7 +1814,7 @@ def setup_operation_preset(): def updateMachine(self, context): print('update machine ') - if not utils._IS_LOADING_DEFAULTS: + if not _IS_LOADING_DEFAULTS: addMachineAreaObject() @@ -1909,7 +1937,7 @@ def updateChipload(self, context): # underestanding of python or programming in genereal. Hopefuly some one can have a look at this and with any luck # we will be one tiny step on the way to a slightly better chipload calculating function. - # self.chipload = ((0.5*(o.cutter_diameter/o.dist_between_paths))/(math.sqrt((o.feedrate*1000)/(o.spindle_rpm*o.cutter_diameter*o.cutter_flutes)*(o.cutter_diameter/o.dist_between_paths)-1))) + # self.chipload = ((0.5*(o.cutter_diameter/o.dist_between_paths))/(sqrt((o.feedrate*1000)/(o.spindle_rpm*o.cutter_diameter*o.cutter_flutes)*(o.cutter_diameter/o.dist_between_paths)-1))) print(o.info.chipload) @@ -2067,3 +2095,88 @@ def update_zbuffer_image(self, context): # from . import updateZbufferImage active_op = bpy.context.scene.cam_operations[bpy.context.scene.cam_active_operation] updateZbufferImage(active_op, bpy.context) + + +# Moved from init - part 3 + +@bpy.app.handlers.persistent +def check_operations_on_load(context): + """checks any broken computations on load and reset them.""" + s = bpy.context.scene + for o in s.cam_operations: + if o.computing: + o.computing = False + # set interface level to previously used level for a new file + if not bpy.data.filepath: + _IS_LOADING_DEFAULTS = True + s.interface.level = bpy.context.preferences.addons["cam"].preferences.default_interface_level + machine_preset = bpy.context.preferences.addons[ + "cam"].preferences.machine_preset = bpy.context.preferences.addons["cam"].preferences.default_machine_preset + if len(machine_preset) > 0: + print("Loading preset:", machine_preset) + # load last used machine preset + bpy.ops.script.execute_preset( + filepath=machine_preset, menu_idname="CAM_MACHINE_MT_presets" + ) + _IS_LOADING_DEFAULTS = False + # check for updated version of the plugin + bpy.ops.render.cam_check_updates() + # copy presets if not there yet + if bpy.context.preferences.addons["cam"].preferences.just_updated: + preset_source_path = Path(__file__).parent / "presets" + preset_target_path = Path(bpy.utils.script_path_user()) / "presets" + + def copy_if_not_exists(src, dst): + if Path(dst).exists() == False: + shutil.copy2(src, dst) + + shutil.copytree( + preset_source_path, + preset_target_path, + copy_function=copy_if_not_exists, + dirs_exist_ok=True, + ) + + bpy.context.preferences.addons["cam"].preferences.just_updated = False + bpy.ops.wm.save_userpref() + + if not bpy.context.preferences.addons["cam"].preferences.op_preset_update: + # Update the Operation presets + op_presets_source = Path(__file__).parent / "presets" / "cam_operations" + op_presets_target = Path(bpy.utils.script_path_user()) / "presets" / "cam_operations" + shutil.copytree(op_presets_source, op_presets_target, dirs_exist_ok=True) + bpy.context.preferences.addons["cam"].preferences.op_preset_update = True + + +# 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 + 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' diff --git a/scripts/addons/cam/voronoi.py b/scripts/addons/cam/voronoi.py index 58cc84d41..eb9ecdf17 100644 --- a/scripts/addons/cam/voronoi.py +++ b/scripts/addons/cam/voronoi.py @@ -63,7 +63,6 @@ ############################################################################# import math import sys -import getopt TOLERANCE = 1e-9 BIG_FLOAT = 1e38