diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 000000000..9bb4f94c7 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 225a8c2965d4f941623896ca75e9fae8 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/_modules/cam.html b/_modules/cam.html new file mode 100644 index 000000000..0b0c15078 --- /dev/null +++ b/_modules/cam.html @@ -0,0 +1,816 @@ + + + + + + + + + + cam — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam

+"""CNC CAM '__init__.py' © 2012 Vilem Novak
+
+Import Modules, Register and Unregister Classes
+"""
+
+# Python Standard Library
+import subprocess
+import sys
+
+# pip Wheels
+import shapely
+import opencamlib
+
+# Blender Library
+import bpy
+from bpy.props import (
+    CollectionProperty,
+    IntProperty,
+    PointerProperty,
+    StringProperty,
+)
+from bpy_extras.object_utils import object_data_add
+
+# 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 (
+    CNCCAM_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 .pie_menu.pie_cam import VIEW3D_MT_PIE_CAM
+from .pie_menu.pie_chains import VIEW3D_MT_PIE_Chains
+from .pie_menu.pie_curvecreators import VIEW3D_MT_PIE_CurveCreators
+from .pie_menu.pie_curvetools import VIEW3D_MT_PIE_CurveTools
+from .pie_menu.pie_info import VIEW3D_MT_PIE_Info
+from .pie_menu.pie_machine import VIEW3D_MT_PIE_Machine
+from .pie_menu.pie_material import VIEW3D_MT_PIE_Material
+from .pie_menu.pie_pack_slice_relief import VIEW3D_MT_PIE_PackSliceRelief
+from .pie_menu.active_op.pie_area import VIEW3D_MT_PIE_Area
+from .pie_menu.active_op.pie_cutter import VIEW3D_MT_PIE_Cutter
+from .pie_menu.active_op.pie_feedrate import VIEW3D_MT_PIE_Feedrate
+from .pie_menu.active_op.pie_gcode import VIEW3D_MT_PIE_Gcode
+from .pie_menu.active_op.pie_movement import VIEW3D_MT_PIE_Movement
+from .pie_menu.active_op.pie_operation import VIEW3D_MT_PIE_Operation
+from .pie_menu.active_op.pie_optimisation import VIEW3D_MT_PIE_Optimisation
+from .pie_menu.active_op.pie_setup import VIEW3D_MT_PIE_Setup
+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,
+)
+
+
+
+[docs] +classes = [ + # CamBackgroundMonitor + + # .autoupdate + UpdateSourceOperator, + Updater, + UpdateChecker, + + # .chain + opReference, + camChain, + + # .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 + CNCCAM_ENGINE, + + # .machine_settings + machineSettings, + + # .ops + 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, + + # .pack + PackObjectsSettings, + + # .preferences + CamAddonPreferences, + + # .preset_managers + CAM_CUTTER_MT_presets, + CAM_OPERATION_MT_presets, + CAM_MACHINE_MT_presets, + AddPresetCamCutter, + AddPresetCamOperation, + AddPresetCamMachine, + + # .slice + SliceObjectsSettings, + + # .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, + + # .pie_menu and .pie_menu.active_op - placed after .ui in case inheritance is possible + VIEW3D_MT_PIE_CAM, + VIEW3D_MT_PIE_Machine, + VIEW3D_MT_PIE_Material, + VIEW3D_MT_PIE_Operation, + VIEW3D_MT_PIE_Chains, + VIEW3D_MT_PIE_Setup, + VIEW3D_MT_PIE_Optimisation, + VIEW3D_MT_PIE_Area, + VIEW3D_MT_PIE_Movement, + VIEW3D_MT_PIE_Feedrate, + VIEW3D_MT_PIE_Cutter, + VIEW3D_MT_PIE_Gcode, + VIEW3D_MT_PIE_Info, + VIEW3D_MT_PIE_PackSliceRelief, + VIEW3D_MT_PIE_CurveCreators, + VIEW3D_MT_PIE_CurveTools, + + # .cam_operation - last to allow dependencies to register before it + camOperation, +]
+ + + +
+[docs] +def register() -> None: + for cls in classes: + bpy.utils.register_class(cls) + + basrelief.register() + + 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", + ) + scene.cam_active_operation = IntProperty( + name="CAM Active Operation", + description="The selected operation", + update=updateOperation, + ) + scene.cam_chains = CollectionProperty( + type=camChain, + ) + scene.cam_import_gcode = PointerProperty( + type=import_settings, + ) + scene.cam_machine = PointerProperty( + type=machineSettings, + ) + scene.cam_operations = CollectionProperty( + type=camOperation, + ) + scene.cam_pack = PointerProperty( + type=PackObjectsSettings, + ) + scene.cam_slice = PointerProperty( + type=SliceObjectsSettings, + ) + scene.cam_text = StringProperty() + scene.interface = PointerProperty( + type=CAM_INTERFACE_Properties, + ) + + for panel in get_panels(): + panel.COMPAT_ENGINES.add("CNCCAM_RENDER") + + wm = bpy.context.window_manager + addon_kc = wm.keyconfigs.addon + + km = addon_kc.keymaps.new(name='Object Mode') + kmi = km.keymap_items.new( + "wm.call_menu_pie", + 'C', + 'PRESS', + alt=True, + ) + kmi.properties.name = 'VIEW3D_MT_PIE_CAM' + kmi.active = True
+ + + +
+[docs] +def unregister() -> None: + for cls in classes: + bpy.utils.unregister_class(cls) + + basrelief.unregister() + + scene = bpy.types.Scene + + # 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 'CNCCAM_RENDER' in panel.COMPAT_ENGINES: + panel.COMPAT_ENGINES.remove('CNCCAM_RENDER') + + wm = bpy.context.window_manager + active_kc = wm.keyconfigs.active + + for key in active_kc.keymaps['Object Mode'].keymap_items: + if (key.idname == 'wm.call_menu' and key.properties.name == 'VIEW3D_MT_PIE_CAM'): + active_kc.keymaps['Object Mode'].keymap_items.remove(key)
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/autoupdate.html b/_modules/cam/autoupdate.html new file mode 100644 index 000000000..221f61039 --- /dev/null +++ b/_modules/cam/autoupdate.html @@ -0,0 +1,635 @@ + + + + + + + + + + cam.autoupdate — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.autoupdate

+"""CNC CAM 'autoupdate.py'
+
+Classes to check for, download and install CNC CAM updates.
+"""
+
+import calendar
+from datetime import date
+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
+
+from .version import __version__ as current_version
+
+
+
+[docs] +class UpdateChecker(bpy.types.Operator): + """Check for Updates""" +
+[docs] + bl_idname = "render.cam_check_updates"
+ +
+[docs] + bl_label = "Check for Updates in CNC CAM Plugin"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + def execute(self, context): + addon_prefs = context.preferences.addons[__package__].preferences + if bpy.app.background: + return {"FINISHED"} + last_update_check = addon_prefs.last_update_check + today = date.today().toordinal() + update_source = addon_prefs.update_source + match = re.match(r"https://github.com/([^/]+/[^/]+)", update_source) + if match: + update_source = f"https://api.github.com/repos/{match.group(1)}/releases" + + print(f"Update Check: {update_source}") + if update_source == "None" or len(update_source) == 0: + return {'FINISHED'} + + addon_prefs.new_version_available = "" + bpy.ops.wm.save_userpref() + # get list of releases from github release + if update_source.endswith("/releases"): + with urlopen(update_source, timeout=2.0) as response: + body = response.read().decode("UTF-8") + # find the tag name + release_list = json.loads(body) + if len(release_list) > 0: + release = release_list[0] + tag = release["tag_name"] + print(f"Found Release: {tag}") + match = re.match(r".*(\d+)\.(\s*\d+)\.(\s*\d+)", tag) + if match: + version_num = tuple(map(int, match.groups())) + print(f"Found version: {version_num}") + addon_prefs.last_update_check = today + + if version_num > current_version: + addon_prefs.new_version_available = ".".join( + [str(x) for x in version_num]) + bpy.ops.wm.save_userpref() + elif update_source.endswith("/commits"): + with urlopen(update_source+"?per_page=1", timeout=2) as response: + body = response.read().decode("UTF-8") + # find the tag name + commit_list = json.loads(body) + commit_sha = commit_list[0]['sha'] + commit_date = commit_list[0]['commit']['author']['date'] + if addon_prefs.last_commit_hash != commit_sha: + addon_prefs.new_version_available = commit_date + bpy.ops.wm.save_userpref() + return {'FINISHED'}
+
+ + + +
+[docs] +class Updater(bpy.types.Operator): + """Update to Newer Version if Possible""" +
+[docs] + bl_idname = "render.cam_update_now"
+ +
+[docs] + bl_label = "Update"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + def execute(self, context): + addon_prefs = context.preferences.addons[__package__].preferences + print("Update Check") + last_update_check = addon_prefs.last_update_check + today = date.today().toordinal() + update_source = addon_prefs.update_source + if update_source == "None" or len(update_source) == 0: + return {'FINISHED'} + match = re.match(r"https://github.com/([^/]+/[^/]+)", update_source) + if match: + update_source = f"https://api.github.com/repos/{match.group(1)}/releases" + + # get list of releases from github release + if update_source.endswith("/releases"): + with urlopen(update_source, timeout=2) as response: + body = response.read().decode("UTF-8") + # find the tag name + release_list = json.loads(body) + if len(release_list) > 0: + release = release_list[0] + tag = release["tag_name"] + print(f"Found Release: {tag}") + match = re.match(r".*(\d+)\.(\s*\d+)\.(\s*\d+)", tag) + if match: + version_num = tuple(map(int, match.groups())) + print(f"Found version: {version_num}") + addon_prefs.last_update_check = today + bpy.ops.wm.save_userpref() + + if version_num > current_version: + print("Version Is Newer, Downloading Source") + zip_url = release["zipball_url"] + self.install_zip_from_url(zip_url) + return {'FINISHED'} + elif update_source.endswith("/commits"): + with urlopen(update_source+"?per_page=1", timeout=2) as response: + body = response.read().decode("UTF-8") + # find the tag name + commit_list = json.loads(body) + commit_sha = commit_list[0]['sha'] + if addon_prefs.last_commit_hash != commit_sha: + # get zipball from this commit + zip_url = update_source.replace( + "/commits", f"/zipball/{commit_sha}") + self.install_zip_from_url(zip_url) + addon_prefs.last_commit_hash = commit_sha + bpy.ops.wm.save_userpref() + return {'FINISHED'}
+ + +
+[docs] + def install_zip_from_url(self, zip_url): + addon_prefs = bpy.context.preferences.addons[__package__].preferences + with urlopen(zip_url) as zip_response: + zip_body = zip_response.read() + buffer = io.BytesIO(zip_body) + zf = zipfile.ZipFile(buffer, mode='r') + files = zf.infolist() + cam_addon_path = pathlib.Path(__file__).parent + for fileinfo in files: + filename = fileinfo.filename + if fileinfo.is_dir() == False: + path_pos = filename.replace("\\", "/").find("/scripts/addons/cam/") + if path_pos != -1: + relative_path = filename[path_pos + len("/scripts/addons/cam/"):] + out_path = cam_addon_path / relative_path + print(out_path) + # check folder exists + out_path.parent.mkdir(parents=True, exist_ok=True) + with zf.open(filename, "r") as in_file, open(out_path, "wb") as out_file: + time_struct = (*fileinfo.date_time, 0, 0, 0) + mtime = calendar.timegm(time_struct) + out_file.write(in_file.read()) + os.utime(out_path, times=(mtime, mtime)) + # TODO: check for newer times + # TODO: what about if a file is deleted... + # updated everything, now mark as updated and reload scripts + addon_prefs.just_updated = True + addon_prefs.new_version_available = "" + bpy.ops.wm.save_userpref() + # unload ourself from python module system + delete_list = [] + for m in sys.modules.keys(): + if m.startswith("cam.") or m == 'cam': + delete_list.append(m) + for d in delete_list: + del sys.modules[d] + bpy.ops.script.reload()
+
+ + + +
+[docs] +class UpdateSourceOperator(bpy.types.Operator): +
+[docs] + bl_idname = "render.cam_set_update_source"
+ +
+[docs] + bl_label = "Set CNC CAM Update Source"
+ + +
+[docs] + new_source: StringProperty( + default='', + )
+ + +
+[docs] + def execute(self, context): + context.preferences.addons[__package__].preferences.update_source = self.new_source + bpy.ops.wm.save_userpref() + return {'FINISHED'}
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/basrelief.html b/_modules/cam/basrelief.html new file mode 100644 index 000000000..31f9fc7c1 --- /dev/null +++ b/_modules/cam/basrelief.html @@ -0,0 +1,2480 @@ + + + + + + + + + + cam.basrelief — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.basrelief

+"""CNC CAM 'basrelief.py'
+
+Module to allow the creation of reliefs from Images or View Layers.
+(https://en.wikipedia.org/wiki/Relief#Bas-relief_or_low_relief)
+"""
+
+from math import (
+    ceil,
+    floor,
+    sqrt
+)
+import re
+import time
+
+import numpy
+
+import bpy
+from bpy.props import (
+    BoolProperty,
+    EnumProperty,
+    FloatProperty,
+    IntProperty,
+    PointerProperty,
+    StringProperty,
+)
+
+
+# ////////////////////////////////////////////////////////////////////
+# // Full Multigrid Algorithm for solving partial differential equations
+# //////////////////////////////////////////////////////////////////////
+# MODYF = 0 #/* 1 or 0 (1 is better) */
+# MINS = 16	#/* minimum size 4 6 or 100 */
+
+
+# SMOOTH_IT = 2 #/* minimum 1  */
+# V_CYCLE = 10 #/* number of v-cycles  2*/
+#ITERATIONS = 5
+
+# // precision
+
+[docs] +EPS = 1.0e-32
+ +
+[docs] +PRECISION = 5
+ +
+[docs] +NUMPYALG = False
+ +# PLANAR_CONST=True + + +
+[docs] +def copy_compbuf_data(inbuf, outbuf): + outbuf[:] = inbuf[:]
+ + + +
+[docs] +def restrictbuf(inbuf, outbuf): + """Restrict the resolution of an input buffer to match an output buffer. + + This function scales down the input buffer `inbuf` to fit the dimensions + of the output buffer `outbuf`. It computes the average of the + neighboring pixels in the input buffer to create a downsampled version + in the output buffer. The method used for downsampling can vary based on + the dimensions of the input and output buffers, utilizing either a + simple averaging method or a more complex numpy-based approach. + + Args: + inbuf (numpy.ndarray): The input buffer to be downsampled, expected to be + a 2D array. + outbuf (numpy.ndarray): The output buffer where the downsampled result will + be stored, also expected to be a 2D array. + + Returns: + None: The function modifies `outbuf` in place. + """ + # scale down array.... + + inx = inbuf.shape[0] + iny = inbuf.shape[1] + + outx = outbuf.shape[0] + outy = outbuf.shape[1] + + dx = inx/outx + dy = iny/outy + + filterSize = 0.5 + xfiltersize = dx*filterSize + + sy = dy/2-0.5 + if dx == 2 and dy == 2: # much simpler method + # if dx<2: + # restricted= + # num=restricted.shape[0]*restricted.shape[1] + outbuf[:] = (inbuf[::2, ::2]+inbuf[1::2, ::2] + + inbuf[::2, 1::2]+inbuf[1::2, 1::2])/4.0 + + elif NUMPYALG: # numpy method + yrange = numpy.arange(0, outy) + xrange = numpy.arange(0, outx) + + w = 0 + sx = dx/2-0.5 + + sxrange = xrange*dx+sx + syrange = yrange*dy+sy + + sxstartrange = numpy.array(numpy.ceil(sxrange-xfiltersize), dtype=int) + sxstartrange[sxstartrange < 0] = 0 + sxendrange = numpy.array(numpy.floor(sxrange+xfiltersize)+1, dtype=int) + sxendrange[sxendrange > inx] = inx + + systartrange = numpy.array(numpy.ceil(syrange-xfiltersize), dtype=int) + systartrange[systartrange < 0] = 0 + syendrange = numpy.array(numpy.floor(syrange+xfiltersize)+1, dtype=int) + syendrange[syendrange > iny] = iny + #np.arange(8*6*3).reshape((8, 6, 3)) + + # 3is the maximum value...?pff. + indices = numpy.arange(outx*outy*2*3).reshape((2, outx*outy, 3)) + + r = sxendrange-sxstartrange + + indices[0] = sxstartrange.repeat(outy) + + indices[1] = systartrange.repeat(outx).reshape( + outx, outy).swapaxes(0, 1).flatten() + + # systartrange=numpy.max(0,numpy.ceil(syrange-xfiltersize)) + # syendrange=numpy.min(numpy.floor(syrange+xfiltersize),iny-1)+1 + + outbuf.fill(0) + tempbuf = inbuf[indices[0], indices[1]] + tempbuf += inbuf[indices[0]+1, indices[1]] + tempbuf += inbuf[indices[0], indices[1]+1] + tempbuf += inbuf[indices[0]+1, indices[1]+1] + tempbuf /= 4.0 + outbuf[:] = tempbuf.reshape((outx, outy)) + # outbuf[:,:]=inbuf[]#inbuf[sxstartrange,systartrange] #+ inbuf[sxstartrange+1,systartrange] + inbuf[sxstartrange,systartrange+1] + inbuf[sxstartrange+1,systartrange+1])/4.0 + + else: # old method + for y in range(0, outy): + + sx = dx/2-0.5 + for x in range(0, outx): + pixVal = 0 + w = 0 + + # + for ix in range(max(0, ceil(sx-dx*filterSize)), min(floor(sx+dx*filterSize), inx-1)+1): + for iy in range(max(0, ceil(sy-dx*filterSize)), min(floor(sy+dx*filterSize), iny-1)+1): + pixVal += inbuf[ix, iy] + w += 1 + outbuf[x, y] = pixVal/w + + sx += dx + sy += dy
+ + + +
+[docs] +def prolongate(inbuf, outbuf): + """Prolongate an input buffer to a larger output buffer. + + This function takes an input buffer and enlarges it to fit the + dimensions of the output buffer. It uses different methods to achieve + this based on the scaling factors derived from the input and output + dimensions. The function can handle specific cases where the scaling + factors are exactly 0.5, as well as a general case that applies a + bilinear interpolation technique for resizing. + + Args: + inbuf (numpy.ndarray): The input buffer to be enlarged, expected to be a 2D array. + outbuf (numpy.ndarray): The output buffer where the enlarged data will be stored, + expected to be a 2D array of larger dimensions than inbuf. + """ + + + inx = inbuf.shape[0] + iny = inbuf.shape[1] + + outx = outbuf.shape[0] + outy = outbuf.shape[1] + + dx = inx/outx + dy = iny/outy + + filterSize = 1 + xfiltersize = dx*filterSize + # outx[:]= + + # outbuf.put(inbuf.repeat(4)) + if dx == 0.5 and dy == 0.5: + outbuf[::2, ::2] = inbuf + outbuf[1::2, ::2] = inbuf + outbuf[::2, 1::2] = inbuf + outbuf[1::2, 1::2] = inbuf + # x=inbuf::.flatten().repeat(2) + elif NUMPYALG: # numpy method + sy = -dy/2 + sx = -dx/2 + xrange = numpy.arange(0, outx) + yrange = numpy.arange(0, outy) + + sxrange = xrange*dx+sx + syrange = yrange*dy+sy + + sxstartrange = numpy.array(numpy.ceil(sxrange-xfiltersize), dtype=int) + sxstartrange[sxstartrange < 0] = 0 + sxendrange = numpy.array(numpy.floor(sxrange+xfiltersize)+1, dtype=int) + sxendrange[sxendrange >= inx] = inx-1 + systartrange = numpy.array(numpy.ceil(syrange-xfiltersize), dtype=int) + systartrange[systartrange < 0] = 0 + syendrange = numpy.array(numpy.floor(syrange+xfiltersize)+1, dtype=int) + syendrange[syendrange >= iny] = iny-1 + + indices = numpy.arange(outx*outy*2).reshape((2, outx*outy)) + indices[0] = sxstartrange.repeat(outy) + indices[1] = systartrange.repeat(outx).reshape( + outx, outy).swapaxes(0, 1).flatten() + + # systartrange=numpy.max(0,numpy.ceil(syrange-xfiltersize)) + # syendrange=numpy.min(numpy.floor(syrange+xfiltersize),iny-1)+1 + # outbuf.fill(0) + tempbuf = inbuf[indices[0], indices[1]] + # tempbuf+=inbuf[indices[0]+1,indices[1]] + # tempbuf+=inbuf[indices[0],indices[1]+1] + # tempbuf+=inbuf[indices[0]+1,indices[1]+1] + tempbuf /= 4.0 + outbuf[:] = tempbuf.reshape((outx, outy)) + + # outbuf.fill(0) + # outbuf[xrange,yrange]=inbuf[sxstartrange,systartrange]# + inbuf[sxendrange,systartrange] + inbuf[sxstartrange,syendrange] + inbuf[sxendrange,syendrange])/4.0 + + else: + sy = -dy/2 + for y in range(0, outy): + sx = -dx/2 + for x in range(0, outx): + pixVal = 0 + weight = 0 + + for ix in range(max(0, ceil(sx-filterSize)), min(floor(sx+filterSize), inx-1)+1): + for iy in range(max(0, ceil(sy-filterSize)), min(floor(sy+filterSize), iny-1)+1): + fx = abs(sx - ix) + fy = abs(sy - iy) + + fval = (1-fx)*(1-fy) + + pixVal += inbuf[ix, iy] * fval + weight += fval + # if weight==0: + # print('error' ) + # return + outbuf[x, y] = pixVal/weight + sx += dx + sy += dy
+ + + +
+[docs] +def idx(r, c, cols): + + return r*cols+c+1
+ + + +# smooth u using f at level +
+[docs] +def smooth(U, F, linbcgiterations, planar): + """Smooth a matrix U using a filter F at a specified level. + + This function applies a smoothing operation on the input matrix U using + the filter F. It utilizes the linear Biconjugate Gradient method for the + smoothing process. The number of iterations for the linear BCG method is + specified by linbcgiterations, and the planar parameter indicates + whether the operation is to be performed in a planar manner. + + Args: + U (numpy.ndarray): The input matrix to be smoothed. + F (numpy.ndarray): The filter used for smoothing. + linbcgiterations (int): The number of iterations for the linear BCG method. + planar (bool): A flag indicating whether to perform the operation in a planar manner. + + Returns: + None: This function modifies the input matrix U in place. + """ + + + iter = 0 + err = 0 + + rows = U.shape[1] + cols = U.shape[0] + + n = U.size + + linbcg(n, F, U, 2, 0.001, linbcgiterations, iter, err, rows, cols, planar)
+ + + +
+[docs] +def calculate_defect(D, U, F): + """Calculate the defect of a grid based on the input fields. + + This function computes the defect values for a grid by comparing the + input field `F` with the values in the grid `U`. The defect is + calculated using finite difference approximations, taking into account + the neighboring values in the grid. The results are stored in the output + array `D`, which is modified in place. + + Args: + D (ndarray): A 2D array where the defect values will be stored. + U (ndarray): A 2D array representing the current state of the grid. + F (ndarray): A 2D array representing the target field to compare against. + + Returns: + None: The function modifies the array `D` in place and does not return a + value. + """ + + + sx = F.shape[0] + sy = F.shape[1] + + h = 1.0/sqrt(sx*sy*1.0) + h2i = 1.0/(h*h) + + h2i = 1 + D[1:-1, 1:-1] = F[1:-1, 1:-1] - U[:-2, 1:-1] - U[2:, 1:-1] - \ + U[1:-1, :-2] - U[1:-1, 2:] + 4*U[1:-1, 1:-1] + # sides + D[1:-1, 0] = F[1:-1, 0] - U[:-2, 0] - U[2:, 0] - U[1:-1, 1] + 3*U[1:-1, 0] + D[1:-1, -1] = F[1:-1, -1] - U[:-2, -1] - \ + U[2:, -1] - U[1:-1, -2] + 3*U[1:-1, -1] + D[0, 1:-1] = F[0, 1:-1] - U[0, :-2] - U[0, :-2] - U[1, 1:-1] + 3*U[0, 1:-1] + D[-1, 1:-1] = F[-1, 1:-1] - U[-1, :-2] - \ + U[-1, :-2] - U[-1, 1:-1] + 3*U[-1, 1:-1] + # coners + D[0, 0] = F[0, 0] - U[0, 1] - U[1, 0] + 2*U[0, 0] + D[0, -1] = F[0, -1] - U[1, -1] - U[0, -2] + 2*U[0, -1] + D[-1, 0] = F[-1, 0] - U[-2, 0] - U[-1, 1] + 2*U[-1, 0] + D[-1, -1] = F[-1, -1] - U[-2, -1] - U[-1, -2] + 2*U[-1, -1]
+ + + # for y in range(0,sy): + # for x in range(0,sx): + # + # w = max(0,x-1) + # n = max(0,y-1) + # e = min(sx, x+1) + # s = min(sy, y+1) + # + # + # D[x,y] = F[x,y] -( U[e,y] + U[w,y] + U[x,n] + U[x,s] - 4.0*U[x,y]) + + +
+[docs] +def add_correction(U, C): + U += C
+ + +# def alloc_compbuf(xmax,ymax,pix, 1): +# ar=numpy.array() + + +
+[docs] +def solve_pde_multigrid(F, U, vcycleiterations, linbcgiterations, smoothiterations, mins, levels, useplanar, planar): + """Solve a partial differential equation using a multigrid method. + + This function implements a multigrid algorithm to solve a given partial + differential equation (PDE). It operates on a grid of varying + resolutions, applying smoothing and correction steps iteratively to + converge towards the solution. The algorithm consists of several key + phases: restriction of the right-hand side to coarser grids, solving on + the coarsest grid, and then interpolating corrections back to finer + grids. The process is repeated for a specified number of V-cycle + iterations. + + Args: + F (numpy.ndarray): The right-hand side of the PDE represented as a 2D array. + U (numpy.ndarray): The initial guess for the solution, which will be updated in place. + vcycleiterations (int): The number of V-cycle iterations to perform. + linbcgiterations (int): The number of iterations for the linear solver used in smoothing. + smoothiterations (int): The number of smoothing iterations to apply at each level. + mins (int): Minimum grid size (not used in the current implementation). + levels (int): The number of levels in the multigrid hierarchy. + useplanar (bool): A flag indicating whether to use planar information during the solution + process. + planar (numpy.ndarray): A 2D array indicating planar information for the grid. + + Returns: + None: The function modifies the input array U in place to contain the final + solution. + + Note: + The function assumes that the input arrays F and U have compatible + shapes + and that the planar array is appropriately defined for the problem + context. + """ + + + xmax = F.shape[0] + ymax = F.shape[1] + + # int i # index for simple loops + # int k # index for iterating through levels + # int k2 # index for iterating through levels in V-cycles + + # 1. restrict f to coarse-grid (by the way count the number of levels) + # k=0: fine-grid = f + # k=levels: coarsest-grid + # pix = CB_VAL#what is this>??? + # int cycle + # int sx, sy + + RHS = [] + IU = [] + VF = [] + PLANAR = [] + for a in range(0, levels+1): + RHS.append(None) + IU.append(None) + VF.append(None) + PLANAR.append(None) + VF[0] = numpy.zeros((xmax, ymax), dtype=numpy.float64) + # numpy.fill(pix)!? TODO + + RHS[0] = F.copy() + IU[0] = U.copy() + PLANAR[0] = planar.copy() + + sx = xmax + sy = ymax + # print(planar) + for k in range(0, levels): + # calculate size of next level + sx = int(sx/2) + sy = int(sy/2) + PLANAR[k+1] = numpy.zeros((sx, sy), dtype=numpy.float64) + RHS[k+1] = numpy.zeros((sx, sy), dtype=numpy.float64) + IU[k+1] = numpy.zeros((sx, sy), dtype=numpy.float64) + VF[k+1] = numpy.zeros((sx, sy), dtype=numpy.float64) + + # restrict from level k to level k+1 (coarser-grid) + restrictbuf(PLANAR[k], PLANAR[k+1]) + PLANAR[k+1] = PLANAR[k+1] > 0 + # numpytoimage(PLANAR[k+1],'planar') + # print(PLANAR[k+1]) + restrictbuf(RHS[k], RHS[k+1]) + # numpytoimage(RHS[k+1],'rhs') + + # 2. find exact sollution at the coarsest-grid (k=levels) + # this was replaced to easify code. exact_sollution( RHS[levels], IU[levels] ) + IU[levels].fill(0.0) + + # 3. nested iterations + + for k in range(levels-1, -1, -1): + print('K:', str(k)) + + # 4. interpolate sollution from last coarse-grid to finer-grid + # interpolate from level k+1 to level k (finer-grid) + prolongate(IU[k+1], IU[k]) + # print('k',k) + # 4.1. first target function is the equation target function + # (following target functions are the defect) + copy_compbuf_data(RHS[k], VF[k]) + + #print('lanar ') + + # 5. V-cycle (twice repeated) + + for cycle in range(0, vcycleiterations): + print('v-cycle iteration:', str(cycle)) + + # 6. downward stroke of V + for k2 in range(k, levels): + # 7. pre-smoothing of initial sollution using target function + # zero for initial guess at smoothing + # (except for level k when iu contains prolongated result) + if(k2 != k): + IU[k2].fill(0.0) + + for i in range(0, smoothiterations): + smooth(IU[k2], VF[k2], linbcgiterations, PLANAR[k2]) + + # 8. calculate defect at level + # d[k2] = Lh * ~u[k2] - f[k2] + + D = numpy.zeros_like(IU[k2]) + # if k2==0: + # IU[k2][planar[k2]]=IU[k2].max() + # print(IU[0]) + if useplanar and k2 == 0: + IU[k2][PLANAR[k2]] = IU[k2].min() + # if k2==0 : + + # VF[k2][PLANAR[k2]]=0.0 + # print(IU[0]) + calculate_defect(D, IU[k2], VF[k2]) + + # 9. restrict deffect as target function for next coarser-grid + # def -> f[k2+1] + restrictbuf(D, VF[k2+1]) + + # 10. solve on coarsest-grid (target function is the deffect) + # iu[levels] should contain sollution for + # the f[levels] - last deffect, iu will now be the correction + IU[levels].fill(0.0) # exact_sollution(VF[levels], IU[levels] ) + + # 11. upward stroke of V + for k2 in range(levels-1, k-1, -1): + print('k2: ', str(k2)) + # 12. interpolate correction from last coarser-grid to finer-grid + # iu[k2+1] -> cor + C = numpy.zeros_like(IU[k2]) + prolongate(IU[k2+1], C) + + # 13. add interpolated correction to initial sollution at level k2 + add_correction(IU[k2], C) + + # 14. post-smoothing of current sollution using target function + for i in range(0, smoothiterations): + + smooth(IU[k2], VF[k2], linbcgiterations, PLANAR[k2]) + + if useplanar and k2 == 0: + IU[0][planar] = IU[0].min() + # print(IU[0]) + + # --- end of V-cycle + + # --- end of nested iteration + + # 15. final sollution + # IU[0] contains the final sollution + + U[:] = IU[0]
+ + + +
+[docs] +def asolve(b, x): + x[:] = -4*b
+ + + +
+[docs] +def atimes(x, res): + """Apply a discrete Laplacian operator to a 2D array. + + This function computes the discrete Laplacian of a given 2D array `x` + and stores the result in the `res` array. The Laplacian is calculated + using finite difference methods, which involve summing the values of + neighboring elements and applying specific boundary conditions for the + edges and corners of the array. + + Args: + x (numpy.ndarray): A 2D array representing the input values. + res (numpy.ndarray): A 2D array where the result will be stored. It must have the same shape + as `x`. + + Returns: + None: The result is stored directly in the `res` array. + """ + + res[1:-1, 1:-1] = x[:-2, 1:-1]+x[2:, 1:-1] + \ + x[1:-1, :-2]+x[1:-1, 2:] - 4*x[1:-1, 1:-1] + # sides + res[1:-1, 0] = x[0:-2, 0]+x[2:, 0]+x[1:-1, 1] - 3*x[1:-1, 0] + res[1:-1, -1] = x[0:-2, -1]+x[2:, -1]+x[1:-1, -2] - 3*x[1:-1, -1] + res[0, 1:-1] = x[0, :-2] + x[0, 2:] + x[1, 1:-1] - 3*x[0, 1:-1] + res[-1, 1:-1] = x[-1, :-2] + x[-1, 2:] + x[-2, 1:-1] - 3*x[-1, 1:-1] + # corners + res[0, 0] = x[1, 0]+x[0, 1]-2*x[0, 0] + res[-1, 0] = x[-2, 0]+x[-1, 1]-2*x[-1, 0] + res[0, -1] = x[0, -2]+x[1, -1]-2*x[0, -1] + res[-1, -1] = x[-1, -2]+x[-2, -1]-2*x[-1, -1]
+ + + +
+[docs] +def snrm(n, sx, itol): + """Calculate the square root of the sum of squares or the maximum absolute + value. + + This function computes a value based on the input parameters. If the + tolerance level (itol) is less than or equal to 3, it calculates the + square root of the sum of squares of the input array (sx). If the + tolerance level is greater than 3, it returns the maximum absolute value + from the input array. + + Args: + n (int): An integer parameter, though it is not used in the current + implementation. + sx (numpy.ndarray): A numpy array of numeric values. + itol (int): An integer that determines which calculation to perform. + + Returns: + float: The square root of the sum of squares if itol <= 3, otherwise the + maximum absolute value. + """ + + + if (itol <= 3): + temp = sx*sx + ans = temp.sum() + return sqrt(ans) + else: + temp = numpy.abs(sx) + return temp.max()
+ + +# /** +# * Biconjugate Gradient Method +# * from Numerical Recipes in C +# */ + + +
+[docs] +def linbcg(n, b, x, itol, tol, itmax, iter, err, rows, cols, planar): + """Solve a linear system using the Biconjugate Gradient Method. + + This function implements the Biconjugate Gradient Method as described in + Numerical Recipes in C. It iteratively refines the solution to a linear + system of equations defined by the matrix-vector product. The method is + particularly useful for large, sparse systems where direct methods are + inefficient. The function takes various parameters to control the + iteration process and convergence criteria. + + Args: + n (int): The size of the linear system. + b (numpy.ndarray): The right-hand side vector of the linear system. + x (numpy.ndarray): The initial guess for the solution vector. + itol (int): The type of norm to use for convergence checks. + tol (float): The tolerance for convergence. + itmax (int): The maximum number of iterations allowed. + iter (int): The current iteration count (should be initialized to 0). + err (float): The error estimate (should be initialized). + rows (int): The number of rows in the matrix. + cols (int): The number of columns in the matrix. + planar (bool): A flag indicating if the problem is planar. + + Returns: + None: The solution is stored in the input array `x`. + """ + + + p = numpy.zeros((cols, rows)) + pp = numpy.zeros((cols, rows)) + r = numpy.zeros((cols, rows)) + rr = numpy.zeros((cols, rows)) + z = numpy.zeros((cols, rows)) + zz = numpy.zeros((cols, rows)) + + iter = 0 + atimes(x, r) + r[:] = b-r + rr[:] = r + + atimes(r, rr) # minimum residual + + znrm = 1.0 + + if (itol == 1): + bnrm = snrm(n, b, itol) + + elif (itol == 2): + asolve(b, z) + bnrm = snrm(n, z, itol) + + elif (itol == 3 or itol == 4): + asolve(b, z) + bnrm = snrm(n, z, itol) + asolve(r, z) + znrm = snrm(n, z, itol) + else: + print("illegal itol in linbcg") + + asolve(r, z) + + while (iter <= itmax): + #print('linbcg iteration:', str(iter)) + iter += 1 + zm1nrm = znrm + asolve(rr, zz) + + bknum = 0.0 + + temp = z*rr + + bknum = temp.sum() # -z[0]*rr[0]???? + + if (iter == 1): + p[:] = z + pp[:] = zz + + else: + bk = bknum/bkden + p = bk*p+z + pp = bk*pp+zz + bkden = bknum + atimes(p, z) + temp = z*pp + akden = temp.sum() + ak = bknum/akden + atimes(pp, zz) + + x += ak*p + r -= ak*z + rr -= ak*zz + + asolve(r, z) + + if (itol == 1 or itol == 2): + znrm = 1.0 + err = snrm(n, r, itol)/bnrm + elif (itol == 3 or itol == 4): + znrm = snrm(n, z, itol) + if (abs(zm1nrm-znrm) > EPS*znrm): + dxnrm = abs(ak)*snrm(n, p, itol) + err = znrm/abs(zm1nrm-znrm)*dxnrm + else: + err = znrm/bnrm + continue + xnrm = snrm(n, x, itol) + + if (err <= 0.5*xnrm): + err /= xnrm + else: + err = znrm/bnrm + continue + if (err <= tol): + break
+ + # if PLANAR_CONST and planar.shape==rr.shape: + # x[planar]=0.0 + + +# -------------------------------------------------------------------- + + +
+[docs] +def numpysave(a, iname): + """Save a NumPy array as an image file in OpenEXR format. + + This function takes a NumPy array and saves it as an image file using + Blender's rendering capabilities. It configures the image settings to + use the OpenEXR format with black and white color mode and a color depth + of 32 bits. The rendered image is saved to the specified filename. + + Args: + a (numpy.ndarray): The NumPy array to be saved as an image. + iname (str): The filename (including path) where the image will be saved. + """ + + 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)
+ + + +
+[docs] +def numpytoimage(a, iname): + """Convert a NumPy array to a Blender image. + + This function takes a NumPy array and converts it into a Blender image. + It first checks if an image with the specified name and dimensions + already exists in Blender. If it does, that image is used; otherwise, a + new image is created with the specified name and dimensions. The + function then reshapes the NumPy array to match the image format and + assigns the pixel data to the image. + + Args: + a (numpy.ndarray): A 2D NumPy array representing the pixel data of the image. + iname (str): The name to assign to the Blender image. + + Returns: + bpy.types.Image: The Blender image created or modified with the pixel data from the NumPy + array. + """ + + t = time.time() + print('Numpy to Image - Here') + t = time.time() + print(a.shape[0], a.shape[1]) + foundimage = False + for image in bpy.data.images: + + if image.name[:len(iname)] == iname and image.size[0] == a.shape[0] and image.size[1] == a.shape[1]: + i = image + foundimage = True + if not foundimage: + bpy.ops.image.new(name=iname, width=a.shape[0], height=a.shape[1], color=( + 0, 0, 0, 1), alpha=True, generated_type='BLANK', float=True) + for image in bpy.data.images: + + if image.name[:len(iname)] == iname and image.size[0] == a.shape[0] and image.size[1] == a.shape[1]: + i = image + + d = a.shape[0]*a.shape[1] + a = a.swapaxes(0, 1) + a = a.reshape(d) + a = a.repeat(4) + a[3::4] = 1 + # i.pixels=a + i.pixels[:] = a[:] # this gives big speedup! + print('\ntime '+str(time.time()-t)) + return i
+ + + +
+[docs] +def imagetonumpy(i): + """Convert an image to a NumPy array. + + This function takes an image object and converts its pixel data into a + NumPy array. It first retrieves the pixel data from the image, then + reshapes and rearranges it to match the image's dimensions. The + resulting array is structured such that the height and width of the + image are preserved, and the color channels are appropriately ordered. + + Args: + i (Image): An image object that contains pixel data. + + Returns: + numpy.ndarray: A 2D NumPy array representing the pixel data of the image. + + Note: + The function optimizes performance by directly accessing pixel data + instead of using slower methods. + """ + + t = time.time() + inc = 0 + + width = i.size[0] + height = i.size[1] + x = 0 + y = 0 + count = 0 + na = numpy.array((0.1), dtype=float64) + + size = width*height + na.resize(size*4) + + # these 2 lines are about 15% faster than na=i.pixels[:].... whyyyyyyyy!!?!?!?!?! Blender image data access is evil. + p = i.pixels[:] + na[:] = p + # na=numpy.array(i.pixels[:])#this was terribly slow... at least I know why now, it probably + na = na[::4] + na = na.reshape(height, width) + na = na.swapaxes(0, 1) + + print('\ntime of image to numpy '+str(time.time()-t)) + return na
+ + + +
+[docs] +def tonemap(i, exponent): + """Apply tone mapping to an image array. + + This function performs tone mapping on the input image array by first + filtering out values that are excessively high, which may indicate that + the depth buffer was not written correctly. It then normalizes the + values between the minimum and maximum heights, and finally applies an + exponentiation to adjust the brightness of the image. + + Args: + i (numpy.ndarray): A numpy array representing the image data. + exponent (float): The exponent used for adjusting the brightness + of the normalized image. + + Returns: + None: The function modifies the input array in place. + """ + + # if depth buffer never got written it gets set + # to a great big value (10000000000.0) + # filter out anything within an order of magnitude of it + # so we only have things that are actually drawn + maxheight = i.max(where=i < 1000000000.0, initial=0) + minheight = i.min() + i[:] = numpy.clip(i, minheight, maxheight) + + i[:] = ((i-minheight))/(maxheight-minheight) + i[:] **= exponent
+ + + +
+[docs] +def vert(column, row, z, XYscaling, Zscaling): + """Create a single vertex in 3D space. + + This function calculates the 3D coordinates of a vertex based on the + provided column and row values, as well as scaling factors for the X-Y + and Z dimensions. The resulting coordinates are scaled accordingly to + fit within a specified 3D space. + + Args: + column (float): The column value representing the X coordinate. + row (float): The row value representing the Y coordinate. + z (float): The Z coordinate value. + XYscaling (float): The scaling factor for the X and Y coordinates. + Zscaling (float): The scaling factor for the Z coordinate. + + Returns: + tuple: A tuple containing the scaled X, Y, and Z coordinates. + """ + return column * XYscaling, row * XYscaling, z * Zscaling
+ + + +
+[docs] +def buildMesh(mesh_z, br): + """Build a 3D mesh from a height map and apply transformations. + + This function constructs a 3D mesh based on the provided height map + (mesh_z) and applies various transformations such as scaling and + positioning based on the parameters defined in the br object. It first + removes any existing BasReliefMesh objects from the scene, then creates + a new mesh from the height data, and finally applies decimation if the + specified ratio is within acceptable limits. + + Args: + mesh_z (numpy.ndarray): A 2D array representing the height values + for the mesh vertices. + br (object): An object containing properties for width, height, + thickness, justification, and decimation ratio. + """ + + global rows + global size + scale = 1 + scalez = 1 + decimateRatio = br.decimate_ratio # get variable from interactive table + bpy.ops.object.select_all(action='DESELECT') + for object in bpy.data.objects: + if re.search("BasReliefMesh", str(object)): + bpy.data.objects.remove(object) + print("old basrelief removed") + + print("Building Mesh") + numY = mesh_z.shape[1] + numX = mesh_z.shape[0] + print(numX, numY) + + verts = list() + faces = list() + + for i, row in enumerate(mesh_z): + for j, col in enumerate(row): + verts.append(vert(i, j, col, scale, scalez)) + + count = 0 + for i in range(0, numY * (numX-1)): + if count < numY-1: + A = i # the first vertex + B = i+1 # the second vertex + C = (i+numY)+1 # the third vertex + D = (i+numY) # the fourth vertex + + face = (A, B, C, D) + faces.append(face) + count = count + 1 + else: + count = 0 + + # Create Mesh Datablock + mesh = bpy.data.meshes.new("displacement") + mesh.from_pydata(verts, [], faces) + + mesh.update() + + # make object from mesh + new_object = bpy.data.objects.new('BasReliefMesh', mesh) + scene = bpy.context.scene + scene.collection.objects.link(new_object) + + # mesh object is made - preparing to decimate. + ob = bpy.data.objects['BasReliefMesh'] + ob.select_set(True) + bpy.context.view_layer.objects.active = ob + bpy.context.active_object.dimensions = ( + br.widthmm/1000, br.heightmm/1000, br.thicknessmm/1000) + bpy.context.active_object.location = (float( + br.justifyx)*br.widthmm/1000, float(br.justifyy)*br.heightmm/1000, float(br.justifyz)*br.thicknessmm/1000) + + print("Faces:" + str(len(ob.data.polygons))) + print("Vertices:" + str(len(ob.data.vertices))) + if decimateRatio > 0.95: + print("Skipping Decimate Ratio > 0.95") + else: + m = ob.modifiers.new(name="Foo", type='DECIMATE') + m.ratio = decimateRatio + print("Decimating with Ratio:"+str(decimateRatio)) + bpy.ops.object.modifier_apply(modifier=m.name) + print("Decimated") + print("Faces:" + str(len(ob.data.polygons))) + print("Vertices:" + str(len(ob.data.vertices)))
+ + +# Switches to cycles render to CYCLES to render the sceen then switches it back to CNCCAM_RENDER for basRelief + + +
+[docs] +def renderScene(width, height, bit_diameter, passes_per_radius, make_nodes, view_layer): + """Render a scene using Blender's Cycles engine. + + This function switches the rendering engine to Cycles, sets up the + necessary nodes for depth rendering if specified, and configures the + render resolution based on the provided parameters. It ensures that the + scene is in object mode before rendering and restores the original + rendering engine after the process is complete. + + Args: + width (int): The width of the render in pixels. + height (int): The height of the render in pixels. + bit_diameter (float): The diameter used to calculate the number of passes. + passes_per_radius (int): The number of passes per radius for rendering. + make_nodes (bool): A flag indicating whether to create render nodes. + view_layer (str): The name of the view layer to be rendered. + + Returns: + None: This function does not return any value. + """ + + print("Rendering Scene") + scene = bpy.context.scene + # make sure we're in object mode or else bad things happen + if bpy.context.active_object: + bpy.ops.object.mode_set(mode='OBJECT') + + scene.render.engine = 'CYCLES' + our_viewer = None + our_renderer = None + if make_nodes: + # make depth render node and viewer node + if scene.use_nodes == False: + scene.use_nodes = True + node_tree = scene.node_tree + nodes = node_tree.nodes + our_viewer = node_tree.nodes.new(type='CompositorNodeViewer') + our_viewer.label = "CAM_basrelief_viewer" + our_renderer = node_tree.nodes.new(type='CompositorNodeRLayers') + our_renderer.label = "CAM_basrelief_renderlayers" + our_renderer.layer = view_layer + node_tree.links.new(our_renderer.outputs[our_renderer.outputs.find( + 'Depth')], our_viewer.inputs[our_viewer.inputs.find("Image")]) + scene.view_layers[view_layer].use_pass_z = True + # set our viewer as active so that it is what gets rendered to viewer node image + nodes.active = our_viewer + + # Set render resolution + passes = bit_diameter/(2*passes_per_radius) + x = round(width/passes) + y = round(height/passes) + print(x, y, passes) + scene.render.resolution_x = x + scene.render.resolution_y = y + scene.render.resolution_percentage = 100 + bpy.ops.render.render(animation=False, write_still=False, + use_viewport=True, layer="", scene="") + if our_renderer is not None: + nodes.remove(our_renderer) + if our_viewer is not None: + nodes.remove(our_viewer) + bpy.context.scene.render.engine = 'CNCCAM_RENDER' + print("Done Rendering")
+ + + +
+[docs] +def problemAreas(br): + """Process image data to identify problem areas based on silhouette + thresholds. + + This function analyzes an image and computes gradients to detect and + recover silhouettes based on specified parameters. It utilizes various + settings from the provided `br` object to adjust the processing, + including silhouette thresholds, scaling factors, and iterations for + smoothing and recovery. The function also handles image scaling and + applies a gradient mask if specified. The resulting data is then + converted back into an image format for further use. + + Args: + br (object): An object containing various parameters for processing, including: + - use_image_source (bool): Flag to determine if a specific image source + should be used. + - source_image_name (str): Name of the source image if + `use_image_source` is True. + - silhouette_threshold (float): Threshold for silhouette detection. + - recover_silhouettes (bool): Flag to indicate if silhouettes should be + recovered. + - silhouette_scale (float): Scaling factor for silhouette recovery. + - min_gridsize (int): Minimum grid size for processing. + - smooth_iterations (int): Number of iterations for smoothing. + - vcycle_iterations (int): Number of iterations for V-cycle processing. + - linbcg_iterations (int): Number of iterations for linear BCG + processing. + - use_planar (bool): Flag to indicate if planar processing should be + used. + - gradient_scaling_mask_use (bool): Flag to indicate if a gradient + scaling mask should be used. + - gradient_scaling_mask_name (str): Name of the gradient scaling mask + image. + - depth_exponent (float): Exponent for depth adjustment. + - silhouette_exponent (int): Exponent for silhouette recovery. + - attenuation (float): Attenuation factor for processing. + + Returns: + None: The function does not return a value but processes the image data and + saves the result. + """ + + t = time.time() + if br.use_image_source: + i = bpy.data.images[br.source_image_name] + else: + i = bpy.data.images["Viewer Node"] + silh_thres = br.silhouette_threshold + recover_silh = br.recover_silhouettes + silh_scale = br.silhouette_scale + MINS = br.min_gridsize + smoothiterations = br.smooth_iterations + vcycleiterations = br.vcycle_iterations + linbcgiterations = br.linbcg_iterations + useplanar = br.use_planar + # scale down before: + if br.gradient_scaling_mask_use: + m = bpy.data.images[br.gradient_scaling_mask_name] + # mask=nar=imagetonumpy(m) + + # if br.scale_down_before_use: + # i.scale(int(i.size[0]*br.scale_down_before),int(i.size[1]*br.scale_down_before)) + # if br.gradient_scaling_mask_use: + # m.scale(int(m.size[0]*br.scale_down_before),int(m.size[1]*br.scale_down_before)) + + nar = imagetonumpy(i) + # return + if br.gradient_scaling_mask_use: + mask = imagetonumpy(m) + # put image to scale + tonemap(nar, br.depth_exponent) + nar = 1-nar # reverse z buffer+ add something + print(nar.min(), nar.max()) + gx = nar.copy() + gx.fill(0) + gx[:-1, :] = nar[1:, :]-nar[:-1, :] + gy = nar.copy() + gy.fill(0) + gy[:, :-1] = nar[:, 1:]-nar[:, :-1] + + # it' ok, we can treat neg and positive silh separately here: + a = br.attenuation + # numpy.logical_or(silhxplanar,silhyplanar)# + planar = nar < (nar.min()+0.0001) + # sqrt for silhouettes recovery: + sqrarx = numpy.abs(gx) + for iter in range(0, br.silhouette_exponent): + sqrarx = numpy.sqrt(sqrarx) + sqrary = numpy.abs(gy) + for iter in range(0, br.silhouette_exponent): + sqrary = numpy.sqrt(sqrary) + + # detect and also recover silhouettes: + silhxpos = gx > silh_thres + gx = gx*(-silhxpos)+recover_silh*(silhxpos*silh_thres*silh_scale)*sqrarx + silhxneg = gx < -silh_thres + gx = gx*(-silhxneg)-recover_silh*(silhxneg*silh_thres*silh_scale)*sqrarx + silhx = numpy.logical_or(silhxpos, silhxneg) + gx = gx*silhx+(1.0/a*numpy.log(1.+a*(gx)))*(-silhx) # attenuate + + # if br.fade_distant_objects: + # gx*=(nar) + # gy*=(nar) + + silhypos = gy > silh_thres + gy = gy*(-silhypos)+recover_silh*(silhypos*silh_thres*silh_scale)*sqrary + silhyneg = gy < -silh_thres + gy = gy*(-silhyneg)-recover_silh*(silhyneg*silh_thres*silh_scale)*sqrary + silhy = numpy.logical_or(silhypos, silhyneg) # both silh + gy = gy*silhy+(1.0/a*numpy.log(1.+a*(gy)))*(-silhy) # attenuate + + # now scale slopes... + if br.gradient_scaling_mask_use: + gx *= mask + gy *= mask + + divg = gx+gy + divga = numpy.abs(divg) + divgp = divga > silh_thres/4.0 + divgp = 1-divgp + for a in range(0, 2): + atimes(divgp, divga) + divga = divgp + + numpytoimage(divga, 'problem')
+ + + +
+[docs] +def relief(br): + """Process an image to enhance relief features. + + This function takes an input image and applies various processing + techniques to enhance the relief features based on the provided + parameters. It utilizes gradient calculations, silhouette recovery, and + optional detail enhancement through Fourier transforms. The processed + image is then used to build a mesh representation. + + Args: + br (object): An object containing various parameters for the relief processing, + including: + - use_image_source (bool): Whether to use a specified image source. + - source_image_name (str): The name of the source image. + - silhouette_threshold (float): Threshold for silhouette detection. + - recover_silhouettes (bool): Flag to indicate if silhouettes should be + recovered. + - silhouette_scale (float): Scale factor for silhouette recovery. + - min_gridsize (int): Minimum grid size for processing. + - smooth_iterations (int): Number of iterations for smoothing. + - vcycle_iterations (int): Number of iterations for V-cycle processing. + - linbcg_iterations (int): Number of iterations for linear BCG + processing. + - use_planar (bool): Flag to indicate if planar processing should be + used. + - gradient_scaling_mask_use (bool): Flag to indicate if a gradient + scaling mask should be used. + - gradient_scaling_mask_name (str): Name of the gradient scaling mask + image. + - depth_exponent (float): Exponent for depth adjustment. + - attenuation (float): Attenuation factor for the processing. + - detail_enhancement_use (bool): Flag to indicate if detail enhancement + should be applied. + - detail_enhancement_freq (float): Frequency for detail enhancement. + - detail_enhancement_amount (float): Amount of detail enhancement to + apply. + + Returns: + None: The function processes the image and builds a mesh but does not return a + value. + + Raises: + ReliefError: If the input image is blank or invalid. + """ + + t = time.time() + + if br.use_image_source: + i = bpy.data.images[br.source_image_name] + else: + i = bpy.data.images["Viewer Node"] + silh_thres = br.silhouette_threshold + recover_silh = br.recover_silhouettes + silh_scale = br.silhouette_scale + MINS = br.min_gridsize + smoothiterations = br.smooth_iterations + vcycleiterations = br.vcycle_iterations + linbcgiterations = br.linbcg_iterations + useplanar = br.use_planar + # scale down before: + if br.gradient_scaling_mask_use: + m = bpy.data.images[br.gradient_scaling_mask_name] + # mask=nar=imagetonumpy(m) + + # if br.scale_down_before_use: + # i.scale(int(i.size[0]*br.scale_down_before),int(i.size[1]*br.scale_down_before)) + # if br.gradient_scaling_mask_use: + # m.scale(int(m.size[0]*br.scale_down_before),int(m.size[1]*br.scale_down_before)) + + nar = imagetonumpy(i) + # return + if br.gradient_scaling_mask_use: + mask = imagetonumpy(m) + # put image to scale + tonemap(nar, br.depth_exponent) + nar = 1-nar # reverse z buffer+ add something + print("Range:", nar.min(), nar.max()) + if nar.min() - nar.max() == 0: + raise ReliefError( + "Input Image Is Blank - Check You Have the Correct View Layer or Input Image Set.") + + gx = nar.copy() + gx.fill(0) + gx[:-1, :] = nar[1:, :]-nar[:-1, :] + gy = nar.copy() + gy.fill(0) + gy[:, :-1] = nar[:, 1:]-nar[:, :-1] + + # it' ok, we can treat neg and positive silh separately here: + a = br.attenuation + # numpy.logical_or(silhxplanar,silhyplanar)# + planar = nar < (nar.min()+0.0001) + # sqrt for silhouettes recovery: + sqrarx = numpy.abs(gx) + for iter in range(0, br.silhouette_exponent): + sqrarx = numpy.sqrt(sqrarx) + sqrary = numpy.abs(gy) + for iter in range(0, br.silhouette_exponent): + sqrary = numpy.sqrt(sqrary) + + # detect and also recover silhouettes: + silhxpos = gx > silh_thres + print("*** silhxpos is %s" % silhxpos) + gx = gx*(~silhxpos)+recover_silh*(silhxpos*silh_thres*silh_scale)*sqrarx + silhxneg = gx < -silh_thres + gx = gx*(~silhxneg)-recover_silh*(silhxneg*silh_thres*silh_scale)*sqrarx + silhx = numpy.logical_or(silhxpos, silhxneg) + gx = gx*silhx+(1.0/a*numpy.log(1.+a*(gx)))*(~silhx) # attenuate + + # if br.fade_distant_objects: + # gx*=(nar) + # gy*=(nar) + + silhypos = gy > silh_thres + gy = gy*(~silhypos)+recover_silh*(silhypos*silh_thres*silh_scale)*sqrary + silhyneg = gy < -silh_thres + gy = gy*(~silhyneg)-recover_silh*(silhyneg*silh_thres*silh_scale)*sqrary + silhy = numpy.logical_or(silhypos, silhyneg) # both silh + gy = gy*silhy+(1.0/a*numpy.log(1.+a*(gy)))*(~silhy) # attenuate + + # now scale slopes... + if br.gradient_scaling_mask_use: + gx *= mask + gy *= mask + + # + # print(silhx) + # silhx=abs(gx)>silh_thres + # gx=gx*(-silhx) + # silhy=abs(gy)>silh_thres + # gy=gy*(-silhy) + + divg = gx+gy + divg[1:, :] = divg[1:, :]-gx[:-1, :] # subtract x + divg[:, 1:] = divg[:, 1:]-gy[:, :-1] # subtract y + + if br.detail_enhancement_use: # fourier stuff here!disabled by now + print("detail enhancement") + rows, cols = gx.shape + crow, ccol = int(rows/2), int(cols/2) + # dist=int(br.detail_enhancement_freq*gx.shape[0]/(2)) + # bandwidth=.1 + # dist= + divgmin = divg.min() + divg += divgmin + divgf = numpy.fft.fft2(divg) + divgfshift = numpy.fft.fftshift(divgf) + #mspectrum = 20*numpy.log(numpy.abs(divgfshift)) + # numpytoimage(mspectrum,'mspectrum') + mask = divg.copy() + pos = numpy.array((crow, ccol)) + + # bpy.context.scene.view_settings.curve_mapping.initialize() + # cur=bpy.context.scene.view_settings.curve_mapping.curves[0] + def filterwindow(x, y, cx=0, cy=0): # , curve=None): + return abs((cx-x))+abs((cy-y)) + # v=(abs((cx-x)/(cx))+abs((cy-y)/(cy))) + # return v + + mask = numpy.fromfunction( + filterwindow, divg.shape, cx=crow, cy=ccol) # , curve=cur) + mask = numpy.sqrt(mask) + # for x in range(mask.shape[0]): + # for y in range(mask.shape[1]): + # mask[x,y]=cur.evaluate(mask[x,y]) + maskmin = mask.min() + maskmax = mask.max() + mask = (mask-maskmin)/(maskmax-maskmin) + mask *= br.detail_enhancement_amount + mask += 1-mask.max() + # mask+=1 + mask[crow-1:crow+1, ccol-1:ccol+1] = 1 # to preserve basic freqencies. + # numpytoimage(mask,'mask') + divgfshift = divgfshift*mask + divgfshift = numpy.fft.ifftshift(divgfshift) + divg = numpy.abs(numpy.fft.ifft2(divgfshift)) + divg -= divgmin + divg = -divg + print("detail enhancement finished") + + levels = 0 + mins = min(nar.shape[0], nar.shape[1]) + while (mins >= MINS): + levels += 1 + mins = mins/2 + + target = numpy.zeros_like(divg) + + solve_pde_multigrid(divg, target, vcycleiterations, linbcgiterations, + smoothiterations, mins, levels, useplanar, planar) + + tonemap(target, 1) + + buildMesh(target, br) + +# ipath=bpy.path.abspath(i.filepath)[:-len(bpy.path.basename(i.filepath))]+br.output_image_name+'.exr' +# numpysave(target,ipath) + t = time.time()-t + print('total time:' + str(t)+'\n')
+ + # numpytoimage(target,br.output_image_name) + + +
+[docs] +class BasReliefsettings(bpy.types.PropertyGroup): +
+[docs] + use_image_source: BoolProperty( + name="Use Image Source", + description="", + default=False, + )
+ +
+[docs] + source_image_name: StringProperty( + name='Image Source', + description='image source', + )
+ +
+[docs] + view_layer_name: StringProperty( + name='View Layer Source', + description='Make a bas-relief from whatever is on this view layer', + )
+ +
+[docs] + bit_diameter: FloatProperty( + name="Diameter of Ball End in mm", + description="Diameter of bit which will be used for carving", + min=0.01, + max=50.0, + default=3.175, + precision=PRECISION, + )
+ +
+[docs] + pass_per_radius: IntProperty( + name="Passes per Radius", + description="Amount of passes per radius\n(more passes, " + "more mesh precision)", + default=2, + min=1, + max=10, + )
+ +
+[docs] + widthmm: IntProperty( + name="Desired Width in mm", + default=200, + min=5, + max=4000, + )
+ +
+[docs] + heightmm: IntProperty( + name="Desired Height in mm", + default=150, + min=5, + max=4000, + )
+ +
+[docs] + thicknessmm: IntProperty( + name="Thickness in mm", + default=15, + min=5, + max=100, + )
+ + +
+[docs] + justifyx: EnumProperty( + name="X", + items=[ + ('1', 'Left', '', 0), + ('-0.5', 'Centered', '', 1), + ('-1', 'Right', '', 2) + ], + default='-1', + )
+ +
+[docs] + justifyy: EnumProperty( + name="Y", + items=[ + ('1', 'Bottom', '', 0), + ('-0.5', 'Centered', '', 2), + ('-1', 'Top', '', 1), + ], + default='-1', + )
+ +
+[docs] + justifyz: EnumProperty( + name="Z", + items=[ + ('-1', 'Below 0', '', 0), + ('-0.5', 'Centered', '', 2), + ('1', 'Above 0', '', 1), + ], + default='-1', + )
+ + +
+[docs] + depth_exponent: FloatProperty( + name="Depth Exponent", + description="Initial depth map is taken to this power. Higher = " + "sharper relief", + min=0.5, + max=10.0, + default=1.0, + precision=PRECISION, + )
+ + +
+[docs] + silhouette_threshold: FloatProperty( + name="Silhouette Threshold", + description="Silhouette threshold", + min=0.000001, + max=1.0, + default=0.003, + precision=PRECISION, + )
+ +
+[docs] + recover_silhouettes: BoolProperty( + name="Recover Silhouettes", + description="", + default=True, + )
+ +
+[docs] + silhouette_scale: FloatProperty( + name="Silhouette Scale", + description="Silhouette scale", + min=0.000001, + max=5.0, + default=0.3, + precision=PRECISION, + )
+ +
+[docs] + silhouette_exponent: IntProperty( + name="Silhouette Square Exponent", + description="If lower, true depth distances between objects will be " + "more visibe in the relief", + default=3, + min=0, + max=5, + )
+ +
+[docs] + attenuation: FloatProperty( + name="Gradient Attenuation", + description="Gradient attenuation", + min=0.000001, + max=100.0, + default=1.0, + precision=PRECISION, + )
+ +
+[docs] + min_gridsize: IntProperty( + name="Minimum Grid Size", + default=16, + min=2, + max=512, + )
+ +
+[docs] + smooth_iterations: IntProperty( + name="Smooth Iterations", + default=1, + min=1, + max=64, + )
+ +
+[docs] + vcycle_iterations: IntProperty( + name="V-Cycle Iterations", + description="Set higher for planar constraint", + default=2, + min=1, + max=128, + )
+ +
+[docs] + linbcg_iterations: IntProperty( + name="LINBCG Iterations", + description="Set lower for flatter relief, and when using " + "planar constraint", + default=5, + min=1, + max=64, + )
+ +
+[docs] + use_planar: BoolProperty( + name="Use Planar Constraint", + description="", + default=False, + )
+ +
+[docs] + gradient_scaling_mask_use: BoolProperty( + name="Scale Gradients with Mask", + description="", + default=False, + )
+ +
+[docs] + decimate_ratio: FloatProperty( + name="Decimate Ratio", + description="Simplify the mesh using the Decimate modifier. " + "The lower the value the more simplyfied", + min=0.01, + max=1.0, + default=0.1, + precision=PRECISION, + )
+ + +
+[docs] + gradient_scaling_mask_name: StringProperty( + name='Scaling Mask Name', + description='Mask name', + )
+ +
+[docs] + scale_down_before_use: BoolProperty( + name="Scale Down Image Before Processing", + description="", + default=False, + )
+ +
+[docs] + scale_down_before: FloatProperty( + name="Image Scale", + description="Image scale", + min=0.025, + max=1.0, + default=.5, + precision=PRECISION, + )
+ +
+[docs] + detail_enhancement_use: BoolProperty( + name="Enhance Details", + description="Enhance details by frequency analysis", + default=False, + )
+ + #detail_enhancement_freq=FloatProperty(name="frequency limit", description="Image scale", min=0.025, max=1.0, default=.5, precision=PRECISION) +
+[docs] + detail_enhancement_amount: FloatProperty( + name="Amount", + description="Image scale", + min=0.025, + max=1.0, + default=.5, + precision=PRECISION, + )
+ + +
+[docs] + advanced: BoolProperty( + name="Advanced Options", + description="Show advanced options", + default=True, + )
+
+ + + +
+[docs] +class BASRELIEF_Panel(bpy.types.Panel): + """Bas Relief Panel""" +
+[docs] + bl_label = "Bas Relief"
+ +
+[docs] + bl_idname = "WORLD_PT_BASRELIEF"
+ + +
+[docs] + bl_space_type = "PROPERTIES"
+ +
+[docs] + bl_region_type = "WINDOW"
+ +
+[docs] + bl_context = "render"
+ + +
+[docs] + COMPAT_ENGINES = {'CNCCAM_RENDER'}
+ + + # def draw_header(self, context): + # self.layout.menu("CAM_CUTTER_MT_presets", text="CAM Cutter") + @classmethod +
+[docs] + def poll(cls, context): + """Check if the current render engine is compatible. + + This class method checks whether the render engine specified in the + provided context is included in the list of compatible engines. It + accesses the render settings from the context and verifies if the engine + is part of the predefined compatible engines. + + Args: + context (Context): The context containing the scene and render settings. + + Returns: + bool: True if the render engine is compatible, False otherwise. + """ + + rd = context.scene.render + return rd.engine in cls.COMPAT_ENGINES
+ + +
+[docs] + def draw(self, context): + """Draw the user interface for the bas relief settings. + + This method constructs the layout for the bas relief settings in the + Blender user interface. It includes various properties and options that + allow users to configure the bas relief calculations, such as selecting + images, adjusting parameters, and setting justification options. The + layout is dynamically updated based on user selections, providing a + comprehensive interface for manipulating bas relief settings. + + Args: + context (bpy.context): The context in which the UI is being drawn. + + Returns: + None: This method does not return any value; it modifies the layout + directly. + """ + + layout = self.layout + # print(dir(layout)) + s = bpy.context.scene + + br = s.basreliefsettings + + # if br: + # cutter preset + layout.operator("scene.calculate_bas_relief", text="Calculate Relief") + layout.prop(br, 'advanced') + layout.prop(br, 'use_image_source') + if br.use_image_source: + layout.prop_search(br, 'source_image_name', bpy.data, "images") + else: + layout.prop_search(br, 'view_layer_name', + bpy.context.scene, "view_layers") + layout.prop(br, 'depth_exponent') + layout.label(text="Project Parameters") + layout.prop(br, 'bit_diameter') + layout.prop(br, 'pass_per_radius') + layout.prop(br, 'widthmm') + layout.prop(br, 'heightmm') + layout.prop(br, 'thicknessmm') + + layout.label(text="Justification") + layout.prop(br, 'justifyx') + layout.prop(br, 'justifyy') + layout.prop(br, 'justifyz') + + layout.label(text="Silhouette") + layout.prop(br, 'silhouette_threshold') + layout.prop(br, 'recover_silhouettes') + if br.recover_silhouettes: + layout.prop(br, 'silhouette_scale') + if br.advanced: + layout.prop(br, 'silhouette_exponent') + # layout.template_curve_mapping(br,'curva') + if br.advanced: + # layout.prop(br,'attenuation') + layout.prop(br, 'min_gridsize') + layout.prop(br, 'smooth_iterations') + layout.prop(br, 'vcycle_iterations') + layout.prop(br, 'linbcg_iterations') + layout.prop(br, 'use_planar') + layout.prop(br, 'decimate_ratio') + + layout.prop(br, 'gradient_scaling_mask_use') + if br.advanced: + if br.gradient_scaling_mask_use: + layout.prop_search( + br, 'gradient_scaling_mask_name', bpy.data, "images") + layout.prop(br, 'detail_enhancement_use') + if br.detail_enhancement_use: + # layout.prop(br,'detail_enhancement_freq') + layout.prop(br, 'detail_enhancement_amount')
+
+ + # print(dir(layout)) + # layout.prop(s.view_settings.curve_mapping,"curves") + #layout.label('Frequency scaling:') + # s.view_settings.curve_mapping.clip_max_y=2 + + #layout.template_curve_mapping(s.view_settings, "curve_mapping") + + # layout.prop(br,'scale_down_before_use') + # if br.scale_down_before_use: + # layout.prop(br,'scale_down_before') + + +
+[docs] +class ReliefError(Exception): + pass
+ + + +
+[docs] +class DoBasRelief(bpy.types.Operator): + """Calculate Bas Relief""" +
+[docs] + bl_idname = "scene.calculate_bas_relief"
+ +
+[docs] + bl_label = "Calculate Bas Relief"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + processes = []
+ + +
+[docs] + def execute(self, context): + """Execute the relief rendering process based on the provided context. + + This function retrieves the scene and its associated bas relief + settings. It checks if an image source is being used and sets the view + layer name accordingly. The function then attempts to render the scene + and generate the relief. If any errors occur during these processes, + they are reported, and the operation is canceled. + + Args: + context: The context in which the function is executed. + + Returns: + dict: A dictionary indicating the result of the operation, either + """ + + s = bpy.context.scene + br = s.basreliefsettings + if not br.use_image_source and br.view_layer_name == "": + br.view_layer_name = bpy.context.view_layer.name + + try: + renderScene(br.widthmm, br.heightmm, br.bit_diameter, br.pass_per_radius, + not br.use_image_source, br.view_layer_name) + except ReliefError as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + + try: + relief(br) + except ReliefError as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + return {'FINISHED'}
+
+ + + +
+[docs] +class ProblemAreas(bpy.types.Operator): + """Find Bas Relief Problem Areas""" +
+[docs] + bl_idname = "scene.problemareas_bas_relief"
+ +
+[docs] + bl_label = "Problem Areas Bas Relief"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + processes = []
+ + + # @classmethod + # def poll(cls, context): + # return context.active_object is not None + +
+[docs] + def execute(self, context): + """Execute the operation related to the bas relief settings in the current + scene. + + This method retrieves the current scene from the Blender context and + accesses the bas relief settings. It then calls the `problemAreas` + function to perform operations related to those settings. The method + concludes by returning a status indicating that the operation has + finished successfully. + + Args: + context (bpy.context): The current Blender context, which provides access + + Returns: + dict: A dictionary with a status key indicating the operation result, + specifically {'FINISHED'}. + """ + + s = bpy.context.scene + br = s.basreliefsettings + problemAreas(br) + return {'FINISHED'}
+
+ + + +
+[docs] +def get_panels(): + """Retrieve a tuple of panel settings and related components. + + This function returns a tuple containing various components related to + Bas Relief settings. The components include BasReliefsettings, + BASRELIEF_Panel, DoBasRelief, and ProblemAreas, which are likely used in + the context of a graphical user interface or a specific application + domain. + + Returns: + tuple: A tuple containing the BasReliefsettings, BASRELIEF_Panel, + DoBasRelief, and ProblemAreas components. + """ + + return( + BasReliefsettings, + BASRELIEF_Panel, + DoBasRelief, + ProblemAreas + )
+ + + +
+[docs] +def register(): + """Register the necessary classes and properties for the add-on. + + This function registers all the panels defined in the add-on by + iterating through the list of panels returned by the `get_panels()` + function. It also adds a new property, `basreliefsettings`, to the + `Scene` type, which is a pointer property that references the + `BasReliefsettings` class. This setup is essential for the proper + functioning of the add-on, allowing users to access and modify the + settings related to bas relief. + """ + + for p in get_panels(): + bpy.utils.register_class(p) + s = bpy.types.Scene + s.basreliefsettings = PointerProperty( + type=BasReliefsettings, + )
+ + + +
+[docs] +def unregister(): + """Unregister all panels and remove basreliefsettings from the Scene type. + + This function iterates through all registered panels and unregisters + each one using Blender's utility functions. Additionally, it removes the + basreliefsettings attribute from the Scene type, ensuring that any + settings related to bas relief are no longer accessible in the current + Blender session. + """ + + for p in get_panels(): + bpy.utils.unregister_class(p) + s = bpy.types.Scene + del s.basreliefsettings
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/bridges.html b/_modules/cam/bridges.html new file mode 100644 index 000000000..08d88026b --- /dev/null +++ b/_modules/cam/bridges.html @@ -0,0 +1,771 @@ + + + + + + + + + + cam.bridges — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.bridges

+"""CNC CAM 'bridges.py' © 2012 Vilem Novak
+
+Functions to add Bridges / Tabs to meshes or curves.
+Called with Operators defined in '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 object_data_add
+from mathutils import Vector
+
+from . import utils
+from . import simple
+
+
+
+[docs] +def addBridge(x, y, rot, sizex, sizey): + """Add a bridge mesh object to the scene. + + This function creates a bridge by adding a primitive plane to the + Blender scene, adjusting its dimensions, and then converting it into a + curve. The bridge is positioned based on the provided coordinates and + rotation. The size of the bridge is determined by the `sizex` and + `sizey` parameters. + + Args: + x (float): The x-coordinate for the bridge's location. + y (float): The y-coordinate for the bridge's location. + rot (float): The rotation angle around the z-axis in radians. + sizex (float): The width of the bridge. + sizey (float): The height of the bridge. + + Returns: + bpy.types.Object: The created bridge object. + """ + + bpy.ops.mesh.primitive_plane_add(size=sizey*2, calc_uvs=True, enter_editmode=False, align='WORLD', + location=(0, 0, 0), rotation=(0, 0, 0)) + b = bpy.context.active_object + b.name = 'bridge' + # b.show_name=True + b.dimensions.x = sizex + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + + bpy.ops.object.editmode_toggle() + bpy.ops.transform.translate(value=(0, sizey / 2, 0), constraint_axis=(False, True, False), + orient_type='GLOBAL', mirror=False, use_proportional_edit=False, + proportional_edit_falloff='SMOOTH', proportional_size=1) + bpy.ops.object.editmode_toggle() + bpy.ops.object.convert(target='CURVE') + + b.location = x, y, 0 + b.rotation_euler.z = rot + return b
+ + + +
+[docs] +def addAutoBridges(o): + """Attempt to add auto bridges as a set of curves. + + This function creates a collection of bridges based on the provided + object. It checks if a collection for bridges already exists; if not, it + creates a new one. The function then iterates through the objects in the + input object, processing curves and meshes to generate bridge + geometries. For each geometry, it calculates the necessary points and + adds bridges at various orientations based on the geometry's bounds. + + Args: + o (object): An object containing properties such as + bridges_collection_name, bridges_width, and cutter_diameter, + along with a list of objects to process. + + Returns: + None: This function does not return a value but modifies the + Blender context by adding bridge objects to the specified + collection. + """ + utils.getOperationSources(o) + bridgecollectionname = o.bridges_collection_name + if bridgecollectionname == '' or bpy.data.collections.get(bridgecollectionname) is None: + bridgecollectionname = 'bridges_' + o.name + bpy.data.collections.new(bridgecollectionname) + bpy.context.collection.children.link(bpy.data.collections[bridgecollectionname]) + g = bpy.data.collections[bridgecollectionname] + o.bridges_collection_name = bridgecollectionname + for ob in o.objects: + + if ob.type == 'CURVE' or ob.type == 'TEXT': + curve = utils.curveToShapely(ob) + if ob.type == 'MESH': + curve = utils.getObjectSilhouete('OBJECTS', [ob]) + for c in curve.geoms: + c = c.exterior + 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, -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, 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)) + p = c.interpolate(d1) + bo = addBridge(p.x, p.y, 0, 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, miny - 1000)) + p = c.interpolate(d1) + bo = addBridge(p.x, p.y, pi, o.bridges_width, o.cutter_diameter * 1) + g.objects.link(bo) + bpy.context.collection.objects.unlink(bo)
+ + + +
+[docs] +def getBridgesPoly(o): + """Generate and prepare bridge polygons from a Blender object. + + This function checks if the provided object has an attribute for bridge + polygons. If not, it retrieves the bridge collection, selects all curve + objects within that collection, duplicates them, and joins them into a + single object. The resulting shape is then converted to a Shapely + geometry. The function buffers the resulting polygon to account for the + cutter diameter and prepares the boundary and polygon for further + processing. + + Args: + o (object): An object containing properties related to bridge + """ + + if not hasattr(o, 'bridgespolyorig'): + bridgecollectionname = o.bridges_collection_name + bridgecollection = bpy.data.collections[bridgecollectionname] + bpy.ops.object.select_all(action='DESELECT') + + for ob in bridgecollection.objects: + if ob.type == 'CURVE': + ob.select_set(state=True) + bpy.context.view_layer.objects.active = ob + bpy.ops.object.duplicate() + bpy.ops.object.join() + ob = bpy.context.active_object + shapes = utils.curveToShapely(ob, o.use_bridge_modifiers) + ob.select_set(state=True) + bpy.ops.object.delete(use_global=False) + bridgespoly = sops.unary_union(shapes) + + # buffer the poly, so the bridges are not actually milled... + o.bridgespolyorig = bridgespoly.buffer(distance=o.cutter_diameter / 2.0) + o.bridgespoly_boundary = o.bridgespolyorig.boundary + o.bridgespoly_boundary_prep = prepared.prep(o.bridgespolyorig.boundary) + o.bridgespoly = prepared.prep(o.bridgespolyorig)
+ + + +
+[docs] +def useBridges(ch, o): + """Add bridges to chunks using a collection of bridge objects. + + This function takes a collection of bridge objects and uses the curves + within it to create bridges over the specified chunks. It calculates the + necessary points for the bridges based on the height and geometry of the + chunks and the bridge objects. The function also handles intersections + with the bridge polygon and adjusts the points accordingly. Finally, it + generates a mesh for the bridges and converts it into a curve object in + Blender. + + Args: + ch (Chunk): The chunk object to which bridges will be added. + o (ObjectOptions): An object containing options such as bridge height, + collection name, and other parameters. + + Returns: + None: The function modifies the chunk object in place and does not return a + value. + """ + bridgecollectionname = o.bridges_collection_name + bridgecollection = bpy.data.collections[bridgecollectionname] + if len(bridgecollection.objects) > 0: + + # get bridgepoly + getBridgesPoly(o) + + #### + + bridgeheight = min(o.max.z, o.min.z + abs(o.bridges_height)) + + vi = 0 + newpoints = [] + ch_points = ch.get_points_np() + p1 = sgeometry.Point(ch_points[0]) + startinside = o.bridgespoly.contains(p1) + interrupted = False + verts = [] + edges = [] + faces = [] + while vi < len(ch_points): + i1 = vi + i2 = vi + chp1 = ch_points[i1] + # Vector(v1)#this is for case of last point and not closed chunk.. + chp2 = ch_points[i1] + if vi + 1 < len(ch_points): + i2 = vi + 1 + chp2 = ch_points[vi + 1] # Vector(ch_points[vi+1]) + v1 = Vector(chp1) + v2 = Vector(chp2) + if v1.z < bridgeheight or v2.z < bridgeheight: + v = v2 - v1 + p2 = sgeometry.Point(chp2) + + if interrupted: + p1 = sgeometry.Point(chp1) + startinside = o.bridgespoly.contains(p1) + interrupted = False + + endinside = o.bridgespoly.contains(p2) + l = sgeometry.LineString([chp1, chp2]) + if o.bridgespoly_boundary_prep.intersects(l): + intersections = o.bridgespoly_boundary.intersection(l) + + else: + intersections = sgeometry.GeometryCollection() + + itpoint = intersections.geom_type == 'Point' + itmpoint = intersections.geom_type == 'MultiPoint' + + if not startinside: + newpoints.append(chp1) + elif startinside: + newpoints.append((chp1[0], chp1[1], max(chp1[2], bridgeheight))) + cpoints = [] + if itpoint: + pt = Vector((intersections.x, intersections.y, intersections.z)) + cpoints = [pt] + + elif itmpoint: + cpoints = [] + for p in intersections.geoms: + pt = Vector((p.x, p.y, p.z)) + cpoints.append(pt) + # ####sort collisions here :( + ncpoints = [] + while len(cpoints) > 0: + mind = 10000000 + mini = -1 + for i, p in enumerate(cpoints): + if min(mind, (p - v1).length) < mind: + mini = i + mind = (p - v1).length + ncpoints.append(cpoints.pop(mini)) + cpoints = ncpoints + # endsorting + + if startinside: + isinside = True + else: + isinside = False + for cp in cpoints: + v3 = cp + # print(v3) + if v.length == 0: + ratio = 1 + else: + fractvect = v3 - v1 + ratio = fractvect.length / v.length + + collisionz = v1.z + v.z * ratio + np1 = (v3.x, v3.y, collisionz) + np2 = (v3.x, v3.y, max(collisionz, bridgeheight)) + if not isinside: + newpoints.extend((np1, np2)) + else: + newpoints.extend((np2, np1)) + isinside = not isinside + + startinside = endinside + vi += 1 + else: + newpoints.append(chp1) + vi += 1 + interrupted = True + ch.set_points(newpoints) + + # create bridge cut curve here + count = 0 + isedge = 0 + x2, y2 = 0, 0 + for pt in newpoints: + x = pt[0] + y = pt[1] + z = pt[2] + 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 = hypot(x - x2, y - y2) + if edgelength > o.bridges_width: + # make new vertex + verts.append(((x + x2)/2, (y + y2)/2, o.minz)) + + isedge += 1 + edge = [count - 2, count - 1] + edges.append(edge) + count += 1 + else: + x2 = x + y2 = y + verts.append((x, y, o.minz)) # make new vertex + isedge += 1 + if isedge > 1: # Two points make an edge + edge = [count - 2, count - 1] + edges.append(edge) + + else: + isedge = 0 + + # verify if vertices have been generated and generate a mesh + if verts: + mesh = bpy.data.meshes.new(name=o.name + "_cut_bridges") # generate new mesh + # integrate coordinates and edges + mesh.from_pydata(verts, edges, faces) + object_data_add(bpy.context, mesh) # create object + bpy.ops.object.convert(target='CURVE') # convert mesh to curve + # join all the new cut bridges curves + simple.join_multiple(o.name + '_cut_bridges') + simple.remove_doubles() # remove overlapping vertices
+ + + +
+[docs] +def auto_cut_bridge(o): + """Automatically processes a bridge collection. + + This function retrieves a bridge collection by its name from the + provided object and checks if there are any objects within that + collection. If there are objects present, it prints "bridges" to the + console. This function is useful for managing and processing bridge + collections in a 3D environment. + + Args: + o (object): An object that contains the attribute + + Returns: + None: This function does not return any value. + """ + + bridgecollectionname = o.bridges_collection_name + bridgecollection = bpy.data.collections[bridgecollectionname] + if len(bridgecollection.objects) > 0: + print("bridges")
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/cam_chunk.html b/_modules/cam/cam_chunk.html new file mode 100644 index 000000000..1ec064bd4 --- /dev/null +++ b/_modules/cam/cam_chunk.html @@ -0,0 +1,1976 @@ + + + + + + + + + + cam.cam_chunk — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.cam_chunk

+"""CNC CAM 'chunk.py' © 2012 Vilem Novak
+ 
+Classes and Functions to build, store and optimize CAM path chunks.
+"""
+
+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
+
+
+
+[docs] +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) +
+[docs] +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 +
+[docs] +class camPathChunkBuilder: + def __init__(self, inpoints=None, startpoints=None, endpoints=None, rotations=None): + if inpoints is None: + inpoints = [] +
+[docs] + self.points = inpoints
+ +
+[docs] + self.startpoints = startpoints or []
+ +
+[docs] + self.endpoints = endpoints or []
+ +
+[docs] + self.rotations = rotations or []
+ +
+[docs] + self.depth = None
+ + +
+[docs] + 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 + + +
+[docs] +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) +
+[docs] + self.poly = None # get polygon just in time
+ +
+[docs] + 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 +
+[docs] + self.closed = False
+ +
+[docs] + self.children = []
+ +
+[docs] + self.parents = []
+ + # self.unsortedchildren=False +
+[docs] + self.sorted = False # if the chunk has allready been milled in the simulation
+ +
+[docs] + self.length = 0 # this is total length of this chunk.
+ +
+[docs] + self.zstart = 0 # this is stored for ramps mainly,
+ + # because they are added afterwards, but have to use layer info +
+[docs] + self.zend = 0 #
+ + +
+[docs] + def update_poly(self): + if len(self.points) > 2: + self.poly = sgeometry.Polygon(self.points[:, 0:2]) + else: + self.poly = sgeometry.Polygon()
+ + +
+[docs] + def get_point(self, n): + return self.points[n].tolist()
+ + +
+[docs] + def get_points(self): + return self.points.tolist()
+ + +
+[docs] + def get_points_np(self): + return self.points
+ + +
+[docs] + def set_points(self, points): + self.points = np.array(points)
+ + +
+[docs] + def count(self): + return len(self.points)
+ + +
+[docs] + 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
+ + +
+[docs] + 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)
+ + +
+[docs] + 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
+ + +
+[docs] + def offsetZ(self, z): + self.points[:, 2] += z
+ + +
+[docs] + def flipX(self, x_centre): + self.points[:, 0] = x_centre - self.points[:, 0]
+ + +
+[docs] + def isbelowZ(self, z): + return np.any(self.points[:, 2] < z)
+ + +
+[docs] + def clampZ(self, z): + np.clip(self.points[:, 2], z, None, self.points[:, 2])
+ + +
+[docs] + def clampmaxZ(self, z): + np.clip(self.points[:, 2], None, z, self.points[:, 2])
+ + +
+[docs] + 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 d2<d1: + # ch.points.reverse() + return min(d1, d2) + else: + return dist2d(pos, self.points[0])
+ + +
+[docs] + def distStart(self, pos, o): + return dist2d(pos, self.points[0])
+ + +
+[docs] + def xyDistanceWithin(self, other, cutoff): + if self.poly is None: + self.update_poly() + if other.poly is None: + other.update_poly() + if not self.poly.is_empty and not other.poly.is_empty: + return self.poly.dwithin(other.poly, cutoff) + else: + return _internalXyDistanceTo(self.points, other.points, cutoff) < cutoff
+ + + # if cutoff is set, then the first distance < cutoff is returned +
+[docs] + def xyDistanceTo(self, other, cutoff=0): + if self.poly is None: + self.update_poly() + if other.poly is None: + other.update_poly() + if not self.poly.is_empty and not other.poly.is_empty: + # both polygons have >2 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)
+ + +
+[docs] + 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)
+ + +
+[docs] + 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
+ + +
+[docs] + 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)
+ + +
+[docs] + def reverse(self): + self.points = np.flip(self.points, axis=0) + self.startpoints.reverse() + self.endpoints.reverse() + self.rotations.reverse()
+ + +
+[docs] + 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)
+ + +
+[docs] + 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, :]
+ + +
+[docs] + 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, + )
+ + +
+[docs] + 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]
+ + +
+[docs] + 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
+ + +
+[docs] + 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]
+ + +
+[docs] + 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)
+ + +
+[docs] + 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 +
+[docs] + 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]) + )
+ + +
+[docs] + 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:], + ) + )
+ + +
+[docs] + 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 Counter Clockwise") + 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)
+
+ + + +
+[docs] +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
+ + + +
+[docs] +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) +
+[docs] +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
+ + + +
+[docs] +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
+ + + +
+[docs] +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
+ + + +
+[docs] +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)
+ + + +
+[docs] +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)
+ + + +
+[docs] +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... ;) +
+[docs] +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
+ + + +
+[docs] +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
+ + + +
+[docs] +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
+ + + +
+[docs] +def restoreVisibility(o, storage): + o.hide_viewport = storage[0] + # print(storage) + for i in range(0, 20): + o.layers[i] = storage[1][i]
+ + + +
+[docs] +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 + co.data.resolution_u = 100 + + # 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
+ + + +
+[docs] +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
+ + + +
+[docs] +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]
+ + + +
+[docs] +def chunkToShapely(chunk): + p = spolygon.Polygon(chunk.points) + return p
+ + + +
+[docs] +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
+ + + +
+[docs] +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
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/cam_operation.html b/_modules/cam/cam_operation.html new file mode 100644 index 000000000..e491ddcc4 --- /dev/null +++ b/_modules/cam/cam_operation.html @@ -0,0 +1,2093 @@ + + + + + + + + + + cam.cam_operation — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.cam_operation

+"""CNC CAM 'cam_operation.py'
+
+All properties of a single CAM Operation.
+"""
+
+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
+
+
+
+[docs] +class camOperation(PropertyGroup): + +
+[docs] + material: PointerProperty( + type=CAM_MATERIAL_Properties + )
+ +
+[docs] + info: PointerProperty( + type=CAM_INFO_Properties + )
+ +
+[docs] + optimisation: PointerProperty( + type=CAM_OPTIMISATION_Properties + )
+ +
+[docs] + movement: PointerProperty( + type=CAM_MOVEMENT_Properties + )
+ + +
+[docs] + name: StringProperty( + name="Operation Name", + default="Operation", + update=updateRest, + )
+ +
+[docs] + filename: StringProperty( + name="File Name", + default="Operation", + update=updateRest, + )
+ +
+[docs] + auto_export: BoolProperty( + name="Auto Export", + description="Export files immediately after path calculation", + default=True, + )
+ +
+[docs] + remove_redundant_points: BoolProperty( + name="Simplify G-code", + description="Remove redundant points sharing the same angle" + " as the start vector", + default=False, + )
+ +
+[docs] + simplify_tol: IntProperty( + name="Tolerance", + description='lower number means more precise', + default=50, + min=1, + max=1000, + )
+ +
+[docs] + hide_all_others: BoolProperty( + name="Hide All Others", + description="Hide all other tool paths except toolpath" + " associated with selected CAM operation", + default=False, + )
+ +
+[docs] + parent_path_to_object: BoolProperty( + name="Parent Path to Object", + description="Parent generated CAM path to source object", + default=False, + )
+ +
+[docs] + object_name: StringProperty( + name='Object', + description='Object handled by this operation', + update=updateOperationValid, + )
+ +
+[docs] + collection_name: StringProperty( + name='Collection', + description='Object collection handled by this operation', + update=updateOperationValid, + )
+ +
+[docs] + curve_object: StringProperty( + name='Curve Source', + description='Curve which will be sampled along the 3D object', + update=operationValid, + )
+ +
+[docs] + curve_object1: StringProperty( + name='Curve Target', + description='Curve which will serve as attractor for the ' + 'cutter when the cutter follows the curve', + update=operationValid, + )
+ +
+[docs] + source_image_name: StringProperty( + name='Image Source', + description='image source', + update=operationValid, + )
+ +
+[docs] + geometry_source: EnumProperty( + name='Data Source', + items=( + ('OBJECT', 'Object', 'a'), + ('COLLECTION', 'Collection of Objects', 'a'), + ('IMAGE', 'Image', 'a') + ), + description='Geometry source', + default='OBJECT', + update=updateOperationValid, + )
+ +
+[docs] + 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, + )
+ +
+[docs] + cutter_object_name: StringProperty( + name='Cutter Object', + description='Object used as custom cutter for this operation', + update=updateZbufferImage, + )
+ + +
+[docs] + 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, + )
+ +
+[docs] + strategy: EnumProperty( + name='Strategy', + items=getStrategyList, + description='Strategy', + update=updateStrategy, + )
+ + +
+[docs] + 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, + )
+ +
+[docs] + 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, + )
+ + +
+[docs] + 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, + )
+ +
+[docs] + 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, + )
+ + +
+[docs] + 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, + )
+ +
+[docs] + inverse: BoolProperty( + name="Inverse Milling", + description="Male to female model conversion", + default=False, + update=updateOffsetImage, + )
+ +
+[docs] + array: BoolProperty( + name="Use Array", + description="Create a repetitive array for producing the " + "same thing many times", + default=False, + update=updateRest, + )
+ +
+[docs] + array_x_count: IntProperty( + name="X Count", + description="X count", + default=1, + min=1, + max=32000, + update=updateRest, + )
+ +
+[docs] + array_y_count: IntProperty( + name="Y Count", + description="Y count", + default=1, + min=1, + max=32000, + update=updateRest, + )
+ +
+[docs] + 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, + )
+ +
+[docs] + 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 +
+[docs] + pocket_option: EnumProperty( + name='Start Position', + items=( + ('INSIDE', 'Inside', 'a'), + ('OUTSIDE', 'Outside', 'a') + ), + description='Pocket starting position', + default='INSIDE', + update=updateRest, + )
+ + +
+[docs] + pocketType: EnumProperty( + name='pocket type', + items=( + ('PERIMETER', 'Perimeter', 'a'), + ('PARALLEL', 'Parallel', 'a'), + ), + description='Type of pocket', + default='PERIMETER', + update=updateRest, + )
+ +
+[docs] + parallelPocketAngle: FloatProperty( + name="Parallel Pocket Angle", + description="Angle for parallel pocket", + min=-180, + max=180.0, + default=45.0, + precision=constants.PRECISION, + update=updateRest, + )
+ + +
+[docs] + parallelPocketCrosshatch: BoolProperty( + name="Crosshatch #", + description="Crosshatch X finish", + default=False, + update=updateRest, + )
+ +
+[docs] + parallelPocketContour: BoolProperty( + name="Contour Finish", + description="Contour path finish", + default=False, + update=updateRest, + )
+ + +
+[docs] + pocketToCurve: BoolProperty( + name="Pocket to Curve", + description="Generates a curve instead of a path", + default=False, + update=updateRest, + )
+ + + # Cutout +
+[docs] + 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, + )
+ +
+[docs] + outlines_count: IntProperty( + name="Outlines Count", + description="Outlines count", + default=1, + min=1, + max=32, + update=updateCutout, + )
+ +
+[docs] + straight: BoolProperty( + name="Overshoot Style", + description="Use overshoot cutout instead of conventional rounded", + default=True, + update=updateRest, + )
+ + # cutter +
+[docs] + 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, + )
+ +
+[docs] + 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, + )
+ +
+[docs] + cylcone_diameter: FloatProperty( + name="Bottom Diameter", + description="Bottom diameter", + min=0.000001, + max=10, + default=0.003, + precision=constants.PRECISION, + unit="LENGTH", + update=updateOffsetImage, + )
+ +
+[docs] + 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, + )
+ +
+[docs] + cutter_flutes: IntProperty( + name="Cutter Flutes", + description="Cutter flutes", + min=1, + max=20, + default=2, + update=updateChipload, + )
+ +
+[docs] + 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, + )
+ +
+[docs] + 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) +
+[docs] + 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, + )
+ + +
+[docs] + cutter_description: StringProperty( + name="Tool Description", + default="", + update=updateOffsetImage, + )
+ + +
+[docs] + Laser_on: StringProperty( + name="Laser ON String", + default="M68 E0 Q100", + )
+ +
+[docs] + Laser_off: StringProperty( + name="Laser OFF String", + default="M68 E0 Q0", + )
+ +
+[docs] + Laser_cmd: StringProperty( + name="Laser Command", + default="M68 E0 Q", + )
+ +
+[docs] + Laser_delay: FloatProperty( + name="Laser ON Delay", + description="Time after fast move to turn on laser and " + "let machine stabilize", + default=0.2, + )
+ +
+[docs] + Plasma_on: StringProperty( + name="Plasma ON String", + default="M03", + )
+ +
+[docs] + Plasma_off: StringProperty( + name="Plasma OFF String", + default="M05", + )
+ +
+[docs] + Plasma_delay: FloatProperty( + name="Plasma ON Delay", + description="Time after fast move to turn on Plasma and " + "let machine stabilize", + default=0.1, + )
+ +
+[docs] + Plasma_dwell: FloatProperty( + name="Plasma Dwell Time", + description="Time to dwell and warm up the torch", + default=0.0, + )
+ + + # steps +
+[docs] + dist_between_paths: FloatProperty( + name="Distance Between Toolpaths", + default=0.001, + min=0.00001, + max=32, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + )
+ +
+[docs] + dist_along_paths: FloatProperty( + name="Distance Along Toolpaths", + default=0.0002, + min=0.00001, + max=32, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + )
+ +
+[docs] + parallel_angle: FloatProperty( + name="Angle of Paths", + default=0, + min=-360, + max=360, + precision=0, + subtype="ANGLE", + unit="ROTATION", + update=updateRest, + )
+ + +
+[docs] + 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, + )
+ + +
+[docs] + 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, + )
+ + +
+[docs] + 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, + )
+ +
+[docs] + enable_A: BoolProperty( + name="Enable A Axis", + description="Rotate A axis", + default=False, + update=updateRotation, + )
+ +
+[docs] + A_along_x: BoolProperty( + name="A Along X ", + description="A Parallel to X", + default=True, + update=updateRest, + )
+ + +
+[docs] + 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, + )
+ +
+[docs] + enable_B: BoolProperty( + name="Enable B Axis", + description="Rotate B axis", + default=False, + update=updateRotation, + )
+ + + # carve only +
+[docs] + carve_depth: FloatProperty( + name="Carve Depth", + default=0.001, + min=-.100, + max=32, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + )
+ + + # drill only +
+[docs] + drill_type: EnumProperty( + name='Holes On', + items=( + ('MIDDLE_SYMETRIC', 'Middle of Symmetric 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 +
+[docs] + slice_detail: FloatProperty( + name="Distance Between Slices", + default=0.001, + min=0.00001, + max=32, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + )
+ +
+[docs] + waterline_fill: BoolProperty( + name="Fill Areas Between Slices", + description="Fill areas between slices in waterline mode", + default=True, + update=updateRest, + )
+ +
+[docs] + waterline_project: BoolProperty( + name="Project Paths - Not Recomended", + description="Project paths in areas between slices", + default=True, + update=updateRest, + )
+ + + # movement and ramps +
+[docs] + use_layers: BoolProperty( + name="Use Layers", + description="Use layers for roughing", + default=True, + update=updateRest, + )
+ +
+[docs] + stepdown: FloatProperty( + name="", + description="Layer height", + default=0.01, + min=0.00001, + max=32, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + )
+ +
+[docs] + lead_in: FloatProperty( + name="Lead-in Radius", + description="Lead in radius for torch or laser to turn off", + min=0.00, + max=1, + default=0.0, + precision=constants.PRECISION, + unit="LENGTH", + )
+ +
+[docs] + 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", + )
+ +
+[docs] + 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) + +
+[docs] + minz: FloatProperty( + name="Operation Depth End", + default=-0.01, + min=-3, + max=3, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + )
+ + +
+[docs] + minz_from: EnumProperty( + name='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, + )
+ + +
+[docs] + 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, + )
+ + +
+[docs] + maxz: FloatProperty( + name="Operation Depth Start", + description='operation starting depth', + default=0, + min=-3, + max=10, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + ) # EXPERIMENTAL
+ + +
+[docs] + 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 + #################################################### + +
+[docs] + source_image_scale_z: FloatProperty( + name="Image Source Depth Scale", + default=0.01, + min=-1, + max=1, + precision=constants.PRECISION, + unit="LENGTH", + update=updateZbufferImage, + )
+ +
+[docs] + source_image_size_x: FloatProperty( + name="Image Source X Size", + default=0.1, + min=-10, + max=10, + precision=constants.PRECISION, + unit="LENGTH", + update=updateZbufferImage, + )
+ +
+[docs] + source_image_offset: FloatVectorProperty( + name='Image Offset', + default=(0, 0, 0), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + update=updateZbufferImage, + )
+ + +
+[docs] + 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, + )
+ +
+[docs] + source_image_crop_start_x: FloatProperty( + name='Crop Start X', + default=0, + min=0, + max=100, + precision=constants.PRECISION, + subtype='PERCENTAGE', + update=updateZbufferImage, + )
+ +
+[docs] + source_image_crop_start_y: FloatProperty( + name='Crop Start Y', + default=0, + min=0, + max=100, + precision=constants.PRECISION, + subtype='PERCENTAGE', + update=updateZbufferImage, + )
+ +
+[docs] + source_image_crop_end_x: FloatProperty( + name='Crop End X', + default=100, + min=0, + max=100, + precision=constants.PRECISION, + subtype='PERCENTAGE', + update=updateZbufferImage, + )
+ +
+[docs] + 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 + ##################################################### + +
+[docs] + ambient_behaviour: EnumProperty( + name='Ambient', + items=(('ALL', 'All', 'a'), ('AROUND', 'Around', 'a')), + description='Handling ambient surfaces', + default='ALL', + update=updateZbufferImage, + )
+ + +
+[docs] + 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') +
+[docs] + use_limit_curve: BoolProperty( + name="Use Limit Curve", + description="A curve limits the operation area", + default=False, + update=updateRest, + )
+ +
+[docs] + 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
+ +
+[docs] + limit_curve: StringProperty( + name='Limit Curve', + description='Curve used to limit the area of the operation', + update=updateRest, + )
+ + + # feeds +
+[docs] + 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, + )
+ +
+[docs] + plunge_feedrate: FloatProperty( + name="Plunge Speed", + description="% of feedrate", + min=0.1, + max=100.0, + default=50.0, + precision=1, + subtype='PERCENTAGE', + update=updateRest, + )
+ +
+[docs] + plunge_angle: FloatProperty( + name="Plunge Angle", + description="What angle is already considered to plunge", + default=pi / 6, + min=0, + max=pi * 0.5, + precision=0, + subtype="ANGLE", + unit="ROTATION", + update=updateRest, + )
+ +
+[docs] + spindle_rpm: FloatProperty( + name="Spindle RPM", + description="Spindle speed ", + min=0, + max=60000, + default=12000, + update=updateChipload, + )
+ + + # optimization and performance + +
+[docs] + do_simulation_feedrate: BoolProperty( + name="Adjust Feedrates with Simulation EXPERIMENTAL", + description="Adjust feedrates with simulation", + default=False, + update=updateRest, + )
+ + +
+[docs] + dont_merge: BoolProperty( + name="Don't Merge Outlines when Cutting", + description="this is usefull when you want to cut around everything", + default=False, + update=updateRest, + )
+ + +
+[docs] + pencil_threshold: FloatProperty( + name="Pencil Threshold", + default=0.00002, + min=0.00000001, + max=1, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + )
+ + +
+[docs] + crazy_threshold1: FloatProperty( + name="Min Engagement", + default=0.02, + min=0.00000001, + max=100, + precision=constants.PRECISION, + update=updateRest, + )
+ +
+[docs] + crazy_threshold5: FloatProperty( + name="Optimal Engagement", + default=0.3, + min=0.00000001, + max=100, + precision=constants.PRECISION, + update=updateRest, + )
+ +
+[docs] + crazy_threshold2: FloatProperty( + name="Max Engagement", + default=0.5, + min=0.00000001, + max=100, + precision=constants.PRECISION, + update=updateRest, + )
+ +
+[docs] + crazy_threshold3: FloatProperty( + name="Max Angle", + default=2, + min=0.00000001, + max=100, + precision=constants.PRECISION, + update=updateRest, + )
+ +
+[docs] + 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 +
+[docs] + add_pocket_for_medial: BoolProperty( + name="Add Pocket Operation", + description="Clean unremoved material after medial axis", + default=True, + update=updateRest, + )
+ + +
+[docs] + add_mesh_for_medial: BoolProperty( + name="Add Medial mesh", + description="Medial operation returns mesh for editing and " + "further processing", + default=False, + update=updateRest, + )
+ + #### +
+[docs] + medial_axis_threshold: FloatProperty( + name="Long Vector Threshold", + default=0.001, + min=0.00000001, + max=100, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + )
+ +
+[docs] + medial_axis_subdivision: FloatProperty( + name="Fine Subdivision", + default=0.0002, + min=0.00000001, + max=100, + precision=constants.PRECISION, + unit="LENGTH", + update=updateRest, + )
+ + # calculations + + # bridges +
+[docs] + use_bridges: BoolProperty( + name="Use Bridges / Tabs", + description="Use bridges in cutout", + default=False, + update=updateBridges, + )
+ +
+[docs] + bridges_width: FloatProperty( + name='Bridge / Tab Width', + default=0.002, + unit='LENGTH', + precision=constants.PRECISION, + update=updateBridges, + )
+ +
+[docs] + bridges_height: FloatProperty( + name='Bridge / Tab Height', + description="Height from the bottom of the cutting operation", + default=0.0005, + unit='LENGTH', + precision=constants.PRECISION, + update=updateBridges, + )
+ +
+[docs] + bridges_collection_name: StringProperty( + name='Bridges / Tabs Collection', + description='Collection of curves used as bridges', + update=operationValid, + )
+ +
+[docs] + use_bridge_modifiers: BoolProperty( + name="Use Bridge / Tab 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) + +
+[docs] + 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 + +
+[docs] + min: FloatVectorProperty( + name='Operation Minimum', + default=(0, 0, 0), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + )
+ +
+[docs] + max: FloatVectorProperty( + name='Operation Maximum', + default=(0, 0, 0), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + )
+ + + # g-code options for operation +
+[docs] + output_header: BoolProperty( + name="Output G-code Header", + description="Output user defined G-code command header" + " at start of operation", + default=False, + )
+ + +
+[docs] + gcode_header: StringProperty( + name="G-code Header", + description="G-code commands at start of operation." + " Use ; for line breaks", + default="G53 G0", + )
+ + +
+[docs] + enable_dust: BoolProperty( + name="Dust Collector", + description="Output user defined g-code command header" + " at start of operation", + default=False, + )
+ + +
+[docs] + gcode_start_dust_cmd: StringProperty( + name="Start Dust Collector", + description="Commands to start dust collection. Use ; for line breaks", + default="M100", + )
+ + +
+[docs] + gcode_stop_dust_cmd: StringProperty( + name="Stop Dust Collector", + description="Command to stop dust collection. Use ; for line breaks", + default="M101", + )
+ + +
+[docs] + enable_hold: BoolProperty( + name="Hold Down", + description="Output hold down command at start of operation", + default=False, + )
+ + +
+[docs] + gcode_start_hold_cmd: StringProperty( + name="G-code Header", + description="G-code commands at start of operation." + " Use ; for line breaks", + default="M102", + )
+ + +
+[docs] + gcode_stop_hold_cmd: StringProperty( + name="G-code Header", + description="G-code commands at end operation. Use ; for line breaks", + default="M103", + )
+ + +
+[docs] + enable_mist: BoolProperty( + name="Mist", + description="Mist command at start of operation", + default=False, + )
+ + +
+[docs] + gcode_start_mist_cmd: StringProperty( + name="Start Mist", + description="Command to start mist. Use ; for line breaks", + default="M104", + )
+ + +
+[docs] + gcode_stop_mist_cmd: StringProperty( + name="Stop Mist", + description="Command to stop mist. Use ; for line breaks", + default="M105", + )
+ + +
+[docs] + output_trailer: BoolProperty( + name="Output G-code Trailer", + description="Output user defined g-code command trailer" + " at end of operation", + default=False, + )
+ + +
+[docs] + 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) +
+[docs] + offset_image = numpy.array([], dtype=float)
+ +
+[docs] + zbuffer_image = numpy.array([], dtype=float)
+ + +
+[docs] + silhouete = sgeometry.Polygon()
+ +
+[docs] + ambient = sgeometry.Polygon()
+ +
+[docs] + operation_limit = sgeometry.Polygon()
+ +
+[docs] + borderwidth = 50
+ +
+[docs] + object = None
+ +
+[docs] + path_object_name: StringProperty( + name='Path Object', + description='Actual CNC path' + )
+ + + # update and tags and related + +
+[docs] + changed: BoolProperty( + name="True if any of the Operation Settings has Changed", + description="Mark for update", + default=False, + )
+ +
+[docs] + update_zbufferimage_tag: BoolProperty( + name="Mark Z-Buffer Image for Update", + description="Mark for update", + default=True, + )
+ +
+[docs] + update_offsetimage_tag: BoolProperty( + name="Mark Offset Image for Update", + description="Mark for update", + default=True, + )
+ +
+[docs] + update_silhouete_tag: BoolProperty( + name="Mark Silhouette Image for Update", + description="Mark for update", + default=True, + )
+ +
+[docs] + update_ambient_tag: BoolProperty( + name="Mark Ambient Polygon for Update", + description="Mark for update", + default=True, + )
+ +
+[docs] + update_bullet_collision_tag: BoolProperty( + name="Mark Bullet Collision World for Update", + description="Mark for update", + default=True, + )
+ + +
+[docs] + valid: BoolProperty( + name="Valid", + description="True if operation is ok for calculation", + default=True, + )
+ +
+[docs] + changedata: StringProperty( + name='Changedata', + description='change data for checking if stuff changed.', + )
+ + + # process related data + +
+[docs] + computing: BoolProperty( + name="Computing Right Now", + description="", + default=False, + )
+ +
+[docs] + pid: IntProperty( + name="Process Id", + description="Background process id", + default=-1, + )
+ +
+[docs] + outtext: StringProperty( + name='Outtext', + description='outtext', + default='', + )
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/chain.html b/_modules/cam/chain.html new file mode 100644 index 000000000..38fdd4bf6 --- /dev/null +++ b/_modules/cam/chain.html @@ -0,0 +1,493 @@ + + + + + + + + + + cam.chain — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.chain

+"""CNC CAM 'chain.py'
+
+All properties of a CAM Chain (a series of Operations), and the Chain's Operation reference.
+"""
+
+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
+
+[docs] +class opReference(PropertyGroup): +
+[docs] + name: StringProperty( + name="Operation Name", + default="Operation", + )
+ +
+[docs] + computing = False # for UiList display
+
+ + + +# chain is just a set of operations which get connected on export into 1 file. +
+[docs] +class camChain(PropertyGroup): +
+[docs] + index: IntProperty( + name="Index", + description="Index in the hard-defined camChains", + default=-1, + )
+ +
+[docs] + active_operation: IntProperty( + name="Active Operation", + description="Active operation in chain", + default=-1, + )
+ +
+[docs] + name: StringProperty( + name="Chain Name", + default="Chain", + )
+ +
+[docs] + filename: StringProperty( + name="File Name", + default="Chain", + ) # filename of
+ +
+[docs] + valid: BoolProperty( + name="Valid", + description="True if whole chain is ok for calculation", + default=True, + )
+ +
+[docs] + computing: BoolProperty( + name="Computing Right Now", + description="", + default=False, + )
+ + # this is to hold just operation names. +
+[docs] + operations: CollectionProperty( + type=opReference, + )
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/collision.html b/_modules/cam/collision.html new file mode 100644 index 000000000..0b389b0e0 --- /dev/null +++ b/_modules/cam/collision.html @@ -0,0 +1,854 @@ + + + + + + + + + + cam.collision — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.collision

+"""CNC CAM 'collision.py' © 2012 Vilem Novak
+
+Functions for Bullet and Cutter collision checks.
+"""
+
+from math import (
+    cos,
+    pi,
+    radians,
+    sin,
+    tan,
+)
+import time
+
+import bpy
+from mathutils import (
+    Euler,
+    Vector,
+)
+
+from .constants import (
+    BULLET_SCALE,
+    CUTTER_OFFSET,
+)
+from .simple import (
+    activate,
+    delob,
+    progress,
+)
+
+
+
+[docs] +def getCutterBullet(o): + """Create a cutter for Rigidbody simulation collisions. + + This function generates a 3D cutter object based on the specified cutter + type and parameters. It supports various cutter types including 'END', + 'BALLNOSE', 'VCARVE', 'CYLCONE', 'BALLCONE', and 'CUSTOM'. The function + also applies rigid body physics to the created cutter for realistic + simulation in Blender. + + Args: + o (object): An object containing properties such as cutter_type, cutter_diameter, + cutter_tip_angle, ball_radius, and cutter_object_name. + + Returns: + bpy.types.Object: The created cutter object with rigid body properties applied. + """ + + s = bpy.context.scene + if s.objects.get('cutter') is not None: + c = s.objects['cutter'] + activate(c) + + type = o.cutter_type + if type == 'END': + bpy.ops.mesh.primitive_cylinder_add(vertices=32, radius=o.cutter_diameter / 2, + depth=o.cutter_diameter, end_fill_type='NGON', + align='WORLD', enter_editmode=False, location=CUTTER_OFFSET, + rotation=(0, 0, 0)) + cutter = bpy.context.active_object + cutter.scale *= BULLET_SCALE + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS') + bpy.ops.rigidbody.object_add(type='ACTIVE') + cutter = bpy.context.active_object + cutter.rigid_body.collision_shape = 'CYLINDER' + elif type == 'BALLNOSE': + + # ballnose ending used mainly when projecting from sides. + # the actual collision shape is capsule in this case. + bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=3, radius=o.cutter_diameter / 2, + align='WORLD', enter_editmode=False, + location=CUTTER_OFFSET, rotation=(0, 0, 0)) + cutter = bpy.context.active_object + cutter.scale *= BULLET_SCALE + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS') + bpy.ops.rigidbody.object_add(type='ACTIVE') + cutter = bpy.context.active_object + # cutter.dimensions.z = 0.2 * BULLET_SCALE # should be sufficient for now... 20 cm. + cutter.rigid_body.collision_shape = 'CAPSULE' + #bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + + elif type == 'VCARVE': + + angle = o.cutter_tip_angle + 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=(pi, 0, 0)) + cutter = bpy.context.active_object + cutter.scale *= BULLET_SCALE + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS') + bpy.ops.rigidbody.object_add(type='ACTIVE') + cutter = bpy.context.active_object + cutter.rigid_body.collision_shape = 'CONE' + elif type == 'CYLCONE': + + angle = o.cutter_tip_angle + 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=(pi, 0, 0)) + cutter = bpy.context.active_object + cutter.scale *= BULLET_SCALE + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS') + bpy.ops.rigidbody.object_add(type='ACTIVE') + cutter = bpy.context.active_object + cutter.rigid_body.collision_shape = 'CONVEX_HULL' + cutter.location = CUTTER_OFFSET + elif type == 'BALLCONE': + angle = radians(o.cutter_tip_angle)/2 + cutter_R = o.cutter_diameter/2 + 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 * (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=-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 = radians(-360) + ob_scr.steps = 32 + ob_scr.merge_threshold = 0 + ob_scr.use_merge_vertices = True + bpy.ops.object.modifier_apply(modifier='scr') + bpy.data.objects['BallConeTool'].select_set(True) + cutter = bpy.context.active_object + cutter.scale *= BULLET_SCALE + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS') + bpy.ops.rigidbody.object_add(type='ACTIVE') + cutter.location = CUTTER_OFFSET + cutter.rigid_body.collision_shape = 'CONVEX_HULL' + cutter.location = CUTTER_OFFSET + elif type == 'CUSTOM': + cutob = bpy.data.objects[o.cutter_object_name] + activate(cutob) + bpy.ops.object.duplicate() + bpy.ops.rigidbody.object_add(type='ACTIVE') + cutter = bpy.context.active_object + scale = o.cutter_diameter / cutob.dimensions.x + cutter.scale *= BULLET_SCALE * scale + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS') + + # print(cutter.dimensions,scale) + bpy.ops.rigidbody.object_add(type='ACTIVE') + cutter.rigid_body.collision_shape = 'CONVEX_HULL' + cutter.location = CUTTER_OFFSET + + cutter.name = 'cam_cutter' + o.cutter_shape = cutter + return cutter
+ + + +
+[docs] +def subdivideLongEdges(ob, threshold): + """Subdivide edges of a mesh object that exceed a specified length. + + This function iteratively checks the edges of a given mesh object and + subdivides those that are longer than a specified threshold. The process + involves toggling the edit mode of the object, selecting the long edges, + and applying a subdivision operation. The function continues to + subdivide until no edges exceed the threshold. + + Args: + ob (bpy.types.Object): The Blender object containing the mesh to be + subdivided. + threshold (float): The length threshold above which edges will be + subdivided. + """ + + print('Subdividing Long Edges') + m = ob.data + scale = (ob.scale.x + ob.scale.y + ob.scale.z) / 3 + subdivides = [] + n = 1 + iter = 0 + while n > 0: + n = 0 + for i, e in enumerate(m.edges): + v1 = m.vertices[e.vertices[0]].co + v2 = m.vertices[e.vertices[1]].co + vec = v2 - v1 + l = vec.length + if l * scale > threshold: + n += 1 + subdivides.append(i) + if n > 0: + print(len(subdivides)) + bpy.ops.object.editmode_toggle() + + # bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') + # bpy.ops.mesh.tris_convert_to_quads() + + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') + bpy.ops.object.editmode_toggle() + for i in subdivides: + m.edges[i].select = True + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.subdivide(smoothness=0) + if iter == 0: + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris( + quad_method='SHORTEST_DIAGONAL', ngon_method='BEAUTY') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.editmode_toggle() + ob.update_from_editmode() + iter += 1
+ + + +# n=0 +# + +
+[docs] +def prepareBulletCollision(o): + """Prepares all objects needed for sampling with Bullet collision. + + This function sets up the Bullet physics simulation by preparing the + specified objects for collision detection. It begins by cleaning up any + existing rigid bodies that are not part of the 'machine' object. Then, + it duplicates the collision objects, converts them to mesh if they are + curves or fonts, and applies necessary modifiers. The function also + handles the subdivision of long edges and configures the rigid body + properties for each object. Finally, it scales the 'machine' objects to + the simulation scale and steps through the simulation frames to ensure + that all objects are up to date. + + Args: + o (Object): An object containing properties and settings for + """ + progress('Preparing Collisions') + + print(o.name) + active_collection = bpy.context.view_layer.active_layer_collection.collection + t = time.time() + s = bpy.context.scene + s.gravity = (0, 0, 0) + # cleanup rigidbodies wrongly placed somewhere in the scene + for ob in bpy.context.scene.objects: + if ob.rigid_body is not None and (bpy.data.objects.find('machine') > -1 + and ob.name not in bpy.data.objects['machine'].objects): + activate(ob) + bpy.ops.rigidbody.object_remove() + + for collisionob in o.objects: + bpy.context.view_layer.objects.active = collisionob + collisionob.select_set(state=True) + bpy.ops.object.duplicate(linked=False) + collisionob = bpy.context.active_object + if collisionob.type == 'CURVE' or collisionob.type == 'FONT': # support for curve objects collision + if collisionob.type == 'CURVE': + odata = collisionob.data.dimensions + collisionob.data.dimensions = '2D' + bpy.ops.object.convert(target='MESH', keep_original=False) + + if o.use_modifiers: + depsgraph = bpy.context.evaluated_depsgraph_get() + mesh_owner = collisionob.evaluated_get(depsgraph) + newmesh = mesh_owner.to_mesh() + + oldmesh = collisionob.data + collisionob.modifiers.clear() + collisionob.data = bpy.data.meshes.new_from_object(mesh_owner.evaluated_get(depsgraph), + preserve_all_data_layers=True, + depsgraph=depsgraph) + bpy.data.meshes.remove(oldmesh) + + # subdivide long edges here: + if o.optimisation.exact_subdivide_edges: + subdivideLongEdges(collisionob, o.cutter_diameter * 2) + + bpy.ops.rigidbody.object_add(type='ACTIVE') + # using active instead of passive because of performance.TODO: check if this works also with 4axis... + collisionob.rigid_body.collision_shape = 'MESH' + collisionob.rigid_body.kinematic = True + # this fixed a serious bug and gave big speedup, rbs could move since they are now active... + collisionob.rigid_body.collision_margin = o.skin * BULLET_SCALE + bpy.ops.transform.resize(value=(BULLET_SCALE, BULLET_SCALE, BULLET_SCALE), + constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, + use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, + texture_space=False, release_confirm=False) + collisionob.location = collisionob.location * BULLET_SCALE + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + bpy.context.view_layer.objects.active = collisionob + if active_collection in collisionob.users_collection: + active_collection.objects.unlink(collisionob) + + getCutterBullet(o) + + # machine objects scaling up to simulation scale + if bpy.data.objects.find('machine') > -1: + for ob in bpy.data.objects['machine'].objects: + activate(ob) + bpy.ops.transform.resize(value=(BULLET_SCALE, BULLET_SCALE, BULLET_SCALE), + constraint_axis=(False, False, False), orient_type='GLOBAL', + mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', + proportional_size=1, texture_space=False, + release_confirm=False) + ob.location = ob.location * BULLET_SCALE + # stepping simulation so that objects are up to date + bpy.context.scene.frame_set(0) + bpy.context.scene.frame_set(1) + bpy.context.scene.frame_set(2) + progress(time.time() - t)
+ + + +
+[docs] +def cleanupBulletCollision(o): + """Clean up bullet collision objects in the scene. + + This function checks for the presence of a 'machine' object in the + Blender scene and removes any rigid body objects that are not part of + the 'machine'. If the 'machine' object is present, it scales the machine + objects up to the simulation scale and adjusts their locations + accordingly. + + Args: + o: An object that may be used in the cleanup process (specific usage not + detailed). + + Returns: + None: This function does not return a value. + """ + + if bpy.data.objects.find('machine') > -1: + machinepresent = True + else: + machinepresent = False + for ob in bpy.context.scene.objects: + if ob.rigid_body is not None and not (machinepresent and ob.name in bpy.data.objects['machine'].objects): + delob(ob) + # machine objects scaling up to simulation scale + if machinepresent: + for ob in bpy.data.objects['machine'].objects: + activate(ob) + bpy.ops.transform.resize(value=(1.0 / BULLET_SCALE, 1.0 / BULLET_SCALE, 1.0 / BULLET_SCALE), + constraint_axis=(False, False, False), orient_type='GLOBAL', + mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', + proportional_size=1, texture_space=False, + release_confirm=False) + ob.location = ob.location / BULLET_SCALE
+ + + +
+[docs] +def getSampleBullet(cutter, x, y, radius, startz, endz): + """Perform a collision test for a 3-axis milling cutter. + + This function simplifies the collision detection process compared to a + full 3D test. It utilizes the Blender Python API to perform a convex + sweep test on the cutter's position within a specified 3D space. The + function checks for collisions between the cutter and other objects in + the scene, adjusting for the cutter's radius to determine the effective + position of the cutter tip. + + Args: + cutter (object): The milling cutter object used for the collision test. + x (float): The x-coordinate of the cutter's position. + y (float): The y-coordinate of the cutter's position. + radius (float): The radius of the cutter, used to adjust the collision detection. + startz (float): The starting z-coordinate for the collision test. + endz (float): The ending z-coordinate for the collision test. + + Returns: + float: The adjusted z-coordinate of the cutter tip if a collision is detected; + otherwise, returns a value 10 units below the specified endz. + """ + scene = bpy.context.scene + pos = scene.rigidbody_world.convex_sweep_test(cutter, (x * BULLET_SCALE, y * BULLET_SCALE, startz * BULLET_SCALE), + (x * BULLET_SCALE, y * BULLET_SCALE, endz * BULLET_SCALE)) + + # radius is subtracted because we are interested in cutter tip position, this gets collision object center + + if pos[3] == 1: + return (pos[0][2] - radius) / BULLET_SCALE + else: + return endz - 10
+ + + +
+[docs] +def getSampleBulletNAxis(cutter, startpoint, endpoint, rotation, cutter_compensation): + """Perform a fully 3D collision test for N-Axis milling. + + This function computes the collision detection between a cutter and a + specified path in a 3D space. It takes into account the cutter's + rotation and compensation to accurately determine if a collision occurs + during the milling process. The function uses Bullet physics for the + collision detection and returns the adjusted position of the cutter if a + collision is detected. + + Args: + cutter (object): The cutter object used in the milling operation. + startpoint (Vector): The starting point of the milling path. + endpoint (Vector): The ending point of the milling path. + rotation (Euler): The rotation applied to the cutter. + cutter_compensation (float): The compensation factor for the cutter's position. + + Returns: + Vector or None: The adjusted position of the cutter if a collision is + detected; + otherwise, returns None. + """ + cutterVec = Vector((0, 0, 1)) * cutter_compensation + # cutter compensation vector - cutter physics object has center in the middle, while cam needs the tip position. + cutterVec.rotate(Euler(rotation)) + start = (startpoint * BULLET_SCALE + cutterVec).to_tuple() + end = (endpoint * BULLET_SCALE + cutterVec).to_tuple() + pos = bpy.context.scene.rigidbody_world.convex_sweep_test(cutter, start, end) + + if pos[3] == 1: + pos = Vector(pos[0]) + # rescale and compensate from center to tip. + res = pos / BULLET_SCALE - cutterVec / BULLET_SCALE + + return res + else: + return None
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/constants.html b/_modules/cam/constants.html new file mode 100644 index 000000000..ba1d8016f --- /dev/null +++ b/_modules/cam/constants.html @@ -0,0 +1,441 @@ + + + + + + + + + + cam.constants — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.constants

+"""CNC CAM 'constants.py'
+
+Package to store all constants of CNC CAM.
+"""
+
+# PRECISION is used in most operations
+
+[docs] +PRECISION = 5
+ + +
+[docs] +CHIPLOAD_PRECISION = 10
+ + +
+[docs] +MAX_OPERATION_TIME = 3200000000 # seconds
+ + +
+[docs] +G64_INCOMPATIBLE_MACHINES = ['GRBL']
+ + +# Upscale factor for higher precision from Bullet library - (Rigidbody Collision World) +
+[docs] +BULLET_SCALE = 10000
+ + +# Cutter object must be present in the scene, so we need to put it aside for sweep collisions, +# otherwise it collides with itself. +
+[docs] +CUTTER_OFFSET = (-5 * BULLET_SCALE, -5 * BULLET_SCALE, -5 * BULLET_SCALE)
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/curvecamcreate.html b/_modules/cam/curvecamcreate.html new file mode 100644 index 000000000..aafb283d1 --- /dev/null +++ b/_modules/cam/curvecamcreate.html @@ -0,0 +1,2603 @@ + + + + + + + + + + cam.curvecamcreate — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.curvecamcreate

+"""CNC CAM 'curvecamcreate.py' © 2021, 2022 Alain Pelletier
+
+Operators to create a number of predefined curve objects.
+"""
+
+from math import (
+    degrees,
+    hypot,
+    pi,
+    radians
+)
+
+from shapely.geometry import (
+    LineString,
+    MultiLineString,
+    box,
+)
+
+import bpy
+from bpy.props import (
+    BoolProperty,
+    EnumProperty,
+    FloatProperty,
+    IntProperty,
+)
+from bpy.types import Operator
+
+from . import (
+    involute_gear,
+    joinery,
+    puzzle_joinery,
+    simple,
+    utils,
+)
+
+
+[docs] +def generate_crosshatch(context, angle, distance, offset, + pocket_shape,join, ob = None): + """Execute the crosshatch generation process based on the provided context. + + Args: + context (bpy.context): The Blender context containing the active object. + angle (float): The angle for rotating the crosshatch pattern. + distance (float): The distance between crosshatch lines. + offset (float): The offset for the bounds or hull. + pocket_shape (str): Determines whether to use bounds, hull, or pocket. + + Returns: + shapely.geometry.MultiLineString: The resulting intersection geometry of the crosshatch. + """ + if ob is None : + ob = context.active_object + else: + bpy.context.view_layer.objects.active = ob + ob.select_set(True) + if ob.data.splines and ob.data.splines[0].type == 'BEZIER': + bpy.ops.object.curve_remove_doubles(merge_distance=0.0001, keep_bezier=True) + else: + bpy.ops.object.curve_remove_doubles() + + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN') + depth = ob.location[2] + + shapes = utils.curveToShapely(ob) + + if pocket_shape == 'HULL': + shapes = shapes.convex_hull + + from shapely import affinity + + coords = [] + minx, miny, maxx, maxy = shapes.bounds + minx -= offset + miny -= offset + maxx += offset + maxy += offset + centery = (miny + maxy) / 2 + length = maxy - miny + width = maxx - minx + centerx = (minx + maxx) / 2 + diagonal = hypot(width, length) + + bound_rectangle = box(minx, miny, maxx, maxy) + amount = int(2 * diagonal / distance) + 1 + + for x in range(amount): + distance_val = x * distance - diagonal + coords.append(((distance_val, diagonal + 0.5), (distance_val, -diagonal - 0.5))) + + # Create a multilinestring shapely object + lines = MultiLineString(coords) + rotated = affinity.rotate(lines, angle, use_radians=True) # Rotate using shapely + rotated_minx, rotated_miny, rotated_maxx, rotated_maxy = rotated.bounds + rotated_centerx = (rotated_minx + rotated_maxx) / 2 + rotated_centery = (rotated_miny + rotated_maxy) / 2 + x_offset = centerx - rotated_centerx + y_offset = centery - rotated_centery + translated = affinity.translate(rotated, xoff=x_offset, yoff=y_offset, zoff=depth) # Move using shapely + + bounds = bound_rectangle + + if pocket_shape == 'BOUNDS': + xing = translated.intersection(bounds) # Intersection with bounding box + else: + xing = translated.intersection(shapes.buffer(offset,join_style=join)) # Intersection with shapes or hull + + # Return the intersection result + return xing
+ + +
+[docs] +class CamCurveHatch(Operator): + """Perform Hatch Operation on Single or Multiple Curves""" # by Alain Pelletier September 2021 +
+[docs] + bl_idname = "object.curve_hatch"
+ +
+[docs] + bl_label = "CrossHatch Curve"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + angle: FloatProperty(default=0, min=-pi/2, max=pi/2, precision=4, subtype="ANGLE")
+ +
+[docs] + distance: FloatProperty(default=0.003, min=0, max=3.0, precision=4, unit="LENGTH")
+ +
+[docs] + offset: FloatProperty(default=0, min=-1.0, max=3.0, precision=4, unit="LENGTH")
+ +
+[docs] + pocket_shape: EnumProperty(name='Pocket Shape',items=(('BOUNDS', 'Bounds Rectangle', 'Uses a bounding rectangle'), + ('HULL', 'Convex Hull', 'Uses a convex hull'), + ('POCKET', 'Pocket', 'Uses the pocket shape'), ), + description='Type of pocket shape', + default='POCKET', + )
+ +
+[docs] + contour: BoolProperty( + name="Contour Curve", + default=False, + )
+ + +
+[docs] + xhatch: BoolProperty( + name="Crosshatch #", + default=False, + )
+ + +
+[docs] + contour_separate: BoolProperty( + name="Contour Separate", + default=False, + )
+ + +
+[docs] + straight: BoolProperty( + name="Overshoot Style", + description="Use overshoot cutout instead of conventional rounded", + default=True, + )
+ + @classmethod +
+[docs] + def poll(cls, context): + return context.active_object is not None and context.active_object.type in ['CURVE', 'FONT']
+ + +
+[docs] + def draw(self, context): + """Draw the layout properties for the given context.""" + layout = self.layout + layout.prop(self, 'angle') + layout.prop(self, 'distance') + layout.prop(self, 'offset') + layout.prop(self, 'pocket_shape') + layout.prop(self, 'xhatch') + if self.pocket_shape=='POCKET': + layout.prop(self, 'straight') + layout.prop(self, 'contour') + if self.contour: + layout.prop(self, 'contour_separate')
+ + +
+[docs] + def execute(self, context): + if self.straight: + join = 2 + else: + join = 1 + ob = context.active_object + obname = ob.name + ob.select_set(True) + simple.remove_multiple("crosshatch") + depth = ob.location[2] + xingOffset = self.offset + + if self.contour: + xingOffset = self.offset - self.distance/2 # contour does not touch the crosshatch + xing = generate_crosshatch( + context, + self.angle, + self.distance, + xingOffset, + self.pocket_shape, + join, + ) + utils.shapelyToCurve('crosshatch_lines', xing, depth) + + if self.xhatch: + simple.make_active(obname) + xingra = generate_crosshatch( + context, + self.angle + pi/2, + self.distance, + xingOffset, + self.pocket_shape, + join, + ) + utils.shapelyToCurve('crosshatch_lines_ra', xingra, depth) + + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN') + simple.join_multiple('crosshatch') + if self.contour: + simple.deselect() + bpy.context.view_layer.objects.active = ob + ob.select_set(True) + bpy.ops.object.silhouete_offset(offset=self.offset) + if self.contour_separate: + simple.active_name('contour_hatch') + simple.deselect() + else: + simple.active_name('crosshatch_contour') + simple.join_multiple('crosshatch') + simple.remove_doubles() + else: + simple.join_multiple('crosshatch') + simple.remove_doubles() + return {'FINISHED'}
+
+ +
+[docs] +class CamCurvePlate(Operator): + """Perform Generates Rounded Plate with Mounting Holes""" # by Alain Pelletier Sept 2021 +
+[docs] + bl_idname = "object.curve_plate"
+ +
+[docs] + bl_label = "Sign Plate"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + radius: FloatProperty( + name="Corner Radius", + default=.025, + min=0, + max=0.1, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + width: FloatProperty( + name="Width of Plate", + default=0.3048, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + height: FloatProperty( + name="Height of Plate", + default=0.457, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + hole_diameter: FloatProperty( + name="Hole Diameter", + default=0.01, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + hole_tolerance: FloatProperty( + name="Hole V Tolerance", + default=0.005, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + hole_vdist: FloatProperty( + name="Hole Vert Distance", + default=0.400, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + hole_hdist: FloatProperty( + name="Hole Horiz Distance", + default=0, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + hole_hamount: IntProperty( + name="Hole Horiz Amount", + default=1, + min=0, + max=50, + )
+ +
+[docs] + resolution: IntProperty( + name="Spline Resolution", + default=50, + min=3, + max=150, + )
+ +
+[docs] + plate_type: EnumProperty( + name='Type Plate', + items=( + ('ROUNDED', 'Rounded corner', 'Makes a rounded corner plate'), + ('COVE', 'Cove corner', + 'Makes a plate with circles cut in each corner '), + ('BEVEL', 'Bevel corner', 'Makes a plate with beveled corners '), + ('OVAL', 'Elipse', 'Makes an oval plate') + ), + description='Type of Plate', + default='ROUNDED', + )
+ + +
+[docs] + def draw(self, context): + """Draw the UI layout for plate properties. + + This method creates a user interface layout for configuring various + properties of a plate, including its type, dimensions, hole + specifications, and resolution. It dynamically adds properties to the + layout based on the selected plate type, allowing users to input + relevant parameters. + + Args: + context: The context in which the UI is being drawn. + """ + + layout = self.layout + layout.prop(self, 'plate_type') + layout.prop(self, 'width') + layout.prop(self, 'height') + layout.prop(self, 'hole_diameter') + layout.prop(self, 'hole_tolerance') + layout.prop(self, 'hole_vdist') + layout.prop(self, 'hole_hdist') + layout.prop(self, 'hole_hamount') + layout.prop(self, 'resolution') + + if self.plate_type in ["ROUNDED", "COVE", "BEVEL"]: + layout.prop(self, 'radius')
+ + +
+[docs] + def execute(self, context): + """Execute the creation of a plate based on specified parameters. + + This function generates a plate shape in Blender based on the defined + attributes such as width, height, radius, and plate type. It supports + different plate types including rounded, oval, cove, and bevel. The + function also handles the creation of holes in the plate if specified. + It utilizes Blender's curve operations to create the geometry and + applies various transformations to achieve the desired shape. + + Args: + context (bpy.context): The Blender context in which the operation is performed. + + Returns: + dict: A dictionary indicating the result of the operation, typically + {'FINISHED'} if successful. + """ + + left = -self.width / 2 + self.radius + bottom = -self.height / 2 + self.radius + right = -left + top = -bottom + + if self.plate_type == "ROUNDED": + # create base + bpy.ops.curve.primitive_bezier_circle_add(radius=self.radius, enter_editmode=False, align='WORLD', + location=(left, bottom, 0), scale=(1, 1, 1)) + simple.active_name("_circ_LB") + bpy.context.object.data.resolution_u = self.resolution + bpy.ops.curve.primitive_bezier_circle_add(radius=self.radius, enter_editmode=False, align='WORLD', + location=(right, bottom, 0), scale=(1, 1, 1)) + simple.active_name("_circ_RB") + bpy.context.object.data.resolution_u = self.resolution + bpy.ops.curve.primitive_bezier_circle_add(radius=self.radius, enter_editmode=False, align='WORLD', + location=(left, top, 0), scale=(1, 1, 1)) + simple.active_name("_circ_LT") + bpy.context.object.data.resolution_u = self.resolution + bpy.ops.curve.primitive_bezier_circle_add(radius=self.radius, enter_editmode=False, align='WORLD', + location=(right, top, 0), scale=(1, 1, 1)) + simple.active_name("_circ_RT") + bpy.context.object.data.resolution_u = self.resolution + + # select the circles for the four corners + simple.select_multiple("_circ") + # perform hull operation on the four corner circles + utils.polygonConvexHull(context) + simple.active_name("plate_base") + simple.remove_multiple("_circ") # remove corner circles + + elif self.plate_type == "OVAL": + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Ellipse', + Simple_a=self.width/2, Simple_b=self.height/2, use_cyclic_u=True, edit_mode=False) + bpy.context.object.data.resolution_u = self.resolution + simple.active_name("plate_base") + + elif self.plate_type == 'COVE': + bpy.ops.curve.primitive_bezier_circle_add(radius=self.radius, enter_editmode=False, align='WORLD', + location=(left-self.radius, bottom-self.radius, 0), scale=(1, 1, 1)) + simple.active_name("_circ_LB") + bpy.context.object.data.resolution_u = self.resolution + bpy.ops.curve.primitive_bezier_circle_add(radius=self.radius, enter_editmode=False, align='WORLD', + location=(right+self.radius, bottom-self.radius, 0), scale=(1, 1, 1)) + simple.active_name("_circ_RB") + bpy.context.object.data.resolution_u = self.resolution + bpy.ops.curve.primitive_bezier_circle_add(radius=self.radius, enter_editmode=False, align='WORLD', + location=(left-self.radius, top+self.radius, 0), scale=(1, 1, 1)) + simple.active_name("_circ_LT") + bpy.context.object.data.resolution_u = self.resolution + bpy.ops.curve.primitive_bezier_circle_add(radius=self.radius, enter_editmode=False, align='WORLD', + location=(right+self.radius, top+self.radius, 0), scale=(1, 1, 1)) + simple.active_name("_circ_RT") + bpy.context.object.data.resolution_u = self.resolution + + simple.join_multiple("_circ") + + bpy.ops.curve.simple(align='WORLD', Simple_Type='Rectangle', + Simple_width=self.width, Simple_length=self.height, outputType='POLY', use_cyclic_u=True, + edit_mode=False) + simple.active_name("_base") + + simple.difference("_", "_base") + simple.rename("_base", "plate_base") + + elif self.plate_type == 'BEVEL': + bpy.ops.curve.simple(align='WORLD', Simple_Type='Rectangle', + Simple_width=self.radius*2, Simple_length=self.radius*2, location=(left-self.radius, bottom-self.radius, 0), + rotation=(0, 0, 0.785398), outputType='POLY', use_cyclic_u=True, edit_mode=False) + simple.active_name("_bev_LB") + bpy.context.object.data.resolution_u = self.resolution + bpy.ops.curve.simple(align='WORLD', Simple_Type='Rectangle', + Simple_width=self.radius*2, Simple_length=self.radius*2, + location=(right+self.radius, + bottom-self.radius, 0), + rotation=(0, 0, 0.785398), outputType='POLY', use_cyclic_u=True, edit_mode=False) + simple.active_name("_bev_RB") + bpy.context.object.data.resolution_u = self.resolution + bpy.ops.curve.simple(align='WORLD', Simple_Type='Rectangle', + Simple_width=self.radius*2, Simple_length=self.radius*2, + location=(left-self.radius, + top+self.radius, 0), + rotation=(0, 0, 0.785398), outputType='POLY', use_cyclic_u=True, edit_mode=False) + + simple.active_name("_bev_LT") + bpy.context.object.data.resolution_u = self.resolution + + bpy.ops.curve.simple(align='WORLD', Simple_Type='Rectangle', + Simple_width=self.radius*2, Simple_length=self.radius*2, + location=(right+self.radius, + top+self.radius, 0), + rotation=(0, 0, 0.785398), outputType='POLY', use_cyclic_u=True, edit_mode=False) + + simple.active_name("_bev_RT") + bpy.context.object.data.resolution_u = self.resolution + + simple.join_multiple("_bev") + + bpy.ops.curve.simple(align='WORLD', Simple_Type='Rectangle', + Simple_width=self.width, Simple_length=self.height, outputType='POLY', use_cyclic_u=True, + edit_mode=False) + simple.active_name("_base") + + simple.difference("_", "_base") + simple.rename("_base", "plate_base") + + if self.hole_diameter > 0 or self.hole_hamount > 0: + bpy.ops.curve.primitive_bezier_circle_add(radius=self.hole_diameter / 2, enter_editmode=False, + align='WORLD', location=(0, self.hole_tolerance / 2, 0), + scale=(1, 1, 1)) + simple.active_name("_hole_Top") + bpy.context.object.data.resolution_u = int(self.resolution / 4) + if self.hole_tolerance > 0: + bpy.ops.curve.primitive_bezier_circle_add(radius=self.hole_diameter / 2, enter_editmode=False, + align='WORLD', location=(0, -self.hole_tolerance / 2, 0), + scale=(1, 1, 1)) + simple.active_name("_hole_Bottom") + bpy.context.object.data.resolution_u = int(self.resolution / 4) + + # select everything starting with _hole and perform a convex hull on them + simple.select_multiple("_hole") + utils.polygonConvexHull(context) + simple.active_name("plate_hole") + simple.move(y=-self.hole_vdist / 2) + simple.duplicate(y=self.hole_vdist) + + simple.remove_multiple("_hole") # remove temporary holes + + simple.join_multiple("plate_hole") # join the holes together + + # horizontal holes + if self.hole_hamount > 1: + if self.hole_hamount % 2 != 0: + for x in range(int((self.hole_hamount - 1) / 2)): + # calculate the distance from the middle + dist = self.hole_hdist * (x + 1) + simple.duplicate() + bpy.context.object.location[0] = dist + simple.duplicate() + bpy.context.object.location[0] = -dist + else: + for x in range(int(self.hole_hamount / 2)): + dist = self.hole_hdist * x + self.hole_hdist / \ + 2 # calculate the distance from the middle + if x == 0: # special case where the original hole only needs to move and not duplicate + bpy.context.object.location[0] = dist + simple.duplicate() + bpy.context.object.location[0] = -dist + else: + simple.duplicate() + bpy.context.object.location[0] = dist + simple.duplicate() + bpy.context.object.location[0] = -dist + simple.join_multiple("plate_hole") # join the holes together + + # select everything starting with plate_ + simple.select_multiple("plate_") + + # Make the plate base active + bpy.context.view_layer.objects.active = bpy.data.objects['plate_base'] + # Remove holes from the base + utils.polygonBoolean(context, "DIFFERENCE") + simple.remove_multiple("plate_") # Remove temporary base and holes + simple.remove_multiple("_") + + simple.active_name("plate") + bpy.context.active_object.select_set(True) + bpy.ops.object.curve_remove_doubles() + + return {'FINISHED'}
+
+ + + +
+[docs] +class CamCurveFlatCone(Operator): + """Generates cone from flat stock""" # by Alain Pelletier Sept 2021 +
+[docs] + bl_idname = "object.curve_flat_cone"
+ +
+[docs] + bl_label = "Cone Flat Calculator"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + small_d: FloatProperty( + name="Small Diameter", + default=.025, + min=0, + max=0.1, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + large_d: FloatProperty( + name="Large Diameter", + default=0.3048, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + height: FloatProperty( + name="Height of Cone", + default=0.457, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + tab: FloatProperty( + name="Tab Witdh", + default=0.01, + min=0, + max=0.100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + intake: FloatProperty( + name="Intake Diameter", + default=0, + min=0, + max=0.200, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + intake_skew: FloatProperty( + name="Intake Skew", + default=1, + min=0.1, + max=4, + )
+ +
+[docs] + resolution: IntProperty( + name="Resolution", + default=12, + min=5, + max=200, + )
+ + +
+[docs] + def execute(self, context): + """Execute the construction of a geometric shape in Blender. + + This method performs a series of operations to create a geometric shape + based on specified dimensions and parameters. It calculates various + dimensions needed for the shape, including height and angles, and then + uses Blender's operations to create segments, rectangles, and ellipses. + The function also handles the positioning and rotation of these shapes + within the 3D space of Blender. + + Args: + context: The context in which the operation is executed, typically containing + information about the current + scene and active objects in Blender. + + Returns: + dict: A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + """ + + y = self.small_d / 2 + z = self.large_d / 2 + x = self.height + h = x * y / (z - y) + a = hypot(h, y) + ab = hypot(x+h, z) + b = ab - a + angle = pi * 2 * y / a + + # create base + 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") + + bpy.ops.curve.simple(align='WORLD', location=(a+b/2, -self.tab/2, 0), rotation=(0, 0, 0), + Simple_Type='Rectangle', + Simple_width=b-0.0050, Simple_length=self.tab, use_cyclic_u=True, edit_mode=False, + shape='3D') + + simple.active_name("_segment") + if self.intake > 0: + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Ellipse', + Simple_a=self.intake, Simple_b=self.intake*self.intake_skew, use_cyclic_u=True, + edit_mode=False, shape='3D') + simple.move(x=ab-3*self.intake/2) + simple.rotate(angle/2) + + bpy.context.object.data.resolution_u = self.resolution + + simple.union("_segment") + + simple.active_name('flat_cone') + + return {'FINISHED'}
+
+ + + +
+[docs] +class CamCurveMortise(Operator): + """Generates Mortise Along a Curve""" # by Alain Pelletier December 2021 +
+[docs] + bl_idname = "object.curve_mortise"
+ +
+[docs] + bl_label = "Mortise"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + finger_size: BoolProperty( + name="Kurf Bending only", + default=False, + )
+ + finger_size: FloatProperty( + name="Maximum Finger Size", + default=0.015, + min=0.005, + max=3.0, + precision=4, + unit="LENGTH", + ) +
+[docs] + min_finger_size: FloatProperty( + name="Minimum Finger Size", + default=0.0025, + min=0.001, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + finger_tolerance: FloatProperty( + name="Finger Play Room", + default=0.000045, + min=0, + max=0.003, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + plate_thickness: FloatProperty( + name="Drawer Plate Thickness", + default=0.00477, + min=0.001, + max=3.0, + unit="LENGTH", + )
+ +
+[docs] + side_height: FloatProperty( + name="Side Height", + default=0.05, + min=0.001, + max=3.0, + unit="LENGTH", + )
+ +
+[docs] + flex_pocket: FloatProperty( + name="Flex Pocket", + default=0.004, + min=0.000, + max=1.0, + unit="LENGTH", + )
+ +
+[docs] + top_bottom: BoolProperty( + name="Side Top & Bottom Fingers", + default=True, + )
+ +
+[docs] + opencurve: BoolProperty( + name="OpenCurve", + default=False, + )
+ +
+[docs] + adaptive: FloatProperty( + name="Adaptive Angle Threshold", + default=0.0, + min=0.000, + max=2, + subtype="ANGLE", + unit="ROTATION", + )
+ +
+[docs] + double_adaptive: BoolProperty( + name="Double Adaptive Pockets", + default=False, + )
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.active_object is not None and (context.active_object.type in ['CURVE', 'FONT'])
+ + +
+[docs] + def execute(self, context): + """Execute the joinery process based on the provided context. + + This function performs a series of operations to duplicate the active + object, convert it to a mesh, and then process its geometry to create + joinery features. It extracts vertex coordinates, converts them into a + LineString data structure, and applies either variable or fixed finger + joinery based on the specified parameters. The function also handles the + creation of flexible sides and pockets if required. + + Args: + context (bpy.context): The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the completion status of the operation. + """ + + o1 = bpy.context.active_object + + bpy.context.object.data.resolution_u = 60 + bpy.ops.object.duplicate() + obj = context.active_object + bpy.ops.object.convert(target='MESH') + simple.active_name("_temp_mesh") + + if self.opencurve: + coords = [] + for v in obj.data.vertices: # extract X,Y coordinates from the vertices data + coords.append((v.co.x, v.co.y)) + # convert coordinates to shapely LineString datastructure + line = LineString(coords) + simple.remove_multiple("-converted") + utils.shapelyToCurve('-converted_curve', line, 0.0) + shapes = utils.curveToShapely(o1) + + for s in shapes.geoms: + if s.boundary.type == 'LineString': + loops = [s.boundary] + else: + loops = s.boundary + + for ci, c in enumerate(loops): + if self.opencurve: + length = line.length + else: + length = c.length + print("loop Length:", length) + if self.opencurve: + loop_length = line.length + else: + loop_length = c.length + print("line Length:", loop_length) + + if self.adaptive > 0.0: + joinery.variable_finger(c, length, self.min_finger_size, self.finger_size, self.plate_thickness, + self.finger_tolerance, self.adaptive) + locations = joinery.variable_finger(c, length, self.min_finger_size, self.finger_size, + self.plate_thickness, self.finger_tolerance, self.adaptive, + True, self.double_adaptive) + joinery.create_flex_side(loop_length, self.side_height, + self.plate_thickness, self.top_bottom) + if self.flex_pocket > 0: + joinery.make_variable_flex_pocket(self.side_height, self.plate_thickness, self.flex_pocket, + locations) + + else: + joinery.fixed_finger(c, length, self.finger_size, + self.plate_thickness, self.finger_tolerance) + joinery.fixed_finger(c, length, self.finger_size, + self.plate_thickness, self.finger_tolerance, True) + joinery.create_flex_side(loop_length, self.side_height, + self.plate_thickness, self.top_bottom) + if self.flex_pocket > 0: + joinery.make_flex_pocket(length, self.side_height, self.plate_thickness, self.finger_size, + self.flex_pocket) + simple.remove_multiple('_') + return {'FINISHED'}
+
+ + + +
+[docs] +class CamCurveInterlock(Operator): + """Generates Interlock Along a Curve""" # by Alain Pelletier December 2021 +
+[docs] + bl_idname = "object.curve_interlock"
+ +
+[docs] + bl_label = "Interlock"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + finger_size: FloatProperty( + name="Finger Size", + default=0.015, + min=0.005, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + finger_tolerance: FloatProperty( + name="Finger Play Room", + default=0.000045, + min=0, + max=0.003, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + plate_thickness: FloatProperty( + name="Plate Thickness", + default=0.00477, + min=0.001, + max=3.0, + unit="LENGTH", + )
+ +
+[docs] + opencurve: BoolProperty( + name="OpenCurve", + default=False, + )
+ +
+[docs] + interlock_type: EnumProperty( + name='Type of Interlock', + items=( + ('TWIST', 'Twist', 'Interlock requires 1/4 turn twist'), + ('GROOVE', 'Groove', 'Simple sliding groove'), + ('PUZZLE', 'Puzzle Interlock', 'Puzzle good for flat joints') + ), + description='Type of interlock', + default='GROOVE', + )
+ +
+[docs] + finger_amount: IntProperty( + name="Finger Amount", + default=2, + min=1, + max=100, + )
+ +
+[docs] + tangent_angle: FloatProperty( + name="Tangent Deviation", + default=0.0, + min=0.000, + max=2, + subtype="ANGLE", + unit="ROTATION", + )
+ +
+[docs] + fixed_angle: FloatProperty( + name="Fixed Angle", + default=0.0, + min=0.000, + max=2, + subtype="ANGLE", + unit="ROTATION", + )
+ + +
+[docs] + def execute(self, context): + """Execute the joinery operation based on the selected objects in the + context. + + This function checks the selected objects in the provided context and + performs different operations depending on the type of the active + object. If the active object is a curve or font and there are selected + objects, it duplicates the object, converts it to a mesh, and processes + its vertices to create a LineString representation. The function then + calculates lengths and applies distributed interlock joinery based on + the specified parameters. If no valid objects are selected, it defaults + to a single interlock operation at the cursor's location. + + Args: + context (bpy.context): The context containing selected objects and active object. + + Returns: + dict: A dictionary indicating the operation's completion status. + """ + + print(len(context.selected_objects), + "selected object", context.selected_objects) + if len(context.selected_objects) > 0 and (context.active_object.type in ['CURVE', 'FONT']): + o1 = bpy.context.active_object + + bpy.context.object.data.resolution_u = 60 + simple.duplicate() + obj = context.active_object + bpy.ops.object.convert(target='MESH') + simple.active_name("_temp_mesh") + + if self.opencurve: + coords = [] + for v in obj.data.vertices: # extract X,Y coordinates from the vertices data + coords.append((v.co.x, v.co.y)) + # convert coordinates to shapely LineString datastructure + line = LineString(coords) + simple.remove_multiple("-converted") + utils.shapelyToCurve('-converted_curve', line, 0.0) + shapes = utils.curveToShapely(o1) + + for s in shapes.geoms: + if s.boundary.type == 'LineString': + loops = [s.boundary] + else: + loops = s.boundary + + for ci, c in enumerate(loops): + if self.opencurve: + length = line.length + else: + length = c.length + print("loop Length:", length) + if self.opencurve: + loop_length = line.length + else: + loop_length = c.length + print("line Length:", loop_length) + + joinery.distributed_interlock(c, length, self.finger_size, self.plate_thickness, + self.finger_tolerance, self.finger_amount, + fixed_angle=self.fixed_angle, tangent=self.tangent_angle, + closed=not self.opencurve, type=self.interlock_type) + + else: + location = bpy.context.scene.cursor.location + joinery.single_interlock(self.finger_size, self.plate_thickness, self.finger_tolerance, location[0], + location[1], self.fixed_angle, self.interlock_type, self.finger_amount) + + bpy.context.scene.cursor.location = location + return {'FINISHED'}
+
+ + + +
+[docs] +class CamCurveDrawer(Operator): + """Generates Drawers""" # by Alain Pelletier December 2021 inspired by The Drawinator +
+[docs] + bl_idname = "object.curve_drawer"
+ +
+[docs] + bl_label = "Drawer"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + depth: FloatProperty( + name="Drawer Depth", + default=0.2, + min=0, + max=1.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + width: FloatProperty( + name="Drawer Width", + default=0.125, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + height: FloatProperty( + name="Drawer Height", + default=0.07, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + finger_size: FloatProperty( + name="Maximum Finger Size", + default=0.015, + min=0.005, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + finger_tolerance: FloatProperty( + name="Finger Play Room", + default=0.000045, + min=0, + max=0.003, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + finger_inset: FloatProperty( + name="Finger Inset", + default=0.0, + min=0.0, + max=0.01, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + drawer_plate_thickness: FloatProperty( + name="Drawer Plate Thickness", + default=0.00477, + min=0.001, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + drawer_hole_diameter: FloatProperty( + name="Drawer Hole Diameter", + default=0.02, + min=0.00001, + max=0.5, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + drawer_hole_offset: FloatProperty( + name="Drawer Hole Offset", + default=0.0, + min=-0.5, + max=0.5, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + overcut: BoolProperty( + name="Add Overcut", + default=False, + )
+ +
+[docs] + overcut_diameter: FloatProperty( + name="Overcut Tool Diameter", + default=0.003175, + min=-0.001, + max=0.5, + precision=4, + unit="LENGTH", + )
+ + +
+[docs] + def draw(self, context): + """Draw the user interface properties for the object. + + This method is responsible for rendering the layout of various + properties related to the object's dimensions and specifications. It + adds properties such as depth, width, height, finger size, finger + tolerance, finger inset, drawer plate thickness, drawer hole diameter, + drawer hole offset, and overcut diameter to the layout. The overcut + diameter property is only added if the overcut option is enabled. + + Args: + context: The context in which the drawing occurs, typically containing + information about the current state and environment. + """ + + layout = self.layout + layout.prop(self, 'depth') + layout.prop(self, 'width') + layout.prop(self, 'height') + layout.prop(self, 'finger_size') + layout.prop(self, 'finger_tolerance') + layout.prop(self, 'finger_inset') + layout.prop(self, 'drawer_plate_thickness') + layout.prop(self, 'drawer_hole_diameter') + layout.prop(self, 'drawer_hole_offset') + layout.prop(self, 'overcut') + if self.overcut: + layout.prop(self, 'overcut_diameter')
+ + +
+[docs] + def execute(self, context): + """Execute the drawer creation process in Blender. + + This method orchestrates the creation of a drawer by calculating the + necessary dimensions for the finger joints, creating the base plate, and + generating the drawer components such as the back, front, sides, and + bottom. It utilizes various helper functions to perform operations like + boolean differences and transformations to achieve the desired geometry. + The method also handles the placement of the drawer components in the 3D + space. + + Args: + context (bpy.context): The Blender context that provides access to the current scene and + objects. + + Returns: + dict: A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + """ + + height_finger_amt = int(joinery.finger_amount( + self.height, self.finger_size)) + height_finger = (self.height + 0.0004) / height_finger_amt + width_finger_amt = int(joinery.finger_amount( + self.width, self.finger_size)) + width_finger = (self.width - self.finger_size) / width_finger_amt + + # create base + joinery.create_base_plate(self.height, self.width, self.depth) + bpy.context.object.data.resolution_u = 64 + bpy.context.scene.cursor.location = (0, 0, 0) + + joinery.vertical_finger(height_finger, self.drawer_plate_thickness, + self.finger_tolerance, height_finger_amt) + + joinery.horizontal_finger(width_finger, self.drawer_plate_thickness, self.finger_tolerance, + width_finger_amt * 2) + simple.make_active('_wfb') + + bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN') + + # make drawer back + finger_pair = joinery.finger_pair( + "_vfa", self.width - self.drawer_plate_thickness - self.finger_inset * 2, 0) + simple.make_active('_wfa') + fronth = bpy.context.active_object + simple.make_active('_back') + finger_pair.select_set(True) + fronth.select_set(True) + bpy.ops.object.curve_boolean(boolean_type='DIFFERENCE') + simple.remove_multiple("_finger_pair") + simple.active_name("drawer_back") + simple.remove_doubles() + simple.add_overcut(self.overcut_diameter, self.overcut) + + # make drawer front + bpy.ops.curve.primitive_bezier_circle_add(radius=self.drawer_hole_diameter / 2, enter_editmode=False, + align='WORLD', location=(0, self.height + self.drawer_hole_offset, 0), + scale=(1, 1, 1)) + simple.active_name("_circ") + front_hole = bpy.context.active_object + simple.make_active('drawer_back') + front_hole.select_set(True) + bpy.ops.object.curve_boolean(boolean_type='DIFFERENCE') + simple.active_name("drawer_front") + simple.remove_doubles() + simple.add_overcut(self.overcut_diameter, self.overcut) + + # place back and front side by side + simple.make_active('drawer_front') + bpy.ops.transform.transform( + mode='TRANSLATION', value=(0.0, 2 * self.height, 0.0, 0.0)) + simple.make_active('drawer_back') + + bpy.ops.transform.transform(mode='TRANSLATION', value=( + self.width + 0.01, 2 * self.height, 0.0, 0.0)) + # make side + + finger_pair = joinery.finger_pair( + "_vfb", self.depth - self.drawer_plate_thickness, 0) + simple.make_active('_side') + finger_pair.select_set(True) + fronth.select_set(True) + bpy.ops.object.curve_boolean(boolean_type='DIFFERENCE') + simple.active_name("drawer_side") + simple.remove_doubles() + simple.add_overcut(self.overcut_diameter, self.overcut) + simple.remove_multiple('_finger_pair') + + # make bottom + simple.make_active("_wfb") + bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'}, + TRANSFORM_OT_translate={"value": (0, -self.drawer_plate_thickness / 2, 0.0)}) + simple.active_name("_wfb0") + joinery.finger_pair("_wfb0", 0, self.depth - + self.drawer_plate_thickness) + simple.active_name('_bot_fingers') + + simple.difference('_bot', '_bottom') + simple.rotate(pi/2) + + joinery.finger_pair("_wfb0", 0, self.width - + self.drawer_plate_thickness - self.finger_inset * 2) + simple.active_name('_bot_fingers') + simple.difference('_bot', '_bottom') + + simple.active_name("drawer_bottom") + + simple.remove_doubles() + simple.add_overcut(self.overcut_diameter, self.overcut) + + # cleanup all temp polygons + simple.remove_multiple("_") + + # move side and bottom to location + simple.make_active("drawer_side") + bpy.ops.transform.transform(mode='TRANSLATION', + value=(self.depth / 2 + 3 * self.width / 2 + 0.02, 2 * self.height, 0.0, 0.0)) + + simple.make_active("drawer_bottom") + bpy.ops.transform.transform(mode='TRANSLATION', + value=(self.depth / 2 + 3 * self.width / 2 + 0.02, self.width / 2, 0.0, 0.0)) + + simple.select_multiple('drawer') + return {'FINISHED'}
+
+ + + +
+[docs] +class CamCurvePuzzle(Operator): + """Generates Puzzle Joints and Interlocks""" # by Alain Pelletier December 2021 +
+[docs] + bl_idname = "object.curve_puzzle"
+ +
+[docs] + bl_label = "Puzzle Joints"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + diameter: FloatProperty( + name="Tool Diameter", + default=0.003175, + min=0.001, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + finger_tolerance: FloatProperty( + name="Finger Play Room", + default=0.00005, + min=0, + max=0.003, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + finger_amount: IntProperty( + name="Finger Amount", + default=1, + min=0, + max=100, + )
+ +
+[docs] + stem_size: IntProperty( + name="Size of the Stem", + default=2, + min=1, + max=200, + )
+ +
+[docs] + width: FloatProperty( + name="Width", + default=0.100, + min=0.005, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + height: FloatProperty( + name="Height or Thickness", + default=0.025, + min=0.005, + max=3.0, + precision=4, + unit="LENGTH", + )
+ + +
+[docs] + angle: FloatProperty( + name="Angle A", + default=pi/4, + min=-10, + max=10, + subtype="ANGLE", + unit="ROTATION", + )
+ +
+[docs] + angleb: FloatProperty( + name="Angle B", + default=pi/4, + min=-10, + max=10, + subtype="ANGLE", + unit="ROTATION", + )
+ + +
+[docs] + radius: FloatProperty( + name="Arc Radius", + default=0.025, + min=0.005, + max=5, + precision=4, + unit="LENGTH", + )
+ + +
+[docs] + interlock_type: EnumProperty( + name='Type of Shape', + items=( + ('JOINT', 'Joint', 'Puzzle Joint interlock'), + ('BAR', 'Bar', 'Bar interlock'), + ('ARC', 'Arc', 'Arc interlock'), + ('MULTIANGLE', 'Multi angle', 'Multi angle joint'), + ('CURVEBAR', 'Arc Bar', 'Arc Bar interlock'), + ('CURVEBARCURVE', 'Arc Bar Arc', 'Arc Bar Arc interlock'), + ('CURVET', 'T curve', 'T curve interlock'), + ('T', 'T Bar', 'T Bar interlock'), + ('CORNER', 'Corner Bar', 'Corner Bar interlock'), + ('TILE', 'Tile', 'Tile interlock'), + ('OPENCURVE', 'Open Curve', 'Corner Bar interlock') + ), + description='Type of interlock', + default='CURVET', + )
+ +
+[docs] + gender: EnumProperty( + name='Type Gender', + items=( + ('MF', 'Male-Receptacle', 'Male and receptacle'), + ('F', 'Receptacle only', 'Receptacle'), + ('M', 'Male only', 'Male') + ), + description='Type of interlock', + default='MF', + )
+ +
+[docs] + base_gender: EnumProperty( + name='Base Gender', + items=( + ('MF', 'Male - Receptacle', 'Male - Receptacle'), + ('F', 'Receptacle', 'Receptacle'), + ('M', 'Male', 'Male') + ), + description='Type of interlock', + default='M', + )
+ +
+[docs] + multiangle_gender: EnumProperty( + name='Multiangle Gender', + items=( + ('MMF', 'Male Male Receptacle', 'M M F'), + ('MFF', 'Male Receptacle Receptacle', 'M F F') + ), + description='Type of interlock', + default='MFF', + )
+ + +
+[docs] + mitre: BoolProperty( + name="Add Mitres", + default=False, + )
+ + +
+[docs] + twist_lock: BoolProperty( + name="Add TwistLock", + default=False, + )
+ +
+[docs] + twist_thick: FloatProperty( + name="Twist Thickness", + default=0.0047, + min=0.001, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + twist_percent: FloatProperty( + name="Twist Neck", + default=0.3, + min=0.1, + max=0.9, + precision=4, + )
+ +
+[docs] + twist_keep: BoolProperty( + name="Keep Twist Holes", + default=False, + )
+ +
+[docs] + twist_line: BoolProperty( + name="Add Twist to Bar", + default=False, + )
+ +
+[docs] + twist_line_amount: IntProperty( + name="Amount of Separators", + default=2, + min=1, + max=600, + )
+ +
+[docs] + twist_separator: BoolProperty( + name="Add Twist Separator", + default=False, + )
+ +
+[docs] + twist_separator_amount: IntProperty( + name="Amount of Separators", + default=2, + min=2, + max=600, + )
+ +
+[docs] + twist_separator_spacing: FloatProperty( + name="Separator Spacing", + default=0.025, + min=-0.004, + max=1.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + twist_separator_edge_distance: FloatProperty( + name="Separator Edge Distance", + default=0.01, + min=0.0005, + max=0.1, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + tile_x_amount: IntProperty( + name="Amount of X Fingers", + default=2, + min=1, + max=600, + )
+ +
+[docs] + tile_y_amount: IntProperty( + name="Amount of Y Fingers", + default=2, + min=1, + max=600, + )
+ +
+[docs] + interlock_amount: IntProperty( + name="Interlock Amount on Curve", + default=2, + min=0, + max=200, + )
+ +
+[docs] + overcut: BoolProperty( + name="Add Overcut", + default=False, + )
+ +
+[docs] + overcut_diameter: FloatProperty( + name="Overcut Tool Diameter", + default=0.003175, + min=-0.001, + max=0.5, + precision=4, + unit="LENGTH", + )
+ + +
+[docs] + def draw(self, context): + """Draws the user interface layout for interlock type properties. + + This method is responsible for creating and displaying the layout of + various properties related to different interlock types in the user + interface. It dynamically adjusts the layout based on the selected + interlock type, allowing users to input relevant parameters such as + dimensions, tolerances, and other characteristics specific to the chosen + interlock type. + + Args: + context: The context in which the layout is being drawn, typically + provided by the user interface framework. + + Returns: + None: This method does not return any value; it modifies the layout + directly. + """ + + layout = self.layout + layout.prop(self, 'interlock_type') + layout.label(text='Puzzle Joint Definition') + layout.prop(self, 'stem_size') + layout.prop(self, 'diameter') + layout.prop(self, 'finger_tolerance') + if self.interlock_type == 'TILE': + layout.prop(self, 'tile_x_amount') + layout.prop(self, 'tile_y_amount') + else: + layout.prop(self, 'finger_amount') + if self.interlock_type != 'JOINT' and self.interlock_type != 'TILE': + layout.prop(self, 'twist_lock') + if self.twist_lock: + layout.prop(self, 'twist_thick') + layout.prop(self, 'twist_percent') + layout.prop(self, 'twist_keep') + layout.prop(self, 'twist_line') + if self.twist_line: + layout.prop(self, 'twist_line_amount') + layout.prop(self, 'twist_separator') + if self.twist_separator: + layout.prop(self, 'twist_separator_amount') + layout.prop(self, 'twist_separator_spacing') + layout.prop(self, 'twist_separator_edge_distance') + + if self.interlock_type == 'OPENCURVE': + layout.prop(self, 'interlock_amount') + layout.separator() + layout.prop(self, 'height') + + if self.interlock_type == 'BAR': + layout.prop(self, 'mitre') + + if self.interlock_type in ["ARC", "CURVEBARCURVE", "CURVEBAR", "MULTIANGLE", 'CURVET'] \ + or (self.interlock_type == 'BAR' and self.mitre): + if self.interlock_type == 'MULTIANGLE': + layout.prop(self, 'multiangle_gender') + elif self.interlock_type != 'CURVET': + layout.prop(self, 'gender') + if not self.mitre: + layout.prop(self, 'radius') + layout.prop(self, 'angle') + if self.interlock_type == 'CURVEBARCURVE' or self.mitre: + layout.prop(self, 'angleb') + + if self.interlock_type in ['BAR', 'CURVEBARCURVE', 'CURVEBAR', "T", 'CORNER', 'CURVET']: + layout.prop(self, 'gender') + if self.interlock_type in ['T', 'CURVET']: + layout.prop(self, 'base_gender') + if self.interlock_type == 'CURVEBARCURVE': + layout.label(text="Width includes 2 radius and thickness") + layout.prop(self, 'width') + if self.interlock_type != 'TILE': + layout.prop(self, 'overcut') + if self.overcut: + layout.prop(self, 'overcut_diameter')
+ + +
+[docs] + def execute(self, context): + """Execute the puzzle joinery process based on the provided context. + + This method processes the selected objects in the given context to + perform various types of puzzle joinery operations. It first checks if + there are any selected objects and if the active object is a curve. If + so, it duplicates the object, applies transformations, and converts it + to a mesh. The method then extracts vertex coordinates and performs + different joinery operations based on the specified interlock type. + Supported interlock types include 'FINGER', 'JOINT', 'BAR', 'ARC', + 'CURVEBARCURVE', 'CURVEBAR', 'MULTIANGLE', 'T', 'CURVET', 'CORNER', + 'TILE', and 'OPENCURVE'. + + Args: + context (Context): The context containing selected objects and the active object. + + Returns: + dict: A dictionary indicating the completion status of the operation. + """ + + curve_detected = False + print(len(context.selected_objects), + "selected object", context.selected_objects) + if len(context.selected_objects) > 0 and context.active_object.type == 'CURVE': + curve_detected = True + # bpy.context.object.data.resolution_u = 60 + simple.duplicate() + bpy.ops.object.transform_apply(location=True) + obj = context.active_object + bpy.ops.object.convert(target='MESH') + bpy.context.active_object.name = "_tempmesh" + + coords = [] + for v in obj.data.vertices: # extract X,Y coordinates from the vertices data + coords.append((v.co.x, v.co.y)) + simple.remove_multiple('_tmp') + # convert coordinates to shapely LineString datastructure + line = LineString(coords) + simple.remove_multiple("_") + + if self.interlock_type == 'FINGER': + puzzle_joinery.finger( + self.diameter, self.finger_tolerance, stem=self.stem_size) + simple.rename('_puzzle', 'receptacle') + puzzle_joinery.finger(self.diameter, 0, stem=self.stem_size) + simple.rename('_puzzle', 'finger') + + if self.interlock_type == 'JOINT': + if self.finger_amount == 0: # cannot be 0 in joints + self.finger_amount = 1 + puzzle_joinery.fingers(self.diameter, self.finger_tolerance, + self.finger_amount, stem=self.stem_size) + + if self.interlock_type == 'BAR': + if not self.mitre: + puzzle_joinery.bar(self.width, self.height, self.diameter, self.finger_tolerance, self.finger_amount, + stem=self.stem_size, twist=self.twist_lock, tneck=self.twist_percent, + tthick=self.twist_thick, twist_keep=self.twist_keep, + twist_line=self.twist_line, twist_line_amount=self.twist_line_amount, + which=self.gender) + else: + puzzle_joinery.mitre(self.width, self.height, self.angle, self.angleb, self.diameter, + self.finger_tolerance, self.finger_amount, stem=self.stem_size, + twist=self.twist_lock, tneck=self.twist_percent, + tthick=self.twist_thick, which=self.gender) + elif self.interlock_type == 'ARC': + puzzle_joinery.arc(self.radius, self.height, self.angle, self.diameter, + self.finger_tolerance, self.finger_amount, + stem=self.stem_size, twist=self.twist_lock, tneck=self.twist_percent, + tthick=self.twist_thick, which=self.gender) + elif self.interlock_type == 'CURVEBARCURVE': + puzzle_joinery.arcbararc(self.width, self.radius, self.height, self.angle, self.angleb, self.diameter, + self.finger_tolerance, self.finger_amount, + stem=self.stem_size, twist=self.twist_lock, tneck=self.twist_percent, + tthick=self.twist_thick, twist_keep=self.twist_keep, + twist_line=self.twist_line, twist_line_amount=self.twist_line_amount, + which=self.gender) + + elif self.interlock_type == 'CURVEBAR': + puzzle_joinery.arcbar(self.width, self.radius, self.height, self.angle, self.diameter, + self.finger_tolerance, self.finger_amount, + stem=self.stem_size, twist=self.twist_lock, tneck=self.twist_percent, + tthick=self.twist_thick, twist_keep=self.twist_keep, + twist_line=self.twist_line, twist_line_amount=self.twist_line_amount, + which=self.gender) + + elif self.interlock_type == 'MULTIANGLE': + 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, + combination=self.multiangle_gender) + + elif self.interlock_type == 'T': + puzzle_joinery.t(self.width, self.height, self.diameter, self.finger_tolerance, self.finger_amount, + stem=self.stem_size, twist=self.twist_lock, tneck=self.twist_percent, + tthick=self.twist_thick, combination=self.gender, base_gender=self.base_gender) + + elif self.interlock_type == 'CURVET': + puzzle_joinery.curved_t(self.width, self.height, self.radius, self.diameter, self.finger_tolerance, + self.finger_amount, + stem=self.stem_size, twist=self.twist_lock, tneck=self.twist_percent, + tthick=self.twist_thick, combination=self.gender, base_gender=self.base_gender) + + elif self.interlock_type == 'CORNER': + puzzle_joinery.t(self.width, self.height, self.diameter, self.finger_tolerance, self.finger_amount, + stem=self.stem_size, twist=self.twist_lock, tneck=self.twist_percent, + tthick=self.twist_thick, combination=self.gender, + base_gender=self.base_gender, corner=True) + + elif self.interlock_type == 'TILE': + puzzle_joinery.tile(self.diameter, self.finger_tolerance, self.tile_x_amount, self.tile_y_amount, + stem=self.stem_size) + + elif self.interlock_type == 'OPENCURVE' and curve_detected: + puzzle_joinery.open_curve(line, self.height, self.diameter, self.finger_tolerance, self.finger_amount, + stem=self.stem_size, twist=self.twist_lock, t_neck=self.twist_percent, + t_thick=self.twist_thick, which=self.gender, twist_amount=self.interlock_amount, + twist_keep=self.twist_keep) + + simple.remove_doubles() + simple.add_overcut(self.overcut_diameter, self.overcut) + + if self.twist_lock and self.twist_separator: + joinery.interlock_twist_separator(self.height, self.twist_thick, self.twist_separator_amount, + self.twist_separator_spacing, self.twist_separator_edge_distance, + finger_play=self.finger_tolerance, + percentage=self.twist_percent) + simple.remove_doubles() + simple.add_overcut(self.overcut_diameter, self.overcut) + return {'FINISHED'}
+
+ + + +
+[docs] +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 + +
+[docs] + bl_idname = "object.curve_gear"
+ +
+[docs] + bl_label = "Gears"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + tooth_spacing: FloatProperty( + name="Distance per Tooth", + default=0.010, + min=0.001, + max=1.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + tooth_amount: IntProperty( + name="Amount of Teeth", + default=7, + min=4, + )
+ + +
+[docs] + spoke_amount: IntProperty( + name="Amount of Spokes", + default=4, + min=0, + )
+ + +
+[docs] + hole_diameter: FloatProperty( + name="Hole Diameter", + default=0.003175, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + rim_size: FloatProperty( + name="Rim Size", + default=0.003175, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + hub_diameter: FloatProperty( + name="Hub Diameter", + default=0.005, + min=0, + max=3.0, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + pressure_angle: FloatProperty( + name="Pressure Angle", + default=radians(20), + min=0.001, + max=pi/2, + precision=4, + subtype="ANGLE", + unit="ROTATION", + )
+ +
+[docs] + clearance: FloatProperty( + name="Clearance", + default=0.00, + min=0, + max=0.1, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + backlash: FloatProperty( + name="Backlash", + default=0.0, + min=0.0, + max=0.1, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + rack_height: FloatProperty( + name="Rack Height", + default=0.012, + min=0.001, + max=1, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + rack_tooth_per_hole: IntProperty( + name="Teeth per Mounting Hole", + default=7, + min=2, + )
+ +
+[docs] + gear_type: EnumProperty( + name='Type of Gear', + items=( + ('PINION', 'Pinion', 'Circular Gear'), + ('RACK', 'Rack', 'Straight Rack') + ), + description='Type of gear', + default='PINION', + )
+ + +
+[docs] + def draw(self, context): + """Draw the user interface properties for gear settings. + + This method sets up the layout for various gear parameters based on the + selected gear type. It dynamically adds properties to the layout for + different gear types, allowing users to input specific values for gear + design. The properties include gear type, tooth spacing, tooth amount, + hole diameter, pressure angle, and backlash. Additional properties are + displayed if the gear type is 'PINION' or 'RACK'. + + Args: + context: The context in which the layout is being drawn. + """ + + layout = self.layout + layout.prop(self, 'gear_type') + layout.prop(self, 'tooth_spacing') + layout.prop(self, 'tooth_amount') + layout.prop(self, 'hole_diameter') + layout.prop(self, 'pressure_angle') + layout.prop(self, 'backlash') + if self.gear_type == 'PINION': + layout.prop(self, 'clearance') + layout.prop(self, 'spoke_amount') + layout.prop(self, 'rim_size') + layout.prop(self, 'hub_diameter') + elif self.gear_type == 'RACK': + layout.prop(self, 'rack_height') + layout.prop(self, 'rack_tooth_per_hole')
+ + +
+[docs] + def execute(self, context): + """Execute the gear generation process based on the specified gear type. + + This method checks the type of gear to be generated (either 'PINION' or + 'RACK') and calls the appropriate function from the `involute_gear` + module to create the gear or rack with the specified parameters. The + parameters include tooth spacing, number of teeth, hole diameter, + pressure angle, clearance, backlash, rim size, hub diameter, and spoke + amount for pinion gears, and additional parameters for rack gears. + + Args: + context: The context in which the execution is taking place. + + Returns: + dict: A dictionary indicating that the operation has finished with a key + 'FINISHED'. + """ + + if self.gear_type == 'PINION': + involute_gear.gear(mm_per_tooth=self.tooth_spacing, number_of_teeth=self.tooth_amount, + hole_diameter=self.hole_diameter, pressure_angle=self.pressure_angle, + clearance=self.clearance, backlash=self.backlash, + rim_size=self.rim_size, hub_diameter=self.hub_diameter, spokes=self.spoke_amount) + elif self.gear_type == 'RACK': + involute_gear.rack(mm_per_tooth=self.tooth_spacing, number_of_teeth=self.tooth_amount, + pressure_angle=self.pressure_angle, height=self.rack_height, + backlash=self.backlash, + tooth_per_hole=self.rack_tooth_per_hole, + hole_diameter=self.hole_diameter) + + return {'FINISHED'}
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/curvecamequation.html b/_modules/cam/curvecamequation.html new file mode 100644 index 000000000..7274a3db9 --- /dev/null +++ b/_modules/cam/curvecamequation.html @@ -0,0 +1,1119 @@ + + + + + + + + + + cam.curvecamequation — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.curvecamequation

+"""CNC CAM 'curvecamequation.py' © 2021, 2022 Alain Pelletier
+
+Operators to create a number of geometric shapes with curves.
+"""
+
+from math import pi, sin, cos, sqrt
+
+import numpy as np
+
+import bpy
+from bpy.props import (
+    EnumProperty,
+    FloatProperty,
+    IntProperty,
+    StringProperty,
+)
+from bpy.types import Operator
+
+from . import parametric
+
+
+
+[docs] +class CamSineCurve(Operator): + """Object Sine """ # by Alain Pelletier april 2021 +
+[docs] + bl_idname = "object.sine"
+ +
+[docs] + bl_label = "Periodic Wave"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + + # zstring: StringProperty(name="Z equation", description="Equation for z=F(u,v)", default="0.05*sin(2*pi*4*t)" ) +
+[docs] + axis: EnumProperty( + name="Displacement Axis", + items=( + ('XY', 'Y to displace X axis', 'Y constant; X sine displacement'), + ('YX', 'X to displace Y axis', 'X constant; Y sine displacement'), + ('ZX', 'X to displace Z axis', 'X constant; Y sine displacement'), + ('ZY', 'Y to displace Z axis', 'X constant; Y sine displacement') + ), + default='ZX', + )
+ +
+[docs] + wave: EnumProperty( + name="Wave", + items=( + ('sine', 'Sine Wave', 'Sine Wave'), + ('triangle', 'Triangle Wave', 'triangle wave'), + ('cycloid', 'Cycloid', 'Sine wave rectification'), + ('invcycloid', 'Inverse Cycloid', 'Sine wave rectification') + ), + default='sine', + )
+ +
+[docs] + amplitude: FloatProperty( + name="Amplitude", + default=.01, + min=0, + max=10, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + period: FloatProperty( + name="Period", + default=.5, + min=0.001, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + beatperiod: FloatProperty( + name="Beat Period Offset", + default=0.0, + min=0.0, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + shift: FloatProperty( + name="Phase Shift", + default=0, + min=-360, + max=360, + precision=4, + unit="ROTATION", + )
+ +
+[docs] + offset: FloatProperty( + name="Offset", + default=0, + min=- + 1.0, + max=1, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + iteration: IntProperty( + name="Iteration", + default=100, + min=50, + max=2000, + )
+ +
+[docs] + maxt: FloatProperty( + name="Wave Ends at X", + default=0.5, + min=-3.0, + max=3, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + mint: FloatProperty( + name="Wave Starts at X", + default=0, + min=-3.0, + max=3, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + wave_distance: FloatProperty( + name="Distance Between Multiple Waves", + default=0.0, + min=0.0, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + wave_angle_offset: FloatProperty( + name="Angle Offset for Multiple Waves", + default=pi/2, + min=-200*pi, + max=200*pi, + precision=4, + unit="ROTATION", + )
+ +
+[docs] + wave_amount: IntProperty( + name="Amount of Multiple Waves", + default=1, + min=1, + max=2000, + )
+ + +
+[docs] + def execute(self, context): + amp = self.amplitude + period = self.period + beatperiod = self.beatperiod + offset = self.offset + shift = self.shift + + # z=Asin(B(x+C))+D + if self.wave == 'sine': + zstring = ssine(amp, period, dc_offset=offset, phase_shift=shift) + if self.beatperiod != 0: + zstring += f"+ {ssine(amp, period+beatperiod, dc_offset=offset, phase_shift=shift)}" + + # build triangle wave from fourier series + elif self.wave == 'triangle': + zstring = f"{round(offset, 6) + triangle(80, period, amp)}" + if self.beatperiod != 0: + zstring += f"+ {triangle(80, period+beatperiod, amp)}" + + elif self.wave == 'cycloid': + zstring = f"abs({ssine(amp, period, dc_offset=offset, phase_shift=shift)})" + + elif self.wave == 'invcycloid': + zstring = f"-1 * abs({ssine(amp, period, dc_offset=offset, phase_shift=shift)})" + + print(zstring) + # make equation from string + + def e(t): + return eval(zstring) + + # build function to be passed to create parametric curve () + def f(t, offset: float = 0.0, angle_offset: float = 0.0): + if self.axis == "XY": + c = (e(t + angle_offset) + offset, t, 0) + elif self.axis == "YX": + c = (t, e(t + angle_offset) + offset, 0) + elif self.axis == "ZX": + c = (t, offset, e(t + angle_offset)) + elif self.axis == "ZY": + c = (offset, t, e(t + angle_offset)) + return c + + for i in range(self.wave_amount): + angle_off = self.wave_angle_offset * 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 + ) + + return {'FINISHED'}
+
+ + + +
+[docs] +class CamLissajousCurve(Operator): + """Lissajous """ # by Alain Pelletier april 2021 +
+[docs] + bl_idname = "object.lissajous"
+ +
+[docs] + bl_label = "Lissajous Figure"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + amplitude_A: FloatProperty( + name="Amplitude A", + default=.1, + min=0, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + waveA: EnumProperty( + name="Wave X", + items=( + ('sine', 'Sine Wave', 'Sine Wave'), + ('triangle', 'Triangle Wave', 'triangle wave') + ), + default='sine', + )
+ + +
+[docs] + amplitude_B: FloatProperty( + name="Amplitude B", + default=.1, + min=0, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + waveB: EnumProperty( + name="Wave Y", + items=( + ('sine', 'Sine Wave', 'Sine Wave'), + ('triangle', 'Triangle Wave', 'triangle wave') + ), + default='sine', + )
+ +
+[docs] + period_A: FloatProperty( + name="Period A", + default=1.1, + min=0.001, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + period_B: FloatProperty( + name="Period B", + default=1.0, + min=0.001, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + period_Z: FloatProperty( + name="Period Z", + default=1.0, + min=0.001, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + amplitude_Z: FloatProperty( + name="Amplitude Z", + default=0.0, + min=0, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + shift: FloatProperty( + name="Phase Shift", + default=0, + min=-360, + max=360, + precision=4, + unit="ROTATION", + )
+ + +
+[docs] + iteration: IntProperty( + name="Iteration", + default=500, + min=50, + max=10000, + )
+ +
+[docs] + maxt: FloatProperty( + name="Wave Ends at X", + default=11, + min=-3.0, + max=1000000, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + mint: FloatProperty( + name="Wave Starts at X", + default=0, + min=-10.0, + max=3, + precision=4, + unit="LENGTH", + )
+ + +
+[docs] + def execute(self, context): + # x=Asin(at+delta ),y=Bsin(bt) + + if self.waveA == 'sine': + xstring = ssine(self.amplitude_A, self.period_A, phase_shift=self.shift) + elif self.waveA == 'triangle': + xstring = f"{triangle(100, self.period_A, self.amplitude_A)}" + + if self.waveB == 'sine': + ystring = ssine(self.amplitude_B, self.period_B) + elif self.waveB == 'triangle': + ystring = f"{triangle(100, self.period_B, self.amplitude_B)}" + + zstring = ssine(self.amplitude_Z, self.period_Z) + + # make equation from string + def x(t): + return eval(xstring) + + def y(t): + return eval(ystring) + + def z(t): + return eval(zstring) + + print(f"x= {xstring}") + print(f"y= {ystring}") + + # build function to be passed to create parametric curve () + def f(t, offset: float = 0.0): + c = (x(t), y(t), z(t)) + return c + + parametric.create_parametric_curve( + f, + offset=0.0, + min=self.mint, + max=self.maxt, + use_cubic=True, + iterations=self.iteration + ) + + return {'FINISHED'}
+
+ + + +
+[docs] +class CamHypotrochoidCurve(Operator): + """Hypotrochoid """ # by Alain Pelletier april 2021 +
+[docs] + bl_idname = "object.hypotrochoid"
+ +
+[docs] + bl_label = "Spirograph Type Figure"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + typecurve: EnumProperty( + name="Type of Curve", + items=( + ('hypo', 'Hypotrochoid', 'Inside ring'), + ('epi', 'Epicycloid', 'Outside inner ring') + ), + )
+ +
+[docs] + R: FloatProperty( + name="Big Circle Radius", + default=0.25, + min=0.001, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + r: FloatProperty( + name="Small Circle Radius", + default=0.18, + min=0.0001, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + d: FloatProperty( + name="Distance from Center of Interior Circle", + default=0.050, + min=0, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + dip: FloatProperty( + name="Variable Depth from Center", + default=0.00, + min=-100, + max=100, + precision=4, + )
+ + +
+[docs] + def execute(self, context): + r = round(self.r, 6) + R = round(self.R, 6) + d = round(self.d, 6) + Rmr = round(R - r, 6) # R-r + 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 * pi * ((np.lcm(round(self.R * 1000), round(self.r * 1000)) / (R * 1000))) + + if self.typecurve == "hypo": + xstring = f"{Rmr} * cos(t) + {d} * cos({Rmror} * t)" + ystring = f"{Rmr} * sin(t) - {d} * sin({Rmror} * t)" + else: + xstring = f"{Rpr} * cos(t) - {d} * cos({Rpror} * t)" + ystring = f"{Rpr} * sin(t) - {d} * sin({Rpror} * t)" + + zstring = f"({round(self.dip, 6)} * (sqrt((({xstring})**2) + (({ystring})**2))))" + + # make equation from string + def x(t): + return eval(xstring) + + def y(t): + return eval(ystring) + + def z(t): + return eval(zstring) + + print(f"x= {xstring}") + print(f"y= {ystring}") + print(f"z= {zstring}") + print(f"maxangle {maxangle}") + + # build function to be passed to create parametric curve () + def f(t, offset: float = 0.0): + c = (x(t), y(t), z(t)) + return c + + iter = int(maxangle * 10) + if iter > 10000: # do not calculate more than 10000 points + print("limiting calculations to 10000 points") + iter = 10000 + parametric.create_parametric_curve( + f, + offset=0.0, + min=0, + max=maxangle, + use_cubic=True, + iterations=iter + ) + + return {'FINISHED'}
+
+ + + +
+[docs] +class CamCustomCurve(Operator): + """Object Custom Curve """ # by Alain Pelletier april 2021 +
+[docs] + bl_idname = "object.customcurve"
+ +
+[docs] + bl_label = "Custom Curve"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + xstring: StringProperty( + name="X Equation", + description="Equation x=F(t)", + default="t", + )
+ +
+[docs] + ystring: StringProperty( + name="Y Equation", + description="Equation y=F(t)", + default="0", + )
+ +
+[docs] + zstring: StringProperty( + name="Z Equation", + description="Equation z=F(t)", + default="0.05*sin(2*pi*4*t)", + )
+ + +
+[docs] + iteration: IntProperty( + name="Iteration", + default=100, + min=50, + max=2000, + )
+ +
+[docs] + maxt: FloatProperty( + name="Wave Ends at X", + default=0.5, + min=-3.0, + max=10, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + mint: FloatProperty( + name="Wave Starts at X", + default=0, + min=-3.0, + max=3, + precision=4, + unit="LENGTH", + )
+ + +
+[docs] + def execute(self, context): + print("x= " + self.xstring) + print("y= " + self.ystring) + print("z= " + self.zstring) + + # make equation from string + def ex(t): + return eval(self.xstring) + + def ey(t): + return eval(self.ystring) + + def ez(t): + return eval(self.zstring) + + # build function to be passed to create parametric curve () + def f(t, offset: float = 0.0): + c = (ex(t), ey(t), ez(t)) + return c + + parametric.create_parametric_curve( + f, + offset=0.0, + min=self.mint, + max=self.maxt, + use_cubic=True, + iterations=self.iteration + ) + + return {'FINISHED'}
+
+ + + +
+[docs] +def triangle(i, T, A): + s = f"{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*pi/(T/2), 8) + if n > 1: + s += '+' + s += f"{a} * sin({b} * t)" + s += ')' + return s
+ + + +
+[docs] +def ssine(A, T, dc_offset=0, phase_shift=0): + args = [ + dc_offset, + phase_shift, + A, + T + ] + for arg in args: + arg = round(arg, 6) + + return f"{dc_offset} + {A} * sin((2 * pi / {T}) * (t + {phase_shift}))"
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/curvecamtools.html b/_modules/cam/curvecamtools.html new file mode 100644 index 000000000..ffd01bab8 --- /dev/null +++ b/_modules/cam/curvecamtools.html @@ -0,0 +1,1632 @@ + + + + + + + + + + cam.curvecamtools — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.curvecamtools

+"""CNC CAM 'curvecamtools.py' © 2012 Vilem Novak, 2021 Alain Pelletier
+
+Operators that perform various functions on existing curves.
+"""
+
+from math import (
+    pi,
+    tan
+)
+
+import shapely
+from shapely.geometry import LineString
+
+import bpy
+from bpy.props import (
+    BoolProperty,
+    EnumProperty,
+    FloatProperty,
+)
+from bpy.types import Operator
+from mathutils import Vector
+
+from . import (
+    polygon_utils_cam,
+    simple,
+    utils,
+)
+
+
+# boolean operations for curve objects
+
+[docs] +class CamCurveBoolean(Operator): + """Perform Boolean Operation on Two or More Curves""" +
+[docs] + bl_idname = "object.curve_boolean"
+ +
+[docs] + bl_label = "Curve Boolean"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + boolean_type: EnumProperty( + name='Type', + items=( + ('UNION', 'Union', ''), + ('DIFFERENCE', 'Difference', ''), + ('INTERSECT', 'Intersect', '') + ), + description='Boolean type', + default='UNION' + )
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.active_object is not None and context.active_object.type in ['CURVE', 'FONT']
+ + +
+[docs] + def execute(self, context): + if len(context.selected_objects) > 1: + utils.polygonBoolean(context, self.boolean_type) + return {'FINISHED'} + else: + self.report({'ERROR'}, 'at least 2 curves must be selected') + return {'CANCELLED'}
+ +
+[docs] + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self)
+
+ + + +
+[docs] +class CamCurveConvexHull(Operator): + """Perform Hull Operation on Single or Multiple Curves""" # by Alain Pelletier april 2021 +
+[docs] + bl_idname = "object.convex_hull"
+ +
+[docs] + bl_label = "Convex Hull"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.active_object is not None and context.active_object.type in ['CURVE', 'FONT']
+ + +
+[docs] + def execute(self, context): + utils.polygonConvexHull(context) + return {'FINISHED'}
+
+ + + +# intarsion or joints +
+[docs] +class CamCurveIntarsion(Operator): + """Makes Curve Cuttable Both Inside and Outside, for Intarsion and Joints""" +
+[docs] + bl_idname = "object.curve_intarsion"
+ +
+[docs] + bl_label = "Intarsion"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + diameter: FloatProperty( + name="Cutter Diameter", + default=.001, + min=0, + max=0.025, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + tolerance: FloatProperty( + name="Cutout Tolerance", + default=.0001, + min=0, + max=0.005, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + backlight: FloatProperty( + name="Backlight Seat", + default=0.000, + min=0, + max=0.010, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + perimeter_cut: FloatProperty( + name="Perimeter Cut Offset", + default=0.000, + min=0, + max=0.100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + base_thickness: FloatProperty( + name="Base Material Thickness", + default=0.000, + min=0, + max=0.100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + intarsion_thickness: FloatProperty( + name="Intarsion Material Thickness", + default=0.000, + min=0, + max=0.100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + backlight_depth_from_top: FloatProperty( + name="Backlight Well Depth", + default=0.000, + min=0, + max=0.100, + precision=4, + unit="LENGTH", + )
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.active_object is not None and (context.active_object.type in ['CURVE', 'FONT'])
+ + +
+[docs] + def execute(self, context): + selected = context.selected_objects # save original selected items + + simple.remove_multiple('intarsion_') + + for ob in selected: + ob.select_set(True) # select original curves + + # Perimeter cut largen then intarsion pocket externally, optional + + # make the diameter 5% larger and compensate for backlight + diam = self.diameter * 1.05 + self.backlight * 2 + utils.silhoueteOffset(context, -diam / 2) + + o1 = bpy.context.active_object + utils.silhoueteOffset(context, diam) + o2 = bpy.context.active_object + utils.silhoueteOffset(context, -diam / 2) + o3 = bpy.context.active_object + o1.select_set(True) + o2.select_set(True) + o3.select_set(False) + # delete o1 and o2 temporary working curves + bpy.ops.object.delete(use_global=False) + o3.name = "intarsion_pocket" # this is the pocket for intarsion + bpy.context.object.location[2] = -self.intarsion_thickness + + if self.perimeter_cut > 0.0: + utils.silhoueteOffset(context, self.perimeter_cut) + bpy.context.active_object.name = "intarsion_perimeter" + bpy.context.object.location[2] = -self.base_thickness + bpy.ops.object.select_all(action='DESELECT') # deselect new curve + + o3.select_set(True) + context.view_layer.objects.active = o3 + # intarsion profile is the inside piece of the intarsion + # make smaller curve for material profile + utils.silhoueteOffset(context, -self.tolerance / 2) + bpy.context.object.location[2] = self.intarsion_thickness + o4 = bpy.context.active_object + bpy.context.active_object.name = "intarsion_profil" + o4.select_set(False) + + if self.backlight > 0.0: # Make a smaller curve for backlighting purposes + utils.silhoueteOffset( + context, (-self.tolerance / 2) - self.backlight) + bpy.context.active_object.name = "intarsion_backlight" + bpy.context.object.location[2] = - \ + self.backlight_depth_from_top - self.intarsion_thickness + o4.select_set(True) + o3.select_set(True) + return {'FINISHED'}
+ +
+[docs] + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self)
+
+ + + +# intarsion or joints +
+[docs] +class CamCurveOvercuts(Operator): + """Adds Overcuts for Slots""" +
+[docs] + bl_idname = "object.curve_overcuts"
+ +
+[docs] + bl_label = "Add Overcuts - A"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + diameter: FloatProperty( + name="Diameter", + default=.003175, + min=0, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + threshold: FloatProperty( + name="Threshold", + default=pi / 2 * .99, + min=-3.14, + max=3.14, + precision=4, + subtype="ANGLE", + unit="ROTATION", + )
+ +
+[docs] + do_outer: BoolProperty( + name="Outer Polygons", + default=True, + )
+ +
+[docs] + invert: BoolProperty( + name="Invert", + default=False, + )
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.active_object is not None and (context.active_object.type in ['CURVE', 'FONT'])
+ + +
+[docs] + def execute(self, context): + bpy.ops.object.curve_remove_doubles() + o1 = bpy.context.active_object + shapes = utils.curveToShapely(o1) + negative_overcuts = [] + positive_overcuts = [] + diameter = self.diameter * 1.001 + for s in shapes.geoms: + s = shapely.geometry.polygon.orient(s, 1) + if s.boundary.geom_type == 'LineString': + from shapely.geometry import MultiLineString + loops = MultiLineString([s.boundary]) + else: + loops = s.boundary + + for ci, c in enumerate(loops.geoms): + if ci > 0 or self.do_outer: + for i, co in enumerate(c.coords): + i1 = i - 1 + if i1 == -1: + i1 = -2 + i2 = i + 1 + if i2 == len(c.coords): + i2 = 0 + + v1 = Vector( + co) - Vector(c.coords[i1]) + v1 = v1.xy # Vector((v1.x,v1.y,0)) + 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) + sign = 1 + + if self.invert: # and ci>0: + sign *= -1 + if (sign < 0 and a < -self.threshold) or (sign > 0 and a > self.threshold): + p = Vector((co[0], co[1])) + v1.normalize() + v2.normalize() + v = v1 - v2 + v.normalize() + p = p - v * diameter / 2 + if abs(a) < pi / 2: + shape = polygon_utils_cam.Circle(diameter / 2, 64) + shape = shapely.affinity.translate( + shape, p.x, p.y) + else: + l = tan(a / 2) * diameter / 2 + p1 = p - sign * v * l + l = shapely.geometry.LineString((p, p1)) + shape = l.buffer( + diameter / 2, resolution=64) + + if sign > 0: + negative_overcuts.append(shape) + else: + positive_overcuts.append(shape) + + negative_overcuts = shapely.ops.unary_union(negative_overcuts) + positive_overcuts = shapely.ops.unary_union(positive_overcuts) + + fs = shapely.ops.unary_union(shapes) + fs = fs.union(positive_overcuts) + fs = fs.difference(negative_overcuts) + utils.shapelyToCurve(o1.name + '_overcuts', fs, o1.location.z) + + return {'FINISHED'}
+ +
+[docs] + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self)
+
+ + +# Overcut type B +
+[docs] +class CamCurveOvercutsB(Operator): + """Adds Overcuts for Slots""" +
+[docs] + bl_idname = "object.curve_overcuts_b"
+ +
+[docs] + bl_label = "Add Overcuts - B"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + diameter: FloatProperty( + name="Tool Diameter", + default=.003175, + description='Tool bit diameter used in cut operation', + min=0, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + style: EnumProperty( + name="Style", + items=( + ('OPEDGE', 'opposite edge', + 'place corner overcuts on opposite edges'), + ('DOGBONE', 'Dog-bone / Corner Point', + 'place overcuts at center of corners'), + ('TBONE', 'T-bone', 'place corner overcuts on the same edge') + ), + default='DOGBONE', + description='style of overcut to use', + )
+ +
+[docs] + threshold: FloatProperty( + name="Max Inside Angle", + default=pi / 2, + min=-3.14, + max=3.14, + description='The maximum angle to be considered as an inside corner', + precision=4, + subtype="ANGLE", + unit="ROTATION", + )
+ +
+[docs] + do_outer: BoolProperty( + name="Include Outer Curve", + description='Include the outer curve if there are curves inside', + default=True, + )
+ +
+[docs] + do_invert: BoolProperty( + name="Invert", + description='invert overcut operation on all curves', + default=True, + )
+ +
+[docs] + otherEdge: BoolProperty( + name="Other Edge", + description='change to the other edge for the overcut to be on', + default=False, + )
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.active_object is not None and context.active_object.type == 'CURVE'
+ + +
+[docs] + def execute(self, context): + bpy.ops.object.curve_remove_doubles() + o1 = bpy.context.active_object + shapes = utils.curveToShapely(o1) + negative_overcuts = [] + positive_overcuts = [] + # count all the corners including inside and out + cornerCnt = 0 + # a list of tuples for defining the inside corner + # tuple is: (pos, v1, v2, angle, allCorners list index) + insideCorners = [] + diameter = self.diameter * 1.002 # make bit size slightly larger to allow cutter + radius = diameter / 2 + 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 + POS, V1, V2, A, IDX = range(5) + + def addOvercut(a): + nonlocal pos, centerv, radius, extendedv, sign, negative_overcuts, positive_overcuts + # move the overcut shape center position 1 radius in direction v + pos -= centerv * radius + print("abs(a)", abs(a)) + if abs(a) <= pi / 2 + 0.0001: + print("<=pi/2") + shape = polygon_utils_cam.Circle(radius, 64) + shape = shapely.affinity.translate(shape, pos.x, pos.y) + else: # elongate overcut circle to make sure tool bit can fit into slot + print(">pi/2") + p1 = pos + (extendedv * radius) + l = shapely.geometry.LineString((pos, p1)) + shape = l.buffer(radius, resolution=64) + + if sign > 0: + negative_overcuts.append(shape) + else: + positive_overcuts.append(shape) + + def setOtherEdge(v1, v2, a): + nonlocal centerv, extendedv + if self.otherEdge: + centerv = v1 + extendedv = v2 + else: + centerv = -v2 + extendedv = -v1 + addOvercut(a) + + def setCenterOffset(a): + nonlocal centerv, extendedv, sign + centerv = v1 - v2 + centerv.normalize() + extendedv = centerv * tan(a / 2) * -sign + addOvercut(a) + + def getCorner(idx, offset): + nonlocal insideCorners + idx += offset + if idx >= len(insideCorners): + idx -= len(insideCorners) + return insideCorners[idx] + + def getCornerDelta(curidx, nextidx): + nonlocal cornerCnt + delta = nextidx - curidx + if delta < 0: + delta += cornerCnt + return delta + + for s in shapes.geoms: + # ensure the shape is counterclockwise + s = shapely.geometry.polygon.orient(s, 1) + + if s.boundary.geom_type == 'LineString': + from shapely import MultiLineString + loops = MultiLineString([s.boundary]) + else: + loops = s.boundary + + outercurve = self.do_outer or len(loops.geoms) == 1 + for ci, c in enumerate(loops.geoms): + if ci > 0 or outercurve: + if isTBone: + cornerCnt = 0 + insideCorners = [] + + for i, co in enumerate(c.coords): + i1 = i - 1 + if i1 == -1: + i1 = -2 + i2 = i + 1 + if i2 == len(c.coords): + i2 = 0 + + 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) + insideCornerFound = False + outsideCornerFound = False + if a < -anglethreshold: + if sign < 0: + insideCornerFound = True + else: + outsideCornerFound = True + elif a > anglethreshold: + if sign > 0: + insideCornerFound = True + else: + outsideCornerFound = True + + if insideCornerFound: + # an inside corner with an overcut has been found + # which means a new side has been found + pos = Vector((co[0], co[1])) + v1.normalize() + v2.normalize() + # figure out which direction vector to use + # v is the main direction vector to move the overcut shape along + # ev is the direction vector used to elongate the overcut shape + if self.style != 'DOGBONE': + # t-bone and opposite edge styles get treated nearly the same + if isTBone: + cornerCnt += 1 + # insideCorner tuplet: (pos, v1, v2, angle, corner index) + insideCorners.append( + (pos, v1, v2, a, cornerCnt - 1)) + # processing of corners for T-Bone are done after all points are processed + continue + + setOtherEdge(v1, v2, a) + + else: # DOGBONE style + setCenterOffset(a) + + elif isTBone and outsideCornerFound: + # add an outside corner to the list + cornerCnt += 1 + + # check if t-bone processing required + # if no inside corners then nothing to do + if isTBone and len(insideCorners) > 0: + print("corner count", cornerCnt, + "inside corner count", len(insideCorners)) + # process all of the inside corners + for i, corner in enumerate(insideCorners): + pos, v1, v2, a, idx = corner + # figure out which side of the corner to do overcut + # if prev corner is outside corner + # calc index distance between current corner and prev + prevCorner = getCorner(i, -1) + print('first:', i, idx, prevCorner[IDX]) + if getCornerDelta(prevCorner[IDX], idx) == 1: + # make sure there is an outside corner + print(getCornerDelta( + getCorner(i, -2)[IDX], idx)) + if getCornerDelta(getCorner(i, -2)[IDX], idx) > 2: + setOtherEdge(v1, v2, a) + print('first won') + continue + + nextCorner = getCorner(i, 1) + print('second:', i, idx, nextCorner[IDX]) + if getCornerDelta(idx, nextCorner[IDX]) == 1: + # make sure there is an outside corner + print(getCornerDelta( + idx, getCorner(i, 2)[IDX])) + if getCornerDelta(idx, getCorner(i, 2)[IDX]) > 2: + print('second won') + setOtherEdge(-v2, -v1, a) + continue + + print('third') + if getCornerDelta(prevCorner[IDX], idx) == 3: + # check if they share the same edge + a1 = v1.angle_signed( + prevCorner[V2]) * 180.0 / pi + print('third won', a1) + if a1 < -135 or a1 > 135: + setOtherEdge(-v2, -v1, a) + continue + + print('fourth') + if getCornerDelta(idx, nextCorner[IDX]) == 3: + # check if they share the same edge + a1 = v2.angle_signed( + nextCorner[V1]) * 180.0 / pi + print('fourth won', a1) + if a1 < -135 or a1 > 135: + setOtherEdge(v1, v2, a) + continue + + print('***No Win***') + # the default if no other rules pass + setCenterOffset(a) + + negative_overcuts = shapely.ops.unary_union(negative_overcuts) + positive_overcuts = shapely.ops.unary_union(positive_overcuts) + fs = shapely.ops.unary_union(shapes) + fs = fs.union(positive_overcuts) + fs = fs.difference(negative_overcuts) + + utils.shapelyToCurve(o1.name + '_overcuts', fs, o1.location.z) + return {'FINISHED'}
+ +
+[docs] + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self)
+
+ + +
+[docs] +class CamCurveRemoveDoubles(Operator): + """Curve Remove Doubles""" +
+[docs] + bl_idname = "object.curve_remove_doubles"
+ +
+[docs] + bl_label = "Remove Curve Doubles"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + +
+[docs] + merge_distance: FloatProperty( + name="Merge distance", + default=0.0001, + min=0, + max=.01, + + )
+ + +
+[docs] + keep_bezier: BoolProperty( + name="Keep bezier", + default=False, + )
+ + + + @classmethod +
+[docs] + def poll(cls, context): + return context.active_object is not None and (context.active_object.type == 'CURVE')
+ + +
+[docs] + def execute(self, context): + obj = bpy.context.selected_objects + for ob in obj: + if ob.type == 'CURVE': + if self.keep_bezier: + if ob.data.splines and ob.data.splines[0].type == 'BEZIER': + bpy.ops.curvetools.operatorsplinesremoveshort() + bpy.context.view_layer.objects.active = ob + ob.data.resolution_u = 64 + if bpy.context.mode == 'OBJECT': + bpy.ops.object.editmode_toggle() + bpy.ops.curve.select_all() + bpy.ops.curve.remove_double(distance=self.merge_distance) + bpy.ops.object.editmode_toggle() + else: + self.merge_distance = 0 + if bpy.context.mode == 'EDIT_CURVE': + bpy.ops.object.editmode_toggle() + bpy.ops.object.convert(target='MESH') + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.remove_doubles(threshold= self.merge_distance) + bpy.ops.object.editmode_toggle() + bpy.ops.object.convert(target='CURVE') + + return {'FINISHED'}
+ + +
+[docs] + def draw(self, context): + layout = self.layout + obj = context.active_object + if obj.type == 'CURVE': + if obj.data.splines and obj.data.splines[0].type == 'BEZIER': + layout.prop(self, "keep_bezier", text="Keep Bezier") + layout.prop(self, "merge_distance", text="Merge Distance")
+ + +
+[docs] + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self)
+
+ + + +
+[docs] +class CamMeshGetPockets(Operator): + """Detect Pockets in a Mesh and Extract Them as Curves""" +
+[docs] + bl_idname = "object.mesh_get_pockets"
+ +
+[docs] + bl_label = "Get Pocket Surfaces"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + threshold: FloatProperty( + name="Horizontal Threshold", + description="How horizontal the surface must be for a pocket: " + "1.0 perfectly flat, 0.0 is any orientation", + default=.99, + min=0, + max=1.0, + precision=4, + )
+ +
+[docs] + zlimit: FloatProperty( + name="Z Limit", + description="Maximum z height considered for pocket operation, " + "default is 0.0", + default=0.0, + min=-1000.0, + max=1000.0, + precision=4, + unit='LENGTH', + )
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.active_object is not None and (context.active_object.type == 'MESH')
+ + +
+[docs] + def execute(self, context): + obs = bpy.context.selected_objects + s = bpy.context.scene + cobs = [] + for ob in obs: + if ob.type == 'MESH': + pockets = {} + mw = ob.matrix_world + mesh = ob.data + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.select_mode( + use_extend=False, use_expand=False, type='FACE') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.editmode_toggle() + i = 0 + for face in mesh.polygons: + # n = mw @ face.normal + n = face.normal.to_4d() + n.w = 0 + n = (mw @ n).to_3d().normalized() + if n.z > self.threshold: + face.select = True + z = (mw @ mesh.vertices[face.vertices[0]].co).z + if z < self.zlimit: + if pockets.get(z) is None: + pockets[z] = [i] + else: + pockets[z].append(i) + i += 1 + print(len(pockets)) + for p in pockets: + print(p) + ao = bpy.context.active_object + i = 0 + for p in pockets: + print(i) + i += 1 + + sf = pockets[p] + for face in mesh.polygons: + face.select = False + + for fi in sf: + face = mesh.polygons[fi] + face.select = True + + bpy.ops.object.editmode_toggle() + + bpy.ops.mesh.select_mode( + use_extend=False, use_expand=False, type='EDGE') + bpy.ops.mesh.region_to_loop() + bpy.ops.mesh.separate(type='SELECTED') + + bpy.ops.mesh.select_mode( + use_extend=False, use_expand=False, type='FACE') + bpy.ops.object.editmode_toggle() + ao.select_set(state=False) + bpy.context.view_layer.objects.active = bpy.context.selected_objects[0] + cobs.append(bpy.context.selected_objects[0]) + bpy.ops.object.convert(target='CURVE') + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') + + bpy.context.selected_objects[0].select_set(False) + ao.select_set(state=True) + bpy.context.view_layer.objects.active = ao + # bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') + + # turn off selection of all objects in 3d view + bpy.ops.object.select_all(action='DESELECT') + # make new curves more visible by making them selected in the 3d view + # This also allows the active object to still work with the operator + # if the user decides to change the horizontal threshold property + col = bpy.data.collections.new('multi level pocket ') + s.collection.children.link(col) + for obj in cobs: + col.objects.link(obj) + + return {'FINISHED'}
+
+ + + +# this operator finds the silhouette of objects(meshes, curves just get converted) and offsets it. +
+[docs] +class CamOffsetSilhouete(Operator): + """Curve Offset Operation """ +
+[docs] + bl_idname = "object.silhouete_offset"
+ +
+[docs] + bl_label = "Silhouette & Offset"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'PRESET'}
+ + +
+[docs] + offset: FloatProperty( + name="Offset", + default=.003, + min=-100, + max=100, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + mitrelimit: FloatProperty( + name="Mitre Limit", + default=2, + min=0.00000001, + max=20, + precision=4, + unit="LENGTH", + )
+ +
+[docs] + style: EnumProperty( + name="Corner Type", + items=( + ('1', 'Round', ''), + ('2', 'Mitre', ''), + ('3', 'Bevel', '') + ), + )
+ +
+[docs] + caps: EnumProperty( + name="Cap Type", + items=( + ('round', 'Round', ''), + ('square', 'Square', ''), + ('flat', 'Flat', '') + ), + )
+ +
+[docs] + align: EnumProperty( + name="Alignment", + items=( + ('worldxy', 'World XY', ''), + ('bottom', 'Base Bottom', ''), + ('top', 'Base Top', '') + ), + )
+ +
+[docs] + opentype: EnumProperty( + name="Curve Type", + items=( + ('dilate', 'Dilate open curve', ''), + ('leaveopen', 'Leave curve open', ''), + ('closecurve', 'Close curve', '') + ), + default='closecurve' + )
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.active_object is not None and ( + context.active_object.type == 'CURVE' or context.active_object.type == 'FONT' or + context.active_object.type == 'MESH')
+ + +
+[docs] + def isStraight(self, geom): + assert geom.geom_type == "LineString", geom.geom_type + length = geom.length + start_pt = geom.interpolate(0) + end_pt = geom.interpolate(1, normalized=True) + straight_dist = start_pt.distance(end_pt) + if straight_dist == 0.0: + if length == 0.0: + return True + return False + elif length / straight_dist == 1: + return True + else: + return False
+ + + # this is almost same as getobjectoutline, just without the need of operation data +
+[docs] + def execute(self, context): + # bpy.ops.object.curve_remove_doubles() + ob = context.active_object + if ob.type == 'FONT': + bpy.context.object.data.resolution_u = 64 + if ob.type == 'CURVE': + if ob.data.splines and ob.data.splines[0].type == 'BEZIER': + bpy.context.object.data.resolution_u = 64 + bpy.ops.object.curve_remove_doubles(merge_distance=0.0001, keep_bezier=True) + else: + bpy.ops.object.curve_remove_doubles() + + bpy.ops.object.duplicate() + obj = context.active_object + if context.active_object.type != 'MESH': + obj.data.dimensions = '3D' + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) # apply all transforms + bpy.ops.object.convert(target='MESH') + bpy.context.active_object.name = "temp_mesh" + + #get the Z align point from the base + if self.align == 'top': + point = max([(bpy.context.object.matrix_world @ v.co).z for v in bpy.context.object.data.vertices]) + elif self.align == 'bottom': + point = min([(bpy.context.object.matrix_world @ v.co).z for v in bpy.context.object.data.vertices]) + else: + point = 0 + + # extract X,Y coordinates from the vertices data and put them into a LineString object + coords = [] + for v in obj.data.vertices: + coords.append((v.co.x, v.co.y)) + simple.remove_multiple('temp_mesh') # delete temporary mesh + simple.remove_multiple('dilation') # delete old dilation objects + + # convert coordinates to shapely LineString datastructure + line = LineString(coords) + + #if curve is a straight segment, change offset type to dilate + if self.isStraight(line) and self.opentype != 'leaveopen': + self.opentype = 'dilate' + + #make the dilate or open curve offset + if (self.opentype != 'closecurve') and ob.type == 'CURVE': + print("line length=", round(line.length * 1000), 'mm') + + if self.style == '3': + style="bevel" + elif self.style == '2': + style="mitre" + else: + style="round" + + if self.opentype == 'leaveopen': + new_shape = shapely.offset_curve(line, self.offset, join_style=style) # use shapely to expand without closing the curve + name = "Offset: " + "%.2f" % round(self.offset * 1000) + 'mm - ' + ob.name + else: + new_shape = line.buffer(self.offset, cap_style=self.caps, resolution=16, join_style=style, mitre_limit=self.mitrelimit) # use shapely to expand, closing the curve + name = "Dilation: " + "%.2f" % round(self.offset * 1000) + 'mm - ' + ob.name + + #create the actual offset object based on the Shapely offset + polygon_utils_cam.shapelyToCurve(name, new_shape, 0, self.opentype != 'leaveopen') + + #position the object according to the calculated point + bpy.context.object.location.z = point + + #if curve is not a straight line and neither dilate or leave open are selected, create a normal offset + else: + bpy.context.view_layer.objects.active = ob + utils.silhoueteOffset(context, self.offset, + int(self.style), self.mitrelimit) + return {'FINISHED'}
+ + +
+[docs] + def draw(self, context): + layout = self.layout + layout.prop(self, "offset", text="Offset") + layout.prop(self, "opentype", text="Type") + layout.prop(self, "style", text="Corner") + if self.style == '2': + layout.prop(self, "mitrelimit", text="Mitre Limit") + if self.opentype == 'dilate': + layout.prop(self, "caps", text="Cap") + if self.opentype != 'closecurve': + layout.prop(self, "align", text="Align")
+ +
+[docs] + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self)
+
+ + + # Finds object silhouette, usefull for meshes, since with curves it's not needed. +
+[docs] +class CamObjectSilhouete(Operator): + """Object Silhouette""" +
+[docs] + bl_idname = "object.silhouete"
+ +
+[docs] + bl_label = "Object Silhouette"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + # return context.active_object is not None and (context.active_object.type == 'CURVE' + # or context.active_object.type == 'FONT' or context.active_object.type == 'MESH') + return context.active_object is not None and ( + context.active_object.type == 'FONT' or + context.active_object.type == 'MESH')
+ + + # this is almost same as getobjectoutline, just without the need of operation data +
+[docs] + def execute(self, context): + ob = bpy.context.active_object + self.silh = utils.getObjectSilhouete( + 'OBJECTS', objects=bpy.context.selected_objects) + bpy.context.scene.cursor.location = (0, 0, 0) + # smp=sgeometry.asMultiPolygon(self.silh) + for smp in self.silh.geoms: + polygon_utils_cam.shapelyToCurve( + ob.name + '_silhouette', smp, 0) # + # bpy.ops.object.convert(target='CURVE') + simple.join_multiple(ob.name + '_silhouette') + bpy.context.scene.cursor.location = ob.location + bpy.ops.object.origin_set(type='ORIGIN_CURSOR') + bpy.ops.object.curve_remove_doubles() + return {'FINISHED'}
+
+ + # --------------------------------------------------- + + +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/engine.html b/_modules/cam/engine.html new file mode 100644 index 000000000..5da2e1edd --- /dev/null +++ b/_modules/cam/engine.html @@ -0,0 +1,511 @@ + + + + + + + + + + cam.engine — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.engine

+"""CNCCAM 'engine.py'
+
+Engine definition, options and panels.
+"""
+
+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
+
+
+
+[docs] +class CNCCAM_ENGINE(RenderEngine): +
+[docs] + bl_idname = "CNCCAM_RENDER"
+ +
+[docs] + bl_label = "CNC CAM"
+ +
+[docs] + bl_use_eevee_viewport = True
+
+ + + +
+[docs] +def get_panels(): + """Retrieve a list of panels for the Blender UI. + + This function compiles a list of UI panels that are compatible with the + Blender rendering engine. It excludes certain predefined panels that are + not relevant for the current context. The function checks all subclasses + of the `bpy.types.Panel` and includes those that have the + `COMPAT_ENGINES` attribute set to include 'BLENDER_RENDER', provided + they are not in the exclusion list. + + Returns: + list: A list of panel classes that are compatible with the + Blender rendering engine, excluding specified 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
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/exception.html b/_modules/cam/exception.html new file mode 100644 index 000000000..83ba02e5f --- /dev/null +++ b/_modules/cam/exception.html @@ -0,0 +1,414 @@ + + + + + + + + + + cam.exception — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.exception

+"""CNC CAM 'exception.py'
+
+Generic CAM Exception class.
+"""
+
+
+
+[docs] +class CamException(Exception): + pass
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/gcodeimportparser.html b/_modules/cam/gcodeimportparser.html new file mode 100644 index 000000000..125c9f96c --- /dev/null +++ b/_modules/cam/gcodeimportparser.html @@ -0,0 +1,1288 @@ + + + + + + + + + + cam.gcodeimportparser — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.gcodeimportparser

+"""CNC CAM 'gcodeimportparser.py'
+
+Code modified from YAGV (Yet Another G-code Viewer) - https://github.com/jonathanwin/yagv
+No license terms found in YAGV repo, will assume GNU release
+"""
+
+import math
+
+import numpy as np
+
+import bpy
+
+np.set_printoptions(suppress=True)  # suppress scientific notation in subdivide functions linspace
+
+
+
+[docs] +def import_gcode(context, filepath): + """Import G-code data into the scene. + + This function reads G-code from a specified file and processes it + according to the settings defined in the context. It utilizes the + GcodeParser to parse the file and classify segments of the model. + Depending on the options set in the scene, it may subdivide the model + and draw it with or without layer splitting. The time taken for the + import process is printed to the console. + + Args: + context (Context): The context containing the scene and tool settings. + filepath (str): The path to the G-code file to be imported. + + Returns: + dict: A dictionary indicating the import status, typically + {'FINISHED'}. + """ + + print("Running read_some_data...") + + scene = context.scene + mytool = scene.cam_import_gcode + import time + then = time.time() + + parse = GcodeParser() + model = parse.parseFile(filepath) + + if mytool.subdivide: + model.subdivide(mytool.max_segment_size) + model.classifySegments() + if mytool.split_layers: + model.draw(split_layers=True) + else: + model.draw(split_layers=False) + + now = time.time() + print("Importing Gcode Took ", round(now - then, 1), "Seconds") + + return {'FINISHED'}
+ + + +
+[docs] +def segments_to_meshdata(segments): + """Convert a list of segments into mesh data consisting of vertices and + edges. + + This function processes a list of segment objects, extracting the + coordinates of vertices and defining edges based on the styles of the + segments. It identifies when to add vertices and edges based on whether + the segments are in 'extrude' or 'travel' styles. The resulting mesh + data can be used for 3D modeling or rendering applications. + + Args: + segments (list): A list of segment objects, each containing 'style' and + 'coords' attributes. + + Returns: + tuple: A tuple containing two elements: + - list: A list of vertices, where each vertex is represented as a + list of coordinates [X, Y, Z]. + - list: A list of edges, where each edge is represented as a list + of indices corresponding to the vertices. + """ + # edges only on extrusion + segs = segments + verts = [] + edges = [] + del_offset = 0 # to travel segs in a row, one gets deleted, need to keep track of index for edges + for i in range(len(segs)): + if i >= len(segs) - 1: + + if segs[i].style == 'extrude': + verts.append([segs[i].coords['X'], segs[i].coords['Y'], segs[i].coords['Z']]) + + break + + # start of extrusion for first time + if segs[i].style == 'travel' and segs[i + 1].style == 'extrude': + verts.append([segs[i].coords['X'], segs[i].coords['Y'], segs[i].coords['Z']]) + verts.append([segs[i + 1].coords['X'], segs[i + 1].coords['Y'], segs[i + 1].coords['Z']]) + edges.append([i - del_offset, (i - del_offset) + 1]) + + # mitte, current and next are extrusion, only add next, current is already in vert list + + if segs[i].style == 'extrude' and segs[i + 1].style == 'extrude': + verts.append([segs[i + 1].coords['X'], segs[i + 1].coords['Y'], segs[i + 1].coords['Z']]) + edges.append([i - del_offset, (i - del_offset) + 1]) + + if segs[i].style == 'travel' and segs[i + 1].style == 'travel': + del_offset += 1 + + return verts, edges
+ + + +
+[docs] +def obj_from_pydata(name, verts, edges=None, close=True, collection_name=None): + """Create a Blender object from provided vertex and edge data. + + This function generates a mesh object in Blender using the specified + vertices and edges. If edges are not provided, it automatically creates + a chain of edges connecting the vertices. The function also allows for + the option to close the mesh by connecting the last vertex back to the + first. Additionally, it can place the created object into a specified + collection within the Blender scene. The object is scaled down to a + smaller size for better visibility in the Blender environment. + + Args: + name (str): The name of the object to be created. + verts (list): A list of vertex coordinates, where each vertex is represented as a + tuple of (x, y, z). + edges (list?): A list of edges defined by pairs of vertex indices. Defaults to None. + close (bool?): Whether to close the mesh by connecting the last vertex to the first. + Defaults to True. + collection_name (str?): The name of the collection to which the object should be added. Defaults + to None. + + Returns: + None: The function does not return a value; it creates an object in the + Blender scene. + """ + + if edges is None: + # join vertices into one uninterrupted chain of edges. + edges = [[i, i + 1] for i in range(len(verts) - 1)] + if close: + edges.append([len(verts) - 1, 0]) # connect last to first + + me = bpy.data.meshes.new(name) + me.from_pydata(verts, edges, []) + obj = bpy.data.objects.new(name, me) + + # Move into collection if specified + if collection_name is not None: # make argument optional + + # collection exists + collection = bpy.data.collections.get(collection_name) + if collection: + bpy.data.collections[collection_name].objects.link(obj) + else: + collection = bpy.data.collections.new(collection_name) + bpy.context.scene.collection.children.link(collection) # link collection to main scene + bpy.data.collections[collection_name].objects.link(obj) + + obj.scale = (0.001, 0.001, 0.001) + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + if bpy.context.scene.cam_import_gcode.output == 'curve': + bpy.ops.object.convert(target='CURVE')
+ + + +
+[docs] +class GcodeParser: +
+[docs] + comment = ""
+ + # global, to access in other classes(to access RGB values in comment above when parsing M163). + # Theres probably better way + + def __init__(self): +
+[docs] + self.model = GcodeModel(self)
+ + +
+[docs] + def parseFile(self, path): + """Parse a G-code file and update the model. + + This function reads a G-code file line by line, increments a line + counter for each line, and processes each line using the `parseLine` + method. The function assumes that the file is well-formed and that each + line can be parsed without errors. After processing all lines, it + returns the updated model. + + Args: + path (str): The file path to the G-code file to be parsed. + + Returns: + model: The updated model after parsing the G-code file. + """ + + # read the gcode file + with open(path, 'r') as f: + # init line counter + self.lineNb = 0 + # for all lines + for line in f: + # inc line counter + self.lineNb += 1 + # remove trailing linefeed + self.line = line.rstrip() + # parse a line + self.parseLine() + return self.model
+ + +
+[docs] + def parseLine(self): + """Parse a line of G-code and execute the corresponding command. + + This method processes a line of G-code by stripping comments, cleaning + the command, and identifying the command code and its arguments. It + handles specific G-code commands and invokes the appropriate parsing + method if available. If the command is unsupported, it prints an error + message. The method also manages tool numbers and coordinates based on + the parsed command. + """ + + # strip comments: + bits = self.line.split(';', 1) + if (len(bits) > 1): + GcodeParser.comment = bits[1] + + # extract & clean command + command = bits[0].strip() + s = "" + a = "" + a_old = "" + for i in range(len(command)): # check each character in the line + a = command[i] + if a.isupper() and a_old != ' ' and i > 0: # add a space if upper case letter and no space is found before + s += ' ' + s += a + a_old = a + print(s) + command = s + + # code is fist word, then args + comm = command.split(None, 1) + code = comm[0] if (len(comm) > 0) else None + args = comm[1] if (len(comm) > 1) else None + + if code: + # convert all G01 and G00 to G1 and G0 + if code == 'G01': + code = 'G1' + if code == 'G00': + code = 'G0' + + if hasattr(self, "parse_" + code): + getattr(self, "parse_" + code)(args) + self.last_command = code + else: + if code[0] == "T": + + self.model.toolnumber = int(code[1:]) + print(self.model.toolnumber) + # if code doesn't start with a G but starts with a coordinate add the last command to the line + elif code[0] == 'X' or code[0] == 'Y' or code[0] == 'Z': + self.line = self.last_command + ' ' + self.line + self.parseLine() # parse this line again with the corrections + else: + pass + print("Unsupported gcode " + str(code))
+ + +
+[docs] + def parseArgs(self, args): + """Parse command-line arguments into a dictionary. + + This function takes a string of arguments, splits it into individual + components, and maps each component's first character to its + corresponding numeric value. If a numeric value cannot be converted from + the string, it defaults to 1. The resulting dictionary contains the + first characters as keys and their associated numeric values as values. + + Args: + args (str): A string of space-separated arguments, where each argument + consists of a letter followed by a numeric value. + + Returns: + dict: A dictionary mapping each letter to its corresponding numeric value. + """ + + dic = {} + if args: + bits = args.split() + for bit in bits: + letter = bit[0] + try: + coord = float(bit[1:]) + except ValueError: + coord = 1 + dic[letter] = coord + return dic
+ + +
+[docs] + def parse_G1(self, args, type="G1"): + # G1: Controlled move + self.model.do_G1(self.parseArgs(args), type)
+ + +
+[docs] + def parse_G0(self, args, type="G0"): + # G1: Controlled move + self.model.do_G1(self.parseArgs(args), type)
+ + +
+[docs] + def parse_G90(self, args): + # G90: Set to Absolute Positioning + self.model.setRelative(False)
+ + +
+[docs] + def parse_G91(self, args): + # G91: Set to Relative Positioning + self.model.setRelative(True)
+ + +
+[docs] + def parse_G92(self, args): + # G92: Set Position + self.model.do_G92(self.parseArgs(args))
+ + +
+[docs] + def warn(self, msg): + print("[WARN] Line %d: %s (Text:'%s')" % (self.lineNb, msg, self.line))
+ + +
+[docs] + def error(self, msg): + """Log an error message and raise an exception. + + This method prints an error message to the console, including the line + number, the provided message, and the text associated with the error. + After logging the error, it raises a generic Exception with the same + message format. + + Args: + msg (str): The error message to be logged. + + Raises: + Exception: Always raises an Exception with the formatted error message. + """ + + print("[ERROR] Line %d: %s (Text:'%s')" % (self.lineNb, msg, self.line)) + raise Exception("[ERROR] Line %d: %s (Text:'%s')" % (self.lineNb, msg, self.line))
+
+ + + +
+[docs] +class GcodeModel: + + def __init__(self, parser): + # save parser for messages +
+[docs] + self.parser = parser
+ + # latest coordinates & extrusion relative to offset, feedrate +
+[docs] + self.relative = { + "X": 0.0, + "Y": 0.0, + "Z": 0.0, + "F": 0.0, + "E": 0.0}
+ + # offsets for relative coordinates and position reset (G92) +
+[docs] + self.offset = { + "X": 0.0, + "Y": 0.0, + "Z": 0.0, + "E": 0.0}
+ + # if true, args for move (G1) are given relatively (default: absolute) +
+[docs] + self.isRelative = False
+ +
+[docs] + self.color = [0, 0, 0, 0, 0, 0, 0, 0] # RGBCMYKW
+ +
+[docs] + self.toolnumber = 0
+ + + # the segments +
+[docs] + self.segments = []
+ +
+[docs] + self.layers = []
+ + +
+[docs] + def do_G1(self, args, type): + """Perform a rapid or controlled movement based on the provided arguments. + + This method updates the current coordinates based on the input + arguments, either in relative or absolute terms. It constructs a segment + representing the movement and adds it to the model if there are changes + in the XYZ coordinates. The function handles unknown axes by issuing a + warning and ensures that the segment is only added if there are actual + changes in position. + + Args: + args (dict): A dictionary containing movement parameters for each axis. + type (str): The type of movement (e.g., 'G0' for rapid move, 'G1' for controlled + move). + """ + + # G0/G1: Rapid/Controlled move + # clone previous coords + coords = dict(self.relative) + + # update changed coords + for axis in args.keys(): + # print(coords) + if axis in coords: + if self.isRelative: + coords[axis] += args[axis] + else: + coords[axis] = args[axis] + else: + self.warn("Unknown axis '%s'" % axis) + + # build segment + absolute = { + "X": self.offset["X"] + coords["X"], + "Y": self.offset["Y"] + coords["Y"], + "Z": self.offset["Z"] + coords["Z"], + "F": coords["F"] # no feedrate offset + } + + # if gcode line has no E = travel move + # but still add E = 0 to segment (so coords dictionaries have same shape for subdividing linspace function) + if "E" not in args: # "E" in coords: + absolute["E"] = 0 + else: + absolute["E"] = args["E"] + + seg = Segment( + type, + absolute, + self.color, + self.toolnumber, + # self.layerIdx, + self.parser.lineNb, + self.parser.line) + + # only add seg if XYZ changes (skips "G1 Fxxx" only lines and avoids double vertices inside Blender, + # because XYZ stays the same on such a segment. + if seg.coords['X'] != self.relative['X'] + self.offset["X"] or seg.coords['Y'] != self.relative['Y'] + \ + self.offset["Y"] or seg.coords['Z'] != self.relative['Z'] + self.offset["Z"]: + self.addSegment(seg) + + # update model coords + self.relative = coords
+ + +
+[docs] + def do_G92(self, args): + """Set the current position of the axes without moving. + + This method updates the current coordinates for the specified axes based + on the provided arguments. If no axes are mentioned, it sets all axes + (X, Y, Z) to zero. The method adjusts the offset values by transferring + the difference between the relative and specified values for each axis. + If an unknown axis is provided, a warning is issued. + + Args: + args (dict): A dictionary containing axis names as keys + (e.g., 'X', 'Y', 'Z') and their corresponding + position values as float. + """ + + # G92: Set Position + # this changes the current coords, without moving, so do not generate a segment + + # no axes mentioned == all axes to 0 + if not len(args.keys()): + args = {"X": 0.0, "Y": 0.0, "Z": 0.0} # , "E":0.0 + # update specified axes + for axis in args.keys(): + if axis in self.offset: + # transfer value from relative to offset + self.offset[axis] += self.relative[axis] - args[axis] + self.relative[axis] = args[axis] + else: + self.warn("Unknown axis '%s'" % axis)
+ + +
+[docs] + def do_M163(self, args): + """Update the color settings for a specific segment based on given + parameters. + + This method modifies the color attributes of an object by updating the + CMYKW values for a specified segment. It first creates a new list from + the existing color attribute to avoid reference issues. The method then + extracts the index and weight from the provided arguments and updates + the color list accordingly. Additionally, it retrieves RGB values from + the last comment and applies them to the color list. + + Args: + args (dict): A dictionary containing the parameters for the operation. + - 'S' (int): The index of the segment to update. + - 'P' (float): The weight to set for the CMYKW color component. + + Returns: + None: This method does not return a value; it modifies the object's state. + """ + + col = list( + self.color) # list() creates new list, otherwise you just change reference and all segs have same color + extr_idx = int(args['S']) # e.g. M163 S0 P1 + weight = args['P'] + # change CMYKW + col[extr_idx + 3] = weight # +3 weil ersten 3 stellen RGB sind, need only CMYKW values for extrude + self.color = col + + # take RGB values for seg from last comment (above first M163 statement) + comment = eval(GcodeParser.comment) # string comment to list + # RGB = [GcodeParser.comment[1], GcodeParser.com + RGB = comment[:3] + self.color[:3] = RGB
+ + +
+[docs] + def setRelative(self, isRelative): + self.isRelative = isRelative
+ + +
+[docs] + def addSegment(self, segment): + self.segments.append(segment)
+ + +
+[docs] + def warn(self, msg): + self.parser.warn(msg)
+ + +
+[docs] + def error(self, msg): + self.parser.error(msg)
+ + +
+[docs] + def classifySegments(self): + """Classify segments into layers based on their coordinates and extrusion + style. + + This method processes a list of segments, determining their extrusion + style (travel, retract, restore, or extrude) based on the movement of + the coordinates and the state of the extruder. It organizes the segments + into layers, which are used for later rendering. The classification is + based on changes in the Z-coordinate and the extruder's position. The + function initializes the coordinates and iterates through each segment, + checking for movements in the X, Y, and Z directions. It identifies when + a new layer begins based on changes in the Z-coordinate and the + extruder's state. Segments are then grouped into layers for further + processing. Raises: None + """ + + + # start model at 0, act as prev_coords + coords = { + "X": 0.0, + "Y": 0.0, + "Z": 0.0, + "F": 0.0, + "E": 0.0} + + # first layer at Z=0 + currentLayerIdx = 0 + currentLayerZ = 0 # better to use self.first_layer_height + layer = [] # add layer to model.layers + + for i, seg in enumerate(self.segments): + # default style is travel (move, no extrusion) + style = "travel" + + # no horizontal movement, but extruder movement: retraction/refill + # if ( + # (seg.coords["X"] == coords["X"]) and + # (seg.coords["Y"] == coords["Y"]) and + # (seg.coords["Z"] == coords["Z"]) and + # (seg.coords["E"] != coords["E"]) ): + # style = "retract" if (seg.coords["E"] < coords["E"]) else "restore" + + # some horizontal movement, and positive extruder movement: extrusion + if ( + ((seg.coords["X"] != coords["X"]) or (seg.coords["Y"] != coords["Y"]) or ( + seg.coords["Z"] != coords["Z"]))): # != coords["E"] + style = "extrude" + # #force extrude if there is some movement + + # segments to layer lists + # look ahead and if next seg has E and differenz Z, add new layer for current segment + if i == len(self.segments) - 1: + layer.append(seg) + currentLayerIdx += 1 + seg.style = style + seg.layerIdx = currentLayerIdx + # add layer to list of Layers, used to later draw single layer objects + self.layers.append(layer) + break + + # positive extruder movement of next point in a different Z signals a layer change for this segment + if self.segments[i].coords["Z"] != currentLayerZ and self.segments[i + 1].coords["E"] > 0: + self.layers.append( + layer) # layer abschließen, add layer to list of Layers, used to later draw single layer objects + layer = [] # start new layer + currentLayerZ = seg.coords["Z"] + currentLayerIdx += 1 + + # lookback, previous point before texrsuion is part of new layer too, both create an edge + + # set style and layer in segment + seg.style = style + seg.layerIdx = currentLayerIdx + layer.append(seg) + coords = seg.coords
+ + +
+[docs] + def subdivide(self, subd_threshold): + """Subdivide segments based on a specified threshold. + + This method processes a list of segments and subdivides them into + smaller segments if the distance between consecutive segments exceeds + the given threshold. The subdivision is performed by interpolating + points between the original segment's coordinates, ensuring that the + resulting segments maintain the original order and properties. This is + particularly useful for manipulating attributes such as color and + continuous deformation in graphical representations. + + Args: + subd_threshold (float): The distance threshold for subdividing segments. + Segments with a distance greater than this value + will be subdivided. + + Returns: + None: The method modifies the instance's segments attribute in place. + """ + + # smart subdivide + # divide edge if > subd_threshold + # do it in parser to keep index order of vertex and travel/extrude info + # segmentation of path necessary for manipulation of color, continous deforming ect. + subdivided_segs = [] + + # start model at 0 + coords = { + "X": 0.0, + "Y": 0.0, + "Z": 0.0, + "F": 0.0, # no interpolation + "E": 0.0} + + for seg in self.segments: + # calc XYZ distance + d = (seg.coords["X"] - coords["X"]) ** 2 + d += (seg.coords["Y"] - coords["Y"]) ** 2 + d += (seg.coords["Z"] - coords["Z"]) ** 2 + seg.distance = math.sqrt(d) + + if seg.distance > subd_threshold: + + subdivs = math.ceil( + seg.distance / subd_threshold) # ceil makes sure that linspace interval is at least 2 + P1 = coords + P2 = seg.coords + + # interpolated points + interp_coords = np.linspace(list(P1.values()), list( + P2.values()), num=subdivs, endpoint=True) + + for i in range(len(interp_coords)): # inteprolated points array back to segment object + + new_coords = {"X": interp_coords[i][0], "Y": interp_coords[i][1], "Z": interp_coords[i][2], + "F": seg.coords["F"]} + # E/subdivs is for relative extrusion, absolute extrusion need "E":interp_coords[i][4] + # print("interp_coords_new:", new_coords) + if seg.coords["E"] > 0: + new_coords["E"] = round(seg.coords["E"] / (subdivs - 1), 5) + else: + new_coords["E"] = 0 + + # make sure P1 hasn't been written before, compare with previous line + if new_coords['X'] != coords['X'] or new_coords['Y'] != coords['Y'] or new_coords['Z'] != \ + coords['Z']: + # write segment only if movement changes, + # avoid double coordinates due to same start and endpoint of linspace + + new_seg = Segment(seg.type, new_coords, seg.color, + seg.toolnumber, seg.lineNb, seg.line) + new_seg.layerIdx = seg.layerIdx + new_seg.style = seg.style + subdivided_segs.append(new_seg) + + else: + subdivided_segs.append(seg) + + coords = seg.coords # P1 becomes P2 + + self.segments = subdivided_segs
+ + + # create blender curve and vertex_info in text file(coords, style, color...) +
+[docs] + def draw(self, split_layers=False): + """Draws a mesh from segments and layers. + + This function creates a Blender curve and vertex information in a text + file, which includes coordinates, style, and color. If the + `split_layers` parameter is set to True, it processes each layer + individually, generating vertices and edges for each layer. If False, it + processes the segments as a whole. + + Args: + split_layers (bool): A flag indicating whether to split the drawing into + separate layers or not. + """ + + if split_layers: + i = 0 + for layer in self.layers: + verts, edges = segments_to_meshdata(layer) + if len(verts) > 0: + obj_from_pydata(str(i), verts, edges, close=False, collection_name="Layers") + i += 1 + + else: + verts, edges = segments_to_meshdata(self.segments) + obj_from_pydata("Gcode", verts, edges, close=False, collection_name="Layers")
+
+ + + +
+[docs] +class Segment: + def __init__(self, type, coords, color, toolnumber, lineNb, line): +
+[docs] + self.type = type
+ +
+[docs] + self.coords = coords
+ +
+[docs] + self.color = color
+ +
+[docs] + self.toolnumber = toolnumber
+ +
+[docs] + self.lineNb = lineNb
+ +
+[docs] + self.line = line
+ +
+[docs] + self.style = None
+ +
+[docs] + self.layerIdx = None
+ + +
+[docs] + def __str__(self): + """Return a string representation of the object. + + This method constructs a string that includes the coordinates, line + number, style, layer index, and color of the object. It formats these + attributes into a readable string format for easier debugging and + logging. + + Returns: + str: A formatted string representing the object's attributes. + """ + + return " <coords=%s, lineNb=%d, style=%s, layerIdx=%d, color=%s" % \ + (str(self.coords), self.lineNb, self.style, self.layerIdx, str(self.color))
+
+ + + +
+[docs] +class Layer: + def __init__(self, Z): +
+[docs] + self.Z = Z
+ +
+[docs] + self.segments = []
+ +
+[docs] + self.distance = None
+ +
+[docs] + self.extrudate = None
+ + +
+[docs] + def __str__(self): + return "<Layer: Z=%f, len(segments)=%d>" % (self.Z, len(self.segments))
+
+ + + +if __name__ == '__main__': +
+[docs] + path = "test.gcode"
+ + + parser = GcodeParser() + model = parser.parseFile(path) +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/gcodepath.html b/_modules/cam/gcodepath.html new file mode 100644 index 000000000..eac2dea20 --- /dev/null +++ b/_modules/cam/gcodepath.html @@ -0,0 +1,1451 @@ + + + + + + + + + + cam.gcodepath — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.gcodepath

+"""CNC CAM 'gcodepath.py' © 2012 Vilem Novak
+
+Generate and Export G-Code based on scene, machine, chain, operation and path settings.
+"""
+
+# G-code Generaton
+from math import (
+    ceil,
+    floor,
+    pi,
+    sqrt
+)
+import time
+
+import numpy
+from shapely.geometry import polygon as spolygon
+
+import bpy
+from mathutils import Euler, Vector
+
+from . import strategy
+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,
+)
+
+
+
+[docs] +def pointonline(a, b, c, tolerence): + """Determine if the angle between two vectors is within a specified + tolerance. + + This function checks if the angle formed by two vectors, defined by + points `b` and `c` relative to point `a`, is less than or equal to a + given tolerance. It converts the points into vectors, calculates the dot + product, and then computes the angle between them using the arccosine + function. If the angle exceeds the specified tolerance, the function + returns False; otherwise, it returns True. + + Args: + a (numpy.ndarray): The origin point as a vector. + b (numpy.ndarray): The first point as a vector. + c (numpy.ndarray): The second point as a vector. + tolerence (float): The maximum allowable angle (in degrees) between the vectors. + + Returns: + bool: True if the angle between vectors b and c is within the specified + tolerance, + False otherwise. + """ + + b = b - a # convert to vector by subtracting origin + c = c - a + dot_pr = b.dot(c) # b dot c + norms = numpy.linalg.norm(b) * numpy.linalg.norm(c) # find norms + # find angle between the two vectors + angle = (numpy.rad2deg(numpy.arccos(dot_pr / norms))) + if angle > tolerence: + return False + else: + return True
+ + + +
+[docs] +def exportGcodePath(filename, vertslist, operations): + """Exports G-code using the Heeks NC Adopted Library. + + This function generates G-code from a list of vertices and operations + specified by the user. It handles various post-processor settings based + on the machine configuration and can split the output into multiple + files if the total number of operations exceeds a specified limit. The + G-code is tailored for different machine types and includes options for + tool changes, spindle control, and various movement commands. + + Args: + filename (str): The name of the file to which the G-code will be exported. + vertslist (list): A list of mesh objects containing vertex data. + operations (list): A list of operations to be performed, each containing + specific parameters for G-code generation. + + Returns: + None: This function does not return a value; it writes the G-code to a file. + """ + print("EXPORT") + progress('Exporting G-code File') + t = time.time() + s = bpy.context.scene + m = s.cam_machine + enable_dust = False + enable_hold = False + enable_mist = False + # find out how many files will be done: + + split = False + + totops = 0 + findex = 0 + if m.eval_splitting: # detect whether splitting will happen + for mesh in vertslist: + totops += len(mesh.vertices) + print(totops) + if totops > m.split_limit: + split = True + filesnum = ceil(totops / m.split_limit) + print('File Will Be Separated Into %i Files' % filesnum) + print('1') + + basefilename = bpy.data.filepath[:- + len(bpy.path.basename(bpy.data.filepath))] + safeFileName(filename) + + extension = '.tap' + if m.post_processor == 'ISO': + from .nc import iso as postprocessor + if m.post_processor == 'MACH3': + from .nc import mach3 as postprocessor + elif m.post_processor == 'EMC': + extension = '.ngc' + from .nc import emc2b as postprocessor + elif m.post_processor == 'FADAL': + extension = '.tap' + from .nc import fadal as postprocessor + elif m.post_processor == 'GRBL': + extension = '.gcode' + from .nc import grbl as postprocessor + elif m.post_processor == 'HM50': + from .nc import hm50 as postprocessor + elif m.post_processor == 'HEIDENHAIN': + extension = '.H' + from .nc import heiden as postprocessor + elif m.post_processor == 'HEIDENHAIN530': + extension = '.H' + from .nc import heiden530 as postprocessor + elif m.post_processor == 'TNC151': + from .nc import tnc151 as postprocessor + elif m.post_processor == 'SIEGKX1': + from .nc import siegkx1 as postprocessor + elif m.post_processor == 'CENTROID': + from .nc import centroid1 as postprocessor + elif m.post_processor == 'ANILAM': + from .nc import anilam_crusader_m as postprocessor + elif m.post_processor == 'GRAVOS': + extension = '.nc' + from .nc import gravos as postprocessor + elif m.post_processor == 'WIN-PC': + extension = '.din' + from .nc import winpc as postprocessor + elif m.post_processor == 'SHOPBOT MTC': + extension = '.sbp' + from .nc import shopbot_mtc as postprocessor + elif m.post_processor == 'LYNX_OTTER_O': + extension = '.nc' + from .nc import lynx_otter_o as postprocessor + + if s.unit_settings.system == 'METRIC': + unitcorr = 1000.0 + elif s.unit_settings.system == 'IMPERIAL': + unitcorr = 1 / 0.0254 + else: + unitcorr = 1 + rotcorr = 180.0 / pi + + use_experimental = bpy.context.preferences.addons[__package__].preferences.experimental + + def startNewFile(): + """Start a new file for G-code generation. + + This function initializes a new file for G-code output based on the + specified parameters. It constructs the filename using a base name, an + optional index, and a file extension. The function also configures the + post-processor settings based on user overrides and the selected unit + system (metric or imperial). Finally, it begins the G-code program and + sets the necessary configurations for the output. + + Returns: + Creator: An instance of the post-processor Creator class configured for + G-code generation. + """ + + fileindex = '' + if split: + fileindex = '_' + str(findex) + filename = basefilename + fileindex + extension + print("writing: ", filename) + c = postprocessor.Creator() + + # process user overrides for post processor settings + + if use_experimental and isinstance(c, iso.Creator): + c.output_block_numbers = m.output_block_numbers + c.start_block_number = m.start_block_number + c.block_number_increment = m.block_number_increment + + c.output_tool_definitions = m.output_tool_definitions + c.output_tool_change = m.output_tool_change + c.output_g43_on_tool_change_line = m.output_g43_on_tool_change + + c.file_open(filename) + + # unit system correction + ############### + if s.unit_settings.system == 'METRIC': + c.metric() + elif s.unit_settings.system == 'IMPERIAL': + c.imperial() + + # start program + c.program_begin(0, filename) + c.flush_nc() + c.comment('G-code Generated with CNC CAM and NC library') + # absolute coordinates + c.absolute() + + # work-plane, by now always xy, + c.set_plane(0) + c.flush_nc() + + return c + + c = startNewFile() + # [o.cutter_id,o.cutter_dameter,o.cutter_type,o.cutter_flutes] + last_cutter = None + + processedops = 0 + last = Vector((0, 0, 0)) + cut_distance = 0 + for i, o in enumerate(operations): + + if use_experimental and o.output_header: + lines = o.gcode_header.split(';') + for aline in lines: + c.write(aline + '\n') + + free_height = o.movement.free_height # o.max.z+ + if o.movement.useG64: + c.set_path_control_mode(2, round(o.movement.G64 * 1000, 5), 0) + + mesh = vertslist[i] + verts = mesh.vertices[:] + if o.machine_axes != '3': + rots = mesh.shape_keys.key_blocks['rotations'].data + + # spindle rpm and direction + ############### + if o.movement.spindle_rotation == 'CW': + spdir_clockwise = True + else: + spdir_clockwise = False + + # write tool, not working yet probably + # print (last_cutter) + if m.output_tool_change and last_cutter != [o.cutter_id, o.cutter_diameter, o.cutter_type, o.cutter_flutes]: + if m.output_tool_change: + c.tool_change(o.cutter_id) + + if m.output_tool_definitions: + c.comment('Tool: D = %s type %s flutes %s' % ( + strInUnits(o.cutter_diameter, 4), o.cutter_type, o.cutter_flutes)) + + c.flush_nc() + + last_cutter = [o.cutter_id, o.cutter_diameter, o.cutter_type, o.cutter_flutes] + if o.cutter_type not in ['LASER', 'PLASMA']: + if o.enable_hold: + c.write('(Hold Down)\n') + lines = o.gcode_start_hold_cmd.split(';') + for aline in lines: + c.write(aline + '\n') + enable_hold = True + stop_hold = o.gcode_stop_hold_cmd + if o.enable_mist: + c.write('(Mist)\n') + lines = o.gcode_start_mist_cmd.split(';') + for aline in lines: + c.write(aline + '\n') + enable_mist = True + stop_mist = o.gcode_stop_mist_cmd + + c.spindle(o.spindle_rpm, spdir_clockwise) # start spindle + c.write_spindle() + c.flush_nc() + c.write('\n') + + if o.enable_dust: + c.write('(Dust collector)\n') + lines = o.gcode_start_dust_cmd.split(';') + for aline in lines: + c.write(aline + '\n') + enable_dust = True + stop_dust = o.gcode_stop_dust_cmd + + if m.spindle_start_time > 0: + c.dwell(m.spindle_start_time) + + # c.rapid(z=free_height*1000) #raise the spindle to safe height + fmh = round(free_height * unitcorr, 2) + if o.cutter_type not in ['LASER', 'PLASMA']: + c.write('G00 Z' + str(fmh) + '\n') + if o.enable_A: + if o.rotation_A == 0: + o.rotation_A = 0.0001 + 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 / pi) + + c.write('\n') + c.flush_nc() + + # dhull c.feedrate(unitcorr*o.feedrate) + + # commands=[] + m = bpy.context.scene.cam_machine + + millfeedrate = min(o.feedrate, m.feedrate_max) + + millfeedrate = unitcorr * max(millfeedrate, m.feedrate_min) + plungefeedrate = millfeedrate * o.plunge_feedrate / 100 + freefeedrate = m.feedrate_max * unitcorr + fadjust = False + if o.do_simulation_feedrate and mesh.shape_keys is not None \ + and mesh.shape_keys.key_blocks.find('feedrates') != -1: + shapek = mesh.shape_keys.key_blocks['feedrates'] + fadjust = True + + if m.use_position_definitions: # dhull + last = Vector((m.starting_position.x, m.starting_position.y, m.starting_position.z)) + + lastrot = Euler((0, 0, 0)) + duration = 0.0 + f = 0.1123456 # nonsense value, so first feedrate always gets written + fadjustval = 1 # if simulation load data is Not present + + downvector = Vector((0, 0, -1)) + plungelimit = (pi / 2 - o.plunge_angle) + + scale_graph = 0.05 # warning this has to be same as in export in utils!!!! + + ii = 0 + offline = 0 + online = 0 + cut = True # active cut variable for laser or plasma + shapes = 0 + for vi, vert in enumerate(verts): + # skip the first vertex if this is a chained operation + # ie: outputting more than one operation + # otherwise the machine gets sent back to 0,0 for each operation which is unecessary + shapes += 1 # Count amount of shapes + if i > 0 and vi == 0: + continue + v = vert.co + # redundant point on line detection + if o.remove_redundant_points and o.strategy != 'DRILL': + nextv = v + if ii == 0: + firstv = v # only happens once + elif ii == 1: + middlev = v + else: + if pointonline(firstv, middlev, nextv, o.simplify_tol / 1000): + middlev = nextv + online += 1 + continue + else: # create new start point with the last tested point + ii = 0 + offline += 1 + firstv = nextv + ii += 1 + # end of redundant point on line detection + if o.machine_axes != '3': + v = v.copy() # we rotate it so we need to copy the vector + r = Euler(rots[vi].co) + # conversion to N-axis coordinates + # this seems to work correctly for 4 axis. + rcompensate = r.copy() + rcompensate.x = -r.x + rcompensate.y = -r.y + rcompensate.z = -r.z + v.rotate(rcompensate) + + if r.x == lastrot.x: + ra = None + else: + + ra = r.x * rotcorr + + if r.y == lastrot.y: + rb = None + else: + rb = r.y * rotcorr + + if vi > 0 and v.x == last.x: + vx = None + else: + vx = v.x * unitcorr + if vi > 0 and v.y == last.y: + vy = None + else: + vy = v.y * unitcorr + if vi > 0 and v.z == last.z: + vz = None + else: + vz = v.z * unitcorr + + if fadjust: + fadjustval = shapek.data[vi].co.z / scale_graph + + # v=(v.x*unitcorr,v.y*unitcorr,v.z*unitcorr) + vect = v - last + l = vect.length + if vi > 0 and l > 0 and downvector.angle(vect) < plungelimit: + if f != plungefeedrate or (fadjust and fadjustval != 1): + f = plungefeedrate * fadjustval + c.feedrate(f) + + if o.machine_axes == '3': + if o.cutter_type in ['LASER', 'PLASMA']: + if not cut: + if o.cutter_type == 'LASER': + c.write("(*************dwell->laser on)\n") + c.write("G04 P" + str(round(o.Laser_delay, 2)) + "\n") + c.write(o.Laser_on + '\n') + elif o.cutter_type == 'PLASMA': + c.write("(*************dwell->PLASMA on)\n") + plasma_delay = round(o.Plasma_delay, 5) + if plasma_delay > 0: + c.write("G04 P" + str(plasma_delay) + "\n") + c.write(o.Plasma_on + '\n') + plasma_dwell = round(o.Plasma_dwell, 5) + if plasma_dwell > 0: + c.write("G04 P" + str(plasma_dwell) + "\n") + cut = True + else: + c.feed(x=vx, y=vy, z=vz) + else: + + c.feed(x=vx, y=vy, z=vz, a=ra, b=rb) + + elif v.z >= free_height or vi == 0: # v.z==last.z==free_height or vi==0 + if f != freefeedrate: + f = freefeedrate + c.feedrate(f) + + if o.machine_axes == '3': + if o.cutter_type in ['LASER', 'PLASMA']: + if cut: + if o.cutter_type == 'LASER': + c.write("(**************laser off)\n") + c.write(o.Laser_off + '\n') + elif o.cutter_type == 'PLASMA': + c.write("(**************Plasma off)\n") + c.write(o.Plasma_off + '\n') + + cut = False + c.rapid(x=vx, y=vy) + else: + c.rapid(x=vx, y=vy, z=vz) + # this is to evaluate operation time and adds a feedrate for fast moves + if vz is not None: + # compensate for multiple fast move accelerations + f = plungefeedrate * fadjustval * 0.35 + if vx is not None or vy is not None: + f = freefeedrate * 0.8 # compensate for free feedrate acceleration + else: + c.rapid(x=vx, y=vy, z=vz, a=ra, b=rb) + + else: + + if f != millfeedrate or (fadjust and fadjustval != 1): + f = millfeedrate * fadjustval + c.feedrate(f) + + if o.machine_axes == '3': + c.feed(x=vx, y=vy, z=vz) + else: + c.feed(x=vx, y=vy, z=vz, a=ra, b=rb) + cut_distance += vect.length * unitcorr + vector_duration = vect.length / f + duration += vector_duration + last = v + if o.machine_axes != '3': + lastrot = r + + processedops += 1 + if split and processedops > m.split_limit: + c.rapid(x=last.x * unitcorr, y=last.y * unitcorr, z=free_height * unitcorr) + # @v=(ch.points[-1][0],ch.points[-1][1],free_height) + c.program_end() + findex += 1 + c.file_close() + c = startNewFile() + c.flush_nc() + c.comment('Tool change - D = %s type %s flutes %s' % ( + strInUnits(o.cutter_diameter, 4), o.cutter_type, o.cutter_flutes)) + c.tool_change(o.cutter_id) + c.spindle(o.spindle_rpm, spdir_clockwise) + c.write_spindle() + c.flush_nc() + + if m.spindle_start_time > 0: + c.dwell(m.spindle_start_time) + c.flush_nc() + + c.feedrate(unitcorr * o.feedrate) + c.rapid(x=last.x * unitcorr, y=last.y * unitcorr, z=free_height * unitcorr) + c.rapid(x=last.x * unitcorr, y=last.y * unitcorr, z=last.z * unitcorr) + processedops = 0 + + if o.remove_redundant_points and o.strategy != "DRILL": + print("online " + str(online) + " offline " + str(offline) + " " + str( + round(online / (offline + online) * 100, 1)) + "% removal") + c.feedrate(unitcorr * o.feedrate) + + if o.output_trailer: + lines = o.gcode_trailer.split(';') + for aline in lines: + c.write(aline + '\n') + + o.info.duration = duration * unitcorr + print("total time:", round(o.info.duration * 60), "seconds") + if bpy.context.scene.unit_settings.system == 'METRIC': + unit_distance = 'm' + cut_distance /= 1000 + else: + unit_distance = 'feet' + cut_distance /= 12 + + print("cut distance:", round(cut_distance, 3), unit_distance) + if enable_dust: + c.write(stop_dust + '\n') + if enable_hold: + c.write(stop_hold + '\n') + if enable_mist: + c.write(stop_mist + '\n') + + c.program_end() + c.file_close() + print(time.time() - t)
+ + + +
+[docs] +async def getPath(context, operation): + """Calculate the path for a given operation in a specified context. + + This function performs various calculations to determine the path based + on the operation's parameters and context. It checks for changes in the + operation's data and updates relevant tags accordingly. Depending on the + number of machine axes specified in the operation, it calls different + functions to handle 3-axis, 4-axis, or 5-axis operations. Additionally, + if automatic export is enabled, it exports the generated G-code path. + + Args: + context: The context in which the operation is being performed. + operation: An object representing the operation with various + attributes such as machine_axes, strategy, and + auto_export. + """ + # should do all path calculations. + t = time.process_time() + # print('ahoj0') + + # these tags are for caching of some of the results. Not working well still + # - although it can save a lot of time during calculation... + + chd = getChangeData(operation) + # print(chd) + # print(o.changedata) + if operation.changedata != chd: # or 1: + operation.update_offsetimage_tag = True + operation.update_zbufferimage_tag = True + operation.changedata = chd + + operation.update_silhouete_tag = True + operation.update_ambient_tag = True + operation.update_bullet_collision_tag = True + + getOperationSources(operation) + + operation.info.warnings = '' + checkMemoryLimit(operation) + + print(operation.machine_axes) + + if operation.machine_axes == '3': + if USE_PROFILER == True: # profiler + import cProfile + import pstats + import io + pr = cProfile.Profile() + pr.enable() + await getPath3axis(context, operation) + pr.disable() + pr.dump_stats(time.strftime("CNCCAM_%Y%m%d_%H%M.prof")) + else: + await getPath3axis(context, operation) + + elif (operation.machine_axes == '5' and operation.strategy5axis == 'INDEXED') or ( + operation.machine_axes == '4' and operation.strategy4axis == 'INDEXED'): + # 5 axis operations are now only 3 axis operations that get rotated... + operation.orientation = prepareIndexed(operation) # TODO RENAME THIS + + await getPath3axis(context, operation) # TODO RENAME THIS + + cleanupIndexed(operation) # TODO RENAME THIS + # transform5axisIndexed + elif operation.machine_axes == '4': + await getPath4axis(context, operation) + + # export gcode if automatic. + if operation.auto_export: + if bpy.data.objects.get("cam_path_{}".format(operation.name)) is None: + return + p = bpy.data.objects["cam_path_{}".format(operation.name)] + exportGcodePath(operation.filename, [p.data], [operation]) + + operation.changed = False + t1 = time.process_time() - t + progress('total time', t1)
+ + + +
+[docs] +def getChangeData(o): + """Check if object properties have changed to determine if image updates + are needed. + + This function inspects the properties of objects specified by the input + parameter to see if any changes have occurred. It concatenates the + location, rotation, and dimensions of the relevant objects into a single + string, which can be used to determine if an image update is necessary + based on changes in the object's state. + + Args: + o (object): An object containing properties that specify the geometry source + and relevant object or collection names. + + Returns: + str: A string representation of the location, rotation, and dimensions of + the specified objects. + """ + changedata = '' + obs = [] + if o.geometry_source == 'OBJECT': + obs = [bpy.data.objects[o.object_name]] + elif o.geometry_source == 'COLLECTION': + obs = bpy.data.collections[o.collection_name].objects + for ob in obs: + changedata += str(ob.location) + changedata += str(ob.rotation_euler) + changedata += str(ob.dimensions) + + return changedata
+ + + +
+[docs] +def checkMemoryLimit(o): + """Check and adjust the memory limit for an object. + + This function calculates the resolution of an object based on its + dimensions and the specified pixel size. If the calculated resolution + exceeds the defined memory limit, it adjusts the pixel size accordingly + to reduce the resolution. A warning message is appended to the object's + info if the pixel size is modified. + + Args: + o (object): An object containing properties such as max, min, optimisation, and + info. + + Returns: + None: This function modifies the object's properties in place and does not + return a value. + """ + + # getBounds(o) + sx = o.max.x - o.min.x + sy = o.max.y - o.min.y + resx = sx / o.optimisation.pixsize + resy = sy / o.optimisation.pixsize + res = resx * resy + limit = o.optimisation.imgres_limit * 1000000 + # print('co se to deje') + if res > limit: + ratio = (res / limit) + 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)
+ + + +# this is the main function. +# FIXME: split strategies into separate file! +
+[docs] +async def getPath3axis(context, operation): + """Generate a machining path based on the specified operation strategy. + + This function evaluates the provided operation's strategy and generates + the corresponding machining path. It supports various strategies such as + 'CUTOUT', 'CURVE', 'PROJECTED_CURVE', 'POCKET', and others. Depending on + the strategy, it performs specific calculations and manipulations on the + input data to create a path that can be used for machining operations. + The function handles different strategies by calling appropriate methods + from the `strategy` module and processes the path samples accordingly. + It also manages the generation of chunks, which represent segments of + the machining path, and applies any necessary transformations based on + the operation's parameters. + + Args: + context (bpy.context): The Blender context containing scene information. + operation (Operation): An object representing the machining operation, + which includes strategy and other relevant parameters. + + Returns: + None: This function does not return a value but modifies the state of + the operation and context directly. + """ + + s = bpy.context.scene + o = operation + getBounds(o) + tw = time.time() + + if o.strategy == 'CUTOUT': + await strategy.cutout(o) + + elif o.strategy == 'CURVE': + await strategy.curve(o) + + elif o.strategy == 'PROJECTED_CURVE': + await strategy.proj_curve(s, o) + + elif o.strategy == 'POCKET': + await strategy.pocket(o) + + elif o.strategy in ['PARALLEL', 'CROSS', 'BLOCK', 'SPIRAL', 'CIRCLES', 'OUTLINEFILL', 'CARVE', 'PENCIL', 'CRAZY']: + + if o.strategy == 'CARVE': + pathSamples = [] + ob = bpy.data.objects[o.curve_object] + pathSamples.extend(curveToChunks(ob)) + # sort before sampling + pathSamples = await sortChunks(pathSamples, o) + pathSamples = chunksRefine(pathSamples, o) + elif o.strategy == 'PENCIL': + await prepareArea(o) + getAmbient(o) + pathSamples = getOffsetImageCavities(o, o.offset_image) + pathSamples = limitChunks(pathSamples, o) + # sort before sampling + pathSamples = await sortChunks(pathSamples, o) + elif o.strategy == 'CRAZY': + await prepareArea(o) + # pathSamples = crazyStrokeImage(o) + # this kind of worked and should work: + millarea = o.zbuffer_image < o.minz + 0.000001 + avoidarea = o.offset_image > o.minz + 0.000001 + + pathSamples = crazyStrokeImageBinary(o, millarea, avoidarea) + ##### + pathSamples = await sortChunks(pathSamples, o) + pathSamples = chunksRefine(pathSamples, o) + + else: + if o.strategy == 'OUTLINEFILL': + getOperationSilhouete(o) + + pathSamples = getPathPattern(o) + + if o.strategy == 'OUTLINEFILL': + 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 connectChunksLow(pathSamples, o) + + # print (minz) + + chunks = [] + layers = strategy.getLayers(o, o.maxz, o.min.z) + + print("SAMPLE", o.name) + chunks.extend(await sampleChunks(o, pathSamples, layers)) + print("SAMPLE OK") + if o.strategy == 'PENCIL': # and bpy.app.debug_value==-3: + chunks = chunksCoherency(chunks) + print('coherency check') + + # and not o.movement.parallel_step_back: + if o.strategy in ['PARALLEL', 'CROSS', 'PENCIL', 'OUTLINEFILL']: + print('sorting') + chunks = await sortChunks(chunks, o) + if o.strategy == 'OUTLINEFILL': + chunks = await connectChunksLow(chunks, o) + if o.movement.ramp: + for ch in chunks: + ch.rampZigZag(ch.zstart, None, o) + # print(chunks) + if o.strategy == 'CARVE': + for ch in chunks: + ch.offsetZ(-o.carve_depth) +# for vi in range(0, len(ch.points)): +# ch.points[vi] = (ch.points[vi][0], ch.points[vi][1], ch.points[vi][2] - o.carve_depth) + if o.use_bridges: + print(chunks) + for bridge_chunk in chunks: + useBridges(bridge_chunk, o) + + strategy.chunksToMesh(chunks, o) + + elif o.strategy == 'WATERLINE' and o.optimisation.use_opencamlib: + getAmbient(o) + chunks = [] + await oclGetWaterline(o, chunks) + chunks = limitChunks(chunks, o) + if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CW') or ( + o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CCW'): + for ch in chunks: + ch.reverse() + + strategy.chunksToMesh(chunks, o) + + elif o.strategy == 'WATERLINE' and not o.optimisation.use_opencamlib: + topdown = True + chunks = [] + await progress_async('retrieving object slices') + await prepareArea(o) + layerstep = 1000000000 + if o.use_layers: + layerstep = floor(o.stepdown / o.slice_detail) + if layerstep == 0: + layerstep = 1 + + # for projection of filled areas + layerstart = o.max.z # + layerend = o.min.z # + layers = [[layerstart, layerend]] + ####################### + nslices = ceil(abs((o.minz-o.maxz) / o.slice_detail)) + lastslice = spolygon.Polygon() # polyversion + layerstepinc = 0 + + slicesfilled = 0 + getAmbient(o) + + for h in range(0, nslices): + layerstepinc += 1 + slicechunks = [] + z = o.minz + h * o.slice_detail + if h == 0: + z += 0.0000001 + # if people do mill flat areas, this helps to reach those... + # otherwise first layer would actually be one slicelevel above min z. + + islice = o.offset_image > z + slicepolys = imageToShapely(o, islice, with_border=True) + + poly = spolygon.Polygon() # polygversion + lastchunks = [] + + for p in slicepolys.geoms: + poly = poly.union(p) # polygversion TODO: why is this added? + nchunks = shapelyToChunks(p, z) + nchunks = limitChunks(nchunks, o, force=True) + lastchunks.extend(nchunks) + slicechunks.extend(nchunks) + if len(slicepolys.geoms) > 0: + slicesfilled += 1 + + # + if o.waterline_fill: + layerstart = min(o.maxz, z + o.slice_detail) # + layerend = max(o.min.z, z - o.slice_detail) # + layers = [[layerstart, layerend]] + ##################################### + # fill top slice for normal and first for inverse, fill between polys + if not lastslice.is_empty or (o.inverse and not poly.is_empty and slicesfilled == 1): + restpoly = None + if not lastslice.is_empty: # between polys + if o.inverse: + restpoly = poly.difference(lastslice) + else: + restpoly = lastslice.difference(poly) + # print('filling between') + if (not o.inverse and poly.is_empty and slicesfilled > 0) or ( + o.inverse and not poly.is_empty and slicesfilled == 1): # first slice fill + restpoly = lastslice + + restpoly = restpoly.buffer(-o.dist_between_paths, + resolution=o.optimisation.circle_detail) + + fillz = z + i = 0 + while not restpoly.is_empty: + nchunks = shapelyToChunks(restpoly, fillz) + # project paths TODO: path projection during waterline is not working + if o.waterline_project: + nchunks = chunksRefine(nchunks, o) + nchunks = await sampleChunks(o, nchunks, layers) + + nchunks = limitChunks(nchunks, o, force=True) + ######################### + slicechunks.extend(nchunks) + parentChildDist(lastchunks, nchunks, o) + lastchunks = nchunks + # slicechunks.extend(polyToChunks(restpoly,z)) + restpoly = restpoly.buffer(-o.dist_between_paths, + resolution=o.optimisation.circle_detail) + + i += 1 + # print(i) + i = 0 + # fill layers and last slice, last slice with inverse is not working yet + # - inverse millings end now always on 0 so filling ambient does have no sense. + if (slicesfilled > 0 and layerstepinc == layerstep) or ( + not o.inverse and not poly.is_empty and slicesfilled == 1) or ( + o.inverse and poly.is_empty and slicesfilled > 0): + fillz = z + layerstepinc = 0 + + bound_rectangle = o.ambient + restpoly = bound_rectangle.difference(poly) + if o.inverse and poly.is_empty and slicesfilled > 0: + restpoly = bound_rectangle.difference(lastslice) + + restpoly = restpoly.buffer(-o.dist_between_paths, + resolution=o.optimisation.circle_detail) + + i = 0 + # 'GeometryCollection':#len(restpoly.boundary.coords)>0: + while not restpoly.is_empty: + # print(i) + nchunks = shapelyToChunks(restpoly, fillz) + ######################### + nchunks = limitChunks(nchunks, o, force=True) + slicechunks.extend(nchunks) + parentChildDist(lastchunks, nchunks, o) + lastchunks = nchunks + restpoly = restpoly.buffer(-o.dist_between_paths, + resolution=o.optimisation.circle_detail) + i += 1 + + percent = int(h / nslices * 100) + await progress_async('waterline layers ', percent) + lastslice = poly + + if (o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CCW') or ( + o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CW'): + for chunk in slicechunks: + chunk.reverse() + slicechunks = await sortChunks(slicechunks, o) + if topdown: + slicechunks.reverse() + # project chunks in between + + chunks.extend(slicechunks) + if topdown: + chunks.reverse() + strategy.chunksToMesh(chunks, o) + + elif o.strategy == 'DRILL': + await strategy.drill(o) + + elif o.strategy == 'MEDIAL_AXIS': + await strategy.medial_axis(o) + await progress_async(f"Done", time.time() - tw, "s")
+ + + +
+[docs] +async def getPath4axis(context, operation): + """Generate a path for a specified axis based on the given operation. + + This function retrieves the bounds of the operation and checks the + strategy associated with the axis. If the strategy is one of the + specified types ('PARALLELR', 'PARALLEL', 'HELIX', 'CROSS'), it + generates path samples and processes them into chunks for meshing. The + function utilizes various helper functions to achieve this, including + obtaining layers and sampling chunks. + + Args: + context: The context in which the operation is executed. + operation: An object that contains the strategy and other + necessary parameters for generating the path. + + Returns: + None: This function does not return a value but modifies + the state of the operation by processing chunks for meshing. + """ + + o = operation + getBounds(o) + if o.strategy4axis in ['PARALLELR', 'PARALLEL', 'HELIX', 'CROSS']: + path_samples = getPathPattern4axis(o) + + depth = path_samples[0].depth + chunks = [] + + layers = strategy.getLayers(o, 0, depth) + + chunks.extend(await sampleChunksNAxis(o, path_samples, layers)) + strategy.chunksToMesh(chunks, o)
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/image_utils.html b/_modules/cam/image_utils.html new file mode 100644 index 000000000..d3c5b9cac --- /dev/null +++ b/_modules/cam/image_utils.html @@ -0,0 +1,2293 @@ + + + + + + + + + + cam.image_utils — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.image_utils

+"""CNC CAM 'image_utils.py' © 2012 Vilem Novak
+
+Functions to render, save, convert and analyze image data.
+"""
+
+from math import (
+    acos,
+    ceil,
+    cos,
+    floor,
+    pi,
+    radians,
+    sin,
+    tan,
+)
+import os
+import random
+import time
+
+import numpy
+
+import bpy
+try:
+    import bl_ext.blender_org.simplify_curves_plus as curve_simplify
+except ImportError:
+    pass
+
+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,
+)
+
+
+
+[docs] +def numpysave(a, iname): + """Save a NumPy array as an image file in OpenEXR format. + + This function converts a NumPy array into an image and saves it using + Blender's rendering capabilities. It sets the image format to OpenEXR + with black and white color mode and a color depth of 32 bits. The image + is saved to the specified filename. + + Args: + a (numpy.ndarray): The NumPy array to be converted and saved as an image. + iname (str): The file path where the image will be saved. + """ + + 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)
+ + + +
+[docs] +def getCircle(r, z): + """Generate a 2D array representing a circle. + + This function creates a 2D NumPy array filled with a specified value for + points that fall within a circle of a given radius. The circle is + centered in the array, and the function uses the Euclidean distance to + determine which points are inside the circle. The resulting array has + dimensions that are twice the radius, ensuring that the entire circle + fits within the array. + + Args: + r (int): The radius of the circle. + z (float): The value to fill the points inside the circle. + + Returns: + numpy.ndarray: A 2D array where points inside the circle are filled + with the value `z`, and points outside are filled with -10. + """ + + car = numpy.full(shape=(r*2, r*2), fill_value=-10, dtype=numpy.double) + res = 2 * r + m = r + v = Vector((0, 0, 0)) + for a in range(0, res): + v.x = (a + 0.5 - m) + for b in range(0, res): + v.y = (b + 0.5 - m) + if v.length <= r: + car[a, b] = z + return car
+ + + +
+[docs] +def getCircleBinary(r): + """Generate a binary representation of a circle in a 2D grid. + + This function creates a 2D boolean array where the elements inside a + circle of radius `r` are set to `True`, and the elements outside the + circle are set to `False`. The circle is centered in the middle of the + array, which has dimensions of (2*r, 2*r). The function iterates over + each point in the grid and checks if it lies within the specified + radius. + + Args: + r (int): The radius of the circle. + + Returns: + numpy.ndarray: A 2D boolean array representing the circle. + """ + + car = numpy.full(shape=(r*2, r*2), fill_value=False, dtype=bool) + res = 2 * r + m = r + v = Vector((0, 0, 0)) + for a in range(0, res): + v.x = (a + 0.5 - m) + for b in range(0, res): + v.y = (b + 0.5 - m) + if (v.length <= r): + car.itemset((a, b), True) + return car
+ + + +# get cutters for the z-buffer image method + + +
+[docs] +def numpytoimage(a, iname): + """Convert a NumPy array to a Blender image. + + This function takes a NumPy array and converts it into a Blender image. + It first checks if an image with the specified name and dimensions + already exists in Blender. If it does not exist, a new image is created + with the specified name and dimensions. The pixel data from the NumPy + array is then reshaped and assigned to the image's pixel buffer. + + Args: + a (numpy.ndarray): A 2D NumPy array representing the image data. + iname (str): The name to assign to the created or found image. + + Returns: + bpy.types.Image: The Blender image object that was created or found. + """ + + print('numpy to image', iname) + t = time.time() + print(a.shape[0], a.shape[1]) + foundimage = False + + for image in bpy.data.images: + + if image.name[:len(iname)] == iname and image.size[0] == a.shape[0] and image.size[1] == a.shape[1]: + i = image + foundimage = True + if not foundimage: + bpy.ops.image.new(name=iname, width=a.shape[0], height=a.shape[1], color=(0, 0, 0, 1), alpha=True, + generated_type='BLANK', float=True) + for image in bpy.data.images: + # print(image.name[:len(iname)],iname, image.size[0],a.shape[0],image.size[1],a.shape[1]) + if image.name[:len(iname)] == iname and image.size[0] == a.shape[0] and image.size[1] == a.shape[1]: + i = image + + d = a.shape[0] * a.shape[1] + a = a.swapaxes(0, 1) + a = a.reshape(d) + a = a.repeat(4) + a[3::4] = 1 + i.pixels[:] = a[:] # this gives big speedup! + print('\ntime ' + str(time.time() - t)) + return i
+ + + +
+[docs] +def imagetonumpy(i): + """Convert a Blender image to a NumPy array. + + This function takes a Blender image object and converts its pixel data + into a NumPy array. It retrieves the pixel data, reshapes it, and swaps + the axes to match the expected format for further processing. The + function also measures the time taken for the conversion and prints it + to the console. + + Args: + i (Image): A Blender image object containing pixel data. + + Returns: + numpy.ndarray: A 2D NumPy array representing the image pixels. + """ + + t = time.time() + + width = i.size[0] + height = i.size[1] + na = numpy.full(shape=(width*height*4,), fill_value=-10, dtype=numpy.double) + + p = i.pixels[:] + # these 2 lines are about 15% faster than na[:]=i.pixels[:].... whyyyyyyyy!!?!?!?!?! + # Blender image data access is evil. + na[:] = p + na = na[::4] + na = na.reshape(height, width) + na = na.swapaxes(0, 1) + + print('\ntime of image to numpy ' + str(time.time() - t)) + return na
+ + + +@jit(nopython=True, parallel=True, fastmath=False, cache=True) +
+[docs] +def _offset_inner_loop(y1, y2, cutterArrayNan, cwidth, sourceArray, width, height, comparearea): + """Offset the inner loop for processing a specified area in a 2D array. + + This function iterates over a specified range of rows and columns in a + 2D array, calculating the maximum value from a source array combined + with a cutter array for each position in the defined area. The results + are stored in the comparearea array, which is updated with the maximum + values found. + + Args: + y1 (int): The starting index for the row iteration. + y2 (int): The ending index for the row iteration. + cutterArrayNan (numpy.ndarray): A 2D array used for modifying the source array. + cwidth (int): The width of the area to consider for the maximum calculation. + sourceArray (numpy.ndarray): The source 2D array from which maximum values are derived. + width (int): The width of the source array. + height (int): The height of the source array. + comparearea (numpy.ndarray): A 2D array where the calculated maximum values are stored. + + Returns: + None: This function modifies the comparearea in place and does not return a + value. + """ + + for y in prange(y1, y2): + for x in range(0, width-cwidth): + comparearea[x, y] = numpy.nanmax(sourceArray[x:x+cwidth, y:y+cwidth] + cutterArrayNan)
+ + + +
+[docs] +async def offsetArea(o, samples): + """Offsets the whole image with the cutter and skin offsets. + + This function modifies the offset image based on the provided cutter and + skin offsets. It calculates the dimensions of the source and cutter + arrays, initializes an offset image, and processes the image in + segments. The function handles the inversion of the source array if + specified and updates the offset image accordingly. Progress is reported + asynchronously during processing. + + Args: + o: An object containing properties such as `update_offsetimage_tag`, + `min`, `max`, `inverse`, and `offset_image`. + samples (numpy.ndarray): A 2D array representing the source image data. + + Returns: + numpy.ndarray: The updated offset image after applying the cutter and skin offsets. + """ + if o.update_offsetimage_tag: + 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 = getCutterArray(o, o.optimisation.pixsize) + + # progress('image size', sourceArray.shape) + + width = len(sourceArray) + height = len(sourceArray[0]) + cwidth = len(cutterArray) + o.offset_image = numpy.full(shape=(width, height), fill_value=-10.0, dtype=numpy.double) + + t = time.time() + + m = int(cwidth / 2.0) + + if o.inverse: + sourceArray = -sourceArray + minz + comparearea = o.offset_image[m: width - cwidth + m, m:height - cwidth + m] + # i=0 + 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 + _offset_inner_loop(y1, y2, cutterArrayNan, cwidth, + sourceArray, width, height, comparearea) + await progress_async('offset depth image', int((y2 * 100) / comparearea.shape[1])) + o.offset_image[m: width - cwidth + m, m:height - cwidth + m] = comparearea + + print('\nOffset Image Time ' + str(time.time() - t)) + + o.update_offsetimage_tag = False + return o.offset_image
+ + + +
+[docs] +def dilateAr(ar, cycles): + """Dilate a binary array using a specified number of cycles. + + This function performs a dilation operation on a 2D binary array. For + each cycle, it updates the array by applying a logical OR operation + between the current array and its neighboring elements. The dilation + effect expands the boundaries of the foreground (True) pixels in the + binary array. + + Args: + ar (numpy.ndarray): A 2D binary array (numpy array) where + dilation will be applied. + cycles (int): The number of dilation cycles to perform. + + Returns: + None: The function modifies the input array in place and does not + return a value. + """ + + for c in range(cycles): + ar[1:-1, :] = numpy.logical_or(ar[1:-1, :], ar[:-2, :]) + ar[:, 1:-1] = numpy.logical_or(ar[:, 1:-1], ar[:, :-2])
+ + + +
+[docs] +def getOffsetImageCavities(o, i): # for pencil operation mainly + """Detects areas in the offset image which are 'cavities' due to curvature + changes. + + This function analyzes the input image to identify regions where the + curvature changes, indicating the presence of cavities. It computes + vertical and horizontal differences in pixel values to detect edges and + applies a threshold to filter out insignificant changes. The resulting + areas are then processed to remove any chunks that do not meet the + minimum criteria for cavity detection. The function returns a list of + valid chunks that represent the detected cavities. + + Args: + o: An object containing parameters and thresholds for the detection + process. + i (numpy.ndarray): A 2D array representing the image data to be analyzed. + + Returns: + list: A list of detected chunks representing the cavities in the image. + """ + # i=numpy.logical_xor(lastislice , islice) + 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: + + ar = numpy.logical_or(vertical, horizontal) + + 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 = getCachePath(o) + '_pencilthres.exr' + # numpysave(ar,iname)#save for comparison before + chunks = imageEdgeSearch_online(o, ar, i) + 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(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) + + return chunks
+ + + +# search edges for pencil strategy, another try. +
+[docs] +def imageEdgeSearch_online(o, ar, zimage): + """Search for edges in an image using a pencil strategy. + + This function implements an edge detection algorithm that simulates a + pencil-like movement across the image represented by a 2D array. It + identifies white pixels and builds chunks of points based on the + detected edges. The algorithm iteratively explores possible directions + to find and track the edges until a specified condition is met, such as + exhausting the available white pixels or reaching a maximum number of + tests. + + Args: + o (object): An object containing parameters such as min, max coordinates, cutter + diameter, + border width, and optimisation settings. + ar (numpy.ndarray): A 2D array representing the image where edge detection is to be + performed. + zimage (numpy.ndarray): A 2D array representing the z-coordinates corresponding to the image. + + Returns: + list: A list of chunks representing the detected edges in the image. + """ + + minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z + r = ceil((o.cutter_diameter/12)/o.optimisation.pixsize) # was commented + coef = 0.75 + maxarx = ar.shape[0] + maxary = ar.shape[1] + + directions = ((-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0)) + + indices = ar.nonzero() # first get white pixels + startpix = ar.sum() + totpix = startpix + chunk_builders = [] + xs = indices[0][0] + ys = indices[1][0] + nchunk = camPathChunkBuilder([(xs, ys, zimage[xs, ys])]) # startposition + dindex = 0 # index in the directions list + last_direction = directions[dindex] + test_direction = directions[dindex] + i = 0 + perc = 0 + itests = 0 + totaltests = 0 + maxtotaltests = startpix * 4 + + ar[xs, ys] = False + + while totpix > 0 and totaltests < maxtotaltests: # a ratio when the algorithm is allowed to end + + if perc != int(100 - 100 * totpix / startpix): + perc = int(100 - 100 * totpix / startpix) + progress('Pencil Path Searching', perc) + # progress('simulation ',int(100*i/l)) + success = False + testangulardistance = 0 # distance from initial direction in the list of direction + testleftright = False # test both sides from last vector + while not success: + xs = nchunk.points[-1][0] + test_direction[0] + ys = nchunk.points[-1][1] + test_direction[1] + + if xs > r and xs < ar.shape[0] - r and ys > r and ys < ar.shape[1] - r: + test = ar[xs, ys] + # print(test) + if test: + success = True + if success: + nchunk.points.append([xs, ys, zimage[xs, ys]]) + last_direction = test_direction + ar[xs, ys] = False + if 0: + print('Success') + print(xs, ys, testlength, testangle) + print(lastvect) + print(testvect) + print(itests) + else: + # nappend([xs,ys])#for debugging purpose + # ar.shape[0] + test_direction = last_direction + if testleftright: + testangulardistance = -testangulardistance + testleftright = False + else: + testangulardistance = -testangulardistance + testangulardistance += 1 # increment angle + testleftright = True + + if abs(testangulardistance) > 6: # /testlength + testangulardistance = 0 + indices = ar.nonzero() + totpix = len(indices[0]) + chunk_builders.append(nchunk) + if len(indices[0] > 0): + xs = indices[0][0] + ys = indices[1][0] + nchunk = camPathChunkBuilder([(xs, ys, zimage[xs, ys])]) # startposition + + ar[xs, ys] = False + else: + nchunk = camPathChunkBuilder([]) + + test_direction = directions[3] + last_direction = directions[3] + success = True + itests = 0 + # print('reset') + if len(nchunk.points) > 0: + if nchunk.points[-1][0] + test_direction[0] < r: + testvect.x = r + if nchunk.points[-1][1] + test_direction[1] < r: + testvect.y = r + if nchunk.points[-1][0] + test_direction[0] > maxarx - r: + testvect.x = maxarx - r + if nchunk.points[-1][1] + test_direction[1] > maxary - r: + testvect.y = maxary - r + + dindexmod = dindex + testangulardistance + while dindexmod < 0: + dindexmod += len(directions) + while dindexmod > len(directions): + dindexmod -= len(directions) + + test_direction = directions[dindexmod] + if 0: + print(xs, ys, test_direction, last_direction, testangulardistance) + print(totpix) + itests += 1 + totaltests += 1 + + i += 1 + if i % 100 == 0: + # print('100 succesfull tests done') + totpix = ar.sum() + # print(totpix) + # print(totaltests) + i = 0 + chunk_builders.append(nchunk) + for ch in chunk_builders: + ch = ch.points + for i in range(0, len(ch)): + ch[i] = ((ch[i][0] + coef - o.borderwidth) * o.optimisation.pixsize + minx, + (ch[i][1] + coef - o.borderwidth) * o.optimisation.pixsize + miny, ch[i][2]) + return [c.to_chunk() for c in chunk_builders]
+ + + +
+[docs] +async def crazyPath(o): + """Execute a greedy adaptive algorithm for path planning. + + This function prepares an area based on the provided object `o`, + calculates the dimensions of the area, and initializes a mill image and + cutter array. The dimensions are determined by the maximum and minimum + coordinates of the object, adjusted by the simulation detail and border + width. The function is currently a stub and requires further + implementation. + + Args: + o (object): An object containing properties such as max, min, optimisation, and + borderwidth. + + Returns: + None: This function does not return a value. + """ + + # TODO: try to do something with this stuff, it's just a stub. It should be a greedy adaptive algorithm. + # started another thing below. + await prepareArea(o) + sx = o.max.x - o.min.x + sy = o.max.y - o.min.y + + resx = ceil(sx / o.optimisation.simulation_detail) + 2 * o.borderwidth + resy = ceil(sy / o.optimisation.simulation_detail) + 2 * o.borderwidth + + o.millimage = numpy.full(shape=(resx, resy), fill_value=0., dtype=numpy.float) + # getting inverted cutter + o.cutterArray = -getCutterArray(o, o.optimisation.simulation_detail)
+ + + +
+[docs] +def buildStroke(start, end, cutterArray): + """Build a stroke array based on start and end points. + + This function generates a 2D stroke array that represents a stroke from + a starting point to an ending point. It calculates the length of the + stroke and creates a grid that is filled based on the positions defined + by the start and end coordinates. The function uses a cutter array to + determine how the stroke interacts with the grid. + + Args: + start (tuple): A tuple representing the starting coordinates (x, y, z). + end (tuple): A tuple representing the ending coordinates (x, y, z). + cutterArray: An object that contains size information used to modify + the stroke array. + + Returns: + numpy.ndarray: A 2D array representing the stroke, filled with + calculated values based on the input parameters. + """ + + strokelength = max(abs(end[0] - start[0]), abs(end[1] - start[1])) + size_x = abs(end[0] - start[0]) + cutterArray.size[0] + size_y = abs(end[1] - start[1]) + cutterArray.size[0] + r = cutterArray.size[0] / 2 + + strokeArray = numpy.full(shape=(size_x, size_y), fill_value=-10.0, dtype=numpy.float) + samplesx = numpy.round(numpy.linspace(start[0], end[0], strokelength)) + samplesy = numpy.round(numpy.linspace(start[1], end[1], strokelength)) + samplesz = numpy.round(numpy.linspace(start[2], end[2], strokelength)) + + for i in range(0, len(strokelength)): + strokeArray[samplesx[i] - r:samplesx[i] + r, samplesy[i] - r:samplesy[i] + r] = numpy.maximum( + strokeArray[samplesx[i] - r:samplesx[i] + r, samplesy[i] - r:samplesy[i] + r], cutterArray + samplesz[i]) + return strokeArray
+ + + +
+[docs] +def testStroke(): + pass
+ + + +
+[docs] +def applyStroke(): + pass
+ + + +
+[docs] +def testStrokeBinary(img, stroke): + pass # buildstroke()
+ + + +
+[docs] +def crazyStrokeImage(o): + """Generate a toolpath for a milling operation using a crazy stroke + strategy. + + This function computes a path for a milling cutter based on the provided + parameters and the offset image. It utilizes a circular cutter + representation and evaluates potential cutting positions based on + various thresholds. The algorithm iteratively tests different angles and + lengths for the cutter's movement until the desired cutting area is + achieved or the maximum number of tests is reached. + + Args: + o (object): An object containing parameters such as cutter diameter, + optimization settings, movement type, and thresholds for + determining cutting effectiveness. + + Returns: + list: A list of chunks representing the computed toolpath for the milling + operation. + """ + + # this surprisingly works, and can be used as a basis for something similar to adaptive milling strategy. + minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z + + # ceil((o.cutter_diameter/12)/o.optimisation.pixsize) + r = int((o.cutter_diameter / 2.0) / o.optimisation.pixsize) + d = 2 * r + coef = 0.75 + + ar = o.offset_image.copy() + maxarx = ar.shape[0] + maxary = ar.shape[1] + + cutterArray = getCircleBinary(r) + cutterArrayNegative = -cutterArray + + cutterimagepix = cutterArray.sum() + # a threshold which says if it is valuable to cut in a direction + satisfypix = cutterimagepix * o.crazy_threshold1 + toomuchpix = cutterimagepix * o.crazy_threshold2 + indices = ar.nonzero() # first get white pixels + startpix = ar.sum() # + totpix = startpix + chunk_builders = [] + xs = indices[0][0] - r + if xs < r: + xs = r + ys = indices[1][0] - r + if ys < r: + ys = r + nchunk = camPathChunkBuilder([(xs, ys)]) # startposition + print(indices) + print(indices[0][0], indices[1][0]) + # vector is 3d, blender somehow doesn't rotate 2d vectors with angles. + lastvect = Vector((r, 0, 0)) + # multiply *2 not to get values <1 pixel + testvect = lastvect.normalized() * r / 2.0 + rot = Euler((0, 0, 1)) + i = 0 + perc = 0 + itests = 0 + totaltests = 0 + maxtests = 500 + maxtotaltests = 1000000 + + print(xs, ys, indices[0][0], indices[1][0], r) + ar[xs - r:xs - r + d, ys - r:ys - r + d] = ar[xs - + r:xs - r + d, ys - r:ys - r + d] * cutterArrayNegative + # range for angle of toolpath vector versus material vector + anglerange = [-pi, pi] + testangleinit = 0 + angleincrement = 0.05 + if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW') or ( + o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW'): + anglerange = [-pi, 0] + testangleinit = 1 + angleincrement = -angleincrement + elif (o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CCW') or ( + o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CW'): + anglerange = [0, pi] + testangleinit = -1 + angleincrement = angleincrement + while totpix > 0 and totaltests < maxtotaltests: # a ratio when the algorithm is allowed to end + + success = False + # define a vector which gets varied throughout the testing, growing and growing angle to sides. + testangle = testangleinit + testleftright = False + testlength = r + + while not success: + xs = nchunk.points[-1][0] + int(testvect.x) + ys = nchunk.points[-1][1] + int(testvect.y) + if xs > r + 1 and xs < ar.shape[0] - r - 1 and ys > r + 1 and ys < ar.shape[1] - r - 1: + testar = ar[xs - r:xs - r + d, ys - r:ys - r + d] * cutterArray + if 0: + print('test') + print(testar.sum(), satisfypix) + print(xs, ys, testlength, testangle) + print(lastvect) + print(testvect) + print(totpix) + + eatpix = testar.sum() + cindices = testar.nonzero() + cx = cindices[0].sum() / eatpix + cy = cindices[1].sum() / eatpix + v = Vector((cx - r, cy - r)) + angle = testvect.to_2d().angle_signed(v) + # this could be righthanded milling? lets see :) + if anglerange[0] < angle < anglerange[1]: + if toomuchpix > eatpix > satisfypix: + success = True + if success: + nchunk.points.append([xs, ys]) + lastvect = testvect + ar[xs - r:xs - r + d, ys - r:ys - r + d] = ar[xs - + r:xs - r + d, ys - r:ys - r + d] * (-cutterArray) + totpix -= eatpix + itests = 0 + if 0: + print('success') + print(xs, ys, testlength, testangle) + print(lastvect) + print(testvect) + print(itests) + else: + # TODO: after all angles were tested into material higher than toomuchpix, it should cancel, + # otherwise there is no problem with long travel in free space..... + # TODO:the testing should start not from the same angle as lastvector, but more towards material. + # So values closer to toomuchpix are obtained rather than satisfypix + testvect = lastvect.normalized() * testlength + right = True + if testangleinit == 0: # meander + if testleftright: + testangle = -testangle + testleftright = False + else: + testangle = abs(testangle) + angleincrement # increment angle + testleftright = True + else: # climb/conv. + testangle += angleincrement + + if abs(testangle) > o.crazy_threshold3: # /testlength + testangle = testangleinit + testlength += r / 4.0 + if nchunk.points[-1][0] + testvect.x < r: + testvect.x = r + if nchunk.points[-1][1] + testvect.y < r: + testvect.y = r + if nchunk.points[-1][0] + testvect.x > maxarx - r: + testvect.x = maxarx - r + if nchunk.points[-1][1] + testvect.y > maxary - r: + testvect.y = maxary - r + + rot.z = testangle + + testvect.rotate(rot) + # if 0: + # print(xs, ys, testlength, testangle) + # print(lastvect) + # print(testvect) + # print(totpix) + itests += 1 + totaltests += 1 + + if itests > maxtests or testlength > r * 1.5: + # print('resetting location') + indices = ar.nonzero() + chunk_builders.append(nchunk) + if len(indices[0]) > 0: + xs = indices[0][0] - r + if xs < r: + xs = r + ys = indices[1][0] - r + if ys < r: + ys = r + nchunk = camPathChunkBuilder([(xs, ys)]) # startposition + ar[xs - r:xs - r + d, ys - r:ys - r + d] = ar[xs - r:xs - r + d, + ys - r:ys - r + d] * cutterArrayNegative + r = random.random() * 2 * pi + e = Euler((0, 0, r)) + testvect = lastvect.normalized() * 4 # multiply *2 not to get values <1 pixel + testvect.rotate(e) + lastvect = testvect.copy() + success = True + itests = 0 + i += 1 + if i % 100 == 0: + print('100 succesfull tests done') + totpix = ar.sum() + print(totpix) + print(totaltests) + i = 0 + chunk_builders.append(nchunk) + for ch in chunk_builders: + ch = ch.points + for i in range(0, len(ch)): + ch[i] = ((ch[i][0] + coef - o.borderwidth) * o.optimisation.pixsize + minx, + (ch[i][1] + coef - o.borderwidth) * o.optimisation.pixsize + miny, 0) + return [c.to_chunk() for c in chunk_builders]
+ + + +
+[docs] +def crazyStrokeImageBinary(o, ar, avoidar): + """Perform a milling operation using a binary image representation. + + This function implements a strategy for milling by navigating through a + binary image. It starts from a defined point and attempts to move in + various directions, evaluating the cutter load to determine the + appropriate path. The algorithm continues until it either exhausts the + available pixels to cut or reaches a predefined limit on the number of + tests. The function modifies the input array to represent the areas that + have been milled and returns the generated path as a list of chunks. + + Args: + o (object): An object containing parameters for the milling operation, including + cutter diameter, thresholds, and movement type. + ar (numpy.ndarray): A 2D binary array representing the image to be milled. + avoidar (numpy.ndarray): A 2D binary array indicating areas to avoid during milling. + + Returns: + list: A list of chunks representing the path taken during the milling + operation. + """ + + # this surprisingly works, and can be used as a basis for something similar to adaptive milling strategy. + # works like this: + # start 'somewhere' + # try to go in various directions. + # if somewhere the cutter load is appropriate - it is correct magnitude and side, continue in that directon + # try to continue straight or around that, looking + minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z + # TODO this should be somewhere else, but here it is now to get at least some ambient for start of the operation. + ar[:o.borderwidth, :] = 0 + ar[-o.borderwidth:, :] = 0 + ar[:, :o.borderwidth] = 0 + ar[:, -o.borderwidth:] = 0 + + # ceil((o.cutter_diameter/12)/o.optimisation.pixsize) + r = int((o.cutter_diameter / 2.0) / o.optimisation.pixsize) + d = 2 * r + coef = 0.75 + maxarx = ar.shape[0] + maxary = ar.shape[1] + + cutterArray = getCircleBinary(r) + cutterArrayNegative = -cutterArray + + cutterimagepix = cutterArray.sum() + + anglelimit = o.crazy_threshold3 + # a threshold which says if it is valuable to cut in a direction + satisfypix = cutterimagepix * o.crazy_threshold1 + toomuchpix = cutterimagepix * o.crazy_threshold2 # same, but upper limit + # (satisfypix+toomuchpix)/2.0# the ideal eating ratio + optimalpix = cutterimagepix * o.crazy_threshold5 + indices = ar.nonzero() # first get white pixels + + startpix = ar.sum() # + totpix = startpix + + chunk_builders = [] + # try to find starting point here + + xs = indices[0][0] - r / 2 + if xs < r: + xs = r + ys = indices[1][0] - r + if ys < r: + ys = r + + nchunk = camPathChunkBuilder([(xs, ys)]) # startposition + print(indices) + print(indices[0][0], indices[1][0]) + # vector is 3d, blender somehow doesn't rotate 2d vectors with angles. + lastvect = Vector((r, 0, 0)) + # multiply *2 not to get values <1 pixel + testvect = lastvect.normalized() * r / 4.0 + rot = Euler((0, 0, 1)) + i = 0 + itests = 0 + totaltests = 0 + maxtests = 2000 + maxtotaltests = 20000 # 1000000 + + margin = 0 + + # print(xs,ys,indices[0][0],indices[1][0],r) + ar[xs - r:xs + r, ys - r:ys + r] = ar[xs - r:xs + r, ys - r:ys + r] * cutterArrayNegative + anglerange = [-pi, pi] + # range for angle of toolpath vector versus material vector - + # probably direction negative to the force applied on cutter by material. + testangleinit = 0 + angleincrement = o.crazy_threshold4 + + if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW') or ( + o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW'): + anglerange = [-pi, 0] + testangleinit = anglelimit + angleincrement = -angleincrement + elif (o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CCW') or ( + o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CW'): + anglerange = [0, pi] + testangleinit = -anglelimit + angleincrement = angleincrement + + while totpix > 0 and totaltests < maxtotaltests: # a ratio when the algorithm is allowed to end + + success = False + # define a vector which gets varied throughout the testing, growing and growing angle to sides. + testangle = testangleinit + testleftright = False + testlength = r + + foundsolutions = [] + while not success: + xs = int(nchunk.points[-1][0] + testvect.x) + ys = int(nchunk.points[-1][1] + testvect.y) + # print(xs,ys,ar.shape) + # print(d) + if xs > r + margin and xs < ar.shape[0] - r - margin and ys > r + margin and ys < ar.shape[1] - r - margin: + # avoidtest=avoidar[xs-r:xs+r,ys-r:ys+r]*cutterArray + if not avoidar[xs, ys]: + testar = ar[xs - r:xs + r, ys - r:ys + r] * cutterArray + eatpix = testar.sum() + cindices = testar.nonzero() + cx = cindices[0].sum() / eatpix + cy = cindices[1].sum() / eatpix + v = Vector((cx - r, cy - r)) + # print(testvect.length,testvect) + + if v.length != 0: + angle = testvect.to_2d().angle_signed(v) + if (anglerange[0] < angle < anglerange[1] and toomuchpix > eatpix > satisfypix) or ( + eatpix > 0 and totpix < startpix * 0.025): + # this could be righthanded milling? + # lets see :) + # print(xs,ys,angle) + foundsolutions.append([testvect.copy(), eatpix]) + # or totpix < startpix*0.025: + if len(foundsolutions) >= 10: + success = True + itests += 1 + totaltests += 1 + + if success: + # fist, try to inter/extrapolate the recieved results. + closest = 100000000 + # print('evaluate') + for s in foundsolutions: + # print(abs(s[1]-optimalpix),optimalpix,abs(s[1])) + if abs(s[1] - optimalpix) < closest: + bestsolution = s + closest = abs(s[1] - optimalpix) + # print('closest',closest) + + # v1#+(v2-v1)*ratio#rewriting with interpolated vect. + testvect = bestsolution[0] + xs = int(nchunk.points[-1][0] + testvect.x) + ys = int(nchunk.points[-1][1] + testvect.y) + nchunk.points.append([xs, ys]) + lastvect = testvect + + ar[xs - r:xs + r, ys - r:ys + r] = ar[xs - + r:xs + r, ys - r:ys + r] * cutterArrayNegative + totpix -= bestsolution[1] + itests = 0 + # if 0: + # print('success') + # print(testar.sum(), satisfypix, toomuchpix) + # print(xs, ys, testlength, testangle) + # print(lastvect) + # print(testvect) + # print(itests) + totaltests = 0 + else: + # TODO: after all angles were tested into material higher than toomuchpix, + # it should cancel, otherwise there is no problem with long travel in free space..... + # TODO:the testing should start not from the same angle as lastvector, but more towards material. + # So values closer to toomuchpix are obtained rather than satisfypix + testvect = lastvect.normalized() * testlength + + if testangleinit == 0: # meander + if testleftright: + testangle = -testangle - angleincrement + testleftright = False + else: + testangle = -testangle + angleincrement # increment angle + testleftright = True + else: # climb/conv. + testangle += angleincrement + + if (abs(testangle) > o.crazy_threshold3 and len(nchunk.points) > 1) or abs( + testangle) > 2 * pi: # /testlength + testangle = testangleinit + testlength += r / 4.0 + # print(itests,testlength) + if nchunk.points[-1][0] + testvect.x < r: + testvect.x = r + if nchunk.points[-1][1] + testvect.y < r: + testvect.y = r + if nchunk.points[-1][0] + testvect.x > maxarx - r: + testvect.x = maxarx - r + if nchunk.points[-1][1] + testvect.y > maxary - r: + testvect.y = maxary - r + + rot.z = testangle + # if abs(testvect.normalized().y<-0.99): + # print(testvect,rot.z) + testvect.rotate(rot) + + # if 0: + # print(xs, ys, testlength, testangle) + # print(lastvect) + # print(testvect) + # print(totpix) + if itests > maxtests or testlength > r * 1.5: + # if len(foundsolutions)>0: + + # print('resetting location') + # print(testlength,r) + andar = numpy.logical_and(ar, numpy.logical_not(avoidar)) + indices = andar.nonzero() + if len(nchunk.points) > 1: + parentChildDist([nchunk], chunks, o, distance=r) + chunk_builders.append(nchunk) + + if totpix > startpix * 0.001: + found = False + ftests = 0 + while not found: + # look for next start point: + index = random.randint(0, len(indices[0]) - 1) + # print(index,len(indices[0])) + # print(indices[index]) + xs = indices[0][index] + ys = indices[1][index] + v = Vector((r - 1, 0, 0)) + randomrot = random.random() * 2 * pi + e = Euler((0, 0, randomrot)) + v.rotate(e) + xs += int(v.x) + ys += int(v.y) + if xs < r: + xs = r + if ys < r: + ys = r + if avoidar[xs, ys] == 0: + + # print(toomuchpix,ar[xs-r:xs-r+d,ys-r:ys-r+d].sum()*pi/4,satisfypix) + testarsum = ar[xs - r:xs - r + d, ys - r:ys - r + d].sum() * pi / 4 + if toomuchpix > testarsum > 0 or ( + totpix < startpix * 0.025): # 0 now instead of satisfypix + found = True + # print(xs,ys,indices[0][index],indices[1][index]) + + nchunk = camPathChunk([(xs, ys)]) # startposition + ar[xs - r:xs + r, ys - r:ys + r] = ar[xs - r:xs + r, + ys - r:ys + r] * cutterArrayNegative + # lastvect=Vector((r,0,0))#vector is 3d, + # blender somehow doesn't rotate 2d vectors with angles. + randomrot = random.random() * 2 * pi + e = Euler((0, 0, randomrot)) + testvect = lastvect.normalized() * 2 # multiply *2 not to get values <1 pixel + testvect.rotate(e) + lastvect = testvect.copy() + if ftests > 2000: + totpix = 0 # this quits the process now. + ftests += 1 + + success = True + itests = 0 + i += 1 + if i % 100 == 0: + print('100 succesfull tests done') + totpix = ar.sum() + print(totpix) + print(totaltests) + i = 0 + if len(nchunk.points) > 1: + parentChildDist([nchunk], chunks, o, distance=r) + chunk_builders.append(nchunk) + + for ch in chunk_builders: + ch = ch.points + for i in range(0, len(ch)): + ch[i] = ((ch[i][0] + coef - o.borderwidth) * o.optimisation.pixsize + minx, + (ch[i][1] + coef - o.borderwidth) * o.optimisation.pixsize + miny, o.minz) + + return [c.to_chunk for c in chunk_builders]
+ + + +
+[docs] +def imageToChunks(o, image, with_border=False): + """Convert an image into chunks based on detected edges. + + This function processes a given image to identify edges and convert them + into polychunks, which are essentially collections of connected edge + segments. It utilizes the properties of the input object `o` to + determine the boundaries and size of the chunks. The function can + optionally include borders in the edge detection process. The output is + a list of chunks that represent the detected polygons in the image. + + Args: + o (object): An object containing properties such as min, max, borderwidth, + and optimisation settings. + image (numpy.ndarray): A 2D array representing the image to be processed, + expected to be in a format compatible with uint8. + with_border (bool?): A flag indicating whether to include borders + in the edge detection. Defaults to False. + + Returns: + list: A list of chunks, where each chunk is represented as a collection of + points that outline the detected edges in the image. + """ + + t = time.time() + minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z + pixsize = o.optimisation.pixsize + + image = image.astype(numpy.uint8) + + # progress('detecting outline') + edges = [] + ar = image[:, :-1] - image[:, 1:] + + indices1 = ar.nonzero() + borderspread = 2 + # o.cutter_diameter/o.optimisation.pixsize#when the border was excluded precisely, sometimes it did remove some silhouette parts + r = o.borderwidth - borderspread + # to prevent outline of the border was 3 before and also (o.cutter_diameter/2)/pixsize+o.borderwidth + if with_border: + # print('border') + r = 0 + w = image.shape[0] + h = image.shape[1] + coef = 0.75 # compensates for imprecisions + for id in range(0, len(indices1[0])): + a = indices1[0][id] + b = indices1[1][id] + if r < a < w - r and r < b < h - r: + edges.append(((a - 1, b), (a, b))) + + ar = image[:-1, :] - image[1:, :] + indices2 = ar.nonzero() + for id in range(0, len(indices2[0])): + a = indices2[0][id] + b = indices2[1][id] + if r < a < w - r and r < b < h - r: + edges.append(((a, b - 1), (a, b))) + + polychunks = [] + # progress(len(edges)) + + d = {} + for e in edges: + d[e[0]] = [] + d[e[1]] = [] + for e in edges: + verts1 = d[e[0]] + verts2 = d[e[1]] + verts1.append(e[1]) + verts2.append(e[0]) + + if len(edges) > 0: + + ch = [edges[0][0], edges[0][1]] # first and his reference + + d[edges[0][0]].remove(edges[0][1]) + + i = 0 + specialcase = 0 + # progress('condensing outline') + while len( + d) > 0 and i < 20000000: + verts = d.get(ch[-1], []) + closed = False + # print(verts) + + if len(verts) <= 1: # this will be good for not closed loops...some time + closed = True + if len(verts) == 1: + ch.append(verts[0]) + verts.remove(verts[0]) + elif len(verts) >= 3: + specialcase += 1 + v1 = ch[-1] + v2 = ch[-2] + white = image[v1[0], v1[1]] + comesfromtop = v1[1] < v2[1] + comesfrombottom = v1[1] > v2[1] + comesfromleft = v1[0] > v2[0] + comesfromright = v1[0] < v2[0] + take = False + for v in verts: + if v[0] == ch[-2][0] and v[1] == ch[-2][1]: + pass + verts.remove(v) + + if not take: + if (not white and comesfromtop) or (white and comesfrombottom): # goes right + if v1[0] + 0.5 < v[0]: + take = True + elif (not white and comesfrombottom) or (white and comesfromtop): # goes left + if v1[0] > v[0] + 0.5: + take = True + elif (not white and comesfromleft) or (white and comesfromright): # goes down + if v1[1] > v[1] + 0.5: + take = True + elif (not white and comesfromright) or (white and comesfromleft): # goes up + if v1[1] + 0.5 < v[1]: + take = True + if take: + ch.append(v) + verts.remove(v) + + else: # here it has to be 2 always + done = False + for vi in range(len(verts) - 1, -1, -1): + if not done: + v = verts[vi] + if v[0] == ch[-2][0] and v[1] == ch[-2][1]: + pass + verts.remove(v) + else: + + ch.append(v) + done = True + verts.remove(v) + # or len(verts)<=1: + if v[0] == ch[0][0] and v[1] == ch[0][1]: + closed = True + + if closed: + polychunks.append(ch) + for si, s in enumerate(ch): + # print(si) + if si > 0: # first one was popped + if d.get(s, None) is not None and len(d[s]) == 0: + # this makes the case much less probable, but i think not impossible + d.pop(s) + if len(d) > 0: + newch = False + while not newch: + v1 = d.popitem() + if len(v1[1]) > 0: + ch = [v1[0], v1[1][0]] + newch = True + + # print(' la problema grandiosa') + i += 1 + if i % 10000 == 0: + print(len(ch)) + # print(polychunks) + print(i) + + vecchunks = [] + + for ch in polychunks: + vecchunk = [] + vecchunks.append(vecchunk) + for i in range(0, len(ch)): + ch[i] = ((ch[i][0] + coef - o.borderwidth) * pixsize + minx, + (ch[i][1] + coef - o.borderwidth) * pixsize + miny, 0) + vecchunk.append(Vector(ch[i])) + # print('optimizing outline') + + # print('directsimplify') + reduxratio = 1.25 # was 1.25 + soptions = ['distance', 'distance', o.optimisation.pixsize * + reduxratio, 5, o.optimisation.pixsize * reduxratio] + nchunks = [] + for i, ch in enumerate(vecchunks): + + s = curve_simplify.simplify_RDP(ch, soptions) + # print(s) + nch = camPathChunkBuilder([]) + for i in range(0, len(s)): + nch.points.append((ch[s[i]].x, ch[s[i]].y)) + + if len(nch.points) > 2: + nchunks.append(nch.to_chunk()) + + return nchunks + else: + return []
+ + + +
+[docs] +def imageToShapely(o, i, with_border=False): + """Convert an image to Shapely polygons. + + This function takes an image and converts it into a series of Shapely + polygon objects. It first processes the image into chunks and then + transforms those chunks into polygon geometries. The `with_border` + parameter allows for the inclusion of borders in the resulting polygons. + + Args: + o: The input image to be processed. + i: Additional input parameters for processing the image. + with_border (bool): A flag indicating whether to include + borders in the resulting polygons. Defaults to False. + + Returns: + list: A list of Shapely polygon objects created from the + image chunks. + """ + + polychunks = imageToChunks(o, i, with_border) + polys = chunksToShapely(polychunks) + + return polys
+ + + +
+[docs] +def getSampleImage(s, sarray, minz): + """Get a sample image value from a 2D array based on given coordinates. + + This function retrieves a value from a 2D array by performing bilinear + interpolation based on the provided coordinates. It checks if the + coordinates are within the bounds of the array and calculates the + interpolated value accordingly. If the coordinates are out of bounds, it + returns -10. + + Args: + s (tuple): A tuple containing the x and y coordinates (float). + sarray (numpy.ndarray): A 2D array from which to sample the image values. + minz (float): A minimum threshold value (not used in the current implementation). + + Returns: + float: The interpolated value from the 2D array, or -10 if the coordinates are + out of bounds. + """ + + x = s[0] + y = s[1] + if (x < 0 or x > len(sarray) - 1) or (y < 0 or y > len(sarray[0]) - 1): + return -10 + else: + minx = floor(x) + maxx = minx + 1 + miny = floor(y) + maxy = miny + 1 + s1a = sarray[minx, miny] + s2a = sarray[maxx, miny] + s1b = sarray[minx, maxy] + s2b = sarray[maxx, maxy] + # s1a = sarray.item(minx, miny) # most optimal access to array so far + # s2a = sarray.item(maxx, miny) + # s1b = sarray.item(minx, maxy) + # s2b = sarray.item(maxx, maxy) + + sa = s1a * (maxx - x) + s2a * (x - minx) + sb = s1b * (maxx - x) + s2b * (x - minx) + z = sa * (maxy - y) + sb * (y - miny) + return z
+ + + +
+[docs] +def getResolution(o): + """Calculate the resolution based on the dimensions of an object. + + This function computes the resolution in both x and y directions by + determining the width and height of the object, adjusting for pixel size + and border width. The resolution is calculated by dividing the + dimensions by the pixel size and adding twice the border width to each + dimension. + + Args: + o (object): An object with attributes `max`, `min`, `optimisation`, + and `borderwidth`. The `max` and `min` attributes should + have `x` and `y` properties representing the coordinates, + while `optimisation` should have a `pixsize` attribute. + + Returns: + None: This function does not return a value; it performs calculations + to determine resolution. + """ + + sx = o.max.x - o.min.x + sy = o.max.y - o.min.y + + resx = ceil(sx / o.optimisation.pixsize) + 2 * o.borderwidth + resy = ceil(sy / o.optimisation.pixsize) + 2 * o.borderwidth
+ + +# this basically renders blender zbuffer and makes it accessible by saving & loading it again. +# that's because blender doesn't allow accessing pixels in render :( + + +
+[docs] +def _backup_render_settings(pairs): + """Backup the render settings of Blender objects. + + This function iterates over a list of pairs consisting of owners and + their corresponding structure names. It retrieves the properties of each + structure and stores them in a backup list. If the structure is a + Blender object, it saves all its properties that do not start with an + underscore. For simple values, it directly appends them to the + properties list. This is useful for preserving render settings that + Blender does not allow direct access to during rendering. + + Args: + pairs (list): A list of tuples where each tuple contains an owner and a structure + name. + + Returns: + list: A list containing the backed-up properties of the specified Blender + objects. + """ + + properties = [] + for owner, struct_name in pairs: + obj = getattr(owner, struct_name) + if isinstance(obj, bpy.types.bpy_struct): + # structure, backup all properties + obj_value = {} + for k in dir(obj): + if not k.startswith("_"): + obj_value[k] = getattr(obj, k) + properties.append(obj_value) + else: + # simple value + properties.append(obj)
+ + + +
+[docs] +def _restore_render_settings(pairs, properties): + """Restore render settings for a given owner and structure. + + This function takes pairs of owners and structure names along with their + corresponding properties. It iterates through these pairs, retrieves the + appropriate object from the owner using the structure name, and sets the + properties on the object. If the object is an instance of + `bpy.types.bpy_struct`, it updates its attributes; otherwise, it + directly sets the value on the owner. + + Args: + pairs (list): A list of tuples where each tuple contains an owner and a structure + name. + properties (list): A list of dictionaries containing property names and their corresponding + values. + """ + + for (owner, struct_name), obj_value in zip(pairs, properties): + obj = getattr(owner, struct_name) + if isinstance(obj, bpy.types.bpy_struct): + for k, v in obj_value.items(): + setattr(obj, k, v) + else: + setattr(owner, struct_name, obj_value)
+ + + +
+[docs] +def renderSampleImage(o): + """Render a sample image based on the provided object settings. + + This function generates a Z-buffer image for a given object by either + rendering it from scratch or loading an existing image from the cache. + It handles different geometry sources and applies various settings to + ensure the image is rendered correctly. The function also manages backup + and restoration of render settings to maintain the scene's integrity + during the rendering process. + + Args: + o (object): An object containing various properties and settings + + Returns: + numpy.ndarray: The generated or loaded Z-buffer image as a NumPy array. + """ + + t = time.time() + progress('Getting Z-Buffer') + # print(o.zbuffer_image) + o.update_offsetimage_tag = True + if o.geometry_source == 'OBJECT' or o.geometry_source == 'COLLECTION': + pixsize = o.optimisation.pixsize + + sx = o.max.x - o.min.x + sy = o.max.y - o.min.y + + 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... + # print('has zbuffer') + return o.zbuffer_image + # ###setup image name + iname = getCachePath(o) + '_z.exr' + if not o.update_zbufferimage_tag: + try: + i = bpy.data.images.load(iname) + if i.size[0] != resx or i.size[1] != resy: + print("Z buffer size changed:", i.size, resx, resy) + o.update_zbufferimage_tag = True + + except: + + o.update_zbufferimage_tag = True + if o.update_zbufferimage_tag: + s = bpy.context.scene + s.use_nodes = True + vl = bpy.context.view_layer + n = s.node_tree + r = s.render + + SETTINGS_TO_BACKUP = [ + (s.render, "resolution_x"), + (s.render, "resolution_x"), + (s.cycles, "samples"), + (s, "camera"), + (vl, "samples"), + (vl.cycles, "use_denoising"), + (s.world, "mist_settings"), + (r, "resolution_x"), + (r, "resolution_y"), + (r, "resolution_percentage"), + ] + for ob in s.objects: + SETTINGS_TO_BACKUP.append((ob, "hide_render")) + backup_settings = None + try: + backup_settings = _backup_render_settings(SETTINGS_TO_BACKUP) + # prepare nodes first + r.resolution_x = resx + r.resolution_y = resy + # use cycles for everything because + # it renders okay on github actions + r.engine = 'CYCLES' + s.cycles.samples = 1 + vl.samples = 1 + vl.cycles.use_denoising = False + + n.links.clear() + n.nodes.clear() + node_in = n.nodes.new('CompositorNodeRLayers') + s.view_layers[node_in.layer].use_pass_mist = True + mist_settings = s.world.mist_settings + s.world.mist_settings.depth = 10.0 + s.world.mist_settings.start = 0 + s.world.mist_settings.falloff = "LINEAR" + s.world.mist_settings.height = 0 + s.world.mist_settings.intensity = 0 + node_out = n.nodes.new("CompositorNodeOutputFile") + node_out.base_path = os.path.dirname(iname) + node_out.format.file_format = 'OPEN_EXR' + node_out.format.color_mode = 'RGB' + node_out.format.color_depth = '32' + node_out.file_slots.new(os.path.basename(iname)) + n.links.new(node_in.outputs[node_in.outputs.find('Mist')], node_out.inputs[-1]) + ################### + + # resize operation image + o.offset_image = numpy.full(shape=(resx, resy), fill_value=-10, dtype=numpy.double) + + # various settings for faster render + r.resolution_percentage = 100 + + # add a new camera settings + bpy.ops.object.camera_add(align='WORLD', enter_editmode=False, location=(0, 0, 0), + rotation=(0, 0, 0)) + camera = bpy.context.active_object + bpy.context.scene.camera = camera + + camera.data.type = 'ORTHO' + camera.data.ortho_scale = max( + resx * o.optimisation.pixsize, resy * o.optimisation.pixsize) + camera.location = (o.min.x + sx / 2, o.min.y + sy / 2, 1) + camera.rotation_euler = (0, 0, 0) + camera.data.clip_end = 10.0 + # if not o.render_all:#removed in 0.3 + + h = [] + + # ob=bpy.data.objects[o.object_name] + for ob in s.objects: + ob.hide_render = True + for ob in o.objects: + ob.hide_render = False + + bpy.ops.render.render() + + n.nodes.remove(node_out) + n.nodes.remove(node_in) + camera.select_set(True) + bpy.ops.object.delete() + + os.replace(iname+"%04d.exr" % (s.frame_current), iname) + finally: + if backup_settings is not None: + _restore_render_settings(SETTINGS_TO_BACKUP, backup_settings) + else: + print("Failed to Backup Scene Settings") + + i = bpy.data.images.load(iname) + bpy.context.scene.render.engine = 'CNCCAM_RENDER' + + a = imagetonumpy(i) + a = 10.0 * a + a = 1.0 - a + o.zbuffer_image = a + o.update_zbufferimage_tag = False + + else: + i = bpy.data.images[o.source_image_name] + if o.source_image_crop: + sx = int(i.size[0] * o.source_image_crop_start_x / 100.0) + ex = int(i.size[0] * o.source_image_crop_end_x / 100.0) + sy = int(i.size[1] * o.source_image_crop_start_y / 100.0) + ey = int(i.size[1] * o.source_image_crop_end_y / 100.0) + else: + sx = 0 + ex = i.size[0] + sy = 0 + ey = i.size[1] + + #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] + progress('Pixel Size in the Image Source', o.optimisation.pixsize) + + rawimage = imagetonumpy(i) + maxa = numpy.max(rawimage) + mina = numpy.min(rawimage) + neg = o.source_image_scale_z < 0 + # waterline strategy needs image border to have ok ambient. + if o.strategy == 'WATERLINE': + a = numpy.full(shape=( + 2 * o.borderwidth + i.size[0], 2 * o.borderwidth + i.size[1]), fill_value=1-neg, dtype=numpy.float) + else: # other operations like parallel need to reach the border + a = numpy.full(shape=( + 2 * o.borderwidth + i.size[0], 2 * o.borderwidth + i.size[1]), fill_value=neg, dtype=numpy.float) + # 2*o.borderwidth + a[o.borderwidth:-o.borderwidth, o.borderwidth:-o.borderwidth] = rawimage + a = a[sx:ex + o.borderwidth * 2, sy:ey + o.borderwidth * 2] + + if o.source_image_scale_z < 0: + # negative images place themselves under the 0 plane by inverting through scale multiplication + # first, put the image down, se we know the image minimum is on 0 + a = (a - mina) + a *= o.source_image_scale_z + + else: # place positive images under 0 plane, this is logical + # first, put the image down, se we know the image minimum is on 0 + a = (a - mina) + a *= o.source_image_scale_z + a -= (maxa - mina) * o.source_image_scale_z + + a += o.source_image_offset.z # after that, image gets offset. + + o.minz = numpy.min(a) # TODO: I really don't know why this is here... + o.min.z = numpy.min(a) + print('min z ', o.min.z) + print('max z ', o.max.z) + print('max image ', numpy.max(a)) + print('min image ', numpy.min(a)) + o.zbuffer_image = a + # progress('got z buffer also with conversion in:') + progress(time.time() - t) + + # progress(a) + o.update_zbufferimage_tag = False + return o.zbuffer_image
+ + + +# return numpy.array([]) + +
+[docs] +async def prepareArea(o): + """Prepare the area for rendering by processing the offset image. + + This function handles the preparation of the area by rendering a sample + image and managing the offset image based on the provided options. It + checks if the offset image needs to be updated and loads it if + necessary. If the inverse option is set, it adjusts the samples + accordingly before calling the offsetArea function. Finally, it saves + the processed offset image. + + Args: + o (object): An object containing various properties and methods + required for preparing the area, including flags for + updating the offset image and rendering options. + """ + + # if not o.use_exact: + renderSampleImage(o) + samples = o.zbuffer_image + + iname = getCachePath(o) + '_off.exr' + + if not o.update_offsetimage_tag: + progress('Loading Offset Image') + try: + o.offset_image = imagetonumpy(bpy.data.images.load(iname)) + + except: + o.update_offsetimage_tag = True + + if o.update_offsetimage_tag: + if o.inverse: + samples = numpy.maximum(samples, o.min.z - 0.00001) + await offsetArea(o, samples) + numpysave(o.offset_image, iname)
+ + + +
+[docs] +def getCutterArray(operation, pixsize): + """Generate a cutter array based on the specified operation and pixel size. + + This function calculates a 2D array representing the cutter shape based + on the cutter type defined in the operation object. The cutter can be of + various types such as 'END', 'BALL', 'VCARVE', 'CYLCONE', 'BALLCONE', or + 'CUSTOM'. The function uses geometric calculations to fill the array + with appropriate values based on the cutter's dimensions and properties. + + Args: + operation (object): An object containing properties of the cutter, including + cutter type, diameter, tip angle, and other relevant parameters. + pixsize (float): The size of each pixel in the generated cutter array. + + Returns: + numpy.ndarray: A 2D array filled with values representing the cutter shape. + """ + + 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
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/involute_gear.html b/_modules/cam/involute_gear.html new file mode 100644 index 000000000..e9d35fee3 --- /dev/null +++ b/_modules/cam/involute_gear.html @@ -0,0 +1,708 @@ + + + + + + + + + + cam.involute_gear — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.involute_gear

+"""CNC CAM 'involute_gear.py' Ported by Alain Pelletier Jan 2022
+
+from:
+Public Domain Parametric Involute Spur Gear (and involute helical gear and involute rack)
+version 1.1
+by Leemon Baird, 2011, Leemon@Leemon.com
+http:www.thingiverse.com/thing:5505
+
+This file is public domain.  Use it for any purpose, including commercial
+applications.  Attribution would be nice, but is not required.  There is
+no warranty of any kind, including its correctness, usefulness, or safety.
+
+This is parameterized involute spur (or helical) gear.  It is much simpler and less powerful than
+others on Thingiverse.  But it is public domain.  I implemented it from scratch from the
+descriptions and equations on Wikipedia and the web, using Mathematica for calculations and testing,
+and I now release it into the public domain.
+
+    http:en.wikipedia.org/wiki/Involute_gear
+    http:en.wikipedia.org/wiki/Gear
+    http:en.wikipedia.org/wiki/List_of_gear_nomenclature
+    http:gtrebaol.free.fr/doc/catia/spur_gear.html
+    http:www.cs.cmu.edu/~rapidproto/mechanisms/chpt7.html
+
+The module gear() gives an involute spur gear, with reasonable defaults for all the parameters.
+Normally, you should just choose the first 4 parameters, and let the rest be default values.
+The module gear() gives a gear in the XY plane, centered on the origin, with one tooth centered on
+the positive Y axis.  The various functions below it take the same parameters, and return various
+measurements for the gear.  The most important is pitch_radius, which tells how far apart to space
+gears that are meshing, and adendum_radius, which gives the size of the region filled by the gear.
+A gear has a "pitch circle", which is an invisible circle that cuts through the middle of each
+tooth (though not the exact center). In order for two gears to mesh, their pitch circles should
+just touch.  So the distance between their centers should be pitch_radius() for one, plus pitch_radius()
+for the other, which gives the radii of their pitch circles.
+
+In order for two gears to mesh, they must have the same mm_per_tooth and pressure_angle parameters.
+mm_per_tooth gives the number of millimeters of arc around the pitch circle covered by one tooth and one
+space between teeth.  The pitch angle controls how flat or bulged the sides of the teeth are.  Common
+values include 14.5 degrees and 20 degrees, and occasionally 25.  Though I've seen 28 recommended for
+plastic gears. Larger numbers bulge out more, giving stronger teeth, so 28 degrees is the default here.
+
+The ratio of number_of_teeth for two meshing gears gives how many times one will make a full
+revolution when the the other makes one full revolution.  If the two numbers are coprime (i.e.
+are not both divisible by the same number greater than 1), then every tooth on one gear
+will meet every tooth on the other, for more even wear.  So coprime numbers of teeth are good.
+
+The module rack() gives a rack, which is a bar with teeth.  A rack can mesh with any
+gear that has the same mm_per_tooth and pressure_angle.
+
+Some terminology:
+The outline of a gear is a smooth circle (the "pitch circle") which has mountains and valleys
+added so it is toothed.  So there is an inner circle (the "root circle") that touches the
+base of all the teeth, an outer circle that touches the tips of all the teeth,
+and the invisible pitch circle in between them.  There is also a "base circle", which can be smaller than
+all three of the others, which controls the shape of the teeth.  The side of each tooth lies on the path
+that the end of a string would follow if it were wrapped tightly around the base circle, then slowly unwound.
+That shape is an "involute", which gives this type of gear its name.
+
+An involute spur gear, with reasonable defaults for all the parameters.
+Normally, you should just choose the first 4 parameters, and let the rest be default values.
+Meshing gears must match in mm_per_tooth, pressure_angle, and twist,
+and be separated by the sum of their pitch radii, which can be found with pitch_radius().
+"""
+
+from math import (
+    acos,
+    cos,
+    degrees,
+    pi,
+    sin,
+    sqrt
+)
+
+from shapely.geometry import Polygon
+
+import bpy
+
+from . import (
+    simple,
+    utils,
+)
+
+# convert gear_polar to cartesian coordinates
+
+
+
+[docs] +def gear_polar(r, theta): + return r * sin(theta), r * cos(theta)
+ + + +# unwind a string this many degrees to go from radius r1 to radius r2 +
+[docs] +def gear_iang(r1, r2): + return sqrt((r2 / r1) * (r2 / r1) - 1) - acos(r1 / r2)
+ + + +# radius a fraction f up the curved side of the tooth +
+[docs] +def gear_q7(f, r, b, r2, t, s): + return gear_q6(b, s, t, (1-f) * max(b, r) + f * r2)
+ + + +# point at radius d on the involute curve +
+[docs] +def gear_q6(b, s, t, d): + return gear_polar(d, s * (gear_iang(b, d) + t))
+ + + +
+[docs] +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): + """Generate a 3D gear model based on specified parameters. + + This function creates a 3D representation of a gear using the provided + parameters such as the circular pitch, number of teeth, hole diameter, + pressure angle, clearance, backlash, rim size, hub diameter, and the + number of spokes. The gear is constructed by calculating various radii + and angles based on the input parameters and then using geometric + operations to form the final shape. The resulting gear is named + according to its specifications. + + Args: + mm_per_tooth (float): The circular pitch of the gear in millimeters (default is 0.003). + number_of_teeth (int): The total number of teeth on the gear (default is 5). + hole_diameter (float): The diameter of the central hole in millimeters (default is 0.003175). + pressure_angle (float): The angle that controls the shape of the tooth sides in radians (default + is 0.3488). + clearance (float): The gap between the top of a tooth and the bottom of a valley on a + meshing gear in millimeters (default is 0.0). + backlash (float): The gap between two meshing teeth along the circumference of the pitch + circle in millimeters (default is 0.0). + rim_size (float): The size of the rim around the gear in millimeters (default is 0.0005). + hub_diameter (float): The diameter of the hub in millimeters (default is 0.006). + spokes (int): The number of spokes on the gear (default is 4). + + Returns: + None: This function does not return a value but modifies the Blender scene to + include the generated gear model. + """ + + simple.deselect() + 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 * 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 + k = - gear_iang(b, p) - t / 2 / p + shapely_gear = Polygon([ + (0, 0), + gear_polar(r, k if r < b else -pi / number_of_teeth), + gear_q7(0, r, b, c, k, 1), + gear_q7(0.1, r, b, c, k, 1), + gear_q7(0.2, r, b, c, k, 1), + gear_q7(0.3, r, b, c, k, 1), + gear_q7(0.4, r, b, c, k, 1), + gear_q7(0.5, r, b, c, k, 1), + gear_q7(0.6, r, b, c, k, 1), + gear_q7(0.7, r, b, c, k, 1), + gear_q7(0.8, r, b, c, k, 1), + gear_q7(0.9, r, b, c, k, 1), + gear_q7(1.0, r, b, c, k, 1), + gear_q7(1.0, r, b, c, k, -1), + gear_q7(0.9, r, b, c, k, -1), + gear_q7(0.8, r, b, c, k, -1), + gear_q7(0.7, r, b, c, k, -1), + gear_q7(0.6, r, b, c, k, -1), + gear_q7(0.5, r, b, c, k, -1), + gear_q7(0.4, r, b, c, k, -1), + gear_q7(0.3, r, b, c, k, -1), + gear_q7(0.2, r, b, c, k, -1), + gear_q7(0.1, r, b, c, k, -1), + gear_q7(0.0, r, b, c, k, -1), + gear_polar(r, -k if r < b else pi / number_of_teeth) + ]) + utils.shapelyToCurve('tooth', shapely_gear, 0.0) + i = number_of_teeth + while i > 1: + simple.duplicate() + simple.rotate(2 * pi / number_of_teeth) + i -= 1 + simple.join_multiple('tooth') + simple.active_name('_teeth') + + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Circle', + Simple_radius=r, shape='3D', use_cyclic_u=True, edit_mode=False) + simple.active_name('_hub') + simple.union('_') + simple.active_name('_gear') + simple.remove_doubles() + + if spokes > 0: + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Circle', + Simple_radius=r-rim_size, shape='3D', use_cyclic_u=True, edit_mode=False) + simple.active_name('_hole') + simple.difference('_', '_gear') + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Circle', + Simple_radius=hub_diameter/2, shape='3D', use_cyclic_u=True, edit_mode=False) + simple.active_name('_hub') + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Circle', + Simple_radius=hole_diameter/2, shape='3D', use_cyclic_u=True, edit_mode=False) + simple.active_name('_hub_hole') + simple.difference('_hub', '_hub') + + simple.join_multiple('_') + + simple.add_rectangle(r-rim_size-((hub_diameter-hole_diameter)/4 + + hole_diameter/2), hub_diameter/2, center_x=False) + simple.move(x=(hub_diameter-hole_diameter)/4 + hole_diameter/2) + simple.active_name('_spoke') + + angle = 2 * pi / spokes + while spokes > 0: + simple.duplicate() + simple.rotate(angle) + spokes -= 1 + simple.union('_spoke') + simple.remove_doubles() + simple.union('_') + else: + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Circle', + Simple_radius=hole_diameter, shape='3D', use_cyclic_u=True, edit_mode=False) + simple.active_name('_hole') + simple.difference('_', '_gear') + + name = 'gear-' + str(round(mm_per_tooth*1000, 1)) + name += 'mm-pitch-' + str(number_of_teeth) + name += 'teeth-PA-' + str(round(degrees(pressure_angle), 1)) + simple.active_name(name)
+ + + +
+[docs] +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): + """Generate a rack gear profile based on specified parameters. + + This function creates a rack gear by calculating the geometry based on + the provided parameters such as millimeters per tooth, number of teeth, + height, pressure angle, backlash, hole diameter, and teeth per hole. It + constructs the gear shape using the Shapely library and duplicates the + tooth to create the full rack. If a hole diameter is specified, it also + creates holes along the rack. The resulting gear is named based on the + input parameters. + + Args: + mm_per_tooth (float): The distance in millimeters for each tooth. Default is 0.01. + number_of_teeth (int): The total number of teeth on the rack. Default is 11. + height (float): The height of the rack. Default is 0.012. + pressure_angle (float): The pressure angle in radians. Default is 0.3488. + backlash (float): The backlash distance in millimeters. Default is 0.0. + hole_diameter (float): The diameter of the holes in millimeters. Default is 0.003175. + tooth_per_hole (int): The number of teeth per hole. Default is 4. + """ + + simple.deselect() + mm_per_tooth *= 1000 + a = mm_per_tooth / pi # addendum + # tooth side is tilted so top/bottom corners move this amount + t = (a * sin(pressure_angle)) + a /= 1000 + mm_per_tooth /= 1000 + t /= 1000 + + shapely_gear = Polygon([ + (-mm_per_tooth * 2/4*1.001, a-height), + (-mm_per_tooth * 2/4*1.001 - backlash, -a), + (-mm_per_tooth * 1/4 + backlash - t, -a), + (-mm_per_tooth * 1/4 + backlash + t, a), + (mm_per_tooth * 1/4 - backlash - t, a), + (mm_per_tooth * 1/4 - backlash + t, -a), + (mm_per_tooth * 2/4*1.001 + backlash, -a), + (mm_per_tooth * 2/4*1.001, a-height) + ]) + + utils.shapelyToCurve('_tooth', shapely_gear, 0.0) + i = number_of_teeth + while i > 1: + simple.duplicate(x=mm_per_tooth) + i -= 1 + simple.union('_tooth') + simple.move(y=height/2) + if hole_diameter > 0: + bpy.ops.curve.simple(align='WORLD', location=(mm_per_tooth/2, 0, 0), rotation=(0, 0, 0), Simple_Type='Circle', + Simple_radius=hole_diameter/2, shape='3D', use_cyclic_u=True, edit_mode=False) + simple.active_name('_hole') + distance = (number_of_teeth-1) * mm_per_tooth + while distance > tooth_per_hole * mm_per_tooth: + simple.duplicate(x=tooth_per_hole * mm_per_tooth) + distance -= tooth_per_hole * mm_per_tooth + simple.difference('_', '_tooth') + + name = 'rack-' + str(round(mm_per_tooth * 1000, 1)) + name += '-PA-' + str(round(degrees(pressure_angle), 1)) + simple.active_name(name)
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/joinery.html b/_modules/cam/joinery.html new file mode 100644 index 000000000..267190c17 --- /dev/null +++ b/_modules/cam/joinery.html @@ -0,0 +1,1190 @@ + + + + + + + + + + cam.joinery — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.joinery

+"""CNC CAM 'joinery.py' © 2021 Alain Pelletier
+
+Functions to create various woodworking joints - mortise, finger etc.
+"""
+
+# 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 . import (
+    puzzle_joinery,
+    simple,
+    utils,
+)
+
+
+# boolean operations for curve objects
+
+
+[docs] +def finger_amount(space, size): + """Calculates the amount of fingers needed from the available space vs the size of the finger + + Args: + space (float):available distance to cover + size (float): size of the finger + """ + finger_amt = space / size + if (finger_amt % 1) != 0: + finger_amt = round(finger_amt) + 1 + if (finger_amt % 2) != 0: + finger_amt = round(finger_amt) + 1 + return finger_amt
+ + + +
+[docs] +def mortise(length, thickness, finger_play, cx=0, cy=0, rotation=0): + """Generates a mortise of length, thickness and finger_play tolerance + cx and cy are the center position and rotation is the angle + + Args: + length (float): length of the mortise + thickness (float): thickness of material + finger_play (float): tolerance for good fit + cx (float): coordinate for x center of the finger + cy (float):coordinate for y center of the finger + rotation (float): angle of rotation + """ + + bpy.ops.curve.simple(align='WORLD', + location=(cx, cy, 0), + rotation=(0, 0, rotation), Simple_Type='Rectangle', + Simple_width=length + finger_play, + Simple_length=thickness, shape='3D', outputType='POLY', + use_cyclic_u=True, + handleType='AUTO', edit_mode=False) + simple.active_name("_mortise")
+ + + +
+[docs] +def interlock_groove(length, thickness, finger_play, cx=0, cy=0, rotation=0): + """Generates an interlocking groove. + + Args: + length (float): Length of groove + thickness (float): thickness of groove + finger_play (float): tolerance for proper fit + cx (float): center offset x + cy (float): center offset y + rotation (float): angle of rotation + """ + mortise(length, thickness, finger_play, 0, 0, 0) + bpy.ops.transform.translate(value=(length / 2 - finger_play / 2, 0.0, 0.0)) + bpy.ops.object.transform_apply(location=True, rotation=False, scale=False) + bpy.context.active_object.rotation_euler.z = rotation + bpy.ops.transform.translate(value=(cx, cy, 0.0)) + simple.active_name("_groove")
+ + + +
+[docs] +def interlock_twist(length, thickness, finger_play, cx=0, cy=0, rotation=0, percentage=0.5): + """Generates an interlocking twist. + + Args: + length (float): Length of groove + thickness (float): thickness of groove + finger_play (float): tolerance for proper fit + cx (float): center offset x + cy (float): center offset y + rotation (float): angle of rotation + percentage (float): percentage amount the twist will take (between 0 and 1) + """ + + mortise(length, thickness, finger_play, 0, 0, 0) + simple.active_name("_tmp") + mortise(length * percentage, thickness, finger_play, 0, 0, pi / 2) + simple.active_name("_tmp") + 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) + simple.active_name("_tmp") + + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Sector', + Simple_startangle=270 + oangle, Simple_endangle=360 - oangle, Simple_radius=h / 2, + use_cyclic_u=True, edit_mode=False) + simple.active_name("_tmp") + + simple.union('_tmp') + simple.rotate(rotation) + simple.move(x=cx, y=cy) + simple.active_name("_groove") + simple.remove_doubles()
+ + + +
+[docs] +def twist_line(length, thickness, finger_play, percentage, amount, distance, center=True): + """Generates a multiple interlocking twist. + + Args: + length (float): Length of groove + thickness (float): thickness of groove + finger_play (float): tolerance for proper fit + percentage (float): percentage amount the twist will take (between 0 and 1) + amount (int):amount of twists generated + distance (float): distance between twists + center (bool): center or not from origin + """ + + spacing = distance / amount + while amount > 0: + position = spacing * amount + interlock_twist(length, thickness, finger_play, percentage=percentage, cx=position) + print('twistline', amount, distance, position) + amount -= 1 + + simple.join_multiple('_groove') + simple.active_name('twist_line') + if center: + simple.move(x=(-distance-spacing)/2)
+ + + +
+[docs] +def twist_separator_slot(length, thickness, finger_play=0.00005, percentage=0.5): + """Generates a slot for interlocking twist separator. + + Args: + length (float): Length of slot + thickness (float): thickness of slot + finger_play (float): tolerance for proper fit + percentage (float): percentage amount the twist will take (between 0 and 1) + """ + + simple.add_rectangle(thickness+finger_play/2, length, center_y=False) + simple.move(y=((length*percentage-finger_play/2)/2)) + simple.duplicate() + simple.mirrory() + simple.join_multiple('simple_rectangle') + simple.active_name('_separator_slot')
+ + + +
+[docs] +def interlock_twist_separator(length, thickness, amount, spacing, edge_distance, finger_play=0.00005, percentage=0.5, + start='rounded', end='rounded'): + """Generates a interlocking twist separator. + + Args: + length (float): Length of separator + thickness (float): thickness of separator + amount (int): quantity of separation grooves + spacing (float): distance between slots + edge_distance (float): distance of the first slots close to the edge + finger_play (float): tolerance for proper fit + percentage (float): percentage amount the twist will take (between 0 and 1) + start (string): type of start wanted (rounded, flat or other) not implemented + start (string): type of end wanted (rounded, flat or other) not implemented + """ + + amount -= 1 + base_width = 2*edge_distance+spacing*amount+thickness + simple.add_rectangle(base_width, length-finger_play*2, center_x=False) + simple.active_name('_base') + twist_separator_slot(length, thickness, finger_play, percentage) + while amount > 0: + simple.duplicate(x=spacing) + amount -= 1 + simple.join_multiple('_separator_slot') + simple.move(x=edge_distance+thickness/2) + simple.difference('_', '_base') + simple.active_name('twist_separator')
+ + + +
+[docs] +def horizontal_finger(length, thickness, finger_play, amount, center=True): + """Generates an interlocking horizontal finger pair _wfa and _wfb. + + _wfa is centered at 0,0 + _wfb is _wfa offset by one length + + Args: + length (float): Length of mortise + thickness (float): thickness of material + amount (int): quantity of fingers + finger_play (float): tolerance for proper fit + center (bool): centered of not + """ + + if center: + for i in range(amount): + if i == 0: + mortise(length, thickness, finger_play, 0, thickness / 2) + simple.active_name("_width_finger") + else: + mortise(length, thickness, finger_play, i * 2 * length, thickness / 2) + simple.active_name("_width_finger") + mortise(length, thickness, finger_play, -i * 2 * length, thickness / 2) + simple.active_name("_width_finger") + else: + for i in range(amount): + mortise(length, thickness, finger_play, length / 2 + 2 * i * length, 0) + simple.active_name("_width_finger") + + simple.join_multiple("_width_finger") + + simple.active_name("_wfa") + bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'}, + TRANSFORM_OT_translate={"value": (length, 0.0, 0.0)}) + simple.active_name("_wfb")
+ + + +
+[docs] +def vertical_finger(length, thickness, finger_play, amount): + """Generates an interlocking horizontal finger pair _vfa and _vfb. + + _vfa is starts at 0,0 + _vfb is _vfa offset by one length + + Args: + length (float): Length of mortise + thickness (float): thickness of material + amount (int): quantity of fingers + finger_play (float): tolerance for proper fit + """ + + for i in range(amount): + mortise(length, thickness, finger_play, 0, i * 2 * + length + length / 2, rotation=pi / 2) + simple.active_name("_height_finger") + + simple.join_multiple("_height_finger") + simple.active_name("_vfa") + bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'}, + TRANSFORM_OT_translate={"value": (0, -length, 0.0)}) + simple.active_name("_vfb")
+ + + +
+[docs] +def finger_pair(name, dx=0, dy=0): + """Creates a duplicate set of fingers. + + Args: + name (str): name of original finger + dx (float): x offset + dy (float): y offset + """ + + simple.make_active(name) + xpos = (dx / 2) * 1.006 + ypos = 1.006 * dy / 2 + + bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'}, + TRANSFORM_OT_translate={"value": (xpos, ypos, 0.0)}) + simple.active_name("_finger_pair") + + simple.make_active(name) + + bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'}, + TRANSFORM_OT_translate={"value": (-xpos, -ypos, 0.0)}) + simple.active_name("_finger_pair") + simple.join_multiple("_finger_pair") + bpy.ops.object.select_all(action='DESELECT') + return bpy.context.active_object
+ + + +
+[docs] +def create_base_plate(height, width, depth): + """ Creates blank plates for a box. + + Args: + height (float): height size for box + width (float): width size for box + depth (float): depth size for box + """ + + bpy.ops.curve.simple(align='WORLD', location=(0, height / 2, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', + Simple_width=width, Simple_length=height, shape='3D', outputType='POLY', + use_cyclic_u=True, + handleType='AUTO', edit_mode=False) + simple.active_name("_back") + bpy.ops.curve.simple(align='WORLD', location=(0, height / 2, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', + Simple_width=depth, Simple_length=height, shape='3D', outputType='POLY', + use_cyclic_u=True, + handleType='AUTO', edit_mode=False) + simple.active_name("_side") + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', + Simple_width=width, Simple_length=depth, shape='3D', outputType='POLY', + use_cyclic_u=True, + handleType='AUTO', edit_mode=False) + simple.active_name("_bottom")
+ + + +
+[docs] +def make_flex_pocket(length, height, finger_thick, finger_width, pocket_width): + """creates pockets using mortise function for kerf bending + + Args: + length (float): Length of pocket + height (float): height of pocket + finger_thick (float): thickness of finger + finger_width (float): width of finger + pocket_width (float): width of pocket + """ + + dist = 3 * finger_width / 2 + while dist < length: + mortise(height - 2 * finger_thick, pocket_width, 0, dist, 0, pi / 2) + simple.active_name("_flex_pocket") + dist += finger_width * 2 + + simple.join_multiple("_flex_pocket") + simple.active_name("flex_pocket")
+ + + +
+[docs] +def make_variable_flex_pocket(height, finger_thick, pocket_width, locations): + """creates pockets pocket using mortise function for kerf bending + + Args: + height (float): height of the side + finger_thick (float): thickness of the finger + pocket_width (float): width of pocket + locations (tuple): coordinates for pocket + """ + + for dist in locations: + mortise(height + 2 * finger_thick, pocket_width, 0, dist, 0, pi / 2) + simple.active_name("_flex_pocket") + + simple.join_multiple("_flex_pocket") + simple.active_name("flex_pocket")
+ + + +
+[docs] +def create_flex_side(length, height, finger_thick, top_bottom=False): + """ crates a flex side for mortise on curve. Assumes the base fingers were created and exist + + Args: + length (float): length of curve + height (float): height of side + finger_thick (float): finger thickness or thickness of material + top_bottom (bool): fingers on top and bottom if true, just on bottom if false + """ + if top_bottom: + fingers = finger_pair("base", 0, height - finger_thick) + else: + simple.make_active("base") + fingers = bpy.context.active_object + bpy.ops.transform.translate(value=(0.0, height / 2 - finger_thick / 2 + 0.0003, 0.0)) + + bpy.ops.curve.simple(align='WORLD', location=(length / 2 + 0.00025, 0, 0), rotation=(0, 0, 0), + Simple_Type='Rectangle', Simple_width=length, Simple_length=height, shape='3D', + outputType='POLY', use_cyclic_u=True, handleType='AUTO', edit_mode=False) + simple.active_name("no_fingers") + + bpy.ops.curve.simple(align='WORLD', location=(length / 2 + 0.00025, 0, 0), rotation=(0, 0, 0), + Simple_Type='Rectangle', Simple_width=length, Simple_length=height, shape='3D', + outputType='POLY', use_cyclic_u=True, handleType='AUTO', edit_mode=False) + simple.active_name("_side") + + simple.make_active('_side') + fingers.select_set(True) + bpy.ops.object.curve_boolean(boolean_type='DIFFERENCE') + + simple.active_name("side") + simple.remove_multiple('_') + simple.remove_multiple('base')
+ + + +
+[docs] +def angle(a, b): + """returns angle of a vector + + Args: + a (tuple): point a x,y coordinates + b (tuple): point b x,y coordinates + """ + + return atan2(b[1] - a[1], b[0] - a[0])
+ + + +
+[docs] +def angle_difference(a, b, c): + """returns the difference between two lines with three points + + Args: + a (tuple): point a x,y coordinates + b (tuple): point b x,y coordinates + c (tuple): point c x,y coordinates + """ + return angle(a, b) - angle(b, c)
+ + + +
+[docs] +def fixed_finger(loop, loop_length, finger_size, finger_thick, finger_tolerance, base=False): + """distributes mortises of a fixed distance. Dynamically changes the finger tolerance with the angle differences + + Args: + loop (list of tuples): takes in a shapely shape + loop_length (float): length of loop + finger_size (float): size of the mortise + finger_thick (float): thickness of the material + finger_tolerance (float): minimum finger tolerance + base (bool): if base exists, it will join with it + """ + + coords = list(loop.coords) + old_mortise_angle = 0 + distance = finger_size / 2 + j = 0 + print("Joinery Loop Length", round(loop_length * 1000), "mm") + for i, p in enumerate(coords): + if i == 0: + p_start = p + + if p != p_start: + not_start = True + else: + not_start = False + pd = loop.project(Point(p)) + + if not_start: + while distance <= pd: + mortise_angle = angle(oldp, p) + mortise_angle_difference = abs(mortise_angle - old_mortise_angle) + 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) + simple.active_name("_base") + else: + mortise_point = loop.interpolate(distance) + mortise(finger_size, finger_thick, finger_tolerance * mad, mortise_point.x, mortise_point.y, + mortise_angle) + + j += 1 + distance = j * 2 * finger_size + finger_size / 2 + old_mortise_angle = mortise_angle + oldp = p + if base: + simple.join_multiple("_base") + simple.active_name("base") + simple.move(x=finger_size) + else: + simple.join_multiple("_mort") + simple.active_name("mortise")
+ + + +
+[docs] +def find_slope(p1, p2): + """returns slope of a vector + + Args: + p1 (tuple): point 1 x,y coordinates + p2 (tuple): point 2 x,y coordinates + """ + return (p2[1] - p1[1]) / max(p2[0] - p1[0], 0.00001)
+ + + +
+[docs] +def slope_array(loop): + """Returns an array of slopes from loop coordinates. + + Args: + loop (list of tuples): list of coordinates for a curve + """ + + simple.remove_multiple("-") + coords = list(loop.coords) + # pnt_amount = round(length / resolution) + sarray = [] + dsarray = [] + for i, p in enumerate(coords): + distance = loop.project(Point(p)) + if i != 0: + slope = find_slope(p, oldp) + sarray.append((distance, slope * -0.001)) + oldp = p + for i, p in enumerate(sarray): + distance = p[0] + if i != 0: + slope = find_slope(p, oldp) + if abs(slope) > 10: + print(distance) + dsarray.append((distance, slope * -0.00001)) + oldp = p + derivative = LineString(sarray) + dderivative = LineString(dsarray) + utils.shapelyToCurve('-derivative', derivative, 0.0) + utils.shapelyToCurve('-doublederivative', dderivative, 0.0) + return sarray
+ + + +
+[docs] +def dslope_array(loop, resolution=0.001): + """Returns a double derivative array or slope of the slope + + Args: + loop (list of tuples): list of coordinates for a curve + resolution (float): granular resolution of the array + """ + length = loop.length + pnt_amount = round(length / resolution) + sarray = [] + dsarray = [] + for i in range(pnt_amount): + distance = i * resolution + pt = loop.interpolate(distance) + p = (pt.x, pt.y) + if i != 0: + slope = abs(angle(p, oldp)) + sarray.append((distance, slope * -0.01)) + oldp = p + for i, p in enumerate(sarray): + distance = p[0] + if i != 0: + slope = find_slope(p, oldp) + if abs(slope) > 10: + print(distance) + dsarray.append((distance, slope * -0.1)) + oldp = p + dderivative = LineString(dsarray) + utils.shapelyToCurve('doublederivative', dderivative, 0.0) + return sarray
+ + + +
+[docs] +def variable_finger(loop, loop_length, min_finger, finger_size, finger_thick, finger_tolerance, adaptive, base=False, + double_adaptive=False): + """Distributes mortises of a fixed distance. Dynamically changes the finger tolerance with the angle differences + + Args: + loop (list of tuples): takes in a shapely shape + loop_length (float): length of loop + finger_size (float): size of the mortise + finger_thick (float): thickness of the material + min_finger (float): minimum finger size + finger_tolerance (float): minimum finger tolerance + adaptive (float): angle threshold to reduce finger size + base (bool): join with base if true + double_adaptive (bool): uses double adaptive algorithm if true +""" + coords = list(loop.coords) + old_mortise_angle = 0 + distance = min_finger / 2 + finger_sz = min_finger + oldfinger_sz = min_finger + hpos = [] # hpos is the horizontal positions of the middle of the mortise + # slope_array(loop) + print("joinery loop length", round(loop_length * 1000), "mm") + for i, p in enumerate(coords): + if i == 0: + p_start = p + + if p != p_start: + not_start = True + else: + not_start = False + pd = loop.project(Point(p)) + + if not_start: + while distance <= pd: + mortise_angle = angle(oldp, p) + mortise_angle_difference = abs(mortise_angle - old_mortise_angle) + 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) + if mad > 2 and double_adaptive: + hpos.append(distance) # saves the mortise center + + hpos.append(distance + finger_sz) # saves the mortise center + if base: + mortise(finger_sz, finger_thick, finger_tolerance * + mad, distance + finger_sz, 0, 0) + simple.active_name("_base") + else: + mortise(finger_sz, finger_thick, finger_tolerance * mad, mortise_point.x, mortise_point.y, + mortise_angle) + if i == 1: + # put a mesh cylinder at the first coordinates to indicate start + simple.remove_multiple("start_here") + bpy.ops.mesh.primitive_cylinder_add(radius=finger_thick / 2, depth=0.025, enter_editmode=False, + align='WORLD', + location=(mortise_point.x, + mortise_point.y, 0), + scale=(1, 1, 1)) + simple.active_name("start_here_mortise") + + old_distance = distance + old_mortise_point = mortise_point + finger_sz = finger_size + next_angle_difference = pi + + # adaptive finger length start + while finger_sz > min_finger and next_angle_difference > adaptive: + # while finger_sz > min_finger and next_angle_difference > adaptive: + # reduce the size of finger by a percentage... the closer to 1.0, the slower + finger_sz *= 0.95 + distance = old_distance + 3 * oldfinger_sz / 2 + finger_sz / 2 + mortise_point = loop.interpolate(distance) # get the next mortise point + next_mortise_angle = angle((old_mortise_point.x, old_mortise_point.y), + (mortise_point.x, mortise_point.y)) # calculate next angle + next_angle_difference = abs(next_mortise_angle - mortise_angle) + + oldfinger_sz = finger_sz + old_mortise_angle = mortise_angle + oldp = p + if base: + simple.join_multiple("_base") + simple.active_name("base") + else: + print("placeholder") + simple.join_multiple("_mort") + simple.active_name("variable_mortise") + return hpos
+ + + +
+[docs] +def single_interlock(finger_depth, finger_thick, finger_tolerance, x, y, groove_angle, type, amount=1, + twist_percentage=0.5): + """Generates a single interlock at coodinate x,y. + + Args: + finger_depth (float): depth of finger + finger_thick (float): thickness of finger + finger_tolerance (float): tolerance for proper fit + x (float): offset x + y (float): offset y + groove_angle (float): angle of rotation + type (str): GROOVE, TWIST, PUZZLE are the valid choices + twist_percentage: percentage of thickness for twist (not used in puzzle or groove) + """ + if type == "GROOVE": + interlock_groove(finger_depth, finger_thick, finger_tolerance, x, y, groove_angle) + elif type == "TWIST": + interlock_twist(finger_depth, finger_thick, finger_tolerance, + x, y, groove_angle, percentage=twist_percentage) + elif type == "PUZZLE": + puzzle_joinery.fingers(finger_thick, finger_tolerance)
+ + + +
+[docs] +def distributed_interlock(loop, loop_length, finger_depth, finger_thick, finger_tolerance, finger_amount, tangent=0, + fixed_angle=0, start=0.01, end=0.01, closed=True, type='GROOVE', twist_percentage=0.5): + """Distributes interlocking joints of a fixed amount. + Dynamically changes the finger tolerance with the angle differences + + Args: + loop (list of tuples): coordinates curve + loop_length (float): length of the curve + finger_depth (float): depth of the mortise + finger_thick (float) thickness of the material + finger_tolerance (float): minimum finger tolerance + finger_amount (int): quantity of fingers + tangent (int): + fixed_angle (float): 0 will be variable, desired angle for the finger + closed (bool): False:open curve - True:closed curved + twist_percentage = portion of twist finger which is the stem (for twist joint only) + type (str): GROOVE, TWIST, PUZZLE are the valid choices + start (float): start distance from first point + end (float): end distance from last point + """ + coords = list(loop.coords) + print(closed) + if not closed: + spacing = (loop_length - start - end) / (finger_amount-1) + distance = start + end_distance = loop_length - end + else: + spacing = loop_length / finger_amount + distance = 0 + end_distance = loop_length + + j = 0 + print("Joinery Loop Length", round(loop_length * 1000), "mm") + print("Distance Between Joints", round(spacing * 1000), "mm") + + for i, p in enumerate(coords): + if i == 0: + p_start = p + + if p != p_start: + not_start = True + else: + not_start = False + pd = loop.project(Point(p)) + + if not_start: + while distance <= pd and end_distance >= distance: + if fixed_angle == 0: + 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 / 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) + + j += 1 + distance = j * spacing + start + oldp = p + + simple.join_multiple("_groove") + simple.active_name("interlock")
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/machine_settings.html b/_modules/cam/machine_settings.html new file mode 100644 index 000000000..4af65d357 --- /dev/null +++ b/_modules/cam/machine_settings.html @@ -0,0 +1,714 @@ + + + + + + + + + + cam.machine_settings — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.machine_settings

+"""CNC CAM 'machine_settings.py'
+
+All CAM machine properties.
+"""
+
+from bpy.props import (
+    BoolProperty,
+    EnumProperty,
+    FloatProperty,
+    FloatVectorProperty,
+    IntProperty,
+)
+from bpy.types import PropertyGroup
+
+from . import constants
+from .utils import updateMachine
+
+
+
+[docs] +class machineSettings(PropertyGroup): + """stores all data for machines""" + # name = StringProperty(name="Machine Name", default="Machine") +
+[docs] + 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: +
+[docs] + use_position_definitions: BoolProperty( + name="Use Position Definitions", + description="Define own positions for op start, " + "toolchange, ending position", + default=False, + )
+ +
+[docs] + starting_position: FloatVectorProperty( + name='Start Position', + default=(0, 0, 0), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + update=updateMachine, + )
+ +
+[docs] + mtc_position: FloatVectorProperty( + name='MTC Position', + default=(0, 0, 0), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + update=updateMachine, + )
+ +
+[docs] + ending_position: FloatVectorProperty( + name='End Position', + default=(0, 0, 0), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + update=updateMachine, + )
+ + +
+[docs] + working_area: FloatVectorProperty( + name='Work Area', + default=(0.500, 0.500, 0.100), + unit='LENGTH', + precision=constants.PRECISION, + subtype="XYZ", + update=updateMachine, + )
+ +
+[docs] + feedrate_min: FloatProperty( + name="Feedrate Minimum /min", + default=0.0, + min=0.00001, + max=320000, + precision=constants.PRECISION, + unit='LENGTH', + )
+ +
+[docs] + feedrate_max: FloatProperty( + name="Feedrate Maximum /min", + default=2, + min=0.00001, + max=320000, + precision=constants.PRECISION, + unit='LENGTH', + )
+ +
+[docs] + feedrate_default: FloatProperty( + name="Feedrate Default /min", + default=1.5, + min=0.00001, + max=320000, + precision=constants.PRECISION, + unit='LENGTH', + )
+ +
+[docs] + hourly_rate: FloatProperty( + name="Price per Hour", + default=100, + min=0.005, + precision=2, + )
+ + + # UNSUPPORTED: + +
+[docs] + spindle_min: FloatProperty( + name="Spindle Speed Minimum RPM", + default=5000, + min=0.00001, + max=320000, + precision=1, + )
+ +
+[docs] + spindle_max: FloatProperty( + name="Spindle Speed Maximum RPM", + default=30000, + min=0.00001, + max=320000, + precision=1, + )
+ +
+[docs] + spindle_default: FloatProperty( + name="Spindle Speed Default RPM", + default=15000, + min=0.00001, + max=320000, + precision=1, + )
+ +
+[docs] + 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, + )
+ + +
+[docs] + axis4: BoolProperty( + name="#4th Axis", + description="Machine has 4th axis", + default=0, + )
+ +
+[docs] + axis5: BoolProperty( + name="#5th Axis", + description="Machine has 5th axis", + default=0, + )
+ + +
+[docs] + eval_splitting: BoolProperty( + name="Split Files", + description="Split gcode file with large number of operations", + default=True, + ) # split large files
+ +
+[docs] + 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) + +
+[docs] + 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 + +
+[docs] + output_block_numbers: BoolProperty( + name="Output Block Numbers", + description="Output block numbers ie N10 at start of line", + default=False, + )
+ + +
+[docs] + start_block_number: IntProperty( + name="Start Block Number", + description="The starting block number ie 10", + default=10, + )
+ + +
+[docs] + block_number_increment: IntProperty( + name="Block Number Increment", + description="How much the block number should " + "increment for the next line", + default=10, + )
+ + +
+[docs] + output_tool_definitions: BoolProperty( + name="Output Tool Definitions", + description="Output tool definitions", + default=True, + )
+ + +
+[docs] + output_tool_change: BoolProperty( + name="Output Tool Change Commands", + description="Output tool change commands ie: Tn M06", + default=True, + )
+ + +
+[docs] + output_g43_on_tool_change: BoolProperty( + name="Output G43 on Tool Change", + description="Output G43 on tool change line", + default=False, + )
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/numba_wrapper.html b/_modules/cam/numba_wrapper.html new file mode 100644 index 000000000..1f9a0535a --- /dev/null +++ b/_modules/cam/numba_wrapper.html @@ -0,0 +1,426 @@ + + + + + + + + + + cam.numba_wrapper — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.numba_wrapper

+"""CNC CAM 'numba_wrapper.py'
+
+Patch to ensure functions will run if numba is unavailable.
+"""
+
+try:
+    from numba import jit, prange
+    print("numba: yes")
+except:
+    print("numba: no")
+
+
+[docs] + def jit(f=None, *args, **kwargs): + def decorator(func): + return func + + if callable(f): + return f + else: + return decorator
+ + prange = range +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/ops.html b/_modules/cam/ops.html new file mode 100644 index 000000000..2135b22be --- /dev/null +++ b/_modules/cam/ops.html @@ -0,0 +1,2189 @@ + + + + + + + + + + cam.ops — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.ops

+"""CNC CAM 'ops.py' © 2012 Vilem Novak
+
+Blender Operator 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.types import (
+    Operator,
+)
+
+from . import (
+    bridges,
+    gcodepath,
+    pack,
+    simple,
+    simulation,
+)
+from .async_op import (
+    AsyncCancelledException,
+    AsyncOperatorMixin,
+    progress_async,
+)
+from .exception import CamException
+from .utils import (
+    addMachineAreaObject,
+    getBoundsWorldspace,
+    isChainValid,
+    isValid,
+    reload_paths,
+    silhoueteOffset,
+    was_hidden_dict,
+)
+
+
+
+[docs] +class threadCom: # object passed to threads to read background process stdout info + def __init__(self, o, proc): +
+[docs] + self.opname = o.name
+ +
+[docs] + self.outtext = ''
+ +
+[docs] + self.proc = proc
+ +
+[docs] + self.lasttext = ''
+
+ + + +
+[docs] +def threadread(tcom): + """Reads the standard output of a background process in a non-blocking + manner. + + This function reads a line from the standard output of a background + process associated with the provided `tcom` object. It searches for a + specific substring that indicates progress information, and if found, + extracts that information and assigns it to the `outtext` attribute of + the `tcom` object. This allows for real-time monitoring of the + background process's output without blocking the main thread. + + Args: + tcom (object): An object that has a `proc` attribute with a `stdout` + stream from which to read the output. + + Returns: + None: This function does not return a value; it modifies the `tcom` + object in place. + """ + inline = tcom.proc.stdout.readline() + inline = str(inline) + s = inline.find('progress{') + if s > -1: + e = inline.find('}') + tcom.outtext = inline[s + 9:e]
+ + + +@bpy.app.handlers.persistent +
+[docs] +def timer_update(context): + """Monitor background processes related to camera path calculations. + + This function checks the status of background processes that are + responsible for calculating camera paths. It retrieves the current + processes and monitors their state. If a process has finished, it + updates the corresponding camera operation and reloads the necessary + paths. If the process is still running, it restarts the associated + thread to continue monitoring. + + Args: + context: The context in which the function is called, typically + containing information about the current scene and operations. + """ + text = '' + s = bpy.context.scene + if hasattr(bpy.ops.object.calculate_cam_paths_background.__class__, 'cam_processes'): + processes = bpy.ops.object.calculate_cam_paths_background.__class__.cam_processes + for p in processes: + # proc=p[1].proc + readthread = p[0] + tcom = p[1] + if not readthread.is_alive(): + readthread.join() + # readthread. + tcom.lasttext = tcom.outtext + if tcom.outtext != '': + print(tcom.opname, tcom.outtext) + tcom.outtext = '' + + if 'finished' in tcom.lasttext: + processes.remove(p) + + o = s.cam_operations[tcom.opname] + o.computing = False + reload_paths(o) + update_zbufferimage_tag = False + update_offsetimage_tag = False + else: + readthread = threading.Thread( + target=threadread, args=([tcom]), daemon=True) + readthread.start() + p[0] = readthread + o = s.cam_operations[tcom.opname] # changes + o.outtext = tcom.lasttext # changes
+ + + +
+[docs] +class PathsBackground(Operator): + """Calculate CAM Paths in Background. File Has to Be Saved Before.""" +
+[docs] + bl_idname = "object.calculate_cam_paths_background"
+ +
+[docs] + bl_label = "Calculate CAM Paths in Background"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + def execute(self, context): + """Execute the camera operation in the background. + + This method initiates a background process to perform camera operations + based on the current scene and active camera operation. It sets up the + necessary paths for the script and starts a subprocess to handle the + camera computations. Additionally, it manages threading to ensure that + the main thread remains responsive while the background operation is + executed. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the completion status of the operation. + """ + + s = bpy.context.scene + o = s.cam_operations[s.cam_active_operation] + self.operation = o + o.computing = True + + bpath = bpy.app.binary_path + fpath = bpy.data.filepath + + for p in bpy.utils.script_paths(): + scriptpath = p + os.sep + 'addons' + os.sep + 'cam' + os.sep + 'backgroundop.py' + print(scriptpath) + if os.path.isfile(scriptpath): + break + proc = subprocess.Popen([bpath, '-b', fpath, '-P', scriptpath, '--', '-o=' + str(s.cam_active_operation)], + bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE) + + tcom = threadCom(o, proc) + readthread = threading.Thread( + target=threadread, args=([tcom]), daemon=True) + readthread.start() + # self.__class__.cam_processes=[] + if not hasattr(bpy.ops.object.calculate_cam_paths_background.__class__, 'cam_processes'): + bpy.ops.object.calculate_cam_paths_background.__class__.cam_processes = [] + bpy.ops.object.calculate_cam_paths_background.__class__.cam_processes.append([ + readthread, tcom]) + return {'FINISHED'}
+
+ + + +
+[docs] +class KillPathsBackground(Operator): + """Remove CAM Path Processes in Background.""" +
+[docs] + bl_idname = "object.kill_calculate_cam_paths_background"
+ +
+[docs] + bl_label = "Kill Background Computation of an Operation"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + def execute(self, context): + """Execute the camera operation in the given context. + + This method retrieves the active camera operation from the scene and + checks if there are any ongoing processes related to camera path + calculations. If such processes exist and match the current operation, + they are terminated. The method then marks the operation as not + computing and returns a status indicating that the execution has + finished. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary with a status key indicating the result of the execution. + """ + + s = bpy.context.scene + o = s.cam_operations[s.cam_active_operation] + self.operation = o + + if hasattr(bpy.ops.object.calculate_cam_paths_background.__class__, 'cam_processes'): + processes = bpy.ops.object.calculate_cam_paths_background.__class__.cam_processes + for p in processes: + tcom = p[1] + if tcom.opname == o.name: + processes.remove(p) + tcom.proc.kill() + o.computing = False + + return {'FINISHED'}
+
+ + + +
+[docs] +async def _calc_path(operator, context): + """Calculate the path for a given operator and context. + + This function processes the current scene's camera operations based on + the specified operator and context. It handles different geometry + sources, checks for valid operation parameters, and manages the + visibility of objects and collections. The function also retrieves the + path using an asynchronous operation and handles any exceptions that may + arise during this process. If the operation is invalid or if certain + conditions are not met, appropriate error messages are reported to the + operator. + + Args: + operator (bpy.types.Operator): The operator that initiated the path calculation. + context (bpy.types.Context): The context in which the operation is executed. + + Returns: + tuple: A tuple indicating the status of the operation. + Returns {'FINISHED', True} if successful, + {'FINISHED', False} if there was an error, + or {'CANCELLED', False} if the operation was cancelled. + """ + + s = bpy.context.scene + o = s.cam_operations[s.cam_active_operation] + if o.geometry_source == 'OBJECT': + ob = bpy.data.objects[o.object_name] + ob.hide_set(False) + if o.geometry_source == 'COLLECTION': + obc = bpy.data.collections[o.collection_name] + for ob in obc.objects: + ob.hide_set(False) + if o.strategy == "CARVE": + curvob = bpy.data.objects[o.curve_object] + curvob.hide_set(False) + '''if o.strategy == 'WATERLINE': + ob = bpy.data.objects[o.object_name] + ob.select_set(True) + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)''' + mesh = bpy.data.meshes.get(f'cam_path_{o.name}') + if mesh: + bpy.data.meshes.remove(mesh) + + if not o.valid: + operator.report({'ERROR_INVALID_INPUT'}, + "Operation can't be performed, see warnings for info") + progress_async("Operation can't be performed, see warnings for info") + return {'FINISHED', False} + + # check for free movement height < maxz and return with error + if(o.movement.free_height < o.maxz): + operator.report({'ERROR_INVALID_INPUT'}, + "Free Movement Height Is Less than Operation Depth Start \n Correct and Try Again.") + progress_async("Operation Can't Be Performed, See Warnings for Info") + return {'FINISHED', False} + + if o.computing: + return {'FINISHED', False} + + o.operator = operator + + if o.use_layers: + o.movement.parallel_step_back = False + try: + await gcodepath.getPath(context, o) + print("Got Path Okay") + except CamException as e: + traceback.print_tb(e.__traceback__) + error_str = "\n".join(textwrap.wrap(str(e), width=80)) + operator.report({'ERROR'}, error_str) + return {'FINISHED', False} + except AsyncCancelledException as e: + return {'CANCELLED', False} + except Exception as e: + print("FAIL", e) + traceback.print_tb(e.__traceback__) + operator.report({'ERROR'}, str(e)) + return {'FINISHED', False} + coll = bpy.data.collections.get('RigidBodyWorld') + if coll: + bpy.data.collections.remove(coll) + + return {'FINISHED', True}
+ + + +
+[docs] +class CalculatePath(Operator, AsyncOperatorMixin): + """Calculate CAM Paths""" +
+[docs] + bl_idname = "object.calculate_cam_path"
+ +
+[docs] + bl_label = "Calculate CAM Paths"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + """Check if the current camera operation is valid. + + This method checks the active camera operation in the given context and + determines if it is valid. It retrieves the active operation from the + scene's camera operations and validates it using the `isValid` function. + If the operation is valid, it returns True; otherwise, it returns False. + + Args: + context (Context): The context containing the scene and camera operations. + + Returns: + bool: True if the active camera operation is valid, False otherwise. + """ + + s = context.scene + o = s.cam_operations[s.cam_active_operation] + if o is not None: + if isValid(o, context): + return True + return False
+ + +
+[docs] + async def execute_async(self, context): + """Execute an asynchronous calculation of a path. + + This method performs an asynchronous operation to calculate a path based + on the provided context. It awaits the result of the calculation and + prints the success status along with the return value. The return value + can be used for further processing or analysis. + + Args: + context (Any): The context in which the path calculation is to be executed. + + Returns: + Any: The result of the path calculation. + """ + + (retval, success) = await _calc_path(self, context) + print(f"CALCULATED PATH (success={success},retval={retval}") + return retval
+
+ + + +
+[docs] +class PathsAll(Operator): + """Calculate All CAM Paths""" +
+[docs] + bl_idname = "object.calculate_cam_paths_all"
+ +
+[docs] + bl_label = "Calculate All CAM Paths"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + def execute(self, context): + """Execute camera operations in the current Blender context. + + This function iterates through the camera operations defined in the + current scene and executes the background calculation for each + operation. It sets the active camera operation index and prints the name + of each operation being processed. This is typically used in a Blender + add-on or script to automate camera path calculations. + + Args: + context (bpy.context): The current Blender context. + + Returns: + dict: A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + """ + + i = 0 + for o in bpy.context.scene.cam_operations: + bpy.context.scene.cam_active_operation = i + print('\nCalculating Path :' + o.name) + print('\n') + bpy.ops.object.calculate_cam_paths_background() + i += 1 + + return {'FINISHED'}
+ + +
+[docs] + def draw(self, context): + """Draws the user interface elements for the operation selection. + + This method utilizes the Blender layout system to create a property + search interface for selecting operations related to camera + functionalities. It links the current instance's operation property to + the available camera operations defined in the Blender scene. + + Args: + context (bpy.context): The context in which the drawing occurs, + """ + + layout = self.layout + layout.prop_search(self, "operation", + bpy.context.scene, "cam_operations")
+
+ + + +
+[docs] +class CamPackObjects(Operator): + """Calculate All CAM Paths""" +
+[docs] + bl_idname = "object.cam_pack_objects"
+ +
+[docs] + bl_label = "Pack Curves on Sheet"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + def execute(self, context): + """Execute the operation in the given context. + + This function sets the Blender object mode to 'OBJECT', retrieves the + currently selected objects, and calls the `packCurves` function from the + `pack` module. It is typically used to finalize operations on selected + objects in Blender. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the completion status of the operation. + """ + + bpy.ops.object.mode_set(mode='OBJECT') # force object mode + obs = bpy.context.selected_objects + pack.packCurves() + # layout. + return {'FINISHED'}
+ + +
+[docs] + def draw(self, context): + layout = self.layout
+
+ + + +
+[docs] +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. +
+[docs] + bl_idname = "object.cam_slice_objects"
+ +
+[docs] + bl_label = "Slice Object - Useful for Lasercut Puzzles etc"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + def execute(self, context): + """Execute the slicing operation on the active Blender object. + + This function retrieves the currently active object in the Blender + context and performs a slicing operation on it using the `sliceObject` + function from the `cam` module. The operation is intended to modify the + object based on the slicing logic defined in the external module. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the result of the operation, + typically containing the key 'FINISHED' upon successful execution. + """ + + from cam import slice + ob = bpy.context.active_object + slice.sliceObject(ob) + return {'FINISHED'}
+ + +
+[docs] + def draw(self, context): + layout = self.layout
+
+ + + +
+[docs] +def getChainOperations(chain): + """Return chain operations associated with a given chain object. + + This function iterates through the operations of the provided chain + object and retrieves the corresponding operations from the current + scene's camera operations in Blender. Due to limitations in Blender, + chain objects cannot store operations directly, so this function serves + to extract and return the relevant operations for further processing. + + Args: + chain (object): The chain object from which to retrieve operations. + + Returns: + list: A list of operations associated with the given chain object. + """ + chop = [] + for cho in chain.operations: + for so in bpy.context.scene.cam_operations: + if so.name == cho.name: + chop.append(so) + return chop
+ + + +
+[docs] +class PathsChain(Operator, AsyncOperatorMixin): + """Calculate a Chain and Export the G-code Alltogether. """ +
+[docs] + bl_idname = "object.calculate_cam_paths_chain"
+ +
+[docs] + bl_label = "Calculate CAM Paths in Current Chain and Export Chain G-code"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + """Check the validity of the active camera chain in the given context. + + This method retrieves the active camera chain from the scene and checks + its validity using the `isChainValid` function. It returns a boolean + value indicating whether the camera chain is valid or not. + + Args: + context (Context): The context containing the scene and camera chain information. + + Returns: + bool: True if the active camera chain is valid, False otherwise. + """ + + s = context.scene + chain = s.cam_chains[s.cam_active_chain] + return isChainValid(chain, context)[0]
+ + +
+[docs] + async def execute_async(self, context): + """Execute asynchronous operations for camera path calculations. + + This method sets the object mode for the Blender scene and processes a + series of camera operations defined in the active camera chain. It + reports the progress of each operation and handles any exceptions that + may occur during the path calculation. After successful calculations, it + exports the resulting mesh data to a specified G-code file. + + Args: + context (bpy.context): The Blender context containing scene and + + Returns: + dict: A dictionary indicating the result of the operation, + typically {'FINISHED'}. + """ + + s = context.scene + bpy.ops.object.mode_set(mode='OBJECT') # force object mode + chain = s.cam_chains[s.cam_active_chain] + chainops = getChainOperations(chain) + meshes = [] + try: + for i in range(0, len(chainops)): + s.cam_active_operation = s.cam_operations.find( + chainops[i].name) + self.report({'INFO'}, f"Calculating Path: {chainops[i].name}") + result, success = await _calc_path(self, context) + if not success and 'FINISHED' in result: + self.report( + {'ERROR'}, f"Couldn't Calculate Path: {chainops[i].name}") + except Exception as e: + print("FAIL", e) + traceback.print_tb(e.__traceback__) + self.report({'ERROR'}, str(e)) + return {'FINISHED'} + + for o in chainops: + meshes.append(bpy.data.objects["cam_path_{}".format(o.name)].data) + gcodepath.exportGcodePath(chain.filename, meshes, chainops) + return {'FINISHED'}
+
+ + + +
+[docs] +class PathExportChain(Operator): + """Calculate a Chain and Export the G-code Together.""" +
+[docs] + bl_idname = "object.cam_export_paths_chain"
+ +
+[docs] + bl_label = "Export CAM Paths in Current Chain as G-code"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + """Check the validity of the active camera chain in the given context. + + This method retrieves the currently active camera chain from the scene + context and checks its validity using the `isChainValid` function. It + returns a boolean indicating whether the active camera chain is valid or + not. + + Args: + context (object): The context containing the scene and camera chain information. + + Returns: + bool: True if the active camera chain is valid, False otherwise. + """ + + s = context.scene + chain = s.cam_chains[s.cam_active_chain] + return isChainValid(chain, context)[0]
+ + +
+[docs] + def execute(self, context): + """Execute the camera path export process. + + This function retrieves the active camera chain from the current scene + and gathers the mesh data associated with the operations of that chain. + It then exports the G-code path using the specified filename and the + collected mesh data. The function is designed to be called within the + context of a Blender operator. + + Args: + context (bpy.context): The context in which the operator is executed. + + Returns: + dict: A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + """ + + s = bpy.context.scene + + chain = s.cam_chains[s.cam_active_chain] + chainops = getChainOperations(chain) + meshes = [] + + # if len(chainops)<4: + + for o in chainops: + # bpy.ops.object.calculate_cam_paths_background() + meshes.append(bpy.data.objects["cam_path_{}".format(o.name)].data) + gcodepath.exportGcodePath(chain.filename, meshes, chainops) + return {'FINISHED'}
+
+ + + +
+[docs] +class PathExport(Operator): + """Export G-code. Can Be Used only when the Path Object Is Present""" +
+[docs] + bl_idname = "object.cam_export"
+ +
+[docs] + bl_label = "Export Operation G-code"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + def execute(self, context): + """Execute the camera operation and export the G-code path. + + This method retrieves the active camera operation from the current scene + and exports the corresponding G-code path to a specified filename. It + prints the filename and relevant operation details to the console for + debugging purposes. The G-code path is generated based on the camera + path data associated with the active operation. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + """ + + + s = bpy.context.scene + operation = s.cam_operations[s.cam_active_operation] + + print("EXPORTING", operation.filename, + bpy.data.objects["cam_path_{}".format(operation.name)].data, operation) + + gcodepath.exportGcodePath(operation.filename, [bpy.data.objects["cam_path_{}".format(operation.name)].data], + [operation]) + return {'FINISHED'}
+
+ + + +
+[docs] +class CAMSimulate(Operator, AsyncOperatorMixin): + """Simulate CAM Operation + This Is Performed by: Creating an Image, Painting Z Depth of the Brush Subtractively. + Works only for Some Operations, Can Not Be Used for 4-5 Axis.""" +
+[docs] + bl_idname = "object.cam_simulate"
+ +
+[docs] + bl_label = "CAM Simulation"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
+ + +
+[docs] + operation: StringProperty( + name="Operation", + description="Specify the operation to calculate", + default='Operation', + )
+ + +
+[docs] + async def execute_async(self, context): + """Execute an asynchronous simulation operation based on the active camera + operation. + + This method retrieves the current scene and the active camera operation. + It constructs the operation name and checks if the corresponding object + exists in the Blender data. If it does, it attempts to run the + simulation asynchronously. If the simulation is cancelled, it returns a + cancellation status. If the object does not exist, it reports an error + and returns a finished status. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the status of the operation, either + {'CANCELLED'} or {'FINISHED'}. + """ + + s = bpy.context.scene + operation = s.cam_operations[s.cam_active_operation] + + operation_name = "cam_path_{}".format(operation.name) + + if operation_name in bpy.data.objects: + try: + await simulation.doSimulation(operation_name, [operation]) + except AsyncCancelledException as e: + return {'CANCELLED'} + else: + self.report({'ERROR'}, 'No Computed Path to Simulate') + return {'FINISHED'} + return {'FINISHED'}
+ + +
+[docs] + def draw(self, context): + """Draws the user interface for selecting camera operations. + + This method creates a layout element in the user interface that allows + users to search and select a specific camera operation from a list of + available operations defined in the current scene. It utilizes the + Blender Python API to integrate with the UI. + + Args: + context: The context in which the drawing occurs, typically + provided by Blender's UI system. + """ + + layout = self.layout + layout.prop_search(self, "operation", + bpy.context.scene, "cam_operations")
+
+ + + +
+[docs] +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.""" +
+[docs] + bl_idname = "object.cam_simulate_chain"
+ +
+[docs] + bl_label = "CAM Simulation"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + """Check the validity of the active camera chain in the scene. + + This method retrieves the currently active camera chain from the scene's + camera chains and checks its validity using the `isChainValid` function. + It returns a boolean indicating whether the active camera chain is + valid. + + Args: + context (object): The context containing the scene and its properties. + + Returns: + bool: True if the active camera chain is valid, False otherwise. + """ + + s = context.scene + chain = s.cam_chains[s.cam_active_chain] + return isChainValid(chain, context)[0]
+ + +
+[docs] + operation: StringProperty( + name="Operation", + description="Specify the operation to calculate", + default='Operation', + )
+ + +
+[docs] + async def execute_async(self, context): + """Execute an asynchronous simulation for a specified camera chain. + + This method retrieves the active camera chain from the current Blender + scene and determines the operations associated with that chain. It + checks if all operations are valid and can be simulated. If valid, it + proceeds to execute the simulation asynchronously. If any operation is + invalid, it logs a message and returns a finished status without + performing the simulation. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the status of the operation, either + operation completed successfully. + """ + + s = bpy.context.scene + chain = s.cam_chains[s.cam_active_chain] + chainops = getChainOperations(chain) + + canSimulate = True + for operation in chainops: + if operation.name not in bpy.data.objects: + canSimulate = True # force true + print("operation name " + str(operation.name)) + if canSimulate: + try: + await simulation.doSimulation(chain.name, chainops) + except AsyncCancelledException as e: + return {'CANCELLED'} + else: + print('no computed path to simulate') + return {'FINISHED'} + return {'FINISHED'}
+ + +
+[docs] + def draw(self, context): + """Draw the user interface for selecting camera operations. + + This function creates a user interface element that allows the user to + search and select a specific camera operation from a list of available + operations in the current scene. It utilizes the Blender Python API to + create a property search layout. + + Args: + context: The context in which the drawing occurs, typically containing + information about the current scene and UI elements. + """ + + layout = self.layout + layout.prop_search(self, "operation", + bpy.context.scene, "cam_operations")
+
+ + + +
+[docs] +class CamChainAdd(Operator): + """Add New CAM Chain""" +
+[docs] + bl_idname = "scene.cam_chain_add"
+ +
+[docs] + bl_label = "Add New CAM Chain"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.scene is not None
+ + +
+[docs] + def execute(self, context): + """Execute the camera chain creation in the given context. + + This function adds a new camera chain to the current scene in Blender. + It updates the active camera chain index and assigns a name and filename + to the newly created chain. The function is intended to be called within + a Blender operator context. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the operation's completion status, + specifically returning {'FINISHED'} upon successful execution. + """ + + # main(context) + s = bpy.context.scene + s.cam_chains.add() + chain = s.cam_chains[-1] + s.cam_active_chain = len(s.cam_chains) - 1 + chain.name = 'Chain_' + str(s.cam_active_chain + 1) + chain.filename = chain.name + chain.index = s.cam_active_chain + + return {'FINISHED'}
+
+ + + +
+[docs] +class CamChainRemove(Operator): + """Remove CAM Chain""" +
+[docs] + bl_idname = "scene.cam_chain_remove"
+ +
+[docs] + bl_label = "Remove CAM Chain"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.scene is not None
+ + +
+[docs] + def execute(self, context): + """Execute the camera chain removal process. + + This function removes the currently active camera chain from the scene + and decrements the active camera chain index if it is greater than zero. + It modifies the Blender context to reflect these changes. + + Args: + context: The context in which the function is executed. + + Returns: + dict: A dictionary indicating the status of the operation, + specifically {'FINISHED'} upon successful execution. + """ + + bpy.context.scene.cam_chains.remove(bpy.context.scene.cam_active_chain) + if bpy.context.scene.cam_active_chain > 0: + bpy.context.scene.cam_active_chain -= 1 + + return {'FINISHED'}
+
+ + + +
+[docs] +class CamChainOperationAdd(Operator): + """Add Operation to Chain""" +
+[docs] + bl_idname = "scene.cam_chain_operation_add"
+ +
+[docs] + bl_label = "Add Operation to Chain"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.scene is not None
+ + +
+[docs] + def execute(self, context): + """Execute an operation in the active camera chain. + + This function retrieves the active camera chain from the current scene + and adds a new operation to it. It increments the active operation index + and assigns the name of the currently selected camera operation to the + newly added operation. This is typically used in the context of managing + camera operations in a 3D environment. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the execution status, typically {'FINISHED'}. + """ + + s = bpy.context.scene + chain = s.cam_chains[s.cam_active_chain] + s = bpy.context.scene + chain.operations.add() + chain.active_operation += 1 + chain.operations[-1].name = s.cam_operations[s.cam_active_operation].name + return {'FINISHED'}
+
+ + + +
+[docs] +class CamChainOperationUp(Operator): + """Add Operation to Chain""" +
+[docs] + bl_idname = "scene.cam_chain_operation_up"
+ +
+[docs] + bl_label = "Add Operation to Chain"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.scene is not None
+ + +
+[docs] + def execute(self, context): + """Execute the operation to move the active camera operation in the chain. + + This function retrieves the current scene and the active camera chain. + If there is an active operation (i.e., its index is greater than 0), it + moves the operation one step up in the chain by adjusting the indices + accordingly. After moving the operation, it updates the active operation + index to reflect the change. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the result of the operation, + specifically returning {'FINISHED'} upon successful execution. + """ + + s = bpy.context.scene + chain = s.cam_chains[s.cam_active_chain] + a = chain.active_operation + if a > 0: + chain.operations.move(a, a - 1) + chain.active_operation -= 1 + return {'FINISHED'}
+
+ + + +
+[docs] +class CamChainOperationDown(Operator): + """Add Operation to Chain""" +
+[docs] + bl_idname = "scene.cam_chain_operation_down"
+ +
+[docs] + bl_label = "Add Operation to Chain"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.scene is not None
+ + +
+[docs] + def execute(self, context): + """Execute the operation to move the active camera operation in the chain. + + This function retrieves the current scene and the active camera chain. + It checks if the active operation can be moved down in the list of + operations. If so, it moves the active operation one position down and + updates the active operation index accordingly. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the result of the operation, + specifically {'FINISHED'} when the operation completes successfully. + """ + + s = bpy.context.scene + chain = s.cam_chains[s.cam_active_chain] + a = chain.active_operation + if a < len(chain.operations) - 1: + chain.operations.move(a, a + 1) + chain.active_operation += 1 + return {'FINISHED'}
+
+ + + +
+[docs] +class CamChainOperationRemove(Operator): + """Remove Operation from Chain""" +
+[docs] + bl_idname = "scene.cam_chain_operation_remove"
+ +
+[docs] + bl_label = "Remove Operation from Chain"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.scene is not None
+ + +
+[docs] + def execute(self, context): + """Execute the operation to remove the active operation from the camera + chain. + + This method accesses the current scene and retrieves the active camera + chain. It then removes the currently active operation from that chain + and adjusts the index of the active operation accordingly. If the active + operation index becomes negative, it resets it to zero to ensure it + remains within valid bounds. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the execution status, typically + containing {'FINISHED'} upon successful completion. + """ + + s = bpy.context.scene + chain = s.cam_chains[s.cam_active_chain] + chain.operations.remove(chain.active_operation) + chain.active_operation -= 1 + if chain.active_operation < 0: + chain.active_operation = 0 + return {'FINISHED'}
+
+ + + +
+[docs] +def fixUnits(): + """Set up units for CNC CAM. + + This function configures the unit settings for the current Blender + scene. It sets the rotation system to degrees and the scale length to + 1.0, ensuring that the units are appropriately configured for use within + CNC CAM. + """ + s = bpy.context.scene + + s.unit_settings.system_rotation = 'DEGREES' + + s.unit_settings.scale_length = 1.0
+ + # Blender CAM doesn't respect this property and there were users reporting problems, not seeing this was changed. + + +
+[docs] +class CamOperationAdd(Operator): + """Add New CAM Operation""" +
+[docs] + bl_idname = "scene.cam_operation_add"
+ +
+[docs] + bl_label = "Add New CAM Operation"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.scene is not None
+ + +
+[docs] + def execute(self, context): + """Execute the camera operation based on the active object in the scene. + + This method retrieves the active object from the Blender context and + performs operations related to camera settings. It checks if an object + is selected and retrieves its bounding box dimensions. If no object is + found, it reports an error and cancels the operation. If an object is + present, it adds a new camera operation to the scene, sets its + properties, and ensures that a machine area object is present. + + Args: + context: The context in which the operation is executed. + """ + + s = bpy.context.scene + fixUnits() + + ob = bpy.context.active_object + if ob is None: + self.report({'ERROR_INVALID_INPUT'}, + "Please Add an Object to Base the Operation on.") + return {'CANCELLED'} + + minx, miny, minz, maxx, maxy, maxz = getBoundsWorldspace([ob]) + s.cam_operations.add() + o = s.cam_operations[-1] + o.object_name = ob.name + o.minz = minz + + s.cam_active_operation = len(s.cam_operations) - 1 + + o.name = f"Op_{ob.name}_{s.cam_active_operation + 1}" + o.filename = o.name + + if s.objects.get('CAM_machine') is None: + addMachineAreaObject() + + return {'FINISHED'}
+
+ + + +
+[docs] +class CamOperationCopy(Operator): + """Copy CAM Operation""" +
+[docs] + bl_idname = "scene.cam_operation_copy"
+ +
+[docs] + bl_label = "Copy Active CAM Operation"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.scene is not None
+ + +
+[docs] + def execute(self, context): + """Execute the camera operation in the given context. + + This method handles the execution of camera operations within the + Blender scene. It first checks if there are any camera operations + available. If not, it returns a cancellation status. If there are + operations, it copies the active operation, increments the active + operation index, and updates the name and filename of the new operation. + The function also ensures that the new operation's name is unique by + appending a copy suffix or incrementing a numeric suffix. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the status of the operation, + either {'CANCELLED'} if no operations are available or + {'FINISHED'} if the operation was successfully executed. + """ + + # main(context) + scene = bpy.context.scene + + fixUnits() + + scene = bpy.context.scene + if len(scene.cam_operations) == 0: + return {'CANCELLED'} + copyop = scene.cam_operations[scene.cam_active_operation] + scene.cam_operations.add() + scene.cam_active_operation += 1 + l = len(scene.cam_operations) - 1 + scene.cam_operations.move(l, scene.cam_active_operation) + o = scene.cam_operations[scene.cam_active_operation] + + for k in copyop.keys(): + value = copyop[k] + if isinstance(value, bpy.types.PropertyGroup): + for subkey in value.keys(): + o[k][subkey] = value[subkey] + elif isinstance(value, (int, float, str, bool, list)): + o[k] = value + + o.computing = False + + # ###get digits in the end + + isdigit = True + numdigits = 0 + num = 0 + if o.name[-1].isdigit(): + numdigits = 1 + while isdigit: + numdigits += 1 + isdigit = o.name[-numdigits].isdigit() + numdigits -= 1 + o.name = o.name[:-numdigits] + \ + str(int(o.name[-numdigits:]) + 1).zfill(numdigits) + o.filename = o.name + else: + o.name = o.name + '_copy' + o.filename = o.filename + '_copy' + + return {'FINISHED'}
+
+ + + +
+[docs] +class CamOperationRemove(Operator): + """Remove CAM Operation""" +
+[docs] + bl_idname = "scene.cam_operation_remove"
+ +
+[docs] + bl_label = "Remove CAM Operation"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.scene is not None
+ + +
+[docs] + def execute(self, context): + """Execute the camera operation in the given context. + + This function performs the active camera operation by deleting the + associated object from the scene. It checks if there are any camera + operations available and handles the deletion of the active operation's + object. If the active operation is removed, it updates the active + operation index accordingly. Additionally, it manages a dictionary that + tracks hidden objects. + + Args: + context (bpy.context): The Blender context containing the scene and operations. + + Returns: + dict: A dictionary indicating the result of the operation, either + {'CANCELLED'} if no operations are available or {'FINISHED'} if the + operation was successfully executed. + """ + + scene = context.scene + try: + if len(scene.cam_operations) == 0: + return {'CANCELLED'} + active_op = scene.cam_operations[scene.cam_active_operation] + active_op_object = bpy.data.objects[active_op.name] + scene.objects.active = active_op_object + bpy.ops.object.delete(True) + except: + pass + + ao = scene.cam_operations[scene.cam_active_operation] + print(was_hidden_dict) + if ao.name in was_hidden_dict: + del was_hidden_dict[ao.name] + + scene.cam_operations.remove(scene.cam_active_operation) + if scene.cam_active_operation > 0: + scene.cam_active_operation -= 1 + + return {'FINISHED'}
+
+ + + +# move cam operation in the list up or down +
+[docs] +class CamOperationMove(Operator): + """Move CAM Operation""" +
+[docs] + bl_idname = "scene.cam_operation_move"
+ +
+[docs] + bl_label = "Move CAM Operation in List"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + +
+[docs] + direction: EnumProperty( + name='Direction', + items=( + ('UP', 'Up', ''), + ('DOWN', 'Down', '') + ), + description='Direction', + default='DOWN', + )
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.scene is not None
+ + +
+[docs] + def execute(self, context): + """Execute a camera operation based on the specified direction. + + This method modifies the active camera operation in the Blender context + based on the direction specified. If the direction is 'UP', it moves the + active operation up in the list, provided it is not already at the top. + Conversely, if the direction is not 'UP', it moves the active operation + down in the list, as long as it is not at the bottom. The method updates + the active operation index accordingly. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the operation has finished, with + the key 'FINISHED'. + """ + + # main(context) + a = bpy.context.scene.cam_active_operation + cops = bpy.context.scene.cam_operations + if self.direction == 'UP': + if a > 0: + cops.move(a, a - 1) + bpy.context.scene.cam_active_operation -= 1 + + else: + if a < len(cops) - 1: + cops.move(a, a + 1) + bpy.context.scene.cam_active_operation += 1 + + return {'FINISHED'}
+
+ + + +
+[docs] +class CamOrientationAdd(Operator): + """Add Orientation to CAM Operation, for Multiaxis Operations""" +
+[docs] + bl_idname = "scene.cam_orientation_add"
+ +
+[docs] + bl_label = "Add Orientation"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.scene is not None
+ + +
+[docs] + def execute(self, context): + """Execute the camera orientation operation in Blender. + + This function retrieves the active camera operation from the current + scene, creates an empty object to represent the camera orientation, and + adds it to a specified group. The empty object is named based on the + operation's name and the current count of objects in the group. The size + of the empty object is set to a predefined value for visibility. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the operation's completion status, + typically {'FINISHED'}. + """ + + s = bpy.context.scene + a = s.cam_active_operation + o = s.cam_operations[a] + gname = o.name + '_orientations' + bpy.ops.object.empty_add(type='ARROWS') + + oriob = bpy.context.active_object + oriob.empty_draw_size = 0.02 # 2 cm + + simple.addToGroup(oriob, gname) + oriob.name = 'ori_' + o.name + '.' + \ + str(len(bpy.data.collections[gname].objects)).zfill(3) + + return {'FINISHED'}
+
+ + + +
+[docs] +class CamBridgesAdd(Operator): + """Add Bridge Objects to Curve""" +
+[docs] + bl_idname = "scene.cam_bridges_add"
+ +
+[docs] + bl_label = "Add Bridges / Tabs"
+ +
+[docs] + bl_options = {'REGISTER', 'UNDO'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.scene is not None
+ + +
+[docs] + def execute(self, context): + """Execute the camera operation in the given context. + + This function retrieves the active camera operation from the current + scene and adds automatic bridges to it. It is typically called within + the context of a Blender operator to perform specific actions related to + camera operations. + + Args: + context: The context in which the operation is executed. + + Returns: + dict: A dictionary indicating the result of the operation, typically + containing the key 'FINISHED' to signify successful completion. + """ + + s = bpy.context.scene + a = s.cam_active_operation + o = s.cam_operations[a] + bridges.addAutoBridges(o) + return {'FINISHED'}
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/pack.html b/_modules/cam/pack.html new file mode 100644 index 000000000..cbfd382cb --- /dev/null +++ b/_modules/cam/pack.html @@ -0,0 +1,728 @@ + + + + + + + + + + cam.pack — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.pack

+"""CNC CAM 'pack.py' © 2012 Vilem Novak
+
+Takes all selected curves, converts them to polygons, offsets them by the pre-set margin
+then chooses a starting location possibly inside the already occupied area and moves and rotates the
+polygon out of the occupied area if one or more positions are found where the poly doesn't overlap,
+it is placed and added to the occupied area - allpoly
+Very slow and STUPID, a collision algorithm would be much much faster...
+"""
+
+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 (
+    constants,
+    polygon_utils_cam,
+    simple,
+    utils,
+)
+
+
+
+[docs] +def srotate(s, r, x, y): + """Rotate a polygon's coordinates around a specified point. + + This function takes a polygon and rotates its exterior coordinates + around a given point (x, y) by a specified angle (r) in radians. It uses + the Euler rotation to compute the new coordinates for each point in the + polygon's exterior. The resulting coordinates are then used to create a + new polygon. + + Args: + s (shapely.geometry.Polygon): The polygon to be rotated. + r (float): The angle of rotation in radians. + x (float): The x-coordinate of the point around which to rotate. + y (float): The y-coordinate of the point around which to rotate. + + Returns: + shapely.geometry.Polygon: A new polygon with the rotated coordinates. + """ + + ncoords = [] + e = Euler((0, 0, r)) + for p in s.exterior.coords: + v1 = Vector((p[0], p[1], 0)) + v2 = Vector((x, y, 0)) + v = v1 - v2 + v.rotate(e) + ncoords.append((v[0], v[1])) + + return sgeometry.Polygon(ncoords)
+ + + +
+[docs] +def packCurves(): + """Pack selected curves into a defined area based on specified settings. + + This function organizes selected curve objects in Blender by packing + them into a specified area defined by the camera pack settings. It + calculates the optimal positions for each curve while considering + parameters such as sheet size, fill direction, distance, tolerance, and + rotation. The function utilizes geometric operations to ensure that the + curves do not overlap and fit within the defined boundaries. The packed + curves are then transformed and their properties are updated + accordingly. The function performs the following steps: 1. Activates + speedup features if available. 2. Retrieves packing settings from the + current scene. 3. Processes each selected object to create polygons from + curves. 4. Attempts to place each polygon within the defined area while + avoiding overlaps and respecting the specified fill direction. 5. + Outputs the final arrangement of polygons. + """ + + if speedups.available: + speedups.enable() + t = time.time() + packsettings = bpy.context.scene.cam_pack + + sheetsizex = packsettings.sheet_x + sheetsizey = packsettings.sheet_y + direction = packsettings.sheet_fill_direction + distance = packsettings.distance + tolerance = packsettings.tolerance + rotate = packsettings.rotate + rotate_angle = packsettings.rotate_angle + + # in this, position, rotation, and actual poly will be stored. + polyfield = [] + for ob in bpy.context.selected_objects: + simple.activate(ob) + bpy.ops.object.make_single_user(type='SELECTED_OBJECTS') + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') + z = ob.location.z + bpy.ops.object.location_clear() + bpy.ops.object.rotation_clear() + + chunks = utils.curveToChunks(ob) + npolys = utils.chunksToShapely(chunks) + # add all polys in silh to one poly + poly = shapely.ops.unary_union(npolys) + + poly = poly.buffer(distance / 1.5, 8) + poly = poly.simplify(0.0003) + polyfield.append([[0, 0], 0.0, poly, ob, z]) + random.shuffle(polyfield) + # primitive layout here: + allpoly = prepared.prep(sgeometry.Polygon()) # main collision poly. + + shift = tolerance # one milimeter by now. + rotchange = rotate_angle # in radians + + xmin, ymin, xmax, ymax = polyfield[0][2].bounds + if direction == 'X': + mindist = -xmin + else: + mindist = -ymin + i = 0 + p = polyfield[0][2] + placedpolys = [] + rotcenter = sgeometry.Point(0, 0) + for pf in polyfield: + print(i) + rot = 0 + porig = pf[2] + placed = False + xmin, ymin, xmax, ymax = p.bounds + if direction == 'X': + x = mindist + y = -ymin + if direction == 'Y': + x = -xmin + y = mindist + + itera = 0 + best = None + hits = 0 + besthit = None + while not placed: + # swap x and y, and add to x + # print(x,y) + p = porig + + if rotate: + ptrans = affinity.rotate(p, rot, origin=rotcenter, use_radians=True) + ptrans = affinity.translate(ptrans, x, y) + else: + ptrans = affinity.translate(p, x, y) + xmin, ymin, xmax, ymax = ptrans.bounds + # print(iter,p.bounds) + + if xmin > 0 and ymin > 0 and ( + (direction == 'Y' and xmax < sheetsizex) or (direction == 'X' and ymax < sheetsizey)): + if not allpoly.intersects(ptrans): + # we do more good solutions, choose best out of them: + hits += 1 + if best is None: + best = [x, y, rot, xmax, ymax] + besthit = hits + if direction == 'X': + if xmax < best[3]: + best = [x, y, rot, xmax, ymax] + besthit = hits + elif ymax < best[4]: + best = [x, y, rot, xmax, ymax] + besthit = hits + + if hits >= 15 or ( + itera > 20000 and hits > 0): # here was originally more, but 90% of best solutions are still 1 + placed = True + pf[3].location.x = best[0] + pf[3].location.y = best[1] + pf[3].location.z = pf[4] + pf[3].rotation_euler.z = best[2] + + pf[3].select_set(state=True) + + # print(mindist) + mindist = mindist - 0.5 * (xmax - xmin) + # print(mindist) + # print(iter) + + # reset polygon to best position here: + ptrans = affinity.rotate(porig, best[2], rotcenter, use_radians=True) + ptrans = affinity.translate(ptrans, best[0], best[1]) + + print(best[0], best[1], itera) + placedpolys.append(ptrans) + allpoly = prepared.prep(sgeometry.MultiPolygon(placedpolys)) + + # cleanup allpoly + print(itera, hits, besthit) + if not placed: + if direction == 'Y': + x += shift + mindist = y + if xmax + shift > sheetsizex: + x = x - xmin + y += shift + if direction == 'X': + y += shift + mindist = x + if ymax + shift > sheetsizey: + y = y - ymin + x += shift + if rotate: + rot += rotchange + itera += 1 + i += 1 + t = time.time() - t + + polygon_utils_cam.shapelyToCurve('test', sgeometry.MultiPolygon(placedpolys), 0) + print(t)
+ + + +
+[docs] +class PackObjectsSettings(PropertyGroup): + """stores all data for machines""" + +
+[docs] + 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", + )
+ +
+[docs] + sheet_x: FloatProperty( + name="X Size", + description="Sheet size", + min=0.001, + max=10, + default=0.5, + precision=constants.PRECISION, + unit="LENGTH", + )
+ +
+[docs] + sheet_y: FloatProperty( + name="Y Size", + description="Sheet size", + min=0.001, + max=10, + default=0.5, + precision=constants.PRECISION, + unit="LENGTH", + )
+ +
+[docs] + 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", + )
+ +
+[docs] + 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", + )
+ +
+[docs] + rotate: BoolProperty( + name="Enable Rotation", + description="Enable rotation of elements", + default=True, + )
+ +
+[docs] + 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", + )
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/parametric.html b/_modules/cam/parametric.html new file mode 100644 index 000000000..4c855762e --- /dev/null +++ b/_modules/cam/parametric.html @@ -0,0 +1,655 @@ + + + + + + + + + + cam.parametric — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.parametric

+"""CNC CAM 'parametric.py' © 2019 Devon (Gorialis) R
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+
+Summary:
+Create a Blender curve from a 3D parametric function.
+This allows for a 3D plot to be made of the function, which can be converted into a mesh.
+
+I have documented the inner workings here, but if you're not interested and just want to
+suit this to your own function, scroll down to the bottom and edit the `f(t)` function and
+the iteration count to your liking.
+
+This code has been checked to work on Blender 2.92.
+"""
+
+from math import pow
+
+import bpy
+from mathutils import Vector
+
+
+
+[docs] +def derive_bezier_handles(a, b, c, d, tb, tc): + """ + Derives bezier handles by using the start and end of the curve with 2 intermediate + points to use for interpolation. + + :param a: + The start point. + :param b: + The first mid-point, located at `tb` on the bezier segment, where 0 < `tb` < 1. + :param c: + The second mid-point, located at `tc` on the bezier segment, where 0 < `tc` < 1. + :param d: + The end point. + :param tb: + The position of the first point in the bezier segment. + :param tc: + The position of the second point in the bezier segment. + :return: + A tuple of the two intermediate handles, that is, the right handle of the start point + and the left handle of the end point. + """ + + # Calculate matrix coefficients + 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 - (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)) + bezier_c = matrix_determinant * ((-matrix_c * final_b) + (matrix_a * final_c)) + + # Return the handle points + return bezier_b, bezier_c
+ + + +
+[docs] +def create_parametric_curve( + function, + *args, + min: float = 0.0, + max: float = 1.0, + use_cubic: bool = True, + iterations: int = 8, + resolution_u: int = 10, + **kwargs): + """ + Creates a Blender bezier curve object from a parametric function. + This "plots" the function in 3D space from `min <= t <= max`. + + :param function: + The function to plot as a Blender curve. + + This function should take in a float value of `t` and return a 3-item tuple or list + of the X, Y and Z coordinates at that point: + `function(t) -> (x, y, z)` + + `t` is plotted according to `min <= t <= max`, but if `use_cubic` is enabled, this function + needs to be able to take values less than `min` and greater than `max`. + :param *args: + Additional positional arguments to be passed to the plotting function. + These are not required. + :param use_cubic: + Whether or not to calculate the cubic bezier handles as to create smoother splines. + Turning this off reduces calculation time and memory usage, but produces more jagged + splines with sharp edges. + :param iterations: + The 'subdivisions' of the parametric to plot. + Setting this higher produces more accurate curves but increases calculation time and + memory usage. + :param resolution_u: + The preview surface resolution in the U direction of the bezier curve. + Setting this to a higher value produces smoother curves in rendering, and increases the + number of vertices the curve will get if converted into a mesh (e.g. for edge looping) + :param **kwargs: + Additional keyword arguments to be passed to the plotting function. + These are not required. + :return: + The new Blender object. + """ + + # Create the Curve to populate with points. + curve = bpy.data.curves.new('Parametric', type='CURVE') + curve.dimensions = '3D' + curve.resolution_u = 30 + + # Add a new spline and give it the appropriate amount of points + spline = curve.splines.new('BEZIER') + spline.bezier_points.add(iterations) + + if use_cubic: + points = [ + function(((i - 3) / (3 * iterations)) * (max - min) + min, *args, **kwargs) + for i in range((3 * (iterations + 2)) + 1) + ] + + # Convert intermediate points into handles + for i in range(iterations + 2): + a = points[(3 * i)] + b = points[(3 * i) + 1] + c = points[(3 * i) + 2] + d = points[(3 * i) + 3] + + bezier_bx, bezier_cx = derive_bezier_handles(a[0], b[0], c[0], d[0], 1 / 3, 2 / 3) + bezier_by, bezier_cy = derive_bezier_handles(a[1], b[1], c[1], d[1], 1 / 3, 2 / 3) + bezier_bz, bezier_cz = derive_bezier_handles(a[2], b[2], c[2], d[2], 1 / 3, 2 / 3) + + points[(3 * i) + 1] = (bezier_bx, bezier_by, bezier_bz) + points[(3 * i) + 2] = (bezier_cx, bezier_cy, bezier_cz) + + # Set point coordinates and handles + for i in range(iterations + 1): + spline.bezier_points[i].co = points[3 * (i + 1)] + + spline.bezier_points[i].handle_left_type = 'FREE' + spline.bezier_points[i].handle_left = Vector(points[(3 * (i + 1)) - 1]) + + spline.bezier_points[i].handle_right_type = 'FREE' + spline.bezier_points[i].handle_right = Vector(points[(3 * (i + 1)) + 1]) + + else: + points = [function(i / iterations, *args, **kwargs) for i in range(iterations + 1)] + + # Set point coordinates, disable handles + for i in range(iterations + 1): + spline.bezier_points[i].co = points[i] + spline.bezier_points[i].handle_left_type = 'VECTOR' + spline.bezier_points[i].handle_right_type = 'VECTOR' + + # Create the Blender object and link it to the scene + curve_object = bpy.data.objects.new('Parametric', curve) + context = bpy.context + scene = context.scene + link_object = scene.collection.objects.link + link_object(curve_object) + + # Return the new object + return curve_object
+ + + +
+[docs] +def make_edge_loops(*objects): + """ + Turns a set of Curve objects into meshes, creates vertex groups, + and merges them into a set of edge loops. + + :param *objects: + Positional arguments for each object to be converted and merged. + """ + context = bpy.context + scene = context.scene + + mesh_objects = [] + vertex_groups = [] + + # Convert all curves to meshes + for obj in objects: + # Unlink old object + unlink_object(obj) + + # Convert curve to a mesh + if bpy.app.version >= (2, 80): + new_mesh = obj.to_mesh().copy() + else: + new_mesh = obj.to_mesh(scene, False, 'PREVIEW') + + # Store name and matrix, then fully delete the old object + name = obj.name + matrix = obj.matrix_world + bpy.data.objects.remove(obj) + + # Attach the new mesh to a new object with the old name + new_object = bpy.data.objects.new(name, new_mesh) + new_object.matrix_world = matrix + + # Make a new vertex group from all vertices on this mesh + vertex_group = new_object.vertex_groups.new(name=name) + vertex_group.add(range(len(new_mesh.vertices)), 1.0, 'ADD') + + vertex_groups.append(vertex_group) + + # Link our new object + link_object(new_object) + + # Add it to our list + mesh_objects.append(new_object) + + # Make a new context + ctx = context.copy() + + # Select our objects in the context + ctx['active_object'] = mesh_objects[0] + ctx['selected_objects'] = mesh_objects + if bpy.app.version >= (2, 80): + ctx['selected_editable_objects'] = mesh_objects + else: + ctx['selected_editable_bases'] = [scene.object_bases[o.name] for o in mesh_objects] + + # Join them together + bpy.ops.object.join(ctx)
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/pattern.html b/_modules/cam/pattern.html new file mode 100644 index 000000000..30bad947e --- /dev/null +++ b/_modules/cam/pattern.html @@ -0,0 +1,1019 @@ + + + + + + + + + + cam.pattern — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.pattern

+"""CNC CAM 'pattern.py' © 2012 Vilem Novak
+
+Functions to read CAM path patterns and return CAM path chunks.
+"""
+
+from math import (
+    ceil,
+    floor,
+    pi,
+    sqrt
+)
+import time
+
+import numpy
+
+import bpy
+from mathutils import (
+    Euler,
+    Vector
+)
+
+from .cam_chunk import (
+    camPathChunk,
+    camPathChunkBuilder,
+    chunksRefine,
+    parentChildDist,
+    shapelyToChunks,
+)
+from .simple import progress
+
+
+
+[docs] +def getPathPatternParallel(o, angle): + """Generate path chunks for parallel movement based on object dimensions + and angle. + + This function calculates a series of path chunks for a given object, + taking into account its dimensions and the specified angle. It utilizes + both a traditional method and an alternative algorithm (currently + disabled) to generate these paths. The paths are constructed by + iterating over calculated vectors and applying transformations based on + the object's properties. The resulting path chunks can be used for + various movement types, including conventional and climb movements. + + Args: + o (object): An object containing properties such as dimensions and movement type. + angle (float): The angle to rotate the path generation. + + Returns: + list: A list of path chunks generated based on the object's dimensions and + angle. + """ + + zlevel = 1 + pathd = o.dist_between_paths + pathstep = o.dist_along_paths + pathchunks = [] + + xm = (o.max.x + o.min.x) / 2 + ym = (o.max.y + o.min.y) / 2 + vm = Vector((xm, ym, 0)) + xdim = o.max.x - o.min.x + ydim = o.max.y - o.min.y + dim = (xdim + ydim) / 2.0 + e = Euler((0, 0, angle)) + reverse = False + if bpy.app.debug_value == 0: # by default off + # this is the original pattern method, slower, but well tested: + dirvect = Vector((0, 1, 0)) + dirvect.rotate(e) + dirvect.normalize() + dirvect *= pathstep + for a in range(int(-dim / pathd), + int(dim / pathd)): # this is highly ineffective, computes path2x the area needed... + chunk = camPathChunkBuilder([]) + v = Vector((a * pathd, int(-dim / pathstep) * pathstep, 0)) + v.rotate(e) + # shifting for the rotation, so pattern rotates around middle... + v += vm + for b in range(int(-dim / pathstep), int(dim / pathstep)): + v += dirvect + + if v.x > o.min.x and v.x < o.max.x and v.y > o.min.y and v.y < o.max.y: + chunk.points.append((v.x, v.y, zlevel)) + if (reverse and o.movement.type == 'MEANDER') or ( + o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW') or ( + o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW'): + chunk.points.reverse() + + if len(chunk.points) > 0: + pathchunks.append(chunk.to_chunk()) + if len(pathchunks) > 1 and reverse and o.movement.parallel_step_back and not o.use_layers: + # parallel step back - for finishing, best with climb movement, saves cutter life by going into + # material with climb, while using move back on the surface to improve finish + # (which would otherwise be a conventional move in the material) + + if o.movement.type == 'CONVENTIONAL' or o.movement.type == 'CLIMB': + pathchunks[-2].reverse() + changechunk = pathchunks[-1] + pathchunks[-1] = pathchunks[-2] + pathchunks[-2] = changechunk + + reverse = not reverse + # print (chunk.points) + else: # alternative algorithm with numpy, didn't work as should so blocked now... + + v = Vector((0, 1, 0)) + v.rotate(e) + e1 = Euler((0, 0, -pi / 2)) + v1 = v.copy() + v1.rotate(e1) + + axis_across_paths = numpy.array((numpy.arange(int(-dim / pathd), int(dim / pathd)) * pathd * v1.x + xm, + numpy.arange(int(-dim / pathd), + int(dim / pathd)) * pathd * v1.y + ym, + numpy.arange(int(-dim / pathd), int(dim / pathd)) * 0)) + + axis_along_paths = numpy.array((numpy.arange(int(-dim / pathstep), int(dim / pathstep)) * pathstep * v.x, + numpy.arange(int(-dim / pathstep), + int(dim / pathstep)) * pathstep * v.y, + numpy.arange(int(-dim / pathstep), + int(dim / pathstep)) * 0 + zlevel)) # rotate this first + progress(axis_along_paths) + chunks = [] + for a in range(0, len(axis_across_paths[0])): + # progress(chunks[a,...,...].shape) + # progress(axis_along_paths.shape) + nax = axis_along_paths.copy() + # progress(nax.shape) + nax[0] += axis_across_paths[0][a] + nax[1] += axis_across_paths[1][a] + # progress(a) + # progress(nax.shape) + # progress(chunks.shape) + # progress(chunks[...,a,...].shape) + xfitmin = nax[0] > o.min.x + xfitmax = nax[0] < o.max.x + xfit = xfitmin & xfitmax + # print(xfit,nax) + nax = numpy.array([nax[0][xfit], nax[1][xfit], nax[2][xfit]]) + yfitmin = nax[1] > o.min.y + yfitmax = nax[1] < o.max.y + yfit = yfitmin & yfitmax + nax = numpy.array([nax[0][yfit], nax[1][yfit], nax[2][yfit]]) + chunks.append(nax.swapaxes(0, 1)) + # chunks + pathchunks = [] + print("WOO") + for ch in chunks: + ch = ch.tolist() + pathchunks.append(camPathChunk(ch)) + # print (ch) + return pathchunks
+ + + +
+[docs] +def getPathPattern(operation): + """Generate a path pattern based on the specified operation strategy. + + This function constructs a path pattern for a given operation by + analyzing its parameters and applying different strategies such as + 'PARALLEL', 'CROSS', 'BLOCK', 'SPIRAL', 'CIRCLES', and 'OUTLINEFILL'. + Each strategy dictates how the path is built, utilizing various + geometric calculations and conditions to ensure the path adheres to the + specified operational constraints. The function also handles the + orientation and direction of the path based on the movement settings + provided in the operation. + + Args: + operation (object): An object containing parameters for path generation, + including strategy, movement type, and geometric bounds. + + Returns: + list: A list of path chunks representing the generated path pattern. + """ + + o = operation + t = time.time() + progress('Building Path Pattern') + minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z + + pathchunks = [] + + zlevel = 1 # minz#this should do layers... + if o.strategy == 'PARALLEL': + pathchunks = getPathPatternParallel(o, o.parallel_angle) + elif o.strategy == 'CROSS': + + pathchunks.extend(getPathPatternParallel(o, o.parallel_angle)) + pathchunks.extend(getPathPatternParallel(o, o.parallel_angle - pi / 2.0)) + + elif o.strategy == 'BLOCK': + + pathd = o.dist_between_paths + pathstep = o.dist_along_paths + maxxp = maxx + maxyp = maxy + minxp = minx + minyp = miny + x = 0.0 + y = 0.0 + incx = 1 + incy = 0 + chunk = camPathChunkBuilder([]) + i = 0 + while maxxp - minxp > 0 and maxyp - minyp > 0: + + y = minyp + for a in range(ceil(minxp / pathstep), ceil(maxxp / pathstep), 1): + x = a * pathstep + chunk.points.append((x, y, zlevel)) + + if i > 0: + minxp += pathd + chunk.points.append((maxxp, minyp, zlevel)) + + x = maxxp + + for a in range(ceil(minyp / pathstep), ceil(maxyp / pathstep), 1): + y = a * pathstep + chunk.points.append((x, y, zlevel)) + + minyp += pathd + chunk.points.append((maxxp, maxyp, zlevel)) + + y = maxyp + for a in range(floor(maxxp / pathstep), ceil(minxp / pathstep), -1): + x = a * pathstep + chunk.points.append((x, y, zlevel)) + + maxxp -= pathd + chunk.points.append((minxp, maxyp, zlevel)) + + x = minxp + for a in range(floor(maxyp / pathstep), ceil(minyp / pathstep), -1): + y = a * pathstep + chunk.points.append((x, y, zlevel)) + chunk.points.append((minxp, minyp, zlevel)) + + maxyp -= pathd + + i += 1 + if o.movement.insideout == 'INSIDEOUT': + chunk.points.reverse() + if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CW') or ( + o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CCW'): + for si in range(0, len(chunk.points)): + s = chunk.points[si] + chunk.points[si] = (o.max.x + o.min.x - s[0], s[1], s[2]) + pathchunks = [chunk.to_chunk()] + + elif o.strategy == 'SPIRAL': + chunk = camPathChunkBuilder([]) + pathd = o.dist_between_paths + pathstep = o.dist_along_paths + midx = (o.max.x + o.min.x) / 2 + midy = (o.max.y + o.min.y) / 2 + x = pathd / 4 + y = pathd / 4 + v = Vector((pathd / 4, 0, 0)) + + # progress(x,y,midx,midy) + e = Euler((0, 0, 0)) + # 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 + # v.y=y-midy + offset = 2 * v.length * pi + e.z = 2 * pi * (pathstep / offset) + v.rotate(e) + + v.length = (v.length + pathd / (offset / pathstep)) + # progress(v.x,v.y) + if o.max.x > midx + v.x > o.min.x and o.max.y > midy + v.y > o.min.y: + chunk.points.append((midx + v.x, midy + v.y, zlevel)) + else: + pathchunks.append(chunk.to_chunk()) + chunk = camPathChunkBuilder([]) + if len(chunk.points) > 0: + pathchunks.append(chunk.to_chunk()) + if o.movement.insideout == 'OUTSIDEIN': + pathchunks.reverse() + for chunk in pathchunks: + if o.movement.insideout == 'OUTSIDEIN': + chunk.reverse() + + if (o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW') or ( + o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW'): + # TODO + chunk.flipX(o.max.x+o.min.x) + # for si in range(0, len(chunk.points)): + # s = chunk.points[si] + # chunk.points[si] = (o.max.x + o.min.x - s[0], s[1], s[2]) + + elif o.strategy == 'CIRCLES': + + pathd = o.dist_between_paths + pathstep = o.dist_along_paths + midx = (o.max.x + o.min.x) / 2 + midy = (o.max.y + o.min.y) / 2 + rx = o.max.x - o.min.x + ry = o.max.y - o.min.y + maxr = sqrt(rx * rx + ry * ry) + + # progress(x,y,midx,midy) + e = Euler((0, 0, 0)) + # pi = pi + chunk = camPathChunkBuilder([]) + chunk.points.append((midx, midy, zlevel)) + pathchunks.append(chunk.to_chunk()) + r = 0 + + while r < maxr: + r += pathd + chunk = camPathChunkBuilder([]) + firstchunk = chunk + v = Vector((-r, 0, 0)) + steps = 2 * pi * r / pathstep + e.z = 2 * pi / steps + laststepchunks = [] + currentstepchunks = [] + for a in range(0, int(steps)): + laststepchunks = currentstepchunks + currentstepchunks = [] + + if o.max.x > midx + v.x > o.min.x and o.max.y > midy + v.y > o.min.y: + chunk.points.append((midx + v.x, midy + v.y, zlevel)) + else: + if len(chunk.points) > 0: + chunk.closed = False + chunk = chunk.to_chunk() + pathchunks.append(chunk) + currentstepchunks.append(chunk) + chunk = camPathChunkBuilder([]) + v.rotate(e) + + if len(chunk.points) > 0: + chunk.points.append(firstchunk.points[0]) + if chunk == firstchunk: + chunk.closed = True + chunk = chunk.to_chunk() + pathchunks.append(chunk) + currentstepchunks.append(chunk) + chunk = camPathChunkBuilder([]) + for ch in laststepchunks: + for p in currentstepchunks: + parentChildDist(p, ch, o) + + if o.movement.insideout == 'OUTSIDEIN': + pathchunks.reverse() + for chunk in pathchunks: + if o.movement.insideout == 'OUTSIDEIN': + chunk.reverse() + if (o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW') or ( + o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW'): + chunk.reverse() + # pathchunks=sortChunks(pathchunks,o)not until they get hierarchy parents! + elif o.strategy == 'OUTLINEFILL': + + polys = o.silhouete.geoms + pathchunks = [] + chunks = [] + for p in polys: + p = p.buffer(-o.dist_between_paths / 10, o.optimisation.circle_detail) + # first, move a bit inside, because otherwise the border samples go crazy very often changin between + # hit/non hit and making too many jumps in the path. + chunks.extend(shapelyToChunks(p, 0)) + + pathchunks.extend(chunks) + lastchunks = chunks + firstchunks = chunks + + approxn = (min(maxx - minx, maxy - miny) / o.dist_between_paths) / 2 + i = 0 + + for porig in polys: + p = porig + while not p.is_empty: + p = p.buffer(-o.dist_between_paths, o.optimisation.circle_detail) + if not p.is_empty: + + nchunks = shapelyToChunks(p, zlevel) + + if o.movement.insideout == 'INSIDEOUT': + parentChildDist(lastchunks, nchunks, o) + else: + parentChildDist(nchunks, lastchunks, o) + pathchunks.extend(nchunks) + lastchunks = nchunks + percent = int(i / approxn * 100) + progress('outlining polygons ', percent) + i += 1 + pathchunks.reverse() + if not o.inverse: # dont do ambient for inverse milling + lastchunks = firstchunks + for p in polys: + d = o.dist_between_paths + steps = o.ambient_radius / o.dist_between_paths + for a in range(0, int(steps)): + dist = d + if a == int(o.cutter_diameter / 2 / o.dist_between_paths): + if o.optimisation.use_exact: + dist += o.optimisation.pixsize * 0.85 + # this is here only because silhouette is still done with zbuffer method, + # even if we use bullet collisions. + else: + dist += o.optimisation.pixsize * 2.5 + p = p.buffer(dist, o.optimisation.circle_detail) + if not p.is_empty: + nchunks = shapelyToChunks(p, zlevel) + if o.movement.insideout == 'INSIDEOUT': + parentChildDist(nchunks, lastchunks, o) + else: + parentChildDist(lastchunks, nchunks, o) + pathchunks.extend(nchunks) + lastchunks = nchunks + + if o.movement.insideout == 'OUTSIDEIN': + pathchunks.reverse() + + for chunk in pathchunks: + if o.movement.insideout == 'OUTSIDEIN': + chunk.reverse() + if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CW') or ( + o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CCW'): + chunk.reverse() + + chunksRefine(pathchunks, o) + progress(time.time() - t) + return pathchunks
+ + + +
+[docs] +def getPathPattern4axis(operation): + """Generate path patterns for a specified operation along a rotary axis. + + This function constructs a series of path chunks based on the provided + operation's parameters, including the rotary axis, strategy, and + dimensions. It calculates the necessary angles and positions for the + cutter based on the specified strategy (PARALLELR, PARALLEL, or HELIX) + and generates the corresponding path chunks for machining operations. + + Args: + operation (object): An object containing parameters for the machining operation, + including min and max coordinates, rotary axis configuration, + distance settings, and movement strategy. + + Returns: + list: A list of path chunks generated for the specified operation. + """ + + o = operation + t = time.time() + progress('Building Path Pattern') + minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z + pathchunks = [] + zlevel = 1 # minz#this should do layers... + + # set axes for various options, Z option is obvious nonsense now. + if o.rotary_axis_1 == 'X': + a1 = 0 + a2 = 1 + a3 = 2 + if o.rotary_axis_1 == 'Y': + a1 = 1 + a2 = 0 + a3 = 2 + if o.rotary_axis_1 == 'Z': + a1 = 2 + a2 = 0 + a3 = 1 + + o.max.z = o.maxz + # set radius for all types of operation + radius = max(o.max.z, 0.0001) + radiusend = o.min.z + + mradius = max(radius, radiusend) + circlesteps = (mradius * pi * 2) / o.dist_along_paths + circlesteps = max(4, circlesteps) + anglestep = 2 * pi / circlesteps + # generalized rotation + e = Euler((0, 0, 0)) + e[a1] = anglestep + + # generalized length of the operation + maxl = o.max[a1] + minl = o.min[a1] + steps = (maxl - minl) / o.dist_between_paths + + # set starting positions for cutter e.t.c. + cutterstart = Vector((0, 0, 0)) + cutterend = Vector((0, 0, 0)) # end point for casting + + if o.strategy4axis == 'PARALLELR': + + for a in range(0, floor(steps) + 1): + chunk = camPathChunkBuilder([]) + + cutterstart[a1] = o.min[a1] + a * o.dist_between_paths + cutterend[a1] = cutterstart[a1] + + cutterstart[a2] = 0 # radius + cutterend[a2] = 0 # radiusend + + cutterstart[a3] = radius + cutterend[a3] = radiusend + + for b in range(0, floor(circlesteps) + 1): + # print(cutterstart,cutterend) + chunk.startpoints.append(cutterstart.to_tuple()) + chunk.endpoints.append(cutterend.to_tuple()) + rot = [0, 0, 0] + rot[a1] = a * 2 * pi + b * anglestep + + chunk.rotations.append(rot) + cutterstart.rotate(e) + cutterend.rotate(e) + + chunk.depth = radiusend - radius + # last point = first + chunk.startpoints.append(chunk.startpoints[0]) + chunk.endpoints.append(chunk.endpoints[0]) + chunk.rotations.append(chunk.rotations[0]) + + pathchunks.append(chunk.to_chunk()) + + if o.strategy4axis == 'PARALLEL': + circlesteps = (mradius * pi * 2) / o.dist_between_paths + steps = (maxl - minl) / o.dist_along_paths + + anglestep = 2 * pi / circlesteps + # generalized rotation + e = Euler((0, 0, 0)) + e[a1] = anglestep + + reverse = False + + for b in range(0, floor(circlesteps) + 1): + chunk = camPathChunkBuilder([]) + cutterstart[a2] = 0 + cutterstart[a3] = radius + + cutterend[a2] = 0 + cutterend[a3] = radiusend + + e[a1] = anglestep * b + + cutterstart.rotate(e) + cutterend.rotate(e) + + for a in range(0, floor(steps) + 1): + cutterstart[a1] = o.min[a1] + a * o.dist_along_paths + cutterend[a1] = cutterstart[a1] + chunk.startpoints.append(cutterstart.to_tuple()) + chunk.endpoints.append(cutterend.to_tuple()) + rot = [0, 0, 0] + rot[a1] = b * anglestep + chunk.rotations.append(rot) + + chunk = chunk.to_chunk() + chunk.depth = radiusend - radius + pathchunks.append(chunk) + + if (reverse and o.movement.type == 'MEANDER') or ( + o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW') or ( + o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW'): + chunk.reverse() + + reverse = not reverse + + if o.strategy4axis == 'HELIX': + print('helix') + + a1step = o.dist_between_paths / circlesteps + + chunk = camPathChunkBuilder([]) # only one chunk, init here + + for a in range(0, floor(steps) + 1): + + cutterstart[a1] = o.min[a1] + a * o.dist_between_paths + cutterend[a1] = cutterstart[a1] + cutterstart[a2] = 0 + cutterstart[a3] = radius + cutterend[a3] = radiusend + + for b in range(0, floor(circlesteps) + 1): + # print(cutterstart,cutterend) + cutterstart[a1] += a1step + cutterend[a1] += a1step + chunk.startpoints.append(cutterstart.to_tuple()) + chunk.endpoints.append(cutterend.to_tuple()) + + rot = [0, 0, 0] + rot[a1] = a * 2 * pi + b * anglestep + chunk.rotations.append(rot) + + cutterstart.rotate(e) + cutterend.rotate(e) + + chunk = chunk.to_chunk() + chunk.depth = radiusend - radius + + pathchunks.append(chunk) + # print(chunk.startpoints) + # print(pathchunks) + # sprint(len(pathchunks)) + # print(o.strategy4axis) + return pathchunks
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/polygon_utils_cam.html b/_modules/cam/polygon_utils_cam.html new file mode 100644 index 000000000..f042e6915 --- /dev/null +++ b/_modules/cam/polygon_utils_cam.html @@ -0,0 +1,662 @@ + + + + + + + + + + cam.polygon_utils_cam — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.polygon_utils_cam

+"""CNC CAM 'polygon_utils_cam.py' © 2012 Vilem Novak
+
+Functions to handle shapely operations and conversions - curve, coords, polygon
+"""
+
+from math import pi
+
+import shapely
+from shapely.geometry import polygon as spolygon
+from shapely import geometry as sgeometry
+
+from mathutils import Euler, Vector
+try:
+    import bl_ext.blender_org.simplify_curves_plus as curve_simplify
+except ImportError:
+    pass
+
+
+
+[docs] +SHAPELY = True
+ + + +
+[docs] +def Circle(r, np): + """Generate a circle defined by a given radius and number of points. + + This function creates a polygon representing a circle by generating a + list of points based on the specified radius and the number of points + (np). It uses vector rotation to calculate the coordinates of each point + around the circle. The resulting points are then used to create a + polygon object. + + Args: + r (float): The radius of the circle. + np (int): The number of points to generate around the circle. + + Returns: + spolygon.Polygon: A polygon object representing the circle. + """ + + c = [] + 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) + + p = spolygon.Polygon(c) + return p
+ + + +
+[docs] +def shapelyRemoveDoubles(p, optimize_threshold): + """Remove duplicate points from the boundary of a shape. + + This function simplifies the boundary of a given shape by removing + duplicate points using the Ramer-Douglas-Peucker algorithm. It iterates + through each contour of the shape, applies the simplification, and adds + the resulting contours to a new shape. The optimization threshold can be + adjusted to control the level of simplification. + + Args: + p (Shape): The shape object containing boundaries to be simplified. + optimize_threshold (float): A threshold value that influences the + simplification process. + + Returns: + Shape: A new shape object with simplified boundaries. + """ + + optimize_threshold *= 0.000001 + + soptions = ['distance', 'distance', 0.0, 5, optimize_threshold, 5, optimize_threshold] + for ci, c in enumerate(p.boundary): # in range(0,len(p)): + + veclist = [] + for v in c: + veclist.append(Vector((v[0], v[1]))) + s = curve_simplify.simplify_RDP(veclist, soptions) + nc = [] + for i in range(0, len(s)): + nc.append(c[s[i]]) + + if len(nc) > 2: + pnew.addContour(nc, p.isHole(ci)) + else: + pnew.addContour(p[ci], p.isHole(ci)) + return pnew
+ + + +
+[docs] +def shapelyToMultipolygon(anydata): + """Convert a Shapely geometry to a MultiPolygon. + + This function takes a Shapely geometry object and converts it to a + MultiPolygon. If the input geometry is already a MultiPolygon, it + returns it as is. If the input is a Polygon and not empty, it wraps the + Polygon in a MultiPolygon. If the input is an empty Polygon, it returns + an empty MultiPolygon. For any other geometry type, it prints a message + indicating that the conversion was aborted and returns an empty + MultiPolygon. + + Args: + anydata (shapely.geometry.base.BaseGeometry): A Shapely geometry object + + Returns: + shapely.geometry.MultiPolygon: A MultiPolygon representation of the input + geometry. + """ + print("geometry type: ", anydata.geom_type) + print("anydata empty? ", anydata.is_empty) + ## bug: empty mesh circle makes anydata empty: geometry type 'GeometryCollection' + if anydata.geom_type == 'MultiPolygon': + return anydata + elif anydata.geom_type == 'Polygon': + if not anydata.is_empty: + return shapely.geometry.MultiPolygon([anydata]) + else: + return sgeometry.MultiPolygon() + else: + print('Shapely Conversion Aborted') + return sgeometry.MultiPolygon()
+ + + +
+[docs] +def shapelyToCoords(anydata): + """Convert a Shapely geometry object to a list of coordinates. + + This function takes a Shapely geometry object and extracts its + coordinates based on the geometry type. It handles various types of + geometries including Polygon, MultiPolygon, LineString, MultiLineString, + and GeometryCollection. If the geometry is empty or of type MultiPoint, + it returns an empty list. The coordinates are returned in a nested list + format, where each sublist corresponds to the exterior or interior + coordinates of the geometries. + + Args: + anydata (shapely.geometry.base.BaseGeometry): A Shapely geometry object + + Returns: + list: A list of coordinates extracted from the input geometry. + The structure of the list depends on the geometry type. + """ + + p = anydata + seq = [] + # print(p.type) + # print(p.geom_type) + if p.is_empty: + return seq + elif p.geom_type == 'Polygon': + + # print('polygon') + clen = len(p.exterior.coords) + # seq = sgeometry.asMultiLineString(p) + seq = [p.exterior.coords] + # print(len(p.interiors)) + for interior in p.interiors: + seq.append(interior.coords) + elif p.geom_type == 'MultiPolygon': + clen = 0 + seq = [] + for sp in p.geoms: + clen += len(sp.exterior.coords) + seq.append(sp.exterior.coords) + for interior in sp.interiors: + seq.append(interior.coords) + + elif p.geom_type == 'MultiLineString': + seq = [] + for linestring in p.geoms: + seq.append(linestring.coords) + elif p.geom_type == 'LineString': + seq = [] + seq.append(p.coords) + + elif p.geom_type == 'MultiPoint': + return + elif p.geom_type == 'GeometryCollection': + # print(dir(p)) + # print(p.geometryType, p.geom_type) + clen = 0 + seq = [] + # print(p.boundary.coordsd) + for sp in p.geoms: # TODO + clen += len(sp.exterior.coords) + seq.append(sp.exterior.coords) + for interior in sp.interiors: + seq.extend(interior.coords) + + return seq
+ + + +
+[docs] +def shapelyToCurve(name, p, z, cyclic = True): + """Create a 3D curve object in Blender from a Shapely geometry. + + This function takes a Shapely geometry and converts it into a 3D curve + object in Blender. It extracts the coordinates from the Shapely geometry + and creates a new curve object with the specified name. The curve is + created in the 3D space at the given z-coordinate, with a default weight + for the points. + + Args: + name (str): The name of the curve object to be created. + p (shapely.geometry): A Shapely geometry object from which to extract + coordinates. + z (float): The z-coordinate for all points of the curve. + + Returns: + bpy.types.Object: The newly created curve object in Blender. + """ + + import bpy + import bmesh + from bpy_extras import object_utils + verts = [] + edges = [] + vi = 0 + ci = 0 + # for c in p.exterior.coords: + + # print(p.type) + seq = shapelyToCoords(p) + w = 1 # weight + + curvedata = bpy.data.curves.new(name=name, type='CURVE') + curvedata.dimensions = '3D' + + objectdata = bpy.data.objects.new(name, curvedata) + objectdata.location = (0, 0, 0) # object origin + bpy.context.collection.objects.link(objectdata) + + for c in seq: + polyline = curvedata.splines.new('POLY') + polyline.points.add(len(c) - 1) + for num in range(len(c)): + x, y = c[num][0], c[num][1] + polyline.points[num].co = (x, y, z, w) + + bpy.context.view_layer.objects.active = objectdata + objectdata.select_set(state=True) + + for c in objectdata.data.splines: + c.use_cyclic_u = cyclic + + return objectdata # bpy.context.active_object
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/preset_managers.html b/_modules/cam/preset_managers.html new file mode 100644 index 000000000..0c6e57e99 --- /dev/null +++ b/_modules/cam/preset_managers.html @@ -0,0 +1,733 @@ + + + + + + + + + + cam.preset_managers — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.preset_managers

+"""CNC CAM 'preset_managers.py'
+
+Operators and Menus for CAM Machine, Cutter and Operation Presets.
+"""
+
+import bpy
+from bl_operators.presets import AddPresetBase
+from bpy.types import (
+    Menu,
+    Operator,
+)
+
+
+
+[docs] +class CAM_CUTTER_MT_presets(Menu): +
+[docs] + bl_label = "Cutter Presets"
+ +
+[docs] + preset_subdir = "cam_cutters"
+ +
+[docs] + preset_operator = "script.execute_preset"
+ +
+[docs] + draw = Menu.draw_preset
+
+ + + +
+[docs] +class CAM_MACHINE_MT_presets(Menu): +
+[docs] + bl_label = "Machine Presets"
+ +
+[docs] + preset_subdir = "cam_machines"
+ +
+[docs] + preset_operator = "script.execute_preset"
+ +
+[docs] + draw = Menu.draw_preset
+ + + @classmethod +
+[docs] + def post_cb(cls, context): + addon_prefs = context.preferences.addons[__package__].preferences + name = cls.bl_label + filepath = bpy.utils.preset_find(name, + cls.preset_subdir, + display_name=True, + ext=".py") + addon_prefs.default_machine_preset = filepath + bpy.ops.wm.save_userpref()
+
+ + + +
+[docs] +class AddPresetCamCutter(AddPresetBase, Operator): + """Add a Cutter Preset""" +
+[docs] + bl_idname = "render.cam_preset_cutter_add"
+ +
+[docs] + bl_label = "Add Cutter Preset"
+ +
+[docs] + preset_menu = "CAM_CUTTER_MT_presets"
+ + +
+[docs] + preset_defines = [ + "d = bpy.context.scene.cam_operations[bpy.context.scene.cam_active_operation]" + ]
+ + +
+[docs] + preset_values = [ + "d.cutter_id", + "d.cutter_type", + "d.cutter_diameter", + "d.cutter_length", + "d.cutter_flutes", + "d.cutter_tip_angle", + "d.cutter_description", + ]
+ + +
+[docs] + preset_subdir = "cam_cutters"
+
+ + + +
+[docs] +class CAM_OPERATION_MT_presets(Menu): +
+[docs] + bl_label = "Operation Presets"
+ +
+[docs] + preset_subdir = "cam_operations"
+ +
+[docs] + preset_operator = "script.execute_preset"
+ +
+[docs] + draw = Menu.draw_preset
+
+ + + +
+[docs] +class AddPresetCamOperation(AddPresetBase, Operator): + """Add an Operation Preset""" +
+[docs] + bl_idname = "render.cam_preset_operation_add"
+ +
+[docs] + bl_label = "Add Operation Preset"
+ +
+[docs] + preset_menu = "CAM_OPERATION_MT_presets"
+ + +
+[docs] + preset_defines = [ + 'from pathlib import Path', + 'bpy.ops.scene.cam_operation_add()', + 'scene = bpy.context.scene', + 'o = scene.cam_operations[scene.cam_active_operation]', + "o.name = f'OP_{o.object_name}_{scene.cam_active_operation + 1}_{Path(__file__).stem}'", + ]
+ + +
+[docs] + 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' + ]
+ + +
+[docs] + preset_subdir = "cam_operations"
+
+ + + +
+[docs] +class AddPresetCamMachine(AddPresetBase, Operator): + """Add a Cam Machine Preset""" +
+[docs] + bl_idname = "render.cam_preset_machine_add"
+ +
+[docs] + bl_label = "Add Machine Preset"
+ +
+[docs] + preset_menu = "CAM_MACHINE_MT_presets"
+ + +
+[docs] + preset_defines = [ + "d = bpy.context.scene.cam_machine", + "s = bpy.context.scene.unit_settings" + ]
+ +
+[docs] + 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", + ]
+ + +
+[docs] + preset_subdir = "cam_machines"
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/puzzle_joinery.html b/_modules/cam/puzzle_joinery.html new file mode 100644 index 000000000..a0aa693bf --- /dev/null +++ b/_modules/cam/puzzle_joinery.html @@ -0,0 +1,1578 @@ + + + + + + + + + + cam.puzzle_joinery — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.puzzle_joinery

+"""CNC CAM 'puzzle_joinery.py' © 2021 Alain Pelletier
+
+Functions to add various puzzle joints as curves.
+"""
+
+from math import (
+    cos,
+    degrees,
+    pi,
+    sin,
+    sqrt,
+    tan,
+)
+
+import bpy
+
+from . import (
+    joinery,
+    simple,
+    utils,
+)
+
+
+[docs] +DT = 1.025
+ + + +
+[docs] +def finger(diameter, stem=2): + """Create a joint shape based on the specified diameter and stem. + + This function generates a 3D joint shape using Blender's curve + operations. It calculates the dimensions of a rectangle and an ellipse + based on the provided diameter and stem parameters. The function then + creates these shapes, duplicates and mirrors them, and performs boolean + operations to form the final joint shape. The resulting object is named + and cleaned up to ensure no overlapping vertices remain. + + Args: + diameter (float): The diameter of the tool for joint creation. + stem (float?): The amount of radius the stem or neck of the joint will have. Defaults + to 2. + + Returns: + None: This function does not return any value. + """ + + # diameter = diameter of the tool for joint creation + # DT = Bit diameter tolerance + # stem = amount of radius the stem or neck of the joint will have + global DT + RESOLUTION = 12 # Data resolution + cube_sx = diameter * DT * (2 + stem - 1) + cube_ty = diameter * DT + cube_sy = 2 * diameter * DT + circle_radius = diameter * DT / 2 + c1x = cube_sx / 2 + c2x = cube_sx / 2 + c2y = 3 * circle_radius + c1y = circle_radius + + bpy.ops.curve.simple(align='WORLD', location=(0, cube_ty, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', + Simple_width=cube_sx, Simple_length=cube_sy, use_cyclic_u=True, edit_mode=False) + bpy.context.active_object.name = "ftmprect" + + bpy.ops.curve.simple(align='WORLD', location=(c2x, c2y, 0), rotation=(0, 0, 0), Simple_Type='Ellipse', + Simple_a=circle_radius, + Simple_b=circle_radius, Simple_sides=4, use_cyclic_u=True, edit_mode=False, shape='3D') + + bpy.context.active_object.name = "ftmpcirc_add" + bpy.context.object.data.resolution_u = RESOLUTION + + bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN') + + simple.duplicate() + simple.mirrorx() + + simple.union('ftmp') + simple.rename('ftmp', '_sum') + + rc1 = circle_radius + + bpy.ops.curve.simple(align='WORLD', location=(c1x, c1y, 0), rotation=(0, 0, 0), Simple_Type='Ellipse', + Simple_a=circle_radius, Simple_b=rc1, Simple_sides=4, use_cyclic_u=True, edit_mode=False, + shape='3D') + + bpy.context.active_object.name = "_circ_delete" + bpy.context.object.data.resolution_u = RESOLUTION + bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN') + + simple.duplicate() + simple.mirrorx() + simple.union('_circ') + + simple.difference('_', '_sum') + bpy.ops.object.curve_remove_doubles() + simple.rename('_sum', "_puzzle")
+ + + +
+[docs] +def fingers(diameter, inside, amount=1, stem=1): + """Create a specified number of fingers for a joint tool. + + This function generates a set of fingers based on the provided diameter + and tolerance values. It calculates the necessary translations for + positioning the fingers and duplicates them if more than one is + required. Additionally, it creates a receptacle using a silhouette + offset from the fingers, allowing for precise joint creation. + + Args: + diameter (float): The diameter of the tool used for joint creation. + inside (float): The tolerance in the joint receptacle. + amount (int?): The number of fingers to create. Defaults to 1. + stem (float?): The amount of radius the stem or neck of the joint will have. Defaults + to 1. + """ + + # diameter = diameter of the tool for joint creation + # inside = Tolerance in the joint receptacle + global DT # Bit diameter tolerance + # stem = amount of radius the stem or neck of the joint will have + # amount = the amount of fingers + + xtranslate = -(4 + 2 * (stem - 1)) * (amount - 1) * diameter * DT / 2 + finger(diameter, stem=stem) # generate male finger + simple.active_name("puzzlem") + simple.move(x=xtranslate, y=-0.00002) + + if amount > 1: + # duplicate translate the amount needed (faster than generating new) + for i in range(amount - 1): + bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'}, + TRANSFORM_OT_translate={ + "value": ((4 + 2 * (stem - 1)) * diameter * DT, 0, 0.0)}) + simple.union('puzzle') + + simple.active_name("fingers") + bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN') + + # Receptacle is made using the silhouette offset from the fingers + if inside > 0: + bpy.ops.object.silhouete_offset(offset=inside, style='1') + simple.active_name('receptacle') + simple.move(y=-inside)
+ + + +
+[docs] +def twistf(name, length, diameter, tolerance, twist, tneck, tthick, twist_keep=False): + """Add a twist lock to a receptacle. + + This function modifies the receptacle by adding a twist lock feature if + the `twist` parameter is set to True. It performs several operations + including interlocking the twist, rotating the object, and moving it to + the correct position. If `twist_keep` is True, it duplicates the twist + lock for further modifications. The function utilizes parameters such as + length, diameter, tolerance, and thickness to accurately create the + twist lock. + + Args: + name (str): The name of the receptacle to be modified. + length (float): The length of the receptacle. + diameter (float): The diameter of the receptacle. + tolerance (float): The tolerance value for the twist lock. + twist (bool): A flag indicating whether to add a twist lock. + tneck (float): The neck thickness for the twist lock. + tthick (float): The thickness of the twist lock. + twist_keep (bool?): A flag indicating whether to keep the twist + lock after duplication. Defaults to False. + """ + + # add twist lock to receptacle + if twist: + joinery.interlock_twist(length, tthick, tolerance, cx=0, cy=0, rotation=0, percentage=tneck) + simple.rotate(pi / 2) + simple.move(y=-tthick / 2 + 2 * diameter + 2 * tolerance) + simple.active_name('xtemptwist') + if twist_keep: + simple.duplicate() + simple.active_name('twist_keep_f') + simple.make_active(name) + simple.active_name('xtemp') + simple.union('xtemp') + simple.active_name(name)
+ + + +
+[docs] +def twistm(name, length, diameter, tolerance, twist, tneck, tthick, angle, twist_keep=False, x=0, y=0): + """Add a twist lock to a male connector. + + This function modifies the geometry of a male connector by adding a + twist lock feature. It utilizes various parameters to determine the + dimensions and positioning of the twist lock. If the `twist_keep` + parameter is set to True, it duplicates the twist lock for further + modifications. The function also allows for adjustments in position + through the `x` and `y` parameters. + + Args: + name (str): The name of the connector to be modified. + length (float): The length of the connector. + diameter (float): The diameter of the connector. + tolerance (float): The tolerance level for the twist lock. + twist (bool): A flag indicating whether to add a twist lock. + tneck (float): The neck thickness for the twist lock. + tthick (float): The thickness of the twist lock. + angle (float): The angle at which to rotate the twist lock. + twist_keep (bool?): A flag indicating whether to keep the twist lock duplicate. Defaults to + False. + x (float?): The x-coordinate for positioning. Defaults to 0. + y (float?): The y-coordinate for positioning. Defaults to 0. + + Returns: + None: This function modifies the state of the connector but does not return a + value. + """ + + # add twist lock to male connector + global DT + if twist: + joinery.interlock_twist(length, tthick, tolerance, cx=0, cy=0, rotation=0, percentage=tneck) + simple.rotate(pi / 2) + simple.move(y=-tthick / 2 + 2 * diameter * DT) + simple.rotate(angle) + simple.move(x=x, y=y) + simple.active_name('_twist') + if twist_keep: + simple.duplicate() + simple.active_name('twist_keep_m') + simple.make_active(name) + simple.active_name('_tmp') + simple.difference('_', '_tmp') + simple.active_name(name)
+ + + +
+[docs] +def bar(width, thick, diameter, tolerance, amount=0, stem=1, twist=False, tneck=0.5, tthick=0.01, twist_keep=False, + twist_line=False, twist_line_amount=2, which='MF'): + """Create a bar with specified dimensions and joint features. + + This function generates a bar with customizable parameters such as + width, thickness, and joint characteristics. It can automatically + determine the number of fingers in the joint if the amount is set to + zero. The function also supports various options for twisting and neck + dimensions, allowing for flexible design of the bar according to the + specified parameters. The resulting bar can be manipulated further based + on the provided options. + + Args: + width (float): The length of the bar. + thick (float): The thickness of the bar. + diameter (float): The diameter of the tool used for joint creation. + tolerance (float): The tolerance in the joint. + amount (int?): The number of fingers in the joint; 0 means auto-generate. Defaults to + 0. + stem (float?): The radius of the stem or neck of the joint. Defaults to 1. + twist (bool?): Whether to add a twist lock. Defaults to False. + tneck (float?): The percentage the twist neck will have compared to thickness. Defaults + to 0.5. + tthick (float?): The thickness of the twist material. Defaults to 0.01. + twist_keep (bool?): Whether to keep the twist feature. Defaults to False. + twist_line (bool?): Whether to add a twist line. Defaults to False. + twist_line_amount (int?): The amount for the twist line. Defaults to 2. + which (str?): Specifies the type of joint; options are 'M', 'F', 'MF', 'MM', 'FF'. + Defaults to 'MF'. + + Returns: + None: This function does not return a value but modifies the state of the 3D + model in Blender. + """ + + + # width = length of the bar + # thick = thickness of the bar + # diameter = diameter of the tool for joint creation + # tolerance = Tolerance in the joint + # amount = amount of fingers in the joint 0 means auto generate + # stem = amount of radius the stem or neck of the joint will have + # twist = twist lock addition + # tneck = percentage the twist neck will have compared to thick + # tthick = thicknest of the twist material + # Which M,F, MF, MM, FF + + global DT + if amount == 0: + amount = round(thick / ((4 + 2 * (stem - 1)) * diameter * DT)) - 1 + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', + Simple_width=width, Simple_length=thick, use_cyclic_u=True, edit_mode=False) + simple.active_name('tmprect') + + fingers(diameter, tolerance, amount, stem=stem) + + if which == 'MM' or which == 'M' or which == 'MF': + simple.rename('fingers', '_tmpfingers') + 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, -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(-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(-pi / 2) + simple.move(x=-width / 2) + + simple.remove_multiple("_") # Remove temporary base and holes + simple.remove_multiple("fingers") # Remove temporary base and holes + + if twist_line: + joinery.twist_line(thick, tthick, tolerance, tneck, twist_line_amount, width) + if twist_keep: + simple.duplicate() + simple.active_name('tmptwist') + simple.difference('tmp', 'tmprect') + simple.rename('tmprect', 'Puzzle_bar') + simple.remove_multiple("tmp") # Remove temporary base and holes + simple.make_active('Puzzle_bar')
+ + + +
+[docs] +def arc(radius, thick, angle, diameter, tolerance, amount=0, stem=1, twist=False, tneck=0.5, tthick=0.01, + twist_keep=False, which='MF'): + """Generate an arc with specified parameters. + + This function creates a 3D arc based on the provided radius, thickness, + angle, and other parameters. It handles the generation of fingers for + the joint and applies twisting features if specified. The function also + manages the orientation and positioning of the generated arc in a 3D + space. + + Args: + radius (float): The radius of the curve. + thick (float): The thickness of the bar. + angle (float): The angle of the arc (must not be zero). + diameter (float): The diameter of the tool for joint creation. + tolerance (float): Tolerance in the joint. + amount (int?): The amount of fingers in the joint; 0 means auto-generate. Defaults to + 0. + stem (float?): The amount of radius the stem or neck of the joint will have. Defaults + to 1. + twist (bool?): Whether to add a twist lock. Defaults to False. + tneck (float?): Percentage the twist neck will have compared to thickness. Defaults to + 0.5. + tthick (float?): Thickness of the twist material. Defaults to 0.01. + twist_keep (bool?): Whether to keep the twist. Defaults to False. + which (str?): Specifies which joint to generate ('M', 'F', 'MF'). Defaults to 'MF'. + + Returns: + None: This function does not return a value but modifies the 3D scene + directly. + """ + + # radius = radius of the curve + # thick = thickness of the bar + # angle = angle of the arc + # diameter = diameter of the tool for joint creation + # tolerance = Tolerance in the joint + # amount = amount of fingers in the joint 0 means auto generate + # stem = amount of radius the stem or neck of the joint will have + # twist = twist lock addition + # tneck = percentage the twist neck will have compared to thick + # tthick = thicknest of the twist material + # which = which joint to generate, Male Female MaleFemale M, F, MF + + global DT # diameter tolerance for diameter of finger creation + + if angle == 0: # angle cannot be 0 + angle = 0.01 + + negative = False + if angle < 0: # if angle < 0 then negative is true + angle = -angle + negative = True + + if amount == 0: + amount = round(thick / ((4 + 2 * (stem - 1)) * diameter * DT)) - 1 + + fingers(diameter, tolerance, amount, stem=stem) + twistf('receptacle', thick, diameter, tolerance, twist, tneck, tthick, twist_keep=twist_keep) + twistf('testing', thick, diameter, tolerance, twist, tneck, tthick, twist_keep=twist_keep) + print("generating arc") + # 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=degrees(angle), + Simple_radius=radius, use_cyclic_u=False, edit_mode=False) + bpy.context.active_object.name = "tmparc" + + simple.rename('fingers', '_tmpfingers') + + simple.rotate(pi) + simple.move(x=radius) + bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN') + + simple.rename('tmparc', '_tmparc') + if which == 'MF' or which == 'M': + simple.union('_tmp') + simple.active_name("base") + twistm('base', thick, diameter, tolerance, twist, tneck, tthick, pi, x=radius) + simple.rename('base', '_tmparc') + + simple.rename('receptacle', '_tmpreceptacle') + simple.mirrory() + simple.move(x=radius) + bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN') + simple.rotate(angle) + simple.make_active('_tmparc') + + if which == 'MF' or which == 'F': + simple.difference('_tmp', '_tmparc') + bpy.context.active_object.name = "PUZZLE_arc" + bpy.ops.object.curve_remove_doubles() + simple.remove_multiple("_") # Remove temporary base and holes + simple.make_active('PUZZLE_arc') + if which == 'M': + simple.rotate(-angle) + simple.mirrory() + bpy.ops.object.transform_apply(location=True, rotation=True, scale=False) + 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(pi / 2) + simple.rename('PUZZLE_arc', 'PUZZLE_arc_receptacle') + else: + simple.move(x=-radius) + # bpy.ops.object.transform_apply(location=True, rotation=False, scale=False, properties=False) + # + if negative: # mirror if angle is negative + simple.mirrory()
+ + # + # bpy.ops.object.curve_remove_doubles() + + +
+[docs] +def arcbararc(length, radius, thick, angle, angleb, diameter, tolerance, amount=0, stem=1, twist=False, + tneck=0.5, tthick=0.01, which='MF', twist_keep=False, twist_line=False, twist_line_amount=2): + """Generate an arc bar joint with specified parameters. + + This function creates a joint consisting of male and female sections + based on the provided parameters. It adjusts the length to account for + the radius and thickness, generates a base rectangle, and then + constructs the male and/or female sections as specified. Additionally, + it can create a twist lock feature if required. The function utilizes + Blender's bpy operations to manipulate 3D objects. + + Args: + length (float): The total width of the segments including 2 * radius and thickness. + radius (float): The radius of the curve. + thick (float): The thickness of the bar. + angle (float): The angle of the female part. + angleb (float): The angle of the male part. + diameter (float): The diameter of the tool for joint creation. + tolerance (float): Tolerance in the joint. + amount (int?): The number of fingers in the joint; 0 means auto-generate. Defaults to + 0. + stem (float?): The amount of radius the stem or neck of the joint will have. Defaults + to 1. + twist (bool?): Whether to add a twist lock feature. Defaults to False. + tneck (float?): Percentage the twist neck will have compared to thickness. Defaults to + 0.5. + tthick (float?): Thickness of the twist material. Defaults to 0.01. + which (str?): Specifies which joint to generate ('M', 'F', or 'MF'). Defaults to 'MF'. + twist_keep (bool?): Whether to keep the twist after creation. Defaults to False. + twist_line (bool?): Whether to create a twist line feature. Defaults to False. + twist_line_amount (int?): Amount for the twist line feature. Defaults to 2. + + Returns: + None: This function does not return a value but modifies the Blender scene + directly. + """ + + # length is the total width of the segments including 2 * radius and thick + # radius = radius of the curve + # thick = thickness of the bar + # angle = angle of the female part + # angleb = angle of the male part + # diameter = diameter of the tool for joint creation + # tolerance = Tolerance in the joint + # amount = amount of fingers in the joint 0 means auto generate + # stem = amount of radius the stem or neck of the joint will have + # twist = twist lock addition + # tneck = percentage the twist neck will have compared to thick + # tthick = thicknest of the twist material + # which = which joint to generate, Male Female MaleFemale M, F, MF + + # adjust length to include 2x radius + thick + length -= (radius * 2 + thick) + + # generate base rectangle + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', + Simple_width=length * 1.005, Simple_length=thick, use_cyclic_u=True, edit_mode=False) + simple.active_name("tmprect") + + # Generate male section and join to the base + if which == 'M' or which == 'MF': + arc(radius, thick, angleb, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, + tthick=tthick, which='M') + simple.move(x=length / 2) + simple.active_name('tmp_male') + simple.select_multiple('tmp') + bpy.ops.object.curve_boolean(boolean_type='UNION') + simple.active_name('male') + simple.remove_multiple('tmp') + simple.rename('male', 'tmprect') + + # Generate female section and join to base + if which == 'F' or which == 'MF': + arc(radius, thick, angle, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, + tthick=tthick, which='F') + simple.move(x=-length / 2) + simple.active_name('tmp_receptacle') + simple.union('tmp') + simple.active_name('tmprect') + + if twist_line: + joinery.twist_line(thick, tthick, tolerance, tneck, twist_line_amount, length) + if twist_keep: + simple.duplicate() + simple.active_name('tmptwist') + simple.difference('tmp', 'tmprect') + + simple.active_name('arcBarArc') + simple.make_active('arcBarArc')
+ + + +
+[docs] +def arcbar(length, radius, thick, angle, diameter, tolerance, amount=0, stem=1, twist=False, + tneck=0.5, tthick=0.01, twist_keep=False, which='MF', twist_line=False, twist_line_amount=2): + """Generate an arc bar joint based on specified parameters. + + This function constructs an arc bar joint by generating male and female + sections according to the specified parameters such as length, radius, + thickness, and joint type. The function adjusts the length to account + for the radius and thickness of the bar and creates the appropriate + geometric shapes for the joint. It also includes options for twisting + and adjusting the neck thickness of the joint. + + Args: + length (float): The total width of the segments including 2 * radius and thickness. + radius (float): The radius of the curve. + thick (float): The thickness of the bar. + angle (float): The angle of the female part. + diameter (float): The diameter of the tool for joint creation. + tolerance (float): Tolerance in the joint. + amount (int?): The number of fingers in the joint; 0 means auto-generate. Defaults to + 0. + stem (float?): The amount of radius the stem or neck of the joint will have. Defaults + to 1. + twist (bool?): Whether to add a twist lock. Defaults to False. + tneck (float?): Percentage the twist neck will have compared to thickness. Defaults to + 0.5. + tthick (float?): Thickness of the twist material. Defaults to 0.01. + twist_keep (bool?): Whether to keep the twist. Defaults to False. + which (str?): Specifies which joint to generate ('M', 'F', 'MF'). Defaults to 'MF'. + twist_line (bool?): Whether to include a twist line. Defaults to False. + twist_line_amount (int?): Amount of twist line. Defaults to 2. + """ + + # length is the total width of the segments including 2 * radius and thick + # radius = radius of the curve + # thick = thickness of the bar + # angle = angle of the female part + # diameter = diameter of the tool for joint creation + # tolerance = Tolerance in the joint + # amount = amount of fingers in the joint 0 means auto generate + # stem = amount of radius the stem or neck of the joint will have + # twist = twist lock addition + # tneck = percentage the twist neck will have compared to thick + # tthick = thicknest of the twist material + # which = which joint to generate, Male Female MaleFemale M, F, MF + if which == 'M': + which = 'MM' + elif which == 'F': + which = 'FF' + # adjust length to include 2x radius + thick + length -= (radius * 2 + thick) + + # generate base rectangle + # Generate male section and join to the base + if which == 'MM' or which == 'MF': + bar(length, thick, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, tthick=tthick, + which='M', twist_keep=twist_keep, twist_line=twist_line, twist_line_amount=twist_line_amount) + simple.active_name('tmprect') + + 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(pi) + simple.active_name('tmprect') + + # Generate female section and join to base + if which == 'FF' or which == 'MF': + arc(radius, thick, angle, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, + tthick=tthick, which='F') + simple.move(x=-length / 2 * 0.998) + simple.active_name('tmp_receptacle') + simple.union('tmp') + simple.active_name('arcBar') + simple.remove_multiple('tmp') + + if which == 'MM': + arc(radius, thick, angle, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, + tthick=tthick, which='M') + bpy.ops.transform.mirror(orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), + orient_matrix_type='GLOBAL', constraint_axis=(True, False, False)) + simple.move(x=-length / 2 * 0.998) + simple.active_name('tmp_receptacle') + simple.union('tmp') + simple.active_name('arcBar') + simple.remove_multiple('tmp') + + simple.make_active('arcBar')
+ + + +
+[docs] +def multiangle(radius, thick, angle, diameter, tolerance, amount=0, stem=1, twist=False, + tneck=0.5, tthick=0.01, combination='MFF'): + """Generate a multi-angle joint based on specified parameters. + + This function creates a multi-angle joint by generating various + geometric shapes using the provided parameters such as radius, + thickness, angle, diameter, and tolerance. It utilizes Blender's + operations to create and manipulate curves, resulting in a joint that + can be customized with different combinations of male and female parts. + The function also allows for automatic generation of the number of + fingers in the joint and includes options for twisting and neck + dimensions. + + Args: + radius (float): The radius of the curve. + thick (float): The thickness of the bar. + angle (float): The angle of the female part. + diameter (float): The diameter of the tool for joint creation. + tolerance (float): Tolerance in the joint. + amount (int?): The amount of fingers in the joint; 0 means auto-generate. Defaults to + 0. + stem (float?): The amount of radius the stem or neck of the joint will have. Defaults + to 1. + twist (bool?): Indicates if a twist lock addition is required. Defaults to False. + tneck (float?): Percentage the twist neck will have compared to thickness. Defaults to + 0.5. + tthick (float?): Thickness of the twist material. Defaults to 0.01. + combination (str?): Specifies which joint to generate ('M', 'F', 'MF', 'MFF', 'MMF'). + Defaults to 'MFF'. + + Returns: + None: This function does not return a value but performs operations in + Blender. + """ + + # length is the total width of the segments including 2 * radius and thick + # radius = radius of the curve + # thick = thickness of the bar + # angle = angle of the female part + # diameter = diameter of the tool for joint creation + # tolerance = Tolerance in the joint + # amount = amount of fingers in the joint 0 means auto generate + # stem = amount of radius the stem or neck of the joint will have + # twist = twist lock addition + # tneck = percentage the twist neck will have compared to thick + # tthick = thicknest of the twist material + # which = which joint to generate, Male Female MaleFemale M, F, MF + + r_exterior = radius + thick / 2 + r_interior = radius - thick / 2 + + 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', + Simple_width=r_interior, Simple_length=r_interior / 2, use_cyclic_u=True, + edit_mode=False, shape='3D') + simple.active_name('tmp_rect') + + 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 * tan(angle)) + simple.active_name('tmpCircle') + + arc(radius, thick, angle, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, tthick=tthick, + which='MF') + simple.active_name('tmp_arc') + if combination == 'MFF': + simple.duplicate() + simple.mirrorx() + elif combination == 'MMF': + arc(radius, thick, angle, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, + tthick=tthick, + which='M') + simple.active_name('tmp_arc') + simple.mirrory() + simple.rotate(pi / 2) + simple.union("tmp_") + simple.difference('tmp', 'tmp_') + simple.active_name('multiAngle60')
+ + + +
+[docs] +def t(length, thick, diameter, tolerance, amount=0, stem=1, twist=False, tneck=0.5, tthick=0.01, combination='MF', + base_gender='M', corner=False): + """Generate a 3D model based on specified parameters. + + This function creates a 3D model by manipulating geometric shapes based + on the provided parameters. It handles different combinations of shapes + and orientations based on the specified gender and corner options. The + function utilizes several helper functions to perform operations such as + moving, duplicating, and uniting shapes to form the final model. + + Args: + length (float): The length of the model. + thick (float): The thickness of the model. + diameter (float): The diameter of the model. + tolerance (float): The tolerance level for the model dimensions. + amount (int?): The amount of material to use. Defaults to 0. + stem (int?): The stem value for the model. Defaults to 1. + twist (bool?): Whether to apply a twist to the model. Defaults to False. + tneck (float?): The neck thickness. Defaults to 0.5. + tthick (float?): The thickness for the neck. Defaults to 0.01. + combination (str?): The combination type ('MF', 'F', 'M'). Defaults to 'MF'. + base_gender (str?): The base gender for the model ('M' or 'F'). Defaults to 'M'. + corner (bool?): Whether to apply corner adjustments. Defaults to False. + + Returns: + None: This function does not return a value but modifies the 3D model + directly. + """ + + if corner: + if combination == 'MF': + base_gender = 'M' + combination = 'f' + elif combination == 'F': + base_gender = 'F' + combination = 'f' + elif combination == 'M': + base_gender = 'M' + combination = 'm' + + bar(length, thick, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, + tthick=tthick, which=base_gender) + simple.active_name('tmp') + fingers(diameter, tolerance, amount=amount, stem=stem) + if combination == 'MF' or combination == 'M' or combination == 'm': + simple.make_active('fingers') + simple.move(y=thick / 2) + simple.duplicate() + simple.active_name('tmp') + simple.union('tmp') + + if combination == 'M': + simple.make_active('fingers') + simple.mirrory() + simple.active_name('tmp') + simple.union('tmp') + + if combination == 'MF' or combination == 'F' or combination == 'f': + simple.make_active('receptacle') + simple.move(y=-thick / 2) + simple.duplicate() + simple.active_name('tmp') + simple.difference('tmp', 'tmp') + + if combination == 'F': + simple.make_active('receptacle') + simple.mirrory() + simple.active_name('tmp') + simple.difference('tmp', 'tmp') + + simple.remove_multiple('receptacle') + simple.remove_multiple('fingers') + + simple.rename('tmp', 't') + simple.make_active('t')
+ + + +
+[docs] +def curved_t(length, thick, radius, diameter, tolerance, amount=0, stem=1, twist=False, tneck=0.5, tthick=0.01, + combination='MF', base_gender='M'): + """Create a curved shape based on specified parameters. + + This function generates a 3D curved shape using the provided dimensions + and characteristics. It utilizes the `bar` and `arc` functions to create + the desired geometry and applies transformations such as mirroring and + union operations to achieve the final shape. The function also allows + for customization based on the gender specification, which influences + the shape's design. + + Args: + length (float): The length of the bar. + thick (float): The thickness of the bar. + radius (float): The radius of the arc. + diameter (float): The diameter used in arc creation. + tolerance (float): The tolerance level for the shape. + amount (int?): The amount parameter for the shape generation. Defaults to 0. + stem (int?): The stem parameter for the shape generation. Defaults to 1. + twist (bool?): A flag indicating whether to apply a twist to the shape. Defaults to + False. + tneck (float?): The neck thickness parameter. Defaults to 0.5. + tthick (float?): The thickness parameter for the neck. Defaults to 0.01. + combination (str?): The combination type for the shape. Defaults to 'MF'. + base_gender (str?): The base gender for the shape design. Defaults to 'M'. + + Returns: + None: This function does not return a value but modifies the 3D model in the + environment. + """ + + bar(length, thick, diameter, tolerance, amount=amount, stem=stem, twist=twist, tneck=tneck, + tthick=tthick, which=combination) + simple.active_name('tmpbar') + + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', + Simple_width=3 * radius, Simple_length=thick, use_cyclic_u=True, edit_mode=False) + simple.active_name("tmp_rect") + + if base_gender == 'MF': + 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, pi / 2, diameter, tolerance, + amount=amount, stem=stem, twist=twist, tneck=tneck, tthick=tthick, which='F') + simple.move(radius) + simple.mirrory() + simple.active_name('tmp_arc') + simple.union('tmp_arc') + simple.duplicate() + simple.mirrorx() + simple.union('tmp_arc') + simple.difference('tmp_', 'tmp_arc') + else: + 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') + if base_gender == 'M': + simple.move(-radius) + else: + simple.move(radius) + simple.duplicate() + simple.mirrorx() + + simple.union('tmp') + simple.active_name('curved_t')
+ + + +
+[docs] +def mitre(length, thick, angle, angleb, diameter, tolerance, amount=0, stem=1, twist=False, + tneck=0.5, tthick=0.01, which='MF'): + """Generate a mitre joint based on specified parameters. + + This function creates a 3D representation of a mitre joint using + Blender's bpy.ops.curve.simple operations. It generates a base rectangle + and cutout shapes, then constructs male and female sections of the joint + based on the provided angles and dimensions. The function allows for + customization of various parameters such as thickness, diameter, + tolerance, and the number of fingers in the joint. The resulting joint + can be either male, female, or a combination of both. + + Args: + length (float): The total width of the segments including 2 * radius and thickness. + thick (float): The thickness of the bar. + angle (float): The angle of the female part. + angleb (float): The angle of the male part. + diameter (float): The diameter of the tool for joint creation. + tolerance (float): Tolerance in the joint. + amount (int?): Amount of fingers in the joint; 0 means auto-generate. Defaults to 0. + stem (float?): Amount of radius the stem or neck of the joint will have. Defaults to 1. + twist (bool?): Indicates if a twist lock addition is required. Defaults to False. + tneck (float?): Percentage the twist neck will have compared to thickness. Defaults to + 0.5. + tthick (float?): Thickness of the twist material. Defaults to 0.01. + which (str?): Specifies which joint to generate ('M', 'F', 'MF'). Defaults to 'MF'. + """ + + # length is the total width of the segments including 2 * radius and thick + # radius = radius of the curve + # thick = thickness of the bar + # angle = angle of the female part + # angleb = angle of the male part + # diameter = diameter of the tool for joint creation + # tolerance = Tolerance in the joint + # amount = amount of fingers in the joint 0 means auto generate + # stem = amount of radius the stem or neck of the joint will have + # twist = twist lock addition + # tneck = percentage the twist neck will have compared to thick + # tthick = thicknest of the twist material + # which = which joint to generate, Male Female MaleFemale M, F, MF + + # generate base rectangle + bpy.ops.curve.simple(align='WORLD', location=(0, -thick / 2, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', + Simple_width=length * 1.005 + 4 * thick, Simple_length=thick, use_cyclic_u=True, + edit_mode=False, + shape='3D') + simple.active_name("tmprect") + + # generate cutout shapes + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', + Simple_width=4 * thick, Simple_length=6 * thick, use_cyclic_u=True, edit_mode=False, + shape='3D') + simple.move(x=2 * thick) + simple.rotate(angle) + simple.move(x=length / 2) + simple.active_name('tmpmitreright') + + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', + Simple_width=4 * thick, Simple_length=6 * thick, use_cyclic_u=True, edit_mode=False, + shape='3D') + simple.move(x=2 * thick) + simple.rotate(angleb) + simple.move(x=length / 2) + simple.mirrorx() + simple.active_name('tmpmitreleft') + simple.difference('tmp', 'tmprect') + simple.make_active('tmprect') + + fingers(diameter, tolerance, amount, stem=stem) + + # Generate male section and join to the base + if which == 'M' or which == 'MF': + simple.make_active('fingers') + simple.duplicate() + simple.active_name('tmpfingers') + simple.rotate(angle - pi / 2) + h = thick / cos(angle) + h /= 2 + simple.move(x=length / 2 + h * sin(angle), y=-thick / 2) + if which == 'M': + simple.rename('fingers', 'tmpfingers') + simple.rotate(angleb - pi / 2) + h = thick / cos(angleb) + h /= 2 + simple.move(x=length / 2 + h * sin(angleb), y=-thick / 2) + simple.mirrorx() + + simple.union('tmp') + simple.active_name('tmprect') + + # Generate female section and join to base + if which == 'MF' or which == 'F': + simple.make_active('receptacle') + simple.mirrory() + simple.duplicate() + simple.active_name('tmpreceptacle') + simple.rotate(angleb - pi / 2) + h = thick / cos(angleb) + h /= 2 + simple.move(x=length / 2 + h * sin(angleb), y=-thick / 2) + simple.mirrorx() + if which == 'F': + simple.rename('receptacle', 'tmpreceptacle2') + simple.rotate(angle - pi / 2) + h = thick / cos(angle) + h /= 2 + simple.move(x=length / 2 + h * sin(angle), y=-thick / 2) + simple.difference('tmp', 'tmprect') + + simple.remove_multiple('receptacle') + simple.remove_multiple('fingers') + simple.rename('tmprect', 'mitre')
+ + + +
+[docs] +def open_curve(line, thick, diameter, tolerance, amount=0, stem=1, twist=False, t_neck=0.5, t_thick=0.01, + twist_amount=1, which='MF', twist_keep=False): + """Open a curve and add puzzle connectors with optional twist lock + connectors. + + This function takes a shapely LineString and creates an open curve with + specified parameters such as thickness, diameter, tolerance, and twist + options. It generates puzzle connectors at the ends of the curve and can + optionally add twist lock connectors along the curve. The function also + handles the creation of the joint based on the provided parameters, + ensuring that the resulting geometry meets the specified design + requirements. + + Args: + line (LineString): A shapely LineString representing the path of the curve. + thick (float): The thickness of the bar used in the joint. + diameter (float): The diameter of the tool for joint creation. + tolerance (float): The tolerance in the joint. + amount (int?): The number of fingers in the joint; 0 means auto-generate. Defaults to + 0. + stem (float?): The amount of radius the stem or neck of the joint will have. Defaults + to 1. + twist (bool?): Whether to add twist lock connectors. Defaults to False. + t_neck (float?): The percentage the twist neck will have compared to thickness. Defaults + to 0.5. + t_thick (float?): The thickness of the twist material. Defaults to 0.01. + twist_amount (int?): The amount of twist distributed on the curve, not counting joint twists. + Defaults to 1. + which (str?): Specifies the type of joint; options include 'M', 'F', 'MF', 'MM', 'FF'. + Defaults to 'MF'. + twist_keep (bool?): Whether to keep the twist lock connectors. Defaults to False. + + Returns: + None: This function does not return a value but modifies the geometry in the + Blender context. + """ + + # puts puzzle connectors at the end of an open curve + # optionally puts twist lock connectors at the puzzle connection + # optionally puts twist lock connectors along the open curve + # line = shapely linestring + # thick = thickness of the bar + # diameter = diameter of the tool for joint creation + # tolerance = Tolerance in the joint + # amount = amount of fingers in the joint 0 means auto generate + # stem = amount of radius the stem or neck of the joint will have + # twist = twist lock addition + # twist_amount = twist amount distributed on the curve not counting the joint twist locks + # tneck = percentage the twist neck will have compared to thick + # tthick = thicknest of the twist material + # Which M,F, MF, MM, FF + + coords = list(line.coords) + + 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] + + print('start angle', start_angle) + print('end angle', end_angle) + + bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', + Simple_width=thick*2, Simple_length=thick * 2, use_cyclic_u=True, edit_mode=False, shape='3D') + simple.active_name('tmprect') + simple.move(y=thick) + simple.duplicate() + simple.rotate(start_angle) + simple.move(x=p_start[0], y=p_start[1]) + simple.make_active('tmprect') + simple.rotate(end_angle) + simple.move(x=p_end[0], y=p_end[1]) + simple.union('tmprect') + dilated = line.buffer(thick/2) # expand shapely object to thickness + utils.shapelyToCurve('tmp_curve', dilated, 0.0) + # truncate curve at both ends with the rectangles + simple.difference('tmp', 'tmp_curve') + + fingers(diameter, tolerance, amount, stem=stem) + simple.make_active('fingers') + simple.rotate(end_angle) + simple.move(x=p_end[0], y=p_end[1]) + simple.active_name('tmp_fingers') + simple.union('tmp_') + simple.active_name('tmp_curve') + twistm('tmp_curve', thick, diameter, tolerance, twist, t_neck, t_thick, end_angle, x=p_end[0], y=p_end[1], + twist_keep=twist_keep) + + twistf('receptacle', thick, diameter, tolerance, twist, t_neck, t_thick, twist_keep=twist_keep) + simple.rename('receptacle', 'tmp') + 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 + 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=pi/2, fixed_angle=0, start=twist_start, end=twist_start, + closed=False, type='TWIST', twist_percentage=t_neck) + if twist_keep: + simple.duplicate() + simple.active_name('twist_keep') + simple.join_multiple('twist_keep') + simple.make_active('interlock') + + simple.active_name('tmp_twist') + simple.difference('tmp', 'tmp_curve') + simple.active_name('puzzle_curve')
+ + + +
+[docs] +def tile(diameter, tolerance, tile_x_amount, tile_y_amount, stem=1): + """Create a tile shape based on specified dimensions and parameters. + + This function calculates the dimensions of a tile based on the provided + diameter and tolerance, as well as the number of tiles in the x and y + directions. It constructs the tile shape by creating a base and adding + features such as fingers for interlocking. The function also handles + transformations such as moving, rotating, and performing boolean + operations to achieve the desired tile geometry. + + Args: + diameter (float): The diameter of the tile. + tolerance (float): The tolerance to be applied to the tile dimensions. + tile_x_amount (int): The number of tiles along the x-axis. + tile_y_amount (int): The number of tiles along the y-axis. + stem (int?): A parameter affecting the tile's features. Defaults to 1. + + Returns: + None: This function does not return a value but modifies global state. + """ + + global DT + diameter = diameter * DT + width = ((tile_x_amount) * (4 + 2 * (stem-1)) + 1) * diameter + height = ((tile_y_amount) * (4 + 2 * (stem - 1)) + 1) * diameter + + print('size:', width, height) + fingers(diameter, tolerance, amount=tile_x_amount, stem=stem) + simple.add_rectangle(width, height) + simple.active_name('_base') + + simple.make_active('fingers') + simple.active_name('_fingers') + simple.intersect('_') + simple.remove_multiple('_fingers') + simple.rename('intersection', '_fingers') + simple.move(y=height/2) + simple.union('_') + simple.active_name('_base') + simple.remove_doubles() + simple.rename('receptacle', '_receptacle') + simple.move(y=-height/2) + simple.difference('_', '_base') + simple.active_name('base') + fingers(diameter, tolerance, amount=tile_y_amount, stem=stem) + simple.rename('base', '_base') + simple.remove_doubles() + simple.rename('fingers', '_fingers') + simple.rotate(pi/2) + simple.move(x=-width/2) + simple.union('_') + simple.active_name('_base') + simple.rename('receptacle', '_receptacle') + simple.rotate(pi/2) + simple.move(x=width/2) + simple.difference('_', '_base') + simple.active_name('tile_ ' + str(tile_x_amount) + '_' + str(tile_y_amount))
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/simple.html b/_modules/cam/simple.html new file mode 100644 index 000000000..b5ca5397e --- /dev/null +++ b/_modules/cam/simple.html @@ -0,0 +1,1395 @@ + + + + + + + + + + cam.simple — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.simple

+"""CNC CAM 'simple.py' © 2012 Vilem Novak
+
+Various helper functions, less complex than those found in the 'utils' files.
+"""
+
+from math import (
+    hypot,
+    pi,
+)
+import os
+import string
+import sys
+import time
+
+from shapely.geometry import Polygon
+
+import bpy
+from mathutils import Vector
+
+from .constants import BULLET_SCALE
+
+
+
+[docs] +def tuple_add(t, t1): # add two tuples as Vectors + """Add two tuples as vectors. + + This function takes two tuples, each representing a vector in three- + dimensional space, and returns a new tuple that is the element-wise sum + of the two input tuples. It assumes that both tuples contain exactly + three numeric elements. + + Args: + t (tuple): A tuple containing three numeric values representing the first vector. + t1 (tuple): A tuple containing three numeric values representing the second vector. + + Returns: + tuple: A tuple containing three numeric values that represent the sum of the + input vectors. + """ + return t[0] + t1[0], t[1] + t1[1], t[2] + t1[2]
+ + + +
+[docs] +def tuple_sub(t, t1): # sub two tuples as Vectors + """Subtract two tuples element-wise. + + This function takes two tuples of three elements each and performs an + element-wise subtraction, treating the tuples as vectors. The result is + a new tuple containing the differences of the corresponding elements + from the input tuples. + + Args: + t (tuple): A tuple containing three numeric values. + t1 (tuple): A tuple containing three numeric values. + + Returns: + tuple: A tuple containing the results of the element-wise subtraction. + """ + return t[0] - t1[0], t[1] - t1[1], t[2] - t1[2]
+ + + +
+[docs] +def tuple_mul(t, c): # multiply two tuples with a number + """Multiply each element of a tuple by a given number. + + This function takes a tuple containing three elements and a numeric + value, then multiplies each element of the tuple by the provided number. + The result is returned as a new tuple containing the multiplied values. + + Args: + t (tuple): A tuple containing three numeric values. + c (numeric): A number by which to multiply each element of the tuple. + + Returns: + tuple: A new tuple containing the results of the multiplication. + """ + return t[0] * c, t[1] * c, t[2] * c
+ + + +
+[docs] +def tuple_length(t): # get length of vector, but passed in as tuple. + """Get the length of a vector represented as a tuple. + + This function takes a tuple as input, which represents the coordinates + of a vector, and returns its length by creating a Vector object from the + tuple. The length is calculated using the appropriate mathematical + formula for vector length. + + Args: + t (tuple): A tuple representing the coordinates of the vector. + + Returns: + float: The length of the vector. + """ + return Vector(t).length
+ + + +# timing functions for optimisation purposes... +
+[docs] +def timinginit(): + """Initialize timing metrics. + + This function sets up the initial state for timing functions by + returning a list containing two zero values. These values can be used to + track elapsed time or other timing-related metrics in subsequent + operations. + + Returns: + list: A list containing two zero values, representing the + initial timing metrics. + """ + return [0, 0]
+ + + +
+[docs] +def timingstart(tinf): + """Start timing by recording the current time. + + This function updates the second element of the provided list with the + current time in seconds since the epoch. It is useful for tracking the + start time of an operation or process. + + Args: + tinf (list): A list where the second element will be updated + with the current time. + """ + t = time.time() + tinf[1] = t
+ + + +
+[docs] +def timingadd(tinf): + """Update the timing information. + + This function updates the first element of the `tinf` list by adding the + difference between the current time and the second element of the list. + It is typically used to track elapsed time in a timing context. + + Args: + tinf (list): A list where the first element is updated with the + """ + t = time.time() + tinf[0] += t - tinf[1]
+ + + +
+[docs] +def timingprint(tinf): + """Print the timing information. + + This function takes a tuple containing timing information and prints it + in a formatted string. It specifically extracts the first element of the + tuple, which is expected to represent time, and appends the string + 'seconds' to it before printing. + + Args: + tinf (tuple): A tuple where the first element is expected to be a numeric value + representing time. + + Returns: + None: This function does not return any value; it only prints output to the + console. + """ + print('time ' + str(tinf[0]) + 'seconds')
+ + + +
+[docs] +def progress(text, n=None): + """Report progress during script execution. + + This function outputs a progress message to the standard output. It is + designed to work for background operations and provides a formatted + string that includes the specified text and an optional numeric progress + value. If the numeric value is provided, it is formatted as a + percentage. + + Args: + text (str): The message to display as progress. + n (float?): A float representing the progress as a + fraction (0.0 to 1.0). If not provided, no percentage will + be displayed. + + Returns: + None: This function does not return a value; it only prints + to the standard output. + """ + text = str(text) + if n is None: + n = '' + else: + n = ' ' + str(int(n * 1000) / 1000) + '%' + sys.stdout.write('progress{%s%s}\n' % (text, n)) + sys.stdout.flush()
+ + + +
+[docs] +def activate(o): + """Makes an object active in Blender. + + This function sets the specified object as the active object in the + current Blender scene. It first deselects all objects, then selects the + given object and makes it the active object in the view layer. This is + useful for operations that require a specific object to be active, such + as transformations or modifications. + + Args: + o (bpy.types.Object): The Blender object to be activated. + """ + s = bpy.context.scene + bpy.ops.object.select_all(action='DESELECT') + o.select_set(state=True) + s.objects[o.name].select_set(state=True) + bpy.context.view_layer.objects.active = o
+ + + +
+[docs] +def dist2d(v1, v2): + """Calculate the distance between two points in 2D space. + + This function computes the Euclidean distance between two points + represented by their coordinates in a 2D plane. It uses the Pythagorean + theorem to calculate the distance based on the differences in the x and + y coordinates of the points. + + Args: + v1 (tuple): A tuple representing the coordinates of the first point (x1, y1). + v2 (tuple): A tuple representing the coordinates of the second point (x2, y2). + + Returns: + float: The Euclidean distance between the two points. + """ + return hypot((v1[0] - v2[0]), (v1[1] - v2[1]))
+ + + +
+[docs] +def delob(ob): + """Delete an object in Blender for multiple uses. + + This function activates the specified object and then deletes it using + Blender's built-in operations. It is designed to facilitate the deletion + of objects within the Blender environment, ensuring that the object is + active before performing the deletion operation. + + Args: + ob (Object): The Blender object to be deleted. + """ + activate(ob) + bpy.ops.object.delete(use_global=False)
+ + + +
+[docs] +def dupliob(o, pos): + """Helper function for visualizing cutter positions in bullet simulation. + + This function duplicates the specified object and resizes it according + to a predefined scale factor. It also removes any existing rigidbody + properties from the duplicated object and sets its location to the + specified position. This is useful for managing multiple cutter + positions in a bullet simulation environment. + + Args: + o (Object): The object to be duplicated. + pos (Vector): The new position to place the duplicated object. + """ + activate(o) + bpy.ops.object.duplicate() + s = 1.0 / BULLET_SCALE + bpy.ops.transform.resize(value=(s, s, s), constraint_axis=(False, False, False), orient_type='GLOBAL', + mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', + proportional_size=1) + o = bpy.context.active_object + bpy.ops.rigidbody.object_remove() + o.location = pos
+ + + +
+[docs] +def addToGroup(ob, groupname): + """Add an object to a specified group in Blender. + + This function activates the given object and checks if the specified + group exists in Blender's data. If the group does not exist, it creates + a new group with the provided name. If the group already exists, it + links the object to that group. + + Args: + ob (Object): The object to be added to the group. + groupname (str): The name of the group to which the object will be added. + """ + activate(ob) + if bpy.data.groups.get(groupname) is None: + bpy.ops.group.create(name=groupname) + else: + bpy.ops.object.group_link(group=groupname)
+ + + +
+[docs] +def compare(v1, v2, vmiddle, e): + """Comparison for optimization of paths. + + This function compares two vectors and checks if the distance between a + calculated vector and a reference vector is less than a specified + threshold. It normalizes the vector difference and scales it by the + length of another vector to determine if the resulting vector is within + the specified epsilon value. + + Args: + v1 (Vector): The first vector for comparison. + v2 (Vector): The second vector for comparison. + vmiddle (Vector): The middle vector used for calculating the + reference vector. + e (float): The threshold value for comparison. + + Returns: + bool: True if the distance is less than the threshold, + otherwise False. + """ + # e=0.0001 + v1 = Vector(v1) + v2 = Vector(v2) + vmiddle = Vector(vmiddle) + vect1 = v2 - v1 + vect2 = vmiddle - v1 + vect1.normalize() + vect1 *= vect2.length + v = vect2 - vect1 + if v.length < e: + return True + return False
+ + + +
+[docs] +def isVerticalLimit(v1, v2, limit): + """Test Path Segment on Verticality Threshold for protect_vertical option. + + This function evaluates the verticality of a path segment defined by two + points, v1 and v2, based on a specified limit. It calculates the angle + between the vertical vector and the vector formed by the two points. If + the angle is within the defined limit, it adjusts the vertical position + of either v1 or v2 to ensure that the segment adheres to the verticality + threshold. + + Args: + v1 (tuple): A 3D point represented as a tuple (x, y, z). + v2 (tuple): A 3D point represented as a tuple (x, y, z). + limit (float): The angle threshold for determining verticality. + + Returns: + tuple: The adjusted 3D points v1 and v2 after evaluating the verticality. + """ + z = abs(v1[2] - v2[2]) + # verticality=0.05 + # this will be better. + # + # print(a) + if z > 0: + v2d = Vector((0, 0, -1)) + v3d = Vector((v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2])) + a = v3d.angle(v2d) + if a > pi / 2: + a = abs(a - pi) + # print(a) + if a < limit: + # print(abs(v1[0]-v2[0])/z) + # print(abs(v1[1]-v2[1])/z) + if v1[2] > v2[2]: + v1 = (v2[0], v2[1], v1[2]) + return v1, v2 + else: + v2 = (v1[0], v1[1], v2[2]) + return v1, v2 + return v1, v2
+ + + +
+[docs] +def getCachePath(o): + """Get the cache path for a given object. + + This function constructs a cache path based on the current Blender + file's filepath and the name of the provided object. It retrieves the + base name of the file, removes the last six characters, and appends a + specified directory and the object's name to create a complete cache + path. + + Args: + o (Object): The Blender object for which the cache path is being generated. + + Returns: + str: The constructed cache path as a string. + """ + fn = bpy.data.filepath + l = len(bpy.path.basename(fn)) + bn = bpy.path.basename(fn)[:-6] + print('fn-l:', fn[:-l]) + print('bn:', bn) + + iname = fn[:-l] + 'temp_cam' + os.sep + bn + '_' + o.name + return iname
+ + + +
+[docs] +def getSimulationPath(): + """Get the simulation path for temporary camera files. + + This function retrieves the file path of the current Blender project and + constructs a new path for temporary camera files by appending 'temp_cam' + to the directory of the current file. The constructed path is returned + as a string. + + Returns: + str: The path to the temporary camera directory. + """ + fn = bpy.data.filepath + l = len(bpy.path.basename(fn)) + iname = fn[:-l] + 'temp_cam' + os.sep + return iname
+ + + +
+[docs] +def safeFileName(name): # for export gcode + """Generate a safe file name from the given string. + + This function takes a string input and removes any characters that are + not considered valid for file names. The valid characters include + letters, digits, and a few special characters. The resulting string can + be used safely as a file name for exporting purposes. + + Args: + name (str): The input string to be sanitized into a safe file name. + + Returns: + str: A sanitized version of the input string that contains only valid + characters for a file name. + """ + valid_chars = "-_.()%s%s" % (string.ascii_letters, string.digits) + filename = ''.join(c for c in name if c in valid_chars) + return filename
+ + + +
+[docs] +def strInUnits(x, precision=5): + """Convert a value to a string representation in the current unit system. + + This function takes a numeric value and converts it to a string + formatted according to the unit system set in the Blender context. If + the unit system is metric, the value is converted to millimeters. If the + unit system is imperial, the value is converted to inches. The precision + of the output can be specified. + + Args: + x (float): The numeric value to be converted. + precision (int?): The number of decimal places to round to. + Defaults to 5. + + Returns: + str: The string representation of the value in the appropriate units. + """ + if bpy.context.scene.unit_settings.system == 'METRIC': + return str(round(x * 1000, precision)) + ' mm ' + elif bpy.context.scene.unit_settings.system == 'IMPERIAL': + return str(round(x * 1000 / 25.4, precision)) + "'' " + else: + return str(x)
+ + + +# select multiple object starting with name +
+[docs] +def select_multiple(name): + """Select multiple objects in the scene based on their names. + + This function deselects all objects in the current Blender scene and + then selects all objects whose names start with the specified prefix. It + iterates through all objects in the scene and checks if their names + begin with the given string. If they do, those objects are selected; + otherwise, they are deselected. + + Args: + name (str): The prefix used to select objects in the scene. + """ + scene = bpy.context.scene + bpy.ops.object.select_all(action='DESELECT') + for ob in scene.objects: # join pocket curve calculations + if ob.name.startswith(name): + ob.select_set(True) + else: + ob.select_set(False)
+ + + +# join multiple objects starting with 'name' renaming final object as 'name' +
+[docs] +def join_multiple(name): + """Join multiple objects and rename the final object. + + This function selects multiple objects in the Blender context, joins + them into a single object, and renames the resulting object to the + specified name. It is assumed that the objects to be joined are already + selected in the Blender interface. + + Args: + name (str): The new name for the joined object. + """ + select_multiple(name) + bpy.ops.object.join() + bpy.context.active_object.name = name # rename object
+ + + +# remove multiple objects starting with 'name'.... useful for fixed name operation +
+[docs] +def remove_multiple(name): + """Remove multiple objects from the scene based on their name prefix. + + This function deselects all objects in the current Blender scene and + then iterates through all objects. If an object's name starts with the + specified prefix, it selects that object and deletes it from the scene. + This is useful for operations that require removing multiple objects + with a common naming convention. + + Args: + name (str): The prefix of the object names to be removed. + """ + scene = bpy.context.scene + bpy.ops.object.select_all(action='DESELECT') + for ob in scene.objects: + if ob.name.startswith(name): + ob.select_set(True) + bpy.ops.object.delete()
+ + + +
+[docs] +def deselect(): + """Deselect all objects in the current Blender context. + + This function utilizes the Blender Python API to deselect all objects in + the current scene. It is useful for clearing selections before + performing other operations on objects. Raises: None + """ + bpy.ops.object.select_all(action='DESELECT')
+ + + +# makes the object with the name active +
+[docs] +def make_active(name): + """Make an object active in the Blender scene. + + This function takes the name of an object and sets it as the active + object in the current Blender scene. It first deselects all objects, + then selects the specified object and makes it active, allowing for + further operations to be performed on it. + + Args: + name (str): The name of the object to be made active. + """ + ob = bpy.context.scene.objects[name] + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = ob + ob.select_set(True)
+ + + +# change the name of the active object +
+[docs] +def active_name(name): + """Change the name of the active object in Blender. + + This function sets the name of the currently active object in the + Blender context to the specified name. It directly modifies the `name` + attribute of the active object, allowing users to rename objects + programmatically. + + Args: + name (str): The new name to assign to the active object. + """ + bpy.context.active_object.name = name
+ + + +# renames and makes active name and makes it active +
+[docs] +def rename(name, name2): + """Rename an object and make it active. + + This function renames an object in the Blender context and sets it as + the active object. It first calls the `make_active` function to ensure + the object is active, then updates the name of the active object to the + new name provided. + + Args: + name (str): The current name of the object to be renamed. + name2 (str): The new name to assign to the active object. + """ + make_active(name) + bpy.context.active_object.name = name2
+ + + +# boolean union of objects starting with name result is object name. +# all objects starting with name will be deleted and the result will be name +
+[docs] +def union(name): + """Perform a boolean union operation on objects. + + This function selects multiple objects that start with the given name, + performs a boolean union operation on them using Blender's operators, + and then renames the resulting object to the specified name. After the + operation, it removes the original objects that were used in the union + process. + + Args: + name (str): The base name of the objects to be unioned. + """ + select_multiple(name) + bpy.ops.object.curve_boolean(boolean_type='UNION') + active_name('unionboolean') + remove_multiple(name) + rename('unionboolean', name)
+ + + +
+[docs] +def intersect(name): + """Perform an intersection operation on a curve object. + + This function selects multiple objects based on the provided name and + then executes a boolean operation to create an intersection of the + selected objects. The resulting intersection is then named accordingly. + + Args: + name (str): The name of the object(s) to be selected for the intersection. + """ + select_multiple(name) + bpy.ops.object.curve_boolean(boolean_type='INTERSECT') + active_name('intersection')
+ + +# boolean difference of objects starting with name result is object from basename. +# all objects starting with name will be deleted and the result will be basename + + +
+[docs] +def difference(name, basename): + """Perform a boolean difference operation on objects. + + This function selects a series of objects specified by `name` and + performs a boolean difference operation with the object specified by + `basename`. After the operation, the resulting object is renamed to + 'booleandifference'. The original objects specified by `name` are + deleted after the operation. + + Args: + name (str): The name of the series of objects to select for the operation. + basename (str): The name of the base object to perform the boolean difference with. + """ + # name is the series to select + # basename is what the base you want to cut including name + select_multiple(name) + bpy.context.view_layer.objects.active = bpy.data.objects[basename] + bpy.ops.object.curve_boolean(boolean_type='DIFFERENCE') + active_name('booleandifference') + remove_multiple(name) + rename('booleandifference', basename)
+ + + +# duplicate active object or duplicate move +# if x or y not the default, duplicate move will be executed +
+[docs] +def duplicate(x=0.0, y=0.0): + """Duplicate an active object or move it based on the provided coordinates. + + This function duplicates the currently active object in Blender. If both + x and y are set to their default values (0), the object is duplicated in + place. If either x or y is non-zero, the object is duplicated and moved + by the specified x and y offsets. + + Args: + x (float): The x-coordinate offset for the duplication. + Defaults to 0. + y (float): The y-coordinate offset for the duplication. + Defaults to 0. + """ + if x == 0.0 and y == 0.0: + bpy.ops.object.duplicate() + else: + bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'}, + TRANSFORM_OT_translate={"value": (x, y, 0.0)})
+ + + +# Mirror active object along the x axis +
+[docs] +def mirrorx(): + """Mirror the active object along the x-axis. + + This function utilizes Blender's operator to mirror the currently active + object in the 3D view along the x-axis. It sets the orientation to + global and applies the transformation based on the specified orientation + matrix and constraint axis. + """ + bpy.ops.transform.mirror(orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), + orient_matrix_type='GLOBAL', constraint_axis=(True, False, False))
+ + + +# mirror active object along y axis +
+[docs] +def mirrory(): + """Mirror the active object along the Y axis. + + This function uses Blender's operator to perform a mirror transformation + on the currently active object in the scene. The mirroring is done with + respect to the global coordinate system, specifically along the Y axis. + This can be useful for creating symmetrical objects or for correcting + the orientation of an object in a 3D environment. Raises: None + """ + bpy.ops.transform.mirror(orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), + orient_matrix_type='GLOBAL', constraint_axis=(False, True, False))
+ + + +# move active object and apply translation +
+[docs] +def move(x=0.0, y=0.0): + """Move the active object in the 3D space by applying a translation. + + This function translates the active object in Blender's 3D view by the + specified x and y values. It uses Blender's built-in operations to + perform the translation and then applies the transformation to the + object's location. + + Args: + x (float): The distance to move the object along the x-axis. Defaults to 0.0. + y (float): The distance to move the object along the y-axis. Defaults to 0.0. + """ + bpy.ops.transform.translate(value=(x, y, 0.0)) + bpy.ops.object.transform_apply(location=True)
+ + + +# Rotate active object and apply rotation +
+[docs] +def rotate(angle): + """Rotate the active object by a specified angle. + + This function modifies the rotation of the currently active object in + the Blender context by setting its Z-axis rotation to the given angle. + After updating the rotation, it applies the transformation to ensure + that the changes are saved to the object's data. + + Args: + angle (float): The angle in radians to rotate the active object + around the Z-axis. + """ + bpy.context.object.rotation_euler[2] = angle + bpy.ops.object.transform_apply(rotation=True)
+ + + +# remove doubles +
+[docs] +def remove_doubles(): + """Remove duplicate vertices from the selected curve object. + + This function utilizes the Blender Python API to remove duplicate + vertices from the currently selected curve object in the Blender + environment. It is essential for cleaning up geometry and ensuring that + the curve behaves as expected without unnecessary complexity. + """ + bpy.ops.object.curve_remove_doubles()
+ + + +# Add overcut to active object +
+[docs] +def add_overcut(diametre, overcut=True): + """Add overcut to the active object. + + This function adds an overcut to the currently active object in the + Blender context. If the `overcut` parameter is set to True, it performs + a series of operations including creating a curve overcut with the + specified diameter, deleting the original object, and renaming the new + object to match the original. The function also ensures that any + duplicate vertices are removed from the resulting object. + + Args: + diametre (float): The diameter to be used for the overcut. + overcut (bool): A flag indicating whether to apply the overcut. Defaults to True. + """ + if overcut: + name = bpy.context.active_object.name + 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() + rename(overcut_name, name) + remove_doubles()
+ + + +# add bounding rectangtle to curve +
+[docs] +def add_bound_rectangle(xmin, ymin, xmax, ymax, name='bounds_rectangle'): + """Add a bounding rectangle to a curve. + + This function creates a rectangle defined by the minimum and maximum x + and y coordinates provided as arguments. The rectangle is added to the + scene at the center of the defined bounds. The resulting rectangle is + named according to the 'name' parameter. + + Args: + xmin (float): The minimum x-coordinate of the rectangle. + ymin (float): The minimum y-coordinate of the rectangle. + xmax (float): The maximum x-coordinate of the rectangle. + ymax (float): The maximum y-coordinate of the rectangle. + name (str): The name of the resulting rectangle object. Defaults to + 'bounds_rectangle'. + """ + + xsize = xmax - xmin + ysize = ymax - ymin + + bpy.ops.curve.simple(align='WORLD', location=(xmin + xsize/2, ymin + ysize/2, 0), rotation=(0, 0, 0), + Simple_Type='Rectangle', + Simple_width=xsize, Simple_length=ysize, use_cyclic_u=True, edit_mode=False, shape='3D') + bpy.ops.object.transform_apply(location=True) + active_name(name)
+ + + +
+[docs] +def add_rectangle(width, height, center_x=True, center_y=True): + """Add a rectangle to the scene. + + This function creates a rectangle in the 3D space using the specified + width and height. The rectangle can be centered at the origin or offset + based on the provided parameters. If `center_x` or `center_y` is set to + True, the rectangle will be positioned at the center of the specified + dimensions; otherwise, it will be positioned based on the offsets. + + Args: + width (float): The width of the rectangle. + height (float): The height of the rectangle. + center_x (bool?): If True, centers the rectangle along the x-axis. Defaults to True. + center_y (bool?): If True, centers the rectangle along the y-axis. Defaults to True. + """ + x_offset = width / 2 + y_offset = height / 2 + + if center_x: + x_offset = 0 + if center_y: + y_offset = 0 + + bpy.ops.curve.simple(align='WORLD', location=(x_offset, y_offset, 0), rotation=(0, 0, 0), + Simple_Type='Rectangle', + Simple_width=width, Simple_length=height, use_cyclic_u=True, edit_mode=False, shape='3D') + bpy.ops.object.transform_apply(location=True) + active_name('simple_rectangle')
+ + + +# Returns coords from active object +
+[docs] +def active_to_coords(): + """Convert the active object to a list of its vertex coordinates. + + This function duplicates the currently active object in the Blender + context, converts it to a mesh, and extracts the X and Y coordinates of + its vertices. After extracting the coordinates, it removes the temporary + mesh object created during the process. The resulting list contains + tuples of (x, y) coordinates for each vertex in the active object. + + Returns: + list: A list of tuples, each containing the X and Y coordinates of the + vertices from the active object. + """ + bpy.ops.object.duplicate() + obj = bpy.context.active_object + bpy.ops.object.convert(target='MESH') + active_name("_tmp_mesh") + + coords = [] + for v in obj.data.vertices: # extract X,Y coordinates from the vertices data + coords.append((v.co.x, v.co.y)) + remove_multiple('_tmp_mesh') + return coords
+ + + +# returns shapely polygon from active object +
+[docs] +def active_to_shapely_poly(): + """Convert the active object to a Shapely polygon. + + This function retrieves the coordinates of the currently active object + and converts them into a Shapely Polygon data structure. It is useful + for geometric operations and spatial analysis using the Shapely library. + + Returns: + Polygon: A Shapely Polygon object created from the active object's coordinates. + """ + # convert coordinates to shapely Polygon datastructure + return Polygon(active_to_coords())
+ + + +#checks for curve splines shorter than three points and subdivides if necessary +
+[docs] +def subdivide_short_lines(co): + """Subdivide all polylines to have at least three points. + + This function iterates through the splines of a curve, checks if they are not bezier + and if they have less or equal to two points. If so, each spline is subdivided to get + at least three points. + + Args: + co (Object): A curve object to be analyzed and modified. + """ + + bpy.ops.object.mode_set(mode="EDIT") + for sp in co.data.splines: + if len(sp.points) == 2 and sp.type != 'BEZIER': + bpy.ops.curve.select_all(action='DESELECT') + for pt in sp.points: + pt.select = True + bpy.ops.curve.subdivide() + bpy.ops.object.editmode_toggle() + bpy.ops.object.select_all(action='SELECT')
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/simulation.html b/_modules/cam/simulation.html new file mode 100644 index 000000000..9ed1a3124 --- /dev/null +++ b/_modules/cam/simulation.html @@ -0,0 +1,802 @@ + + + + + + + + + + cam.simulation — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.simulation

+"""CNC CAM 'simulation.py' © 2012 Vilem Novak
+
+Functions to generate a mesh simulation from CAM Chain / Operation data.
+"""
+
+import math
+import time
+
+import numpy as np
+
+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,
+)
+
+
+
+[docs] +def createSimulationObject(name, operations, i): + """Create a simulation object in Blender. + + This function creates a simulation object in Blender with the specified + name and operations. If an object with the given name already exists, it + retrieves that object; otherwise, it creates a new plane object and + applies several modifiers to it. The function also sets the object's + location and scale based on the provided operations and assigns a + texture to the object. + + Args: + name (str): The name of the simulation object to be created. + operations (list): A list of operation objects that contain bounding box information. + i: The image to be used as a texture for the simulation object. + """ + oname = 'csim_' + name + + o = operations[0] + + if oname in bpy.data.objects: + ob = bpy.data.objects[oname] + else: + bpy.ops.mesh.primitive_plane_add( + align='WORLD', enter_editmode=False, location=(0, 0, 0), rotation=(0, 0, 0)) + ob = bpy.context.active_object + ob.name = oname + + bpy.ops.object.modifier_add(type='SUBSURF') + ss = ob.modifiers[-1] + ss.subdivision_type = 'SIMPLE' + ss.levels = 6 + ss.render_levels = 6 + bpy.ops.object.modifier_add(type='SUBSURF') + ss = ob.modifiers[-1] + ss.subdivision_type = 'SIMPLE' + ss.levels = 4 + ss.render_levels = 3 + bpy.ops.object.modifier_add(type='DISPLACE') + + ob.location = ((o.max.x + o.min.x) / 2, (o.max.y + o.min.y) / 2, o.min.z) + ob.scale.x = (o.max.x - o.min.x) / 2 + ob.scale.y = (o.max.y - o.min.y) / 2 + print(o.max.x, o.min.x) + print(o.max.y, o.min.y) + print('bounds') + disp = ob.modifiers[-1] + disp.direction = 'Z' + disp.texture_coords = 'LOCAL' + disp.mid_level = 0 + + if oname in bpy.data.textures: + t = bpy.data.textures[oname] + + t.type = 'IMAGE' + disp.texture = t + + t.image = i + else: + bpy.ops.texture.new() + for t in bpy.data.textures: + if t.name == 'Texture': + t.type = 'IMAGE' + t.name = oname + t = t.type_recast() + t.type = 'IMAGE' + t.image = i + disp.texture = t + ob.hide_render = True + bpy.ops.object.shade_smooth()
+ + + +
+[docs] +async def doSimulation(name, operations): + """Perform simulation of operations for a 3-axis system. + + This function iterates through a list of operations, retrieves the + necessary sources for each operation, and computes the bounds for the + operations. It then generates a simulation image based on the operations + and their limits, saves the image to a specified path, and finally + creates a simulation object in Blender using the generated image. + + Args: + name (str): The name to be used for the simulation object. + operations (list): A list of operations to be simulated. + """ + for o in operations: + 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 = getCachePath(operations[0])[:-len(operations[0].name)] + name + cp = getSimulationPath()+name + print('cp=', cp) + iname = cp + '_sim.exr' + + numpysave(i, iname) + i = bpy.data.images.load(iname) + createSimulationObject(name, operations, i)
+ + + +
+[docs] +async def generateSimulationImage(operations, limits): + """Generate a simulation image based on provided operations and limits. + + This function creates a 2D simulation image by processing a series of + operations that define how the simulation should be conducted. It uses + the limits provided to determine the boundaries of the simulation area. + The function calculates the necessary resolution for the simulation + image based on the specified simulation detail and border width. It + iterates through each operation, simulating the effect of each operation + on the image, and updates the shape keys of the corresponding Blender + object to reflect the simulation results. The final output is a 2D array + representing the simulated image. + + Args: + operations (list): A list of operation objects that contain details + about the simulation, including feed rates and other parameters. + limits (tuple): A tuple containing the minimum and maximum coordinates + (minx, miny, minz, maxx, maxy, maxz) that define the simulation + boundaries. + + Returns: + np.ndarray: A 2D array representing the simulated image. + """ + + minx, miny, minz, maxx, maxy, maxz = limits + # print(minx,miny,minz,maxx,maxy,maxz) + sx = maxx - minx + sy = maxy - miny + + o = operations[0] # getting sim detail and others from first op. + simulation_detail = o.optimisation.simulation_detail + borderwidth = o.borderwidth + resx = math.ceil(sx / simulation_detail) + 2 * borderwidth + resy = math.ceil(sy / simulation_detail) + 2 * borderwidth + + # create array in which simulation happens, similar to an image to be painted in. + si = np.full(shape=(resx, resy), fill_value=maxz, dtype=float) + + num_operations = len(operations) + + start_time = time.time() + + for op_count, o in enumerate(operations): + ob = bpy.data.objects["cam_path_{}".format(o.name)] + m = ob.data + verts = m.vertices + + if o.do_simulation_feedrate: + kname = 'feedrates' + m.attributes.new(".edge_creases","FLOAT","EDGE") + + if m.shape_keys is None or m.shape_keys.key_blocks.find(kname) == -1: + ob.shape_key_add() + if len(m.shape_keys.key_blocks) == 1: + ob.shape_key_add() + shapek = m.shape_keys.key_blocks[-1] + shapek.name = kname + else: + shapek = m.shape_keys.key_blocks[kname] + shapek.data[0].co = (0.0, 0, 0) + + totalvolume = 0.0 + + cutterArray = getCutterArray(o, simulation_detail) + cutterArray = -cutterArray + lasts = verts[1].co + perc = -1 + vtotal = len(verts) + dropped = 0 + + xs = 0 + ys = 0 + + for i, vert in enumerate(verts): + if perc != int(100 * i / vtotal): + perc = int(100 * i / vtotal) + total_perc = (perc + op_count*100) / num_operations + await progress_async(f'Simulation', int(total_perc)) + + if i > 0: + volume = 0 + volume_partial = 0 + s = vert.co + v = s - lasts + + l = v.length + if (lasts.z < maxz or s.z < maxz) and not ( + v.x == 0 and v.y == 0 and v.z > 0): # only simulate inside material, and exclude lift-ups + if ( + v.x == 0 and v.y == 0 and v.z < 0): + # if the cutter goes straight down, we don't have to interpolate. + pass + + elif v.length > simulation_detail: # and not : + + v.length = simulation_detail + lastxs = xs + lastys = ys + while v.length < l: + xs = int((lasts.x + v.x - minx) / simulation_detail + + borderwidth + simulation_detail / 2) + # -middle + ys = int((lasts.y + v.y - miny) / simulation_detail + + borderwidth + simulation_detail / 2) + # -middle + z = lasts.z + v.z + # print(z) + if lastxs != xs or lastys != ys: + volume_partial = simCutterSpot( + xs, ys, z, cutterArray, si, o.do_simulation_feedrate) + if o.do_simulation_feedrate: + totalvolume += volume + volume += volume_partial + lastxs = xs + lastys = ys + else: + dropped += 1 + v.length += simulation_detail + + xs = int((s.x - minx) / simulation_detail + + borderwidth + simulation_detail / 2) # -middle + ys = int((s.y - miny) / simulation_detail + + borderwidth + simulation_detail / 2) # -middle + volume_partial = simCutterSpot( + xs, ys, s.z, cutterArray, si, o.do_simulation_feedrate) + if o.do_simulation_feedrate: # compute volumes and write data into shapekey. + volume += volume_partial + totalvolume += volume + if l > 0: + load = volume / l + else: + load = 0 + + # this will show the shapekey as debugging graph and will use same data to estimate parts + # with heavy load + if l != 0: + shapek.data[i].co.y = (load) * 0.000002 + else: + shapek.data[i].co.y = shapek.data[i - 1].co.y + shapek.data[i].co.x = shapek.data[i - 1].co.x + l * 0.04 + shapek.data[i].co.z = 0 + lasts = s + + # print('dropped '+str(dropped)) + if o.do_simulation_feedrate: # smoothing ,but only backward! + xcoef = shapek.data[len(shapek.data) - 1].co.x / len(shapek.data) + for a in range(0, 10): + # print(shapek.data[-1].co) + nvals = [] + val1 = 0 # + val2 = 0 + w1 = 0 # + w2 = 0 + + for i, d in enumerate(shapek.data): + val = d.co.y + + if i > 1: + d1 = shapek.data[i - 1].co + val1 = d1.y + if d1.x - d.co.x != 0: + w1 = 1 / (abs(d1.x - d.co.x) / xcoef) + + if i < len(shapek.data) - 1: + d2 = shapek.data[i + 1].co + val2 = d2.y + if d2.x - d.co.x != 0: + w2 = 1 / (abs(d2.x - d.co.x) / xcoef) + + # print(val,val1,val2,w1,w2) + + val = (val + val1 * w1 + val2 * w2) / (1.0 + w1 + w2) + nvals.append(val) + for i, d in enumerate(shapek.data): + d.co.y = nvals[i] + + # apply mapping - convert the values to actual feedrates. + total_load = 0 + max_load = 0 + for i, d in enumerate(shapek.data): + total_load += d.co.y + max_load = max(max_load, d.co.y) + normal_load = total_load / len(shapek.data) + + thres = 0.5 + + scale_graph = 0.05 # warning this has to be same as in export in utils!!!! + + totverts = len(shapek.data) + for i, d in enumerate(shapek.data): + if d.co.y > normal_load: + d.co.z = scale_graph * max(0.3, normal_load / d.co.y) + else: + d.co.z = scale_graph * 1 + if i < totverts - 1: + m.attributes[".edge_creases"].data[i].value = d.co.y / (normal_load * 4) + + si = si[borderwidth:-borderwidth, borderwidth:-borderwidth] + si += -minz + + await progress_async("Simulated:", time.time()-start_time, 's') + return si
+ + + +
+[docs] +def simCutterSpot(xs, ys, z, cutterArray, si, getvolume=False): + """Simulates a cutter cutting into stock and optionally returns the volume + removed. + + This function takes the position of a cutter and modifies a stock image + by simulating the cutting process. It updates the stock image based on + the cutter's dimensions and position, ensuring that the stock does not + go below a certain level defined by the cutter's height. If requested, + it also calculates and returns the volume of material that has been + milled away. + + Args: + xs (int): The x-coordinate of the cutter's position. + ys (int): The y-coordinate of the cutter's position. + z (float): The height of the cutter. + cutterArray (numpy.ndarray): A 2D array representing the cutter's shape. + si (numpy.ndarray): A 2D array representing the stock image to be modified. + getvolume (bool?): If True, the function returns the volume removed. Defaults to False. + + Returns: + float: The volume of material removed if `getvolume` is True; otherwise, + returns 0. + """ + m = int(cutterArray.shape[0] / 2) + size = cutterArray.shape[0] + # whole cutter in image there + if xs > m and xs < si.shape[0] - m and ys > m and ys < si.shape[1] - m: + if getvolume: + volarray = si[xs - m:xs - m + size, ys - m:ys - m + size].copy() + si[xs - m:xs - m + size, ys - m:ys - m + size] = np.minimum(si[xs - m:xs - m + size, ys - m:ys - m + size], + cutterArray + z) + if getvolume: + volarray = si[xs - m:xs - m + size, ys - m:ys - m + size] - volarray + vsum = abs(volarray.sum()) + # print(vsum) + return vsum + + elif xs > -m and xs < si.shape[0] + m and ys > -m and ys < si.shape[1] + m: + # part of cutter in image, for extra large cutters + + startx = max(0, xs - m) + starty = max(0, ys - m) + endx = min(si.shape[0], xs - m + size) + endy = min(si.shape[0], ys - m + size) + castartx = max(0, m - xs) + castarty = max(0, m - ys) + caendx = min(size, si.shape[0] - xs + m) + caendy = min(size, si.shape[1] - ys + m) + + if getvolume: + volarray = si[startx:endx, starty:endy].copy() + si[startx:endx, starty:endy] = np.minimum(si[startx:endx, starty:endy], + cutterArray[castartx:caendx, castarty:caendy] + z) + if getvolume: + volarray = si[startx:endx, starty:endy] - volarray + vsum = abs(volarray.sum()) + # print(vsum) + return vsum + return 0
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/slice.html b/_modules/cam/slice.html new file mode 100644 index 000000000..b1003f9b0 --- /dev/null +++ b/_modules/cam/slice.html @@ -0,0 +1,628 @@ + + + + + + + + + + cam.slice — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.slice

+"""CNC CAM 'slice.py' © 2021 Alain Pelletier
+
+Very simple slicing for 3D meshes, useful for plywood cutting.
+Completely rewritten April 2021.
+"""
+
+import bpy
+from bpy.props import (
+    BoolProperty,
+    FloatProperty,
+)
+from bpy.types import PropertyGroup
+
+from . import (
+    constants,
+    utils,
+)
+
+
+
+[docs] +def slicing2d(ob, height): + """Slice a 3D object at a specified height and convert it to a curve. + + This function applies transformations to the given object, switches to + edit mode, selects all vertices, and performs a bisect operation to + slice the object at the specified height. After slicing, it resets the + object's location and applies transformations again before converting + the object to a curve. If the conversion fails (for instance, if the + mesh was empty), the function deletes the mesh and returns False. + Otherwise, it returns True. + + Args: + ob (bpy.types.Object): The Blender object to be sliced and converted. + height (float): The height at which to slice the object. + + Returns: + bool: True if the conversion to curve was successful, False otherwise. + """ + # April 2020 Alain Pelletier + # let's slice things + bpy.ops.object.transform_apply(location=True, rotation=False, scale=False) + bpy.ops.object.mode_set(mode='EDIT') # force edit mode + bpy.ops.mesh.select_all(action='SELECT') # select all vertices + # actual slicing here + bpy.ops.mesh.bisect(plane_co=(0.0, 0.0, height), plane_no=(0.0, 0.0, 1.0), use_fill=True, clear_inner=True, + clear_outer=True) + # slicing done + bpy.ops.object.mode_set(mode='OBJECT') # force object mode + # bring all the slices to 0 level and reset location transform + ob.location[2] = -1 * height + bpy.ops.object.transform_apply(location=True, rotation=False, scale=False) + bpy.ops.object.convert(target='CURVE') # convert it to curve + if bpy.context.active_object.type != 'CURVE': # conversion failed because mesh was empty so delete mesh + bpy.ops.object.delete(use_global=False, confirm=False) + return False + bpy.ops.object.select_all(action='DESELECT') # deselect everything + return True
+ + + +
+[docs] +def slicing3d(ob, start, end): + """Slice a 3D object along specified planes. + + This function applies transformations to a given object and slices it in + the Z-axis between two specified values, `start` and `end`. It first + ensures that the object is in edit mode and selects all vertices before + performing the slicing operations using the `bisect` method. After + slicing, it resets the object's location and applies the transformations + to maintain the changes. + + Args: + ob (Object): The 3D object to be sliced. + start (float): The starting Z-coordinate for the slice. + end (float): The ending Z-coordinate for the slice. + + Returns: + bool: True if the slicing operation was successful. + """ + # April 2020 Alain Pelletier + # let's slice things + bpy.ops.object.transform_apply(location=True, rotation=False, scale=False) + bpy.ops.object.mode_set(mode='EDIT') # force edit mode + bpy.ops.mesh.select_all(action='SELECT') # select all vertices + # actual slicing here + bpy.ops.mesh.bisect(plane_co=(0.0, 0.0, start), plane_no=(0.0, 0.0, 1.0), use_fill=False, clear_inner=True, + clear_outer=False) + bpy.ops.mesh.select_all(action='SELECT') # select all vertices which + bpy.ops.mesh.bisect(plane_co=(0.0, 0.0, end), plane_no=(0.0, 0.0, 1.0), use_fill=True, clear_inner=False, + clear_outer=True) + # slicing done + bpy.ops.object.mode_set(mode='OBJECT') # force object mode + # bring all the slices to 0 level and reset location transform + ob.location[2] = -1 * start + bpy.ops.object.transform_apply(location=True, rotation=False, scale=False) + + bpy.ops.object.select_all(action='DESELECT') # deselect everything + return True
+ + + +
+[docs] +def sliceObject(ob): + """Slice a 3D object into layers based on a specified thickness. + + This function takes a 3D object and slices it into multiple layers + according to the specified thickness. It creates a new collection for + the slices and optionally creates text labels for each slice if the + indexes parameter is set. The slicing can be done in either 2D or 3D + based on the user's selection. The function also handles the positioning + of the slices based on the object's bounding box. + + Args: + ob (bpy.types.Object): The 3D object to be sliced. + """ + # April 2020 Alain Pelletier + # get variables from menu + thickness = bpy.context.scene.cam_slice.slice_distance + slice3d = bpy.context.scene.cam_slice.slice_3d + indexes = bpy.context.scene.cam_slice.indexes + above0 = bpy.context.scene.cam_slice.slice_above0 + # setup the collections + scollection = bpy.data.collections.new("Slices") + bpy.context.scene.collection.children.link(scollection) + if indexes: + tcollection = bpy.data.collections.new("Text") + bpy.context.scene.collection.children.link(tcollection) + + bpy.ops.object.mode_set(mode='OBJECT') # force object mode + minx, miny, minz, maxx, maxy, maxz = utils.getBoundsWorldspace([ob]) + + start_height = minz + if above0 and minz < 0: + start_height = 0 + + # calculate amount of layers needed + layeramt = 1 + int((maxz - start_height) // thickness) + + for layer in range(layeramt): + height = round(layer * thickness, 6) # height of current layer + t = str(layer) + "-" + str(height * 1000) + slicename = "slice_" + t # name for the current slice + tslicename = "t_" + t # name for the current slice text + height += start_height + print(slicename) + + ob.select_set(True) # select object to be sliced + bpy.context.view_layer.objects.active = ob # make object to be sliced active + bpy.ops.object.duplicate() # make a copy of object to be sliced + bpy.context.view_layer.objects.active.name = slicename # change the name of object + + # attribute active object to obslice + obslice = bpy.context.view_layer.objects.active + scollection.objects.link(obslice) # link obslice to scollecton + if slice3d: + # slice 3d at desired height and stop at desired height + slicesuccess = slicing3d(obslice, height, height + thickness) + else: + # slice object at desired height + slicesuccess = slicing2d(obslice, height) + + if indexes and slicesuccess: + # text objects + bpy.ops.object.text_add() # new text object + textob = bpy.context.active_object + textob.data.size = 0.006 # change size of object + textob.data.body = t # text content + textob.location = (0, 0, 0) # text location + textob.name = tslicename # change the name of object + bpy.ops.object.select_all(action='DESELECT') # deselect everything + tcollection.objects.link(textob) # add to text collection + textob.parent = obslice # make textob child of obslice + + # select all slices + for obj in bpy.data.collections['Slices'].all_objects: + obj.select_set(True)
+ + + +
+[docs] +class SliceObjectsSettings(PropertyGroup): + """Stores All Data for Machines""" + +
+[docs] + 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", + )
+ +
+[docs] + slice_above0: BoolProperty( + name="Slice Above 0", + description="only slice model above 0", + default=False, + )
+ +
+[docs] + slice_3d: BoolProperty( + name="3D Slice", + description="For 3D carving", + default=False, + )
+ +
+[docs] + indexes: BoolProperty( + name="Add Indexes", + description="Adds index text of layer + index", + default=True, + )
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/strategy.html b/_modules/cam/strategy.html new file mode 100644 index 000000000..d29ee3645 --- /dev/null +++ b/_modules/cam/strategy.html @@ -0,0 +1,1658 @@ + + + + + + + + + + cam.strategy — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.strategy

+"""CNC CAM 'strategy.py' © 2012 Vilem Novak
+
+Strategy functionality of CNC CAM - e.g. Cutout, Parallel, Spiral, Waterline
+The functions here are called with operators defined in 'ops.py'
+"""
+
+from math import (
+    ceil,
+    pi,
+    radians,
+    sqrt,
+    tan,
+)
+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,
+    subdivide_short_lines,
+)
+from .utils import (
+    Add_Pocket,
+    checkEqual,
+    extendChunks5axis,
+    getObjectOutline,
+    getObjectSilhouete,
+    getOperationSilhouete,
+    getOperationSources,
+    Helix,
+    # Point,
+    sampleChunksNAxis,
+    sortChunks,
+    unique,
+)
+from .curvecamcreate import(
+    generate_crosshatch
+)
+
+
+[docs] +SHAPELY = True
+ + + +# cutout strategy is completely here: +
+[docs] +async def cutout(o): + """Perform a cutout operation based on the provided parameters. + + This function calculates the necessary cutter offset based on the cutter + type and its parameters. It processes a list of objects to determine how + to cut them based on their geometry and the specified cutting type. The + function handles different cutter types such as 'VCARVE', 'CYLCONE', + 'BALLCONE', and 'BALLNOSE', applying specific calculations for each. It + also manages the layering and movement strategies for the cutting + operation, including options for lead-ins, ramps, and bridges. + + Args: + o (object): An object containing parameters for the cutout operation, + including cutter type, diameter, depth, and other settings. + + Returns: + None: This function does not return a value but performs operations + on the provided object. + """ + + max_depth = checkminz(o) + cutter_angle = radians(o.cutter_tip_angle / 2) + c_offset = o.cutter_diameter / 2 # cutter offset + print("cuttertype:", o.cutter_type, "max_depth:", max_depth) + if o.cutter_type == 'VCARVE': + c_offset = -max_depth * tan(cutter_angle) + elif o.cutter_type == 'CYLCONE': + c_offset = -max_depth * tan(cutter_angle) + o.cylcone_diameter / 2 + elif o.cutter_type == 'BALLCONE': + 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 = sqrt(r ** 2 - (r + max_depth) ** 2) + print("offset:", c_offset) + if c_offset > o.cutter_diameter / 2: + c_offset = o.cutter_diameter / 2 + c_offset += o.skin # add skin for profile + if o.straight: + join = 2 + else: + join = 1 + print('Operation: Cutout') + offset = True + + for ob in o.objects: + if ob.type == 'CURVE': + if ob.data.splines and ob.data.splines[0].type == 'BEZIER': + activate(ob) + bpy.ops.object.curve_remove_doubles(merge_distance=0.0001, keep_bezier=True) + else: + bpy.ops.object.curve_remove_doubles() + #make sure all polylines are at least three points long + subdivide_short_lines(ob) + + if o.cut_type == 'ONLINE' and o.onlycurves: # is separate to allow open curves :) + print('separate') + chunksFromCurve = [] + for ob in o.objects: + chunksFromCurve.extend(curveToChunks(ob, o.use_modifiers)) + + # chunks always have polys now + # for ch in chunksFromCurve: + # # print(ch.points) + + # if len(ch.points) > 2: + # ch.poly = chunkToShapely(ch) + + # p.addContour(ch.poly) + else: + chunksFromCurve = [] + if o.cut_type == 'ONLINE': + p = getObjectOutline(0, o, True) + + else: + offset = True + if o.cut_type == 'INSIDE': + offset = False + + p = getObjectOutline(c_offset, o, offset) + if o.outlines_count > 1: + for i in range(1, o.outlines_count): + chunksFromCurve.extend(shapelyToChunks(p, -1)) + path_distance = o.dist_between_paths + if o.cut_type == "INSIDE": + path_distance *= -1 + p = p.buffer(distance=path_distance, resolution=o.optimisation.circle_detail, join_style=join, + mitre_limit=2) + + chunksFromCurve.extend(shapelyToChunks(p, -1)) + if o.outlines_count > 1 and o.movement.insideout == 'OUTSIDEIN': + chunksFromCurve.reverse() + + # parentChildPoly(chunksFromCurve,chunksFromCurve,o) + chunksFromCurve = limitChunks(chunksFromCurve, o) + if not o.dont_merge: + parentChildPoly(chunksFromCurve, chunksFromCurve, o) + if o.outlines_count == 1: + chunksFromCurve = await sortChunks(chunksFromCurve, o) + + if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW') or ( + o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW'): + for ch in chunksFromCurve: + ch.reverse() + + if o.cut_type == 'INSIDE': # there would bee too many conditions above, + # so for now it gets reversed once again when inside cutting. + for ch in chunksFromCurve: + ch.reverse() + + layers = getLayers(o, o.maxz, checkminz(o)) + extendorder = [] + + if o.first_down: # each shape gets either cut all the way to bottom, + # or every shape gets cut 1 layer, then all again. has to create copies, + # because same chunks are worked with on more layers usually + for chunk in chunksFromCurve: + dir_switch = False # needed to avoid unnecessary lifting of cutter with open chunks + # and movement set to "MEANDER" + for layer in layers: + chunk_copy = chunk.copy() + if dir_switch: + chunk_copy.reverse() + extendorder.append([chunk_copy, layer]) + if (not chunk.closed) and o.movement.type == "MEANDER": + dir_switch = not dir_switch + else: + for layer in layers: + for chunk in chunksFromCurve: + extendorder.append([chunk.copy(), layer]) + + for chl in extendorder: # Set Z for all chunks + chunk = chl[0] + layer = chl[1] + print(layer[1]) + chunk.setZ(layer[1]) + + chunks = [] + + if o.use_bridges: # add bridges to chunks + print('Using Bridges') + remove_multiple(o.name+'_cut_bridges') + print("Old Briddge Cut Removed") + + bridgeheight = min(o.max.z, o.min.z + abs(o.bridges_height)) + + for chl in extendorder: + chunk = chl[0] + layer = chl[1] + if layer[1] < bridgeheight: + useBridges(chunk, o) + + if o.profile_start > 0: + print("Cutout Change Profile Start") + for chl in extendorder: + chunk = chl[0] + if chunk.closed: + chunk.changePathStart(o) + + # Lead in + if o.lead_in > 0.0 or o.lead_out > 0: + print("Cutout Lead-in") + for chl in extendorder: + chunk = chl[0] + if chunk.closed: + chunk.breakPathForLeadinLeadout(o) + chunk.leadContour(o) + + if o.movement.ramp: # add ramps or simply add chunks + for chl in extendorder: + chunk = chl[0] + layer = chl[1] + if chunk.closed: + chunk.rampContour(layer[0], layer[1], o) + chunks.append(chunk) + else: + chunk.rampZigZag(layer[0], layer[1], o) + chunks.append(chunk) + else: + for chl in extendorder: + chunks.append(chl[0]) + + chunksToMesh(chunks, o)
+ + + +
+[docs] +async def curve(o): + """Process and convert curve objects into mesh chunks. + + This function takes an operation object and processes the curves + contained within it. It first checks if all objects are curves; if not, + it raises an exception. The function then converts the curves into + chunks, sorts them, and refines them. If layers are to be used, it + applies layer information to the chunks, adjusting their Z-offsets + accordingly. Finally, it converts the processed chunks into a mesh. + + Args: + o (Operation): An object containing operation parameters, including a list of + objects, flags for layer usage, and movement constraints. + + Returns: + None: This function does not return a value; it performs operations on the + input. + + Raises: + CamException: If not all objects in the operation are curves. + """ + + print('Operation: Curve') + pathSamples = [] + getOperationSources(o) + if not o.onlycurves: + raise CamException("All Objects Must Be Curves for This Operation.") + + for ob in o.objects: + #make sure all polylines are at least three points long + subdivide_short_lines(ob) + # make the chunks from curve here + pathSamples.extend(curveToChunks(ob)) + # sort before sampling + pathSamples = await sortChunks(pathSamples, o) + pathSamples = chunksRefine(pathSamples, o) # simplify + + # layers here + if o.use_layers: + layers = getLayers(o, o.maxz, round(checkminz(o), 6)) + # layers is a list of lists [[0.00,l1],[l1,l2],[l2,l3]] containg the start and end of each layer + extendorder = [] + chunks = [] + for layer in layers: + for ch in pathSamples: + # include layer information to chunk list + extendorder.append([ch.copy(), layer]) + + for chl in extendorder: # Set offset Z for all chunks according to the layer information, + chunk = chl[0] + layer = chl[1] + print('layer: ' + str(layer[1])) + chunk.offsetZ(o.maxz * 2 - o.minz + layer[1]) + chunk.clampZ(o.minz) # safety to not cut lower than minz + # safety, not higher than free movement height + chunk.clampmaxZ(o.movement.free_height) + + for chl in extendorder: # strip layer information from extendorder and transfer them to chunks + chunks.append(chl[0]) + + chunksToMesh(chunks, o) # finish by converting to mesh + + else: # no layers, old curve + for ch in pathSamples: + ch.clampZ(o.minz) # safety to not cut lower than minz + # safety, not higher than free movement height + ch.clampmaxZ(o.movement.free_height) + chunksToMesh(pathSamples, o)
+ + + +
+[docs] +async def proj_curve(s, o): + """Project a curve onto another curve object. + + This function takes a source object and a target object, both of which + are expected to be curve objects. It projects the points of the source + curve onto the target curve, adjusting the start and end points based on + specified extensions. The resulting projected points are stored in the + source object's path samples. + + Args: + s (object): The source object containing the curve to be projected. + o (object): An object containing references to the curve objects + involved in the projection. + + Returns: + None: This function does not return a value; it modifies the + source object's path samples in place. + + Raises: + CamException: If the target curve is not of type 'CURVE'. + """ + + print('Operation: Projected Curve') + pathSamples = [] + chunks = [] + ob = bpy.data.objects[o.curve_object] + pathSamples.extend(curveToChunks(ob)) + + targetCurve = s.objects[o.curve_object1] + + from cam import cam_chunk + if targetCurve.type != 'CURVE': + raise CamException('Projection Target and Source Have to Be Curve Objects!') + + if 1: + extend_up = 0.1 + extend_down = 0.04 + tsamples = curveToChunks(targetCurve) + for chi, ch in enumerate(pathSamples): + cht = tsamples[chi].get_points() + ch.depth = 0 + ch_points = ch.get_points() + for i, s in enumerate(ch_points): + # move the points a bit + ep = Vector(cht[i]) + sp = Vector(ch_points[i]) + # extend startpoint + vecs = sp - ep + vecs.normalize() + vecs *= extend_up + sp += vecs + ch.startpoints.append(sp) + + # extend endpoint + vece = sp - ep + vece.normalize() + vece *= extend_down + ep -= vece + ch.endpoints.append(ep) + + ch.rotations.append((0, 0, 0)) + + vec = sp - ep + ch.depth = min(ch.depth, -vec.length) + ch_points[i] = sp.copy() + ch.set_points(ch_points) + layers = getLayers(o, 0, ch.depth) + + chunks.extend(sampleChunksNAxis(o, pathSamples, layers)) + chunksToMesh(chunks, o)
+ + + +
+[docs] +async def pocket(o): + """Perform pocketing operation based on the provided parameters. + + This function executes a pocketing operation using the specified + parameters from the object `o`. It calculates the cutter offset based on + the cutter type and depth, processes curves, and generates the necessary + chunks for the pocketing operation. The function also handles various + movement types and optimizations, including helix entry and retract + movements. + + Args: + o (object): An object containing parameters for the pocketing + + Returns: + None: The function modifies the scene and generates geometry + based on the pocketing operation. + """ + if o.straight: + join = 2 + else: + join = 1 + print('Operation: Pocket') + scene = bpy.context.scene + + remove_multiple("3D_poc") + + max_depth = checkminz(o) + o.skin + cutter_angle = radians(o.cutter_tip_angle / 2) + c_offset = o.cutter_diameter / 2 + if o.cutter_type == 'VCARVE': + c_offset = -max_depth * tan(cutter_angle) + elif o.cutter_type == 'CYLCONE': + c_offset = -max_depth * tan(cutter_angle) + o.cylcone_diameter / 2 + elif o.cutter_type == 'BALLCONE': + 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) + obname = o.object_name + c_ob =bpy.data.objects[obname] + for ob in o.objects: + if ob.type == 'CURVE': + if ob.data.splines and ob.data.splines[0].type == 'BEZIER': + activate(ob) + bpy.ops.object.curve_remove_doubles(merge_distance=0.0001, keep_bezier=True) + else: + bpy.ops.object.curve_remove_doubles() + chunksFromCurve = [] + angle = radians(o.parallelPocketAngle) + distance = o.dist_between_paths + offset= -c_offset + pocket_shape = "" + n_angle= angle-pi/2 + pr = getObjectOutline(0, o, False) + if o.pocketType == 'PARALLEL': + if o.parallelPocketContour: + offset= -(c_offset+distance/2) + p = pr.buffer(-c_offset, resolution = o.optimisation.circle_detail, + join_style = join, mitre_limit = 2) + nchunks = shapelyToChunks(p, o.min.z) + chunksFromCurve.extend(nchunks) + crosshatch_result = generate_crosshatch(bpy.context, angle, distance, + offset, pocket_shape, join, c_ob) + nchunks = shapelyToChunks(crosshatch_result, o.min.z) + chunksFromCurve.extend(nchunks) + + if o.parallelPocketCrosshatch: + crosshatch_result = generate_crosshatch(bpy.context, n_angle, + distance, offset, pocket_shape,join,c_ob) + nchunks = shapelyToChunks(crosshatch_result, o.min.z) + chunksFromCurve.extend(nchunks) + + else: + p = pr.buffer(-c_offset, resolution = o.optimisation.circle_detail, + join_style = join, mitre_limit = 2) + approxn = (min(o.max.x - o.min.x, o.max.y - o.min.y) / o.dist_between_paths) / 2 + print("Approximative:" + str(approxn)) + print(o) + + i = 0 + chunks = [] + lastchunks = [] + centers = None + firstoutline = p # for testing in the end. + prest = p.buffer(-c_offset, o.optimisation.circle_detail) + + while not p.is_empty: + if o.pocketToCurve: + # make a curve starting with _3dpocket + shapelyToCurve('3dpocket', p, 0.0) + + nchunks = shapelyToChunks(p, o.min.z) + # print("nchunks") + pnew = p.buffer(-o.dist_between_paths, o.optimisation.circle_detail,join_style=join, mitre_limit=2) + if pnew.is_empty: + + # test if the last curve will leave material + pt = p.buffer(-c_offset, o.optimisation.circle_detail,join_style=join, mitre_limit=2) + if not pt.is_empty: + pnew = pt + # print("pnew") + + nchunks = limitChunks(nchunks, o) + chunksFromCurve.extend(nchunks) + parentChildDist(lastchunks, nchunks, o) + lastchunks = nchunks + + percent = int(i / approxn * 100) + progress('Outlining Polygons ', percent) + p = pnew + + i += 1 + + # if (o.poc)#TODO inside outside! + if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CW') or ( + o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CCW'): + for ch in chunksFromCurve: + ch.reverse() + + chunksFromCurve = await sortChunks(chunksFromCurve, o) + + chunks = [] + layers = getLayers(o, o.maxz, checkminz(o)) + + for l in layers: + lchunks = setChunksZ(chunksFromCurve, l[1]) + if o.movement.ramp: + for ch in lchunks: + ch.zstart = l[0] + ch.zend = l[1] + + # helix_enter first try here TODO: check if helix radius is not out of operation area. + if o.movement.helix_enter: + helix_radius = c_offset * o.movement.helix_diameter * 0.01 # 90 percent of cutter radius + helix_circumference = helix_radius * pi * 2 + + revheight = helix_circumference * tan(o.movement.ramp_in_angle) + for chi, ch in enumerate(lchunks): + if not chunksFromCurve[chi].children: + # TODO:intercept closest next point when it should stay low + p = ch.get_point(0) + # first thing to do is to check if helix enter can really enter. + checkc = Circle(helix_radius + c_offset, o.optimisation.circle_detail) + checkc = affinity.translate(checkc, p[0], p[1]) + covers = False + for poly in o.silhouete.geoms: + if poly.contains(checkc): + covers = True + break + + if covers: + revolutions = (l[0] - p[2]) / revheight + # print(revolutions) + h = Helix(helix_radius, o.optimisation.circle_detail, l[0], p, revolutions) + # invert helix if not the typical direction + if (o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW') or ( + o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW'): + nhelix = [] + for v in h: + nhelix.append((2 * p[0] - v[0], v[1], v[2])) + h = nhelix + ch.extend(h, at_index=0) +# ch.points = h + ch.points + + else: + o.info.warnings += 'Helix entry did not fit! \n ' + ch.closed = True + ch.rampZigZag(l[0], l[1], o) + # Arc retract here first try: + # TODO: check for entry and exit point before actual computing... will be much better. + if o.movement.retract_tangential: + # TODO: fix this for CW and CCW! + for chi, ch in enumerate(lchunks): + # print(chunksFromCurve[chi]) + # print(chunksFromCurve[chi].parents) + if chunksFromCurve[chi].parents == [] or len(chunksFromCurve[chi].parents) == 1: + + revolutions = 0.25 + v1 = Vector(ch.get_point(-1)) + i = -2 + v2 = Vector(ch.get_point(i)) + v = v1 - v2 + while v.length == 0: + i = i - 1 + v2 = Vector(ch.get_point(i)) + v = v1 - v2 + + v.normalize() + rotangle = Vector((v.x, v.y)).angle_signed(Vector((1, 0))) + e = Euler((0, 0, pi / 2.0)) # TODO:#CW CLIMB! + v.rotate(e) + p = v1 + v * o.movement.retract_radius + center = p + p = (p.x, p.y, p.z) + + # progress(str((v1,v,p))) + h = Helix(o.movement.retract_radius, o.optimisation.circle_detail, + p[2] + o.movement.retract_height, p, revolutions) + + # angle to rotate whole retract move + e = Euler((0, 0, rotangle + pi)) + rothelix = [] + c = [] # polygon for outlining and checking collisions. + for p in h: # rotate helix to go from tangent of vector + v1 = Vector(p) + + v = v1 - center + v.x = -v.x # flip it here first... + v.rotate(e) + p = center + v + rothelix.append(p) + c.append((p[0], p[1])) + + c = sgeometry.Polygon(c) + # print('çoutline') + # print(c) + coutline = c.buffer(c_offset, o.optimisation.circle_detail) + # print(h) + # print('çoutline') + # print(coutline) + # polyToMesh(coutline,0) + rothelix.reverse() + + covers = False + for poly in o.silhouete.geoms: + if poly.contains(coutline): + covers = True + break + + if covers: + ch.extend(rothelix) + + chunks.extend(lchunks) + + if o.movement.ramp: + for ch in chunks: + ch.rampZigZag(ch.zstart, ch.get_point(0)[2], o) + + if o.first_down: + if o.pocket_option == "OUTSIDE": + chunks.reverse() + chunks = await sortChunks(chunks, o) + + if o.pocketToCurve: # make curve instead of a path + join_multiple("3dpocket") + + else: + chunksToMesh(chunks, o) # make normal pocket path
+ + + +
+[docs] +async def drill(o): + """Perform a drilling operation on the specified objects. + + This function iterates through the objects in the provided context, + activating each object and applying transformations. It duplicates the + objects and processes them based on their type (CURVE or MESH). For + CURVE objects, it calculates the bounding box and center points of the + splines and bezier points, and generates chunks based on the specified + drill type. For MESH objects, it generates chunks from the vertices. The + function also manages layers and chunk depths for the drilling + operation. + + Args: + o (object): An object containing properties and methods required + for the drilling operation, including a list of + objects to drill, drill type, and depth parameters. + + Returns: + None: This function does not return a value but performs operations + that modify the state of the Blender context. + """ + + print('Operation: Drill') + chunks = [] + for ob in o.objects: + activate(ob) + + bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'}, + TRANSFORM_OT_translate={"value": (0, 0, 0), + "constraint_axis": (False, False, False), + "orient_type": 'GLOBAL', "mirror": False, + "use_proportional_edit": False, + "proportional_edit_falloff": 'SMOOTH', + "proportional_size": 1, "snap": False, + "snap_target": 'CLOSEST', "snap_point": (0, 0, 0), + "snap_align": False, "snap_normal": (0, 0, 0), + "texture_space": False, "release_confirm": False}) + # bpy.ops.collection.objects_remove_all() + bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') + + ob = bpy.context.active_object + if ob.type == 'CURVE': + ob.data.dimensions = '3D' + try: + bpy.ops.object.transform_apply(location=True, rotation=False, scale=False) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + + except: + pass + l = ob.location + + if ob.type == 'CURVE': + + for c in ob.data.splines: + maxx, minx, maxy, miny, maxz, minz = -10000, 10000, -10000, 10000, -10000, 10000 + for p in c.points: + if o.drill_type == 'ALL_POINTS': + chunks.append(camPathChunk([(p.co.x + l.x, p.co.y + l.y, p.co.z + l.z)])) + minx = min(p.co.x, minx) + maxx = max(p.co.x, maxx) + miny = min(p.co.y, miny) + maxy = max(p.co.y, maxy) + minz = min(p.co.z, minz) + maxz = max(p.co.z, maxz) + for p in c.bezier_points: + if o.drill_type == 'ALL_POINTS': + chunks.append(camPathChunk([(p.co.x + l.x, p.co.y + l.y, p.co.z + l.z)])) + minx = min(p.co.x, minx) + maxx = max(p.co.x, maxx) + miny = min(p.co.y, miny) + maxy = max(p.co.y, maxy) + minz = min(p.co.z, minz) + maxz = max(p.co.z, maxz) + cx = (maxx + minx) / 2 + cy = (maxy + miny) / 2 + cz = (maxz + minz) / 2 + + center = (cx, cy) + aspect = (maxx - minx) / (maxy - miny) + if (1.3 > aspect > 0.7 and o.drill_type == 'MIDDLE_SYMETRIC') or o.drill_type == 'MIDDLE_ALL': + chunks.append(camPathChunk([(center[0] + l.x, center[1] + l.y, cz + l.z)])) + + elif ob.type == 'MESH': + for v in ob.data.vertices: + chunks.append(camPathChunk([(v.co.x + l.x, v.co.y + l.y, v.co.z + l.z)])) + delob(ob) # delete temporary object with applied transforms + + layers = getLayers(o, o.maxz, checkminz(o)) + + chunklayers = [] + for layer in layers: + for chunk in chunks: + # If using object for minz then use z from points in object + if o.minz_from == 'OBJECT': + z = chunk.get_point(0)[2] + else: # using operation minz + z = o.minz + # only add a chunk layer if the chunk z point is in or lower than the layer + if z <= layer[0]: + if z <= layer[1]: + z = layer[1] + # perform peck drill + newchunk = chunk.copy() + newchunk.setZ(z) + chunklayers.append(newchunk) + # retract tool to maxz (operation depth start in ui) + newchunk = chunk.copy() + newchunk.setZ(o.maxz) + chunklayers.append(newchunk) + + chunklayers = await sortChunks(chunklayers, o) + chunksToMesh(chunklayers, o)
+ + + +
+[docs] +async def medial_axis(o): + """Generate the medial axis for a given operation. + + This function computes the medial axis of the specified operation, which + involves processing various cutter types and their parameters. It starts + by removing any existing medial mesh, then calculates the maximum depth + based on the cutter type and its properties. The function refines curves + and computes the Voronoi diagram for the points derived from the + operation's silhouette. It filters points and edges based on their + positions relative to the computed shapes, and generates a mesh + representation of the medial axis. Finally, it handles layers and + optionally adds a pocket operation if specified. + + Args: + o (Operation): An object containing parameters for the operation, including + cutter type, dimensions, and other relevant properties. + + Returns: + dict: A dictionary indicating the completion status of the operation. + + Raises: + CamException: If an unsupported cutter type is provided or if the input curve + is not closed. + """ + + print('Operation: Medial Axis') + + remove_multiple("medialMesh") + + from .voronoi import Site, computeVoronoiDiagram + + chunks = [] + + gpoly = spolygon.Polygon() + angle = o.cutter_tip_angle + 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': + angle = o.cutter_tip_angle + # start the max depth calc from the "start depth" of the operation. + maxdepth = o.maxz - slope * o.cutter_diameter / 2 - o.skin + # don't cut any deeper than the "end depth" of the operation. + if maxdepth < o.minz: + maxdepth = o.minz + # the effective cutter diameter can be reduced from it's max + # since we will be cutting shallower than the original maxdepth + # without this, the curve is calculated as if the diameter was at the original maxdepth and we get the bit + # pulling away from the desired cut surface + new_cutter_diameter = (maxdepth - o.maxz) / (- slope) * 2 + elif o.cutter_type == 'BALLNOSE': + maxdepth = - new_cutter_diameter / 2 - o.skin + else: + raise CamException("Only Ballnose and V-carve Cutters Are Supported for Medial Axis.") + # remember resolutions of curves, to refine them, + # otherwise medial axis computation yields too many branches in curved parts + resolutions_before = [] + + for ob in o.objects: + if ob.type == 'CURVE': + if ob.data.splines and ob.data.splines[0].type == 'BEZIER': + activate(ob) + bpy.ops.object.curve_remove_doubles(merge_distance=0.0001, keep_bezier=True) + else: + bpy.ops.object.curve_remove_doubles() + + for ob in o.objects: + if ob.type == 'CURVE' or ob.type == 'FONT': + resolutions_before.append(ob.data.resolution_u) + if ob.data.resolution_u < 64: + ob.data.resolution_u = 64 + + polys = getOperationSilhouete(o) + if isinstance(polys, list): + if len(polys) == 1 and isinstance(polys[0], shapely.MultiPolygon): + mpoly = polys[0] + else: + mpoly = sgeometry.MultiPolygon(polys) + elif isinstance(polys, shapely.MultiPolygon): + # just a multipolygon + mpoly = polys + else: + raise CamException("Failed Getting Object Silhouette. Is Input Curve Closed?") + + mpoly_boundary = mpoly.boundary + ipol = 0 + for poly in mpoly.geoms: + ipol = ipol + 1 + schunks = shapelyToChunks(poly, -1) + schunks = chunksRefineThreshold(schunks, o.medial_axis_subdivision, + o.medial_axis_threshold) # chunksRefine(schunks,o) + + verts = [] + for ch in schunks: + verts.extend(ch.get_points()) + # for pt in ch.get_points(): + # # pvoro = Site(pt[0], pt[1]) + # verts.append(pt) # (pt[0], pt[1]), pt[2]) + # verts= points#[[vert.x, vert.y, vert.z] for vert in vertsPts] + nDupli, nZcolinear = unique(verts) + nVerts = len(verts) + print(str(nDupli) + " Duplicates Points Ignored") + print(str(nZcolinear) + " Z Colinear Points Excluded") + if nVerts < 3: + print("Not Enough Points") + return {'FINISHED'} + # Check colinear + xValues = [pt[0] for pt in verts] + yValues = [pt[1] for pt in verts] + if checkEqual(xValues) or checkEqual(yValues): + print("Points Are Colinear") + return {'FINISHED'} + # Create diagram + print("Tesselation... (" + str(nVerts) + " Points)") + xbuff, ybuff = 5, 5 # % + zPosition = 0 + vertsPts = [Point(vert[0], vert[1], vert[2]) for vert in verts] + # vertsPts= [Point(vert[0], vert[1]) for vert in verts] + + pts, edgesIdx = computeVoronoiDiagram( + vertsPts, xbuff, ybuff, polygonsOutput=False, formatOutput=True) + + # pts=[[pt[0], pt[1], zPosition] for pt in pts] + newIdx = 0 + vertr = [] + filteredPts = [] + print('Filter Points') + ipts = 0 + for p in pts: + ipts = ipts + 1 + if ipts % 500 == 0: + sys.stdout.write('\r') + # the exact output you're looking for: + prog_message = "Points: " + str(ipts) + " / " + str(len(pts)) + " " + str( + round(100 * ipts / len(pts))) + "%" + sys.stdout.write(prog_message) + sys.stdout.flush() + + if not poly.contains(sgeometry.Point(p)): + vertr.append((True, -1)) + else: + vertr.append((False, newIdx)) + if o.cutter_type == 'VCARVE': + # start the z depth calc from the "start depth" of the operation. + z = o.maxz - mpoly.boundary.distance(sgeometry.Point(p)) * slope + if z < maxdepth: + z = maxdepth + elif o.cutter_type == 'BALL' or o.cutter_type == 'BALLNOSE': + d = mpoly_boundary.distance(sgeometry.Point(p)) + r = new_cutter_diameter / 2.0 + if d >= r: + z = -r + else: + # print(r, d) + z = -r + sqrt(r * r - d * d) + else: + z = 0 # + # print(mpoly.distance(sgeometry.Point(0,0))) + # if(z!=0):print(z) + filteredPts.append((p[0], p[1], z)) + newIdx += 1 + + print('Filter Edges') + filteredEdgs = [] + ledges = [] + for e in edgesIdx: + do = True + # p1 = pts[e[0]] + # p2 = pts[e[1]] + # print(p1,p2,len(vertr)) + if vertr[e[0]][0]: # exclude edges with allready excluded points + do = False + elif vertr[e[1]][0]: + do = False + if do: + filteredEdgs.append((vertr[e[0]][1], vertr[e[1]][1])) + ledges.append(sgeometry.LineString( + (filteredPts[vertr[e[0]][1]], filteredPts[vertr[e[1]][1]]))) + # print(ledges[-1].has_z) + + bufpoly = poly.buffer(-new_cutter_diameter / 2, resolution=64) + + lines = shapely.ops.linemerge(ledges) + # print(lines.type) + + if bufpoly.type == 'Polygon' or bufpoly.type == 'MultiPolygon': + lines = lines.difference(bufpoly) + chunks.extend(shapelyToChunks(bufpoly, maxdepth)) + chunks.extend(shapelyToChunks(lines, 0)) + + # generate a mesh from the medial calculations + if o.add_mesh_for_medial: + shapelyToCurve('medialMesh', lines, 0.0) + bpy.ops.object.convert(target='MESH') + + oi = 0 + for ob in o.objects: + if ob.type == 'CURVE' or ob.type == 'FONT': + ob.data.resolution_u = resolutions_before[oi] + oi += 1 + + # bpy.ops.object.join() + chunks = await sortChunks(chunks, o) + + layers = getLayers(o, o.maxz, o.min.z) + + chunklayers = [] + + for layer in layers: + for chunk in chunks: + if chunk.isbelowZ(layer[0]): + newchunk = chunk.copy() + newchunk.clampZ(layer[1]) + chunklayers.append(newchunk) + + if o.first_down: + chunklayers = await sortChunks(chunklayers, o) + + if o.add_mesh_for_medial: # make curve instead of a path + 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 + Add_Pocket(maxdepth, m_o_ob, new_cutter_diameter)
+ + + +
+[docs] +def getLayers(operation, startdepth, enddepth): + """Returns a list of layers bounded by start depth and end depth. + + This function calculates the layers between the specified start and end + depths based on the step down value defined in the operation. If the + operation is set to use layers, it computes the number of layers by + dividing the difference between start and end depths by the step down + value. The function raises an exception if the start depth is lower than + the end depth. + + Args: + operation (object): An object that contains the properties `use_layers`, + `stepdown`, and `maxz` which are used to determine + how layers are generated. + startdepth (float): The starting depth for layer calculation. + enddepth (float): The ending depth for layer calculation. + + Returns: + list: A list of layers, where each layer is represented as a list + containing the start and end depths of that layer. + + Raises: + CamException: If the start depth is lower than the end depth. + """ + if startdepth < enddepth: + raise CamException("Start Depth Is Lower than End Depth. " + "if You Have Set a Custom Depth End, It Must Be Lower than Depth Start, " + "and Should Usually Be Negative. Set This in the CAM Operation Area Panel.") + if operation.use_layers: + layers = [] + n = ceil((startdepth - enddepth) / operation.stepdown) + print("Start " + str(startdepth) + " End " + str(enddepth) + " n " + str(n)) + + layerstart = operation.maxz + for x in range(0, n): + layerend = round(max(startdepth - ((x + 1) * operation.stepdown), enddepth), 6) + if int(layerstart * 10 ** 8) != int(layerend * 10 ** 8): + # it was possible that with precise same end of operation, + # last layer was done 2x on exactly same level... + layers.append([layerstart, layerend]) + layerstart = layerend + else: + layers = [[round(startdepth, 6), round(enddepth, 6)]] + + return layers
+ + + +
+[docs] +def chunksToMesh(chunks, o): + """Convert sampled chunks into a mesh path for a given optimization object. + + This function takes a list of sampled chunks and converts them into a + mesh path based on the specified optimization parameters. It handles + different machine axes configurations and applies optimizations as + needed. The resulting mesh is created in the Blender context, and the + function also manages the lifting and dropping of the cutter based on + the chunk positions. + + Args: + chunks (list): A list of chunk objects to be converted into a mesh. + o (object): An object containing optimization parameters and settings. + + Returns: + None: The function creates a mesh in the Blender context but does not return a + value. + """ + t = time.time() + s = bpy.context.scene + m = s.cam_machine + verts = [] + + free_height = o.movement.free_height # o.max.z + + + if o.machine_axes == '3': + if m.use_position_definitions: + origin = (m.starting_position.x, m.starting_position.y, m.starting_position.z) # dhull + else: + origin = (0, 0, free_height) + + verts = [origin] + if o.machine_axes != '3': + verts_rotations = [] # (0,0,0) + if (o.machine_axes == '5' and o.strategy5axis == 'INDEXED') or ( + o.machine_axes == '4' and o.strategy4axis == 'INDEXED'): + extendChunks5axis(chunks, o) + + if o.array: + nchunks = [] + for x in range(0, o.array_x_count): + for y in range(0, o.array_y_count): + print(x, y) + for ch in chunks: + ch = ch.copy() + ch.shift(x * o.array_x_distance, y * o.array_y_distance, 0) + nchunks.append(ch) + chunks = nchunks + + progress('Building Paths from Chunks') + e = 0.0001 + lifted = True + + for chi in range(0, len(chunks)): + + ch = chunks[chi] + # print(chunks) + # print (ch) + # TODO: there is a case where parallel+layers+zigzag ramps send empty chunks here... + if ch.count() > 0: + # print(len(ch.points)) + nverts = [] + if o.optimisation.optimize: + ch = optimizeChunk(ch, o) + + # lift and drop + + if lifted: # did the cutter lift before? if yes, put a new position above of the first point of next chunk. + if o.machine_axes == '3' or (o.machine_axes == '5' and o.strategy5axis == 'INDEXED') or ( + o.machine_axes == '4' and o.strategy4axis == 'INDEXED'): + v = (ch.get_point(0)[0], ch.get_point(0)[1], free_height) + else: # otherwise, continue with the next chunk without lifting/dropping + v = ch.startpoints[0] # startpoints=retract points + verts_rotations.append(ch.rotations[0]) + verts.append(v) + + # add whole chunk + verts.extend(ch.get_points()) + + # add rotations for n-axis + if o.machine_axes != '3': + verts_rotations.extend(ch.rotations) + + lift = True + # check if lifting should happen + if chi < len(chunks) - 1 and chunks[chi + 1].count() > 0: + # TODO: remake this for n axis, and this check should be somewhere else... + last = Vector(ch.get_point(-1)) + first = Vector(chunks[chi + 1].get_point(0)) + vect = first - last + if (o.machine_axes == '3' and (o.strategy == 'PARALLEL' or o.strategy == 'CROSS') + and vect.z == 0 and vect.length < o.dist_between_paths * 2.5) \ + or (o.machine_axes == '4' and vect.length < o.dist_between_paths * 2.5): + # case of neighbouring paths + lift = False + # case of stepdown by cutting. + if abs(vect.x) < e and abs(vect.y) < e: + lift = False + + if lift: + if o.machine_axes == '3' or (o.machine_axes == '5' and o.strategy5axis == 'INDEXED') or ( + o.machine_axes == '4' and o.strategy4axis == 'INDEXED'): + v = (ch.get_point(-1)[0], ch.get_point(-1)[1], free_height) + else: + v = ch.startpoints[-1] + verts_rotations.append(ch.rotations[-1]) + verts.append(v) + lifted = lift + # print(verts_rotations) + if o.optimisation.use_exact and not o.optimisation.use_opencamlib: + cleanupBulletCollision(o) + print(time.time() - t) + t = time.time() + + # actual blender object generation starts here: + edges = [] + for a in range(0, len(verts) - 1): + edges.append((a, a + 1)) + + oname = "cam_path_{}".format(o.name) + + mesh = bpy.data.meshes.new(oname) + mesh.name = oname + mesh.from_pydata(verts, edges, []) + + if oname in s.objects: + s.objects[oname].data = mesh + ob = s.objects[oname] + else: + ob = object_utils.object_data_add(bpy.context, mesh, operator=None) + + if o.machine_axes != '3': + # store rotations into shape keys, only way to store large arrays with correct floating point precision + # - object/mesh attributes can only store array up to 32000 intems. + + ob.shape_key_add() + ob.shape_key_add() + shapek = mesh.shape_keys.key_blocks[1] + shapek.name = 'rotations' + print(len(shapek.data)) + print(len(verts_rotations)) + + # TODO: optimize this. this is just rewritten too many times... + for i, co in enumerate(verts_rotations): + shapek.data[i].co = co + + print(time.time() - t) + + ob.location = (0, 0, 0) + o.path_object_name = oname + + # parent the path object to source object if object mode + if (o.geometry_source == 'OBJECT') and o.parent_path_to_object: + activate(o.objects[0]) + ob.select_set(state=True, view_layer=None) + bpy.ops.object.parent_set(type='OBJECT', keep_transform=True) + else: + ob.select_set(state=True, view_layer=None)
+ + + +
+[docs] +def checkminz(o): + """Check the minimum value based on the specified condition. + + This function evaluates the 'minz_from' attribute of the input object + 'o'. If 'minz_from' is set to 'MATERIAL', it returns the value of + 'min.z'. Otherwise, it returns the value of 'minz'. + + Args: + o (object): An object that has attributes 'minz_from', 'min', and 'minz'. + + Returns: + The minimum value, which can be either 'o.min.z' or 'o.minz' depending + on the condition. + """ + if o.minz_from == 'MATERIAL': + return o.min.z + else: + return o.minz
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/testing.html b/_modules/cam/testing.html new file mode 100644 index 000000000..5ea3fed52 --- /dev/null +++ b/_modules/cam/testing.html @@ -0,0 +1,750 @@ + + + + + + + + + + cam.testing — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.testing

+"""CNC CAM 'testing.py' © 2012 Vilem Novak
+
+Functions for automated testing.
+"""
+
+import bpy
+
+from .gcodepath import getPath
+from .simple import activate
+
+
+
+[docs] +def addTestCurve(loc): + """Add a test curve to the Blender scene. + + This function creates a Bezier circle at the specified location in the + Blender scene. It first adds a primitive Bezier circle, then enters edit + mode to duplicate the circle twice, resizing each duplicate to half its + original size. The function ensures that the transformations are applied + in the global orientation and does not use proportional editing. + + Args: + loc (tuple): A tuple representing the (x, y, z) coordinates where + the Bezier circle will be added in the 3D space. + """ + bpy.ops.curve.primitive_bezier_circle_add( + radius=.05, align='WORLD', enter_editmode=False, location=loc) + bpy.ops.object.editmode_toggle() + bpy.ops.curve.duplicate() + bpy.ops.transform.resize(value=(0.5, 0.5, 0.5), constraint_axis=(False, False, False), + orient_type='GLOBAL', mirror=False, use_proportional_edit=False, + proportional_edit_falloff='SMOOTH', proportional_size=1) + bpy.ops.curve.duplicate() + bpy.ops.transform.resize(value=(0.5, 0.5, 0.5), constraint_axis=(False, False, False), + orient_type='GLOBAL', mirror=False, use_proportional_edit=False, + proportional_edit_falloff='SMOOTH', proportional_size=1) + bpy.ops.object.editmode_toggle()
+ + + +
+[docs] +def addTestMesh(loc): + """Add a test mesh to the Blender scene. + + This function creates a monkey mesh and a plane mesh at the specified + location in the Blender scene. It first adds a monkey mesh with a small + radius, rotates it, and applies the transformation. Then, it toggles + into edit mode, adds a plane mesh, resizes it, and translates it + slightly before toggling back out of edit mode. + + Args: + loc (tuple): A tuple representing the (x, y, z) coordinates where + the meshes will be added in the Blender scene. + """ + bpy.ops.mesh.primitive_monkey_add(radius=.01, align='WORLD', enter_editmode=False, location=loc) + bpy.ops.transform.rotate(value=-1.5708, axis=(1, 0, 0), constraint_axis=(True, False, False), + orient_type='GLOBAL') + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.primitive_plane_add(radius=1, align='WORLD', enter_editmode=False, location=loc) + bpy.ops.transform.resize(value=(0.01, 0.01, 0.01), constraint_axis=(False, False, False), + orient_type='GLOBAL') + bpy.ops.transform.translate(value=(-0.01, 0, 0), constraint_axis=(True, False, False), + orient_type='GLOBAL') + + bpy.ops.object.editmode_toggle()
+ + + +
+[docs] +def deleteFirstVert(ob): + """Delete the first vertex of a given object. + + This function activates the specified object, enters edit mode, + deselects all vertices, selects the first vertex, and then deletes it. + The function ensures that the object is properly updated after the + deletion. + + Args: + ob (bpy.types.Object): The Blender object from which the first + """ + activate(ob) + bpy.ops.object.editmode_toggle() + + bpy.ops.mesh.select_all(action='DESELECT') + + bpy.ops.object.editmode_toggle() + for i, v in enumerate(ob.data.vertices): + v.select = False + if i == 0: + v.select = True + ob.data.update() + + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.delete(type='VERT') + bpy.ops.object.editmode_toggle()
+ + + +
+[docs] +def testCalc(o): + """Test the calculation of the camera path for a given object. + + This function invokes the Blender operator to calculate the camera path + for the specified object and then deletes the first vertex of that + object. It is intended to be used within a Blender environment where the + bpy module is available. + + Args: + o (Object): The Blender object for which the camera path is to be calculated. + """ + bpy.ops.object.calculate_cam_path() + deleteFirstVert(bpy.data.objects[o.name])
+ + + +
+[docs] +def testCutout(pos): + """Test the cutout functionality in the scene. + + This function adds a test curve based on the provided position, performs + a camera operation, and sets the strategy to 'CUTOUT'. It then calls the + `testCalc` function to perform further calculations on the camera + operation. + + Args: + pos (tuple): A tuple containing the x and y coordinates for the + position of the test curve. + """ + addTestCurve((pos[0], pos[1], -.05)) + bpy.ops.scene.cam_operation_add() + o = bpy.context.scene.cam_operations[-1] + o.strategy = 'CUTOUT' + testCalc(o)
+ + + +
+[docs] +def testPocket(pos): + """Test the pocket operation in a 3D scene. + + This function sets up a pocket operation by adding a test curve based on + the provided position. It configures the camera operation settings for + the pocket strategy, enabling helix entry and tangential retraction. + Finally, it performs a calculation based on the configured operation. + + Args: + pos (tuple): A tuple containing the x and y coordinates for + the position of the test curve. + """ + addTestCurve((pos[0], pos[1], -.01)) + bpy.ops.scene.cam_operation_add() + o = bpy.context.scene.cam_operations[-1] + o.strategy = 'POCKET' + o.movement.helix_enter = True + o.movement.retract_tangential = True + testCalc(o)
+ + + +
+[docs] +def testParallel(pos): + """Test the parallel functionality of the camera operations. + + This function adds a test mesh at a specified position and then performs + camera operations in the Blender environment. It sets the ambient + behavior of the camera operation to 'AROUND' and configures the material + radius around the model. Finally, it calculates the camera path based on + the current scene settings. + + Args: + pos (tuple): A tuple containing the x and y coordinates for + positioning the test mesh. + """ + addTestMesh((pos[0], pos[1], -.02)) + bpy.ops.scene.cam_operation_add() + o = bpy.context.scene.cam_operations[-1] + o.ambient_behaviour = 'AROUND' + o.material.radius_around_model = 0.01 + bpy.ops.object.calculate_cam_path()
+ + + +
+[docs] +def testWaterline(pos): + """Test the waterline functionality in the scene. + + This function adds a test mesh at a specified position and then performs + a camera operation with the strategy set to 'WATERLINE'. It also + configures the optimization pixel size for the operation. The function + is intended for use in a 3D environment where waterline calculations are + necessary for rendering or simulation. + + Args: + pos (tuple): A tuple containing the x and y coordinates for + the position of the test mesh. + """ + addTestMesh((pos[0], pos[1], -.02)) + bpy.ops.scene.cam_operation_add() + o = bpy.context.scene.cam_operations[-1] + o.strategy = 'WATERLINE' + o.optimisation.pixsize = .0002 + # o.ambient_behaviour='AROUND' + # o.material_radius_around_model=0.01 + + testCalc(o)
+ + + +# bpy.ops.object.cam_simulate() + + +
+[docs] +def testSimulation(): + """Testsimulation function.""" + pass
+ + + +
+[docs] +def cleanUp(): + """Clean up the Blender scene by removing all objects and camera + operations. + + This function selects all objects in the current Blender scene and + deletes them. It also removes any camera operations that are present in + the scene. This is useful for resetting the scene to a clean state + before performing further operations. + """ + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete(use_global=False) + while len(bpy.context.scene.cam_operations): + bpy.ops.scene.cam_operation_remove()
+ + + +
+[docs] +def testOperation(i): + """Test the operation of a camera path in Blender. + + This function tests a specific camera operation by comparing the + generated camera path with an existing reference path. It retrieves the + camera operation from the scene and checks if the generated path matches + the expected path in terms of vertex count and positions. If there is no + existing reference path, it marks the new result as comparable. The + function generates a report detailing the results of the comparison, + including any discrepancies found. + + Args: + i (int): The index of the camera operation to test. + + Returns: + str: A report summarizing the results of the operation test. + """ + s = bpy.context.scene + o = s.cam_operations[i] + report = '' + report += 'testing operation ' + o.name + '\n' + + getPath(bpy.context, o) + + newresult = bpy.data.objects[o.path_object_name] + origname = "test_cam_path_" + o.name + if origname not in s.objects: + report += 'Operation Test Has Nothing to Compare with, Making the New Result as Comparable Result.\n\n' + newresult.name = origname + else: + testresult = bpy.data.objects[origname] + m1 = testresult.data + m2 = newresult.data + test_ok = True + if len(m1.vertices) != len(m2.vertices): + report += "Vertex Counts Don't Match\n\n" + test_ok = False + else: + different_co_count = 0 + for i in range(0, len(m1.vertices)): + v1 = m1.vertices[i] + v2 = m2.vertices[i] + if v1.co != v2.co: + different_co_count += 1 + if different_co_count > 0: + report += 'Vertex Position Is Different on %i Vertices \n\n' % (different_co_count) + test_ok = False + if test_ok: + report += 'Test Ok\n\n' + else: + report += 'Test Result Is Different\n \n ' + print(report) + return report
+ + + +
+[docs] +def testAll(): + """Run tests on all camera operations in the current scene. + + This function iterates through all camera operations defined in the + current Blender scene and executes a test for each operation. The + results of these tests are collected into a report string, which is then + printed to the console. This is useful for verifying the functionality + of camera operations within the Blender environment. + """ + s = bpy.context.scene + report = '' + for i in range(0, len(s.cam_operations)): + report += testOperation(i) + print(report)
+ + + +
+[docs] +tests = [ + testCutout, + testParallel, + testWaterline, + testPocket, + +]
+ + +cleanUp() + +# deleteFirstVert(bpy.context.active_object) +for i, t in enumerate(tests): +
+[docs] + p = i * .2
+ + t((p, 0, 0)) +# cleanUp() + + +# cleanUp() +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/ui.html b/_modules/cam/ui.html new file mode 100644 index 000000000..5838013ff --- /dev/null +++ b/_modules/cam/ui.html @@ -0,0 +1,672 @@ + + + + + + + + + + cam.ui — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.ui

+"""CNC CAM 'ui.py' © 2012 Vilem Novak
+
+Panels displayed in the 3D Viewport - Curve Tools, Creators and Import G-code
+"""
+
+from bpy_extras.io_utils import ImportHelper
+from bpy.props import (
+    BoolProperty,
+    EnumProperty,
+    FloatProperty,
+    StringProperty,
+)
+from bpy.types import (
+    Panel,
+    Operator,
+    UIList,
+    PropertyGroup,
+)
+
+from .gcodeimportparser import import_gcode
+
+
+
+[docs] +class CAM_UL_orientations(UIList): +
+[docs] + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + if self.layout_type in {'DEFAULT', 'COMPACT'}: + + layout.label(text=item.name, translate=False, icon_value=icon) + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon_value=icon)
+
+ + + +# panel containing all tools + +
+[docs] +class VIEW3D_PT_tools_curvetools(Panel): +
+[docs] + bl_space_type = 'VIEW_3D'
+ +
+[docs] + bl_region_type = 'TOOLS'
+ +
+[docs] + bl_context = "objectmode"
+ +
+[docs] + bl_label = "Curve CAM Tools"
+ + +
+[docs] + def draw(self, context): + layout = self.layout + layout.operator("object.curve_boolean") + layout.operator("object.convex_hull") + layout.operator("object.curve_intarsion") + layout.operator("object.curve_overcuts") + layout.operator("object.curve_overcuts_b") + layout.operator("object.silhouete") + layout.operator("object.silhouete_offset") + layout.operator("object.curve_remove_doubles") + layout.operator("object.mesh_get_pockets")
+
+ + + +
+[docs] +class VIEW3D_PT_tools_create(Panel): +
+[docs] + bl_space_type = 'VIEW_3D'
+ +
+[docs] + bl_region_type = 'TOOLS'
+ +
+[docs] + bl_context = "objectmode"
+ +
+[docs] + bl_label = "Curve CAM Creators"
+ +
+[docs] + bl_option = 'DEFAULT_CLOSED'
+ + +
+[docs] + def draw(self, context): + layout = self.layout + layout.operator("object.curve_plate") + layout.operator("object.curve_drawer") + layout.operator("object.curve_mortise") + layout.operator("object.curve_interlock") + layout.operator("object.curve_puzzle") + layout.operator("object.sine") + layout.operator("object.lissajous") + layout.operator("object.hypotrochoid") + layout.operator("object.customcurve") + layout.operator("object.curve_hatch") + layout.operator("object.curve_gear") + layout.operator("object.curve_flat_cone")
+
+ + +# Gcode import panel--------------------------------------------------------------- +# ------------------------------------------------------------------------ +# Panel in Object Mode +# ------------------------------------------------------------------------ + + +
+[docs] +class CustomPanel(Panel): +
+[docs] + bl_space_type = 'VIEW_3D'
+ +
+[docs] + bl_region_type = 'TOOLS'
+ +
+[docs] + bl_context = "objectmode"
+ +
+[docs] + bl_label = "Import G-code"
+ +
+[docs] + bl_idname = "OBJECT_PT_importgcode"
+ + +
+[docs] + bl_options = {'DEFAULT_CLOSED'}
+ + + @classmethod +
+[docs] + def poll(cls, context): + return context.mode in {'OBJECT', + 'EDIT_MESH'} # with this poll addon is visibly even when no object is selected
+ + +
+[docs] + def draw(self, context): + layout = self.layout + scene = context.scene + isettings = scene.cam_import_gcode + layout.prop(isettings, 'output') + layout.prop(isettings, "split_layers") + + layout.prop(isettings, "subdivide") + col = layout.column(align=True) + col = col.row(align=True) + col.split() + col.label(text="Segment Length") + + col.prop(isettings, "max_segment_size") + col.enabled = isettings.subdivide + col.separator() + + col = layout.column() + col.scale_y = 2.0 + col.operator("wm.gcode_import")
+
+ + + +
+[docs] +class WM_OT_gcode_import(Operator, ImportHelper): + """Import G-code, Travel Lines Don't Get Drawn""" +
+[docs] + bl_idname = "wm.gcode_import" # important since its how bpy.ops.import_test.some_data is constructed
+ +
+[docs] + bl_label = "Import G-code"
+ + + # ImportHelper mixin class uses this +
+[docs] + filename_ext = ".txt"
+ + +
+[docs] + filter_glob: StringProperty( + default="*.*", + options={'HIDDEN'}, + maxlen=255, # Max internal buffer length, longer would be clamped. + )
+ + +
+[docs] + def execute(self, context): + print(self.filepath) + return import_gcode(context, self.filepath)
+
+ + + +
+[docs] +class import_settings(PropertyGroup): +
+[docs] + split_layers: BoolProperty( + name="Split Layers", + description="Save every layer as single Objects in Collection", + default=False, + )
+ +
+[docs] + subdivide: BoolProperty( + name="Subdivide", + description="Only Subdivide gcode segments that are " + "bigger than 'Segment length' ", + default=False, + )
+ +
+[docs] + output: EnumProperty( + name="Output Type", + items=( + ("mesh", "Mesh", "Make a mesh output"), + ("curve", "Curve", "Make curve output"), + ), + default="curve", + )
+ +
+[docs] + max_segment_size: FloatProperty( + name="", + description="Only Segments bigger than this value get subdivided", + default=0.001, + min=0.0001, + max=1.0, + unit="LENGTH", + )
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/utils.html b/_modules/cam/utils.html new file mode 100644 index 000000000..247bbbbe0 --- /dev/null +++ b/_modules/cam/utils.html @@ -0,0 +1,3797 @@ + + + + + + + + + + cam.utils — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.utils

+"""CNC CAM 'utils.py' © 2012 Vilem Novak
+
+Main functionality of CNC CAM.
+The functions here are called with operators defined in 'ops.py'
+"""
+
+from math import (
+    ceil,
+    pi
+)
+from pathlib import Path
+import pickle
+import shutil
+import sys
+import time
+
+import numpy
+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
+
+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,
+    oclResampleChunks,
+)
+from .polygon_utils_cam import shapelyToCurve, shapelyToMultipolygon
+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..
+
+[docs] +SHAPELY = True
+ + + +# Import OpencamLib +# Return available OpenCamLib version on success, None otherwise +
+[docs] +def opencamlib_version(): + """Return the version of the OpenCamLib library. + + This function attempts to import the OpenCamLib library and returns its + version. If the library is not available, it will return None. The + function first tries to import the library using the name 'ocl', and if + that fails, it attempts to import it using 'opencamlib' as an alias. If + both imports fail, it returns None. + + Returns: + str or None: The version of OpenCamLib if available, None otherwise. + """ + + try: + import ocl + except ImportError: + try: + import opencamlib as ocl + except ImportError as e: + return + return ocl.version()
+ + + +
+[docs] +def positionObject(operation): + """Position an object based on specified operation parameters. + + This function adjusts the location of a Blender object according to the + provided operation settings. It calculates the bounding box of the + object in world space and modifies its position based on the material's + center settings and specified z-positioning (BELOW, ABOVE, or CENTERED). + The function also applies transformations to the object if it is not of + type 'CURVE'. + + Args: + operation (OperationType): An object containing parameters for positioning, + including object_name, use_modifiers, and material + settings. + """ + + ob = bpy.data.objects[operation.object_name] + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') + ob.select_set(True) + bpy.context.view_layer.objects.active = ob + + minx, miny, minz, maxx, maxy, maxz = getBoundsWorldspace([ob], operation.use_modifiers) + totx = maxx - minx + toty = maxy - miny + totz = maxz - minz + if operation.material.center_x: + ob.location.x -= minx + totx / 2 + else: + ob.location.x -= minx + + if operation.material.center_y: + ob.location.y -= miny + toty / 2 + else: + ob.location.y -= miny + + if operation.material.z_position == 'BELOW': + ob.location.z -= maxz + elif operation.material.z_position == 'ABOVE': + ob.location.z -= minz + elif operation.material.z_position == 'CENTERED': + ob.location.z -= minz + totz / 2 + + if ob.type != 'CURVE': + bpy.ops.object.transform_apply(location=True, rotation=False, scale=False)
+ + # addMaterialAreaObject() + + +
+[docs] +def getBoundsWorldspace(obs, use_modifiers=False): + """Get the bounding box of a list of objects in world space. + + This function calculates the minimum and maximum coordinates that + encompass all the specified objects in the 3D world space. It iterates + through each object, taking into account their transformations and + modifiers if specified. The function supports different object types, + including meshes and fonts, and handles the conversion of font objects + to mesh format for accurate bounding box calculations. + + Args: + obs (list): A list of Blender objects to calculate bounds for. + use_modifiers (bool): If True, apply modifiers to the objects + before calculating bounds. Defaults to False. + + Returns: + tuple: A tuple containing the minimum and maximum coordinates + in the format (minx, miny, minz, maxx, maxy, maxz). + + Raises: + CamException: If an object type does not support CAM operations. + """ + + # progress('getting bounds of object(s)') + t = time.time() + + maxx = maxy = maxz = -10000000 + minx = miny = minz = 10000000 + for ob in obs: + # bb=ob.bound_box + mw = ob.matrix_world + if ob.type == 'MESH': + if use_modifiers: + depsgraph = bpy.context.evaluated_depsgraph_get() + mesh_owner = ob.evaluated_get(depsgraph) + mesh = mesh_owner.to_mesh() + else: + mesh = ob.data + + for c in mesh.vertices: + coord = c.co + worldCoord = mw @ Vector((coord[0], coord[1], coord[2])) + minx = min(minx, worldCoord.x) + miny = min(miny, worldCoord.y) + minz = min(minz, worldCoord.z) + maxx = max(maxx, worldCoord.x) + maxy = max(maxy, worldCoord.y) + maxz = max(maxz, worldCoord.z) + + if use_modifiers: + mesh_owner.to_mesh_clear() + + elif ob.type == "FONT": + activate(ob) + bpy.ops.object.duplicate() + co = bpy.context.active_object + bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') + bpy.ops.object.convert(target='MESH', keep_original=False) + mesh = co.data + for c in mesh.vertices: + coord = c.co + worldCoord = mw @ Vector((coord[0], coord[1], coord[2])) + minx = min(minx, worldCoord.x) + miny = min(miny, worldCoord.y) + minz = min(minz, worldCoord.z) + maxx = max(maxx, worldCoord.x) + maxy = max(maxy, worldCoord.y) + maxz = max(maxz, worldCoord.z) + bpy.ops.object.delete() + bpy.ops.outliner.orphans_purge() + else: + if not hasattr(ob.data, "splines"): + raise CamException("Can't do CAM operation on the selected object type") + # for coord in bb: + for c in ob.data.splines: + for p in c.bezier_points: + coord = p.co + # this can work badly with some imported curves, don't know why... + # worldCoord = mw * Vector((coord[0]/ob.scale.x, coord[1]/ob.scale.y, coord[2]/ob.scale.z)) + worldCoord = mw @ Vector((coord[0], coord[1], coord[2])) + minx = min(minx, worldCoord.x) + miny = min(miny, worldCoord.y) + minz = min(minz, worldCoord.z) + maxx = max(maxx, worldCoord.x) + maxy = max(maxy, worldCoord.y) + maxz = max(maxz, worldCoord.z) + for p in c.points: + coord = p.co + # this can work badly with some imported curves, don't know why... + # worldCoord = mw * Vector((coord[0]/ob.scale.x, coord[1]/ob.scale.y, coord[2]/ob.scale.z)) + worldCoord = mw @ Vector((coord[0], coord[1], coord[2])) + minx = min(minx, worldCoord.x) + miny = min(miny, worldCoord.y) + minz = min(minz, worldCoord.z) + maxx = max(maxx, worldCoord.x) + maxy = max(maxy, worldCoord.y) + maxz = max(maxz, worldCoord.z) + # progress(time.time()-t) + return minx, miny, minz, maxx, maxy, maxz
+ + + +
+[docs] +def getSplineBounds(ob, curve): + """Get the bounding box of a spline object. + + This function calculates the minimum and maximum coordinates (x, y, z) + of the given spline object by iterating through its bezier points and + regular points. It transforms the local coordinates to world coordinates + using the object's transformation matrix. The resulting bounds can be + used for various purposes, such as collision detection or rendering. + + Args: + ob (Object): The object containing the spline whose bounds are to be calculated. + curve (Curve): The curve object that contains the bezier points and regular points. + + Returns: + tuple: A tuple containing the minimum and maximum coordinates in the + format (minx, miny, minz, maxx, maxy, maxz). + """ + + # progress('getting bounds of object(s)') + maxx = maxy = maxz = -10000000 + minx = miny = minz = 10000000 + mw = ob.matrix_world + + for p in curve.bezier_points: + coord = p.co + # this can work badly with some imported curves, don't know why... + # worldCoord = mw * Vector((coord[0]/ob.scale.x, coord[1]/ob.scale.y, coord[2]/ob.scale.z)) + worldCoord = mw @ Vector((coord[0], coord[1], coord[2])) + minx = min(minx, worldCoord.x) + miny = min(miny, worldCoord.y) + minz = min(minz, worldCoord.z) + maxx = max(maxx, worldCoord.x) + maxy = max(maxy, worldCoord.y) + maxz = max(maxz, worldCoord.z) + for p in curve.points: + coord = p.co + # this can work badly with some imported curves, don't know why... + # worldCoord = mw * Vector((coord[0]/ob.scale.x, coord[1]/ob.scale.y, coord[2]/ob.scale.z)) + worldCoord = mw @ Vector((coord[0], coord[1], coord[2])) + minx = min(minx, worldCoord.x) + miny = min(miny, worldCoord.y) + minz = min(minz, worldCoord.z) + maxx = max(maxx, worldCoord.x) + maxy = max(maxy, worldCoord.y) + maxz = max(maxz, worldCoord.z) + # progress(time.time()-t) + return minx, miny, minz, maxx, maxy, maxz
+ + + +
+[docs] +def getOperationSources(o): + """Get operation sources based on the geometry source type. + + This function retrieves and sets the operation sources for a given + object based on its geometry source type. It handles three types of + geometry sources: 'OBJECT', 'COLLECTION', and 'IMAGE'. For 'OBJECT', it + selects the specified object and applies rotations if enabled. For + 'COLLECTION', it retrieves all objects within the specified collection. + For 'IMAGE', it sets a specific optimization flag. Additionally, it + determines whether the objects are curves or meshes based on the + geometry source. + + Args: + o (Object): An object containing properties such as geometry_source, + object_name, collection_name, rotation_A, rotation_B, + enable_A, enable_B, old_rotation_A, old_rotation_B, + A_along_x, and optimisation. + + Returns: + None: This function does not return a value but modifies the + properties of the input object. + """ + + if o.geometry_source == 'OBJECT': + # bpy.ops.object.select_all(action='DESELECT') + ob = bpy.data.objects[o.object_name] + o.objects = [ob] + ob.select_set(True) + bpy.context.view_layer.objects.active = ob + if o.enable_B or o.enable_A: + if o.old_rotation_A != o.rotation_A or o.old_rotation_B != o.rotation_B: + o.old_rotation_A = o.rotation_A + o.old_rotation_B = o.rotation_B + ob = bpy.data.objects[o.object_name] + ob.select_set(True) + bpy.context.view_layer.objects.active = ob + if o.A_along_x: # A parallel with X + if o.enable_A: + bpy.context.active_object.rotation_euler.x = o.rotation_A + if o.enable_B: + bpy.context.active_object.rotation_euler.y = o.rotation_B + else: # A parallel with Y + if o.enable_A: + bpy.context.active_object.rotation_euler.y = o.rotation_A + if o.enable_B: + bpy.context.active_object.rotation_euler.x = o.rotation_B + + elif o.geometry_source == 'COLLECTION': + collection = bpy.data.collections[o.collection_name] + o.objects = collection.objects + elif o.geometry_source == 'IMAGE': + o.optimisation.use_exact = False + + if o.geometry_source == 'OBJECT' or o.geometry_source == 'COLLECTION': + o.onlycurves = True + for ob in o.objects: + if ob.type == 'MESH': + o.onlycurves = False + else: + o.onlycurves = False
+ + + +
+[docs] +def getBounds(o): + """Calculate the bounding box for a given object. + + This function determines the minimum and maximum coordinates of an + object's bounding box based on its geometry source. It handles different + geometry types such as OBJECT, COLLECTION, and CURVE. The function also + considers material properties and image cropping if applicable. The + bounding box is adjusted according to the object's material settings and + the optimization parameters defined in the object. + + Args: + o (object): An object containing geometry and material properties, as well as + optimization settings. + + Returns: + None: This function modifies the input object in place and does not return a + value. + """ + + # print('kolikrat sem rpijde') + if o.geometry_source == 'OBJECT' or o.geometry_source == 'COLLECTION' or o.geometry_source == 'CURVE': + print("Valid Geometry") + minx, miny, minz, maxx, maxy, maxz = getBoundsWorldspace(o.objects, o.use_modifiers) + + if o.minz_from == 'OBJECT': + if minz == 10000000: + minz = 0 + print("Minz from Object:" + str(minz)) + o.min.z = minz + o.minz = o.min.z + else: + o.min.z = o.minz # max(bb[0][2]+l.z,o.minz)# + print("Not Minz from Object") + + if o.material.estimate_from_model: + print("Estimate Material from Model") + + o.min.x = minx - o.material.radius_around_model + o.min.y = miny - o.material.radius_around_model + o.max.z = max(o.maxz, maxz) + + o.max.x = maxx + o.material.radius_around_model + o.max.y = maxy + o.material.radius_around_model + else: + print("Not Material from Model") + o.min.x = o.material.origin.x + o.min.y = o.material.origin.y + o.min.z = o.material.origin.z - o.material.size.z + o.max.x = o.min.x + o.material.size.x + o.max.y = o.min.y + o.material.size.y + o.max.z = o.material.origin.z + + else: + i = bpy.data.images[o.source_image_name] + if o.source_image_crop: + sx = int(i.size[0] * o.source_image_crop_start_x / 100) + ex = int(i.size[0] * o.source_image_crop_end_x / 100) + sy = int(i.size[1] * o.source_image_crop_start_y / 100) + ey = int(i.size[1] * o.source_image_crop_end_y / 100) + else: + sx = 0 + ex = i.size[0] + sy = 0 + ey = i.size[1] + + o.optimisation.pixsize = o.source_image_size_x / i.size[0] + + o.min.x = o.source_image_offset.x + sx * o.optimisation.pixsize + o.max.x = o.source_image_offset.x + ex * o.optimisation.pixsize + o.min.y = o.source_image_offset.y + sy * o.optimisation.pixsize + o.max.y = o.source_image_offset.y + ey * o.optimisation.pixsize + o.min.z = o.source_image_offset.z + o.minz + o.max.z = o.source_image_offset.z + s = bpy.context.scene + m = s.cam_machine + # make sure this message only shows once and goes away once fixed + o.info.warnings.replace('Operation Exceeds Your Machine Limits\n', '') + if o.max.x - o.min.x > m.working_area.x or o.max.y - o.min.y > m.working_area.y \ + or o.max.z - o.min.z > m.working_area.z: + o.info.warnings += 'Operation Exceeds Your Machine Limits\n'
+ + + +
+[docs] +def getBoundsMultiple(operations): + """Gets bounds of multiple operations for simulations or rest milling. + + This function iterates through a list of operations to determine the + minimum and maximum bounds in three-dimensional space (x, y, z). It + initializes the bounds to extreme values and updates them based on the + bounds of each operation. The function is primarily intended for use in + simulations or rest milling processes, although it is noted that the + implementation may not be optimal. + + Args: + operations (list): A list of operation objects, each containing + 'min' and 'max' attributes with 'x', 'y', + and 'z' coordinates. + + Returns: + tuple: A tuple containing the minimum and maximum bounds in the + order (minx, miny, minz, maxx, maxy, maxz). + """ + maxx = maxy = maxz = -10000000 + minx = miny = minz = 10000000 + for o in operations: + getBounds(o) + maxx = max(maxx, o.max.x) + maxy = max(maxy, o.max.y) + maxz = max(maxz, o.max.z) + minx = min(minx, o.min.x) + miny = min(miny, o.min.y) + minz = min(minz, o.min.z) + + return minx, miny, minz, maxx, maxy, maxz
+ + + +
+[docs] +def samplePathLow(o, ch1, ch2, dosample): + """Generate a sample path between two channels. + + This function computes a series of points that form a path between two + given channels. It calculates the direction vector from the end of the + first channel to the start of the second channel and generates points + along this vector up to a specified distance. If sampling is enabled, it + modifies the z-coordinate of the generated points based on the cutter + shape or image sampling, ensuring that the path accounts for any + obstacles or features in the environment. + + Args: + o: An object containing optimization parameters and properties related to + the path generation. + ch1: The first channel object, which provides a point for the starting + location of the path. + ch2: The second channel object, which provides a point for the ending + location of the path. + dosample (bool): A flag indicating whether to perform sampling along the generated path. + + Returns: + camPathChunk: An object representing the generated path points. + """ + + v1 = Vector(ch1.get_point(-1)) + v2 = Vector(ch2.get_point(0)) + + v = v2 - v1 + d = v.length + v.normalize() + + vref = Vector((0, 0, 0)) + bpath_points = [] + i = 0 + while vref.length < d: + i += 1 + vref = v * o.dist_along_paths * i + if vref.length < d: + p = v1 + vref + bpath_points.append([p.x, p.y, p.z]) + # print('between path') + # print(len(bpath)) + pixsize = o.optimisation.pixsize + if dosample: + if not (o.optimisation.use_opencamlib and o.optimisation.use_exact): + if o.optimisation.use_exact: + if o.update_bullet_collision_tag: + prepareBulletCollision(o) + o.update_bullet_collision_tag = False + + cutterdepth = o.cutter_shape.dimensions.z / 2 + for p in bpath_points: + z = getSampleBullet(o.cutter_shape, p[0], p[1], cutterdepth, 1, o.minz) + if z > p[2]: + p[2] = z + else: + for p in bpath_points: + xs = (p[0] - o.min.x) / pixsize + o.borderwidth + pixsize / 2 # -m + ys = (p[1] - o.min.y) / pixsize + o.borderwidth + pixsize / 2 # -m + z = getSampleImage((xs, ys), o.offset_image, o.minz) + o.skin + if z > p[2]: + p[2] = z + return camPathChunk(bpath_points)
+ + + +# def threadedSampling():#not really possible at all without running more blenders for same operation :( python! +# samples in both modes now - image and bullet collision too. +
+[docs] +async def sampleChunks(o, pathSamples, layers): + """Sample chunks of paths based on the provided parameters. + + This function processes the given path samples and layers to generate + chunks of points that represent the sampled paths. It takes into account + various optimization settings and strategies to determine how the points + are sampled and organized into layers. The function handles different + scenarios based on the object's properties and the specified layers, + ensuring that the resulting chunks are correctly structured for further + processing. + + Args: + o (object): An object containing various properties and settings + related to the sampling process. + pathSamples (list): A list of path samples to be processed. + layers (list): A list of layers defining the z-coordinate ranges + for sampling. + + Returns: + list: A list of sampled chunks, each containing points that represent + the sampled paths. + """ + + # + minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z + getAmbient(o) + + if o.optimisation.use_exact: # prepare collision world + if o.optimisation.use_opencamlib: + await oclSample(o, pathSamples) + cutterdepth = 0 + else: + if o.update_bullet_collision_tag: + prepareBulletCollision(o) + + o.update_bullet_collision_tag = False + # print (o.ambient) + cutter = o.cutter_shape + cutterdepth = cutter.dimensions.z / 2 + else: + # or prepare offset image, but not in some strategies. + if o.strategy != 'WATERLINE': + await prepareArea(o) + + pixsize = o.optimisation.pixsize + + coordoffset = o.borderwidth + pixsize / 2 # -m + + res = ceil(o.cutter_diameter / o.optimisation.pixsize) + m = res / 2 + + t = time.time() + # print('sampling paths') + + totlen = 0 # total length of all chunks, to estimate sampling time. + for ch in pathSamples: + totlen += ch.count() + layerchunks = [] + minz = o.minz - 0.000001 # correction for image method problems + layeractivechunks = [] + lastrunchunks = [] + + for l in layers: + layerchunks.append([]) + layeractivechunks.append(camPathChunkBuilder([])) + lastrunchunks.append([]) + + zinvert = 0 + if o.inverse: + ob = bpy.data.objects[o.object_name] + zinvert = ob.location.z + maxz # ob.bound_box[6][2] + + print(f"Total Sample Points {totlen}") + + n = 0 + last_percent = -1 + # timing for optimisation + samplingtime = timinginit() + sortingtime = timinginit() + totaltime = timinginit() + timingstart(totaltime) + lastz = minz + for patternchunk in pathSamples: + thisrunchunks = [] + for l in layers: + thisrunchunks.append([]) + lastlayer = None + currentlayer = None + lastsample = None + # threads_count=4 + + # for t in range(0,threads): + our_points = patternchunk.get_points_np() + ambient_contains = shapely.contains(o.ambient, shapely.points(our_points[:, 0:2])) + for s, in_ambient in zip(our_points, ambient_contains): + if o.strategy != 'WATERLINE' and int(100 * n / totlen) != last_percent: + last_percent = int(100 * n / totlen) + await progress_async('sampling paths ', last_percent) + n += 1 + x = s[0] + y = s[1] + if not in_ambient: + newsample = (x, y, 1) + else: + if o.optimisation.use_opencamlib and o.optimisation.use_exact: + z = s[2] + if minz > z: + z = minz + newsample = (x, y, z) + # ampling + elif o.optimisation.use_exact and not o.optimisation.use_opencamlib: + + if lastsample is not None: # this is an optimalization, + # search only for near depths to the last sample. Saves about 30% of sampling time. + z = getSampleBullet(cutter, x, y, cutterdepth, 1, + lastsample[2] - o.dist_along_paths) # first try to the last sample + if z < minz - 1: + z = getSampleBullet(cutter, x, y, cutterdepth, + lastsample[2] - o.dist_along_paths, minz) + else: + z = getSampleBullet(cutter, x, y, cutterdepth, 1, minz) + + # print(z) + else: + timingstart(samplingtime) + xs = (x - minx) / pixsize + coordoffset + ys = (y - miny) / pixsize + coordoffset + timingadd(samplingtime) + z = getSampleImage((xs, ys), o.offset_image, minz) + o.skin + + ################################ + # handling samples + ############################################ + + if minz > z: + z = minz + newsample = (x, y, z) + + for i, l in enumerate(layers): + terminatechunk = False + + ch = layeractivechunks[i] + + if l[1] <= newsample[2] <= l[0]: + lastlayer = None # rather the last sample here ? has to be set to None, + # since sometimes lastsample vs lastlayer didn't fit and did ugly ugly stuff.... + if lastsample is not None: + for i2, l2 in enumerate(layers): + if l2[1] <= lastsample[2] <= l2[0]: + lastlayer = i2 + + currentlayer = i + # and lastsample[2]!=newsample[2]: + if lastlayer is not None and lastlayer != currentlayer: + # #sampling for sorted paths in layers- to go to the border of the sampled layer at least... + # there was a bug here, but should be fixed. + if currentlayer < lastlayer: + growing = True + r = range(currentlayer, lastlayer) + spliti = 1 + else: + r = range(lastlayer, currentlayer) + growing = False + spliti = 0 + # print(r) + li = 0 + for ls in r: + splitz = layers[ls][1] + # print(ls) + + v1 = lastsample + v2 = newsample + if o.movement.protect_vertical: + v1, v2 = isVerticalLimit(v1, v2, o.movement.protect_vertical_limit) + v1 = Vector(v1) + v2 = Vector(v2) + # print(v1,v2) + ratio = (splitz - v1.z) / (v2.z - v1.z) + # print(ratio) + betweensample = v1 + (v2 - v1) * ratio + + # ch.points.append(betweensample.to_tuple()) + + if growing: + if li > 0: + layeractivechunks[ls].points.insert(-1, + betweensample.to_tuple()) + else: + layeractivechunks[ls].points.append(betweensample.to_tuple()) + layeractivechunks[ls + 1].points.append(betweensample.to_tuple()) + else: + # print(v1,v2,betweensample,lastlayer,currentlayer) + layeractivechunks[ls].points.insert(-1, betweensample.to_tuple()) + layeractivechunks[ls + 1].points.insert(0, betweensample.to_tuple()) + + li += 1 + # this chunk is terminated, and allready in layerchunks / + + # ch.points.append(betweensample.to_tuple())# + ch.points.append(newsample) + elif l[1] > newsample[2]: + ch.points.append((newsample[0], newsample[1], l[1])) + elif l[0] < newsample[2]: # terminate chunk + terminatechunk = True + + if terminatechunk: + if len(ch.points) > 0: + as_chunk = ch.to_chunk() + layerchunks[i].append(as_chunk) + thisrunchunks[i].append(as_chunk) + layeractivechunks[i] = camPathChunkBuilder([]) + lastsample = newsample + + for i, l in enumerate(layers): + ch = layeractivechunks[i] + if len(ch.points) > 0: + as_chunk = ch.to_chunk() + layerchunks[i].append(as_chunk) + thisrunchunks[i].append(as_chunk) + layeractivechunks[i] = camPathChunkBuilder([]) + + # PARENTING + if o.strategy == 'PARALLEL' or o.strategy == 'CROSS' or o.strategy == 'OUTLINEFILL': + timingstart(sortingtime) + parentChildDist(thisrunchunks[i], lastrunchunks[i], o) + timingadd(sortingtime) + + lastrunchunks = thisrunchunks + + # print(len(layerchunks[i])) + progress('Checking Relations Between Paths') + timingstart(sortingtime) + + if o.strategy == 'PARALLEL' or o.strategy == 'CROSS' or o.strategy == 'OUTLINEFILL': + if len(layers) > 1: # sorting help so that upper layers go first always + for i in range(0, len(layers) - 1): + parents = [] + children = [] + # only pick chunks that should have connectivity assigned - 'last' and 'first' ones of the layer. + for ch in layerchunks[i + 1]: + if not ch.children: + parents.append(ch) + for ch1 in layerchunks[i]: + if not ch1.parents: + children.append(ch1) + + # parent only last and first chunk, before it did this for all. + parentChild(parents, children, o) + timingadd(sortingtime) + chunks = [] + + for i, l in enumerate(layers): + if o.movement.ramp: + for ch in layerchunks[i]: + ch.zstart = layers[i][0] + ch.zend = layers[i][1] + chunks.extend(layerchunks[i]) + timingadd(totaltime) + print(samplingtime) + print(sortingtime) + print(totaltime) + return chunks
+ + + +
+[docs] +async def sampleChunksNAxis(o, pathSamples, layers): + """Sample chunks along a specified axis based on provided paths and layers. + + This function processes a set of path samples and organizes them into + chunks according to specified layers. It prepares the collision world if + necessary, updates the cutter's rotation based on the path samples, and + handles the sampling of points along the paths. The function also + manages the relationships between the sampled points and their + respective layers, ensuring that the correct points are added to each + chunk. The resulting chunks can be used for further processing in a 3D + environment. + + Args: + o (object): An object containing properties such as min/max coordinates, + cutter shape, and other relevant parameters. + pathSamples (list): A list of path samples, each containing start points, + end points, and rotations. + layers (list): A list of layer definitions that specify the boundaries + for sampling. + + Returns: + list: A list of sampled chunks organized by layers. + """ + + # + minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z + + # prepare collision world + if o.update_bullet_collision_tag: + prepareBulletCollision(o) + # print('getting ambient') + getAmbient(o) + o.update_bullet_collision_tag = False + # print (o.ambient) + cutter = o.cutter_shape + cutterdepth = cutter.dimensions.z / 2 + + t = time.time() + print('Sampling Paths') + + totlen = 0 # total length of all chunks, to estimate sampling time. + for chs in pathSamples: + totlen += len(chs.startpoints) + layerchunks = [] + minz = o.minz + layeractivechunks = [] + lastrunchunks = [] + + for l in layers: + layerchunks.append([]) + layeractivechunks.append(camPathChunkBuilder([])) + lastrunchunks.append([]) + n = 0 + + last_percent = -1 + lastz = minz + for patternchunk in pathSamples: + # print (patternchunk.endpoints) + thisrunchunks = [] + for l in layers: + thisrunchunks.append([]) + lastlayer = None + currentlayer = None + lastsample = None + # threads_count=4 + lastrotation = (0, 0, 0) + # for t in range(0,threads): + # print(len(patternchunk.startpoints),len( patternchunk.endpoints)) + spl = len(patternchunk.startpoints) + # ,startp in enumerate(patternchunk.startpoints): + for si in range(0, spl): + # #TODO: seems we are writing into the source chunk , + # and that is why we need to write endpoints everywhere too? + + percent = int(100 * n / totlen) + if percent != last_percent: + await progress_async('sampling paths', percent) + last_percent = percent + n += 1 + sampled = False + # print(si) + + # get the vector to sample + startp = Vector(patternchunk.startpoints[si]) + endp = Vector(patternchunk.endpoints[si]) + rotation = patternchunk.rotations[si] + sweepvect = endp - startp + sweepvect.normalize() + # sampling + if rotation != lastrotation: + + cutter.rotation_euler = rotation + # cutter.rotation_euler.x=-cutter.rotation_euler.x + # print(rotation) + + if o.cutter_type == 'VCARVE': # Bullet cone is always pointing Up Z in the object + cutter.rotation_euler.x += pi + cutter.update_tag() + # this has to be :( it resets the rigidbody world. + bpy.context.scene.frame_set(1) + # No other way to update it probably now :( + # actually 2 frame jumps are needed. + bpy.context.scene.frame_set(2) + bpy.context.scene.frame_set(0) + + newsample = getSampleBulletNAxis(cutter, startp, endp, rotation, cutterdepth) + + # print('totok',startp,endp,rotation,newsample) + ################################ + # handling samples + ############################################ + # this is weird, but will leave it this way now.. just prototyping here. + if newsample is not None: + sampled = True + else: # TODO: why was this here? + newsample = startp + sampled = True + # print(newsample) + + # elif o.ambient_behaviour=='ALL' and not o.inverse:#handle ambient here + # newsample=(x,y,minz) + if sampled: + for i, l in enumerate(layers): + terminatechunk = False + ch = layeractivechunks[i] + + # print(i,l) + # print(l[1],l[0]) + v = startp - newsample + distance = -v.length + + if l[1] <= distance <= l[0]: + lastlayer = currentlayer + currentlayer = i + + if lastsample is not None and lastlayer is not None and currentlayer is not None \ + and lastlayer != currentlayer: # sampling for sorted paths in layers- + # to go to the border of the sampled layer at least... + # there was a bug here, but should be fixed. + if currentlayer < lastlayer: + growing = True + r = range(currentlayer, lastlayer) + spliti = 1 + else: + r = range(lastlayer, currentlayer) + growing = False + spliti = 0 + # print(r) + li = 0 + + for ls in r: + splitdistance = layers[ls][1] + + ratio = (splitdistance - lastdistance) / (distance - lastdistance) + # print(ratio) + betweensample = lastsample + (newsample - lastsample) * ratio + # this probably doesn't work at all!!!! check this algoritm> + betweenrotation = tuple_add(lastrotation, + tuple_mul(tuple_sub(rotation, lastrotation), ratio)) + # startpoint = retract point, it has to be always available... + betweenstartpoint = laststartpoint + \ + (startp - laststartpoint) * ratio + # here, we need to have also possible endpoints always.. + betweenendpoint = lastendpoint + (endp - lastendpoint) * ratio + if growing: + if li > 0: + layeractivechunks[ls].points.insert(-1, betweensample) + layeractivechunks[ls].rotations.insert(-1, betweenrotation) + layeractivechunks[ls].startpoints.insert( + -1, betweenstartpoint) + layeractivechunks[ls].endpoints.insert(-1, betweenendpoint) + else: + layeractivechunks[ls].points.append(betweensample) + layeractivechunks[ls].rotations.append(betweenrotation) + layeractivechunks[ls].startpoints.append(betweenstartpoint) + layeractivechunks[ls].endpoints.append(betweenendpoint) + layeractivechunks[ls + 1].points.append(betweensample) + layeractivechunks[ls + 1].rotations.append(betweenrotation) + layeractivechunks[ls + 1].startpoints.append(betweenstartpoint) + layeractivechunks[ls + 1].endpoints.append(betweenendpoint) + else: + + layeractivechunks[ls].points.insert(-1, betweensample) + layeractivechunks[ls].rotations.insert(-1, betweenrotation) + layeractivechunks[ls].startpoints.insert(-1, betweenstartpoint) + layeractivechunks[ls].endpoints.insert(-1, betweenendpoint) + + layeractivechunks[ls + 1].points.append(betweensample) + layeractivechunks[ls + 1].rotations.append(betweenrotation) + layeractivechunks[ls + 1].startpoints.append(betweenstartpoint) + layeractivechunks[ls + 1].endpoints.append(betweenendpoint) + + # layeractivechunks[ls+1].points.insert(0,betweensample) + li += 1 + # this chunk is terminated, and allready in layerchunks / + + # ch.points.append(betweensample)# + ch.points.append(newsample) + ch.rotations.append(rotation) + ch.startpoints.append(startp) + ch.endpoints.append(endp) + lastdistance = distance + + elif l[1] > distance: + v = sweepvect * l[1] + p = startp - v + ch.points.append(p) + ch.rotations.append(rotation) + ch.startpoints.append(startp) + ch.endpoints.append(endp) + elif l[0] < distance: # retract to original track + ch.points.append(startp) + ch.rotations.append(rotation) + ch.startpoints.append(startp) + ch.endpoints.append(endp) + + lastsample = newsample + lastrotation = rotation + laststartpoint = startp + lastendpoint = endp + + # convert everything to actual chunks + # rather than chunkBuilders + for i, l in enumerate(layers): + layeractivechunks[i] = layeractivechunks[i].to_chunk( + ) if layeractivechunks[i] is not None else None + + for i, l in enumerate(layers): + ch = layeractivechunks[i] + if ch.count() > 0: + layerchunks[i].append(ch) + thisrunchunks[i].append(ch) + layeractivechunks[i] = camPathChunkBuilder([]) + + if o.strategy == 'PARALLEL' or o.strategy == 'CROSS' or o.strategy == 'OUTLINEFILL': + parentChildDist(thisrunchunks[i], lastrunchunks[i], o) + + lastrunchunks = thisrunchunks + + # print(len(layerchunks[i])) + + progress('Checking Relations Between Paths') + """#this algorithm should also work for n-axis, but now is "sleeping" + if (o.strategy=='PARALLEL' or o.strategy=='CROSS'): + if len(layers)>1:# sorting help so that upper layers go first always + for i in range(0,len(layers)-1): + #print('layerstuff parenting') + parentChild(layerchunks[i+1],layerchunks[i],o) + """ + chunks = [] + for i, l in enumerate(layers): + chunks.extend(layerchunks[i]) + + return chunks
+ + + +
+[docs] +def extendChunks5axis(chunks, o): + """Extend chunks with 5-axis cutter start and end points. + + This function modifies the provided chunks by appending calculated start + and end points for a cutter based on the specified orientation and + movement parameters. It determines the starting position of the cutter + based on the machine's settings and the object's movement constraints. + The function iterates through each point in the chunks and updates their + start and end points accordingly. + + Args: + chunks (list): A list of chunk objects that will be modified. + o (object): An object containing movement and orientation data. + """ + + s = bpy.context.scene + m = s.cam_machine + s = bpy.context.scene + free_height = o.movement.free_height # o.max.z + + if m.use_position_definitions: # dhull + cutterstart = Vector((m.starting_position.x, m.starting_position.y, + max(o.max.z, m.starting_position.z))) # start point for casting + else: + # start point for casting + cutterstart = Vector((0, 0, max(o.max.z, free_height))) + cutterend = Vector((0, 0, o.min.z)) + oriname = o.name + ' orientation' + ori = s.objects[oriname] + # rotationaxes = rotTo2axes(ori.rotation_euler,'CA')#warning-here it allready is reset to 0!! + print('rot', o.rotationaxes) + a, b = o.rotationaxes # this is all nonsense by now. + for chunk in chunks: + for v in chunk.points: + cutterstart.x = v[0] + cutterstart.y = v[1] + cutterend.x = v[0] + cutterend.y = v[1] + chunk.startpoints.append(cutterstart.to_tuple()) + chunk.endpoints.append(cutterend.to_tuple()) + chunk.rotations.append( + (a, b, 0)) # TODO: this is a placeholder. It does 99.9% probably write total nonsense.
+ + + +
+[docs] +def curveToShapely(cob, use_modifiers=False): + """Convert a curve object to Shapely polygons. + + This function takes a curve object and converts it into a list of + Shapely polygons. It first breaks the curve into chunks and then + transforms those chunks into Shapely-compatible polygon representations. + The `use_modifiers` parameter allows for additional processing of the + curve before conversion, depending on the specific requirements of the + application. + + Args: + cob: The curve object to be converted. + use_modifiers (bool): A flag indicating whether to apply modifiers + during the conversion process. Defaults to False. + + Returns: + list: A list of Shapely polygons created from the curve object. + """ + + chunks = curveToChunks(cob, use_modifiers) + polys = chunksToShapely(chunks) + return polys
+ + + +# separate function in blender, so you can offset any curve. +# FIXME: same algorithms as the cutout strategy, because that is hierarchy-respecting. + +
+[docs] +def silhoueteOffset(context, offset, style, mitrelimit): + """Offset the silhouette of a curve or font object in Blender. + + This function takes an active curve or font object in Blender and + creates an offset silhouette based on the specified parameters. It first + retrieves the silhouette of the object and then applies a buffer + operation to create the offset shape. The resulting shape is then + converted back into a curve object in the Blender scene. + + Args: + context (bpy.context): The current Blender context. + offset (float): The distance to offset the silhouette. + style (int?): The join style for the offset. Defaults to 1. + mitrelimit (float?): The mitre limit for the offset. Defaults to 1.0. + + Returns: + dict: A dictionary indicating the operation is finished. + """ + + bpy.context.scene.cursor.location = (0, 0, 0) + ob = bpy.context.active_object + if ob.type == 'CURVE' or ob.type == 'FONT': + silhs = curveToShapely(ob) + else: + silhs = getObjectSilhouete('OBJECTS', [ob]) + mp = silhs.buffer(offset, cap_style=1, join_style=style, resolution=16, mitre_limit=mitrelimit) + shapelyToCurve(ob.name + '_offset_' + str(round(offset, 5)), mp, ob.location.z) + bpy.ops.object.curve_remove_doubles() + return {'FINISHED'}
+ + + +
+[docs] +def polygonBoolean(context, boolean_type): + """Perform a boolean operation on selected polygons. + + This function takes the active object and applies a specified boolean + operation (UNION, DIFFERENCE, or INTERSECT) with respect to other + selected objects in the Blender context. It first converts the polygons + of the active object and the selected objects into a Shapely + MultiPolygon. Depending on the boolean type specified, it performs the + corresponding boolean operation and then converts the result back into a + Blender curve. + + Args: + context (bpy.context): The Blender context containing scene and object data. + boolean_type (str): The type of boolean operation to perform. + Must be one of 'UNION', 'DIFFERENCE', or 'INTERSECT'. + + Returns: + dict: A dictionary indicating the operation result, typically {'FINISHED'}. + """ + + bpy.context.scene.cursor.location = (0, 0, 0) + ob = bpy.context.active_object + obs = [] + for ob1 in bpy.context.selected_objects: + if ob1 != ob: + obs.append(ob1) + plist = curveToShapely(ob) + p1 = MultiPolygon(plist) + polys = [] + for o in obs: + plist = curveToShapely(o) + p2 = MultiPolygon(plist) + polys.append(p2) + # print(polys) + if boolean_type == 'UNION': + for p2 in polys: + p1 = p1.union(p2) + elif boolean_type == 'DIFFERENCE': + for p2 in polys: + p1 = p1.difference(p2) + elif boolean_type == 'INTERSECT': + for p2 in polys: + p1 = p1.intersection(p2) + + shapelyToCurve('boolean', p1, ob.location.z) + # bpy.ops.object.convert(target='CURVE') + # bpy.context.scene.cursor_location=ob.location + # bpy.ops.object.origin_set(type='ORIGIN_CURSOR') + + return {'FINISHED'}
+ + + +
+[docs] +def polygonConvexHull(context): + """Generate the convex hull of a polygon from the given context. + + This function duplicates the current object, joins it, and converts it + into a 3D mesh. It then extracts the X and Y coordinates of the vertices + to create a MultiPoint data structure using Shapely. Finally, it + computes the convex hull of these points and converts the result back + into a curve named 'ConvexHull'. Temporary objects created during this + process are deleted to maintain a clean workspace. + + Args: + context: The context in which the operation is performed, typically + related to Blender's current state. + + Returns: + dict: A dictionary indicating the operation's completion status. + """ + + coords = [] + + bpy.ops.object.duplicate() + bpy.ops.object.join() + bpy.context.object.data.dimensions = '3D' # force curve to be a 3D curve + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + bpy.context.active_object.name = "_tmp" + + bpy.ops.object.convert(target='MESH') + obj = bpy.context.view_layer.objects.active + + for v in obj.data.vertices: # extract X,Y coordinates from the vertices data + c = (v.co.x, v.co.y) + coords.append(c) + + select_multiple('_tmp') # delete temporary mesh + select_multiple('ConvexHull') # delete old hull + + # convert coordinates to shapely MultiPoint datastructure + points = sgeometry.MultiPoint(coords) + + hull = points.convex_hull + shapelyToCurve('ConvexHull', hull, 0.0) + + return {'FINISHED'}
+ + + +
+[docs] +def Helix(r, np, zstart, pend, rev): + """Generate a helix of points in 3D space. + + This function calculates a series of points that form a helix based on + the specified parameters. It starts from a given radius and + z-coordinate, and generates points by rotating around the z-axis while + moving linearly along the z-axis. The number of points generated is + determined by the number of turns (revolutions) and the number of points + per revolution. + + Args: + r (float): The radius of the helix. + np (int): The number of points per revolution. + zstart (float): The starting z-coordinate for the helix. + pend (tuple): A tuple containing the x, y, and z coordinates of the endpoint. + rev (int): The number of revolutions to complete. + + Returns: + list: A list of tuples representing the coordinates of the points in the + helix. + """ + + c = [] + 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))) + v.rotate(e) + c.append((v.x + pend[0], v.y + pend[1], pend[2])) + + return c
+ + + +
+[docs] +def comparezlevel(x): + return x[5]
+ + + +
+[docs] +def overlaps(bb1, bb2): + """Determine if one bounding box is a child of another. + + This function checks if the first bounding box (bb1) is completely + contained within the second bounding box (bb2). It does this by + comparing the coordinates of both bounding boxes to see if all corners + of bb1 are within the bounds of bb2. + + Args: + bb1 (tuple): A tuple representing the coordinates of the first bounding box + in the format (x_min, y_min, x_max, y_max). + bb2 (tuple): A tuple representing the coordinates of the second bounding box + in the format (x_min, y_min, x_max, y_max). + + Returns: + bool: True if bb1 is a child of bb2, otherwise False. + """ + # true if bb1 is child of bb2 + ch1 = bb1 + ch2 = bb2 + if (ch2[1] > ch1[1] > ch1[0] > ch2[0] and ch2[3] > ch1[3] > ch1[2] > ch2[2]): + return True
+ + + +
+[docs] +async def connectChunksLow(chunks, o): + """Connects chunks that are close to each other without lifting, sampling + them 'low'. + + This function processes a list of chunks and connects those that are + within a specified distance based on the provided options. It takes into + account various strategies for connecting the chunks, including 'CARVE', + 'PENCIL', and 'MEDIAL_AXIS', and adjusts the merging distance + accordingly. The function also handles specific movement settings, such + as whether to stay low or to merge distances, and may resample chunks if + certain optimization conditions are met. + + Args: + chunks (list): A list of chunk objects to be connected. + o (object): An options object containing movement and strategy parameters. + + Returns: + list: A list of connected chunk objects. + """ + if not o.movement.stay_low or (o.strategy == 'CARVE' and o.carve_depth > 0): + return chunks + + connectedchunks = [] + chunks_to_resample = [] # for OpenCAMLib sampling + mergedist = 3 * o.dist_between_paths + if o.strategy == 'PENCIL': # this is bigger for pencil path since it goes on the surface to clean up the rests, + # and can go to close points on the surface without fear of going deep into material. + mergedist = 10 * o.dist_between_paths + + if o.strategy == 'MEDIAL_AXIS': + mergedist = 1 * o.medial_axis_subdivision + + if o.movement.parallel_step_back: + mergedist *= 2 + + if o.movement.merge_dist > 0: + mergedist = o.movement.merge_dist + # mergedist=10 + lastch = None + i = len(chunks) + pos = (0, 0, 0) + + for ch in chunks: + if ch.count() > 0: + if lastch is not None and (ch.distStart(pos, o) < mergedist): + # CARVE should lift allways, when it goes below surface... + # print(mergedist,ch.dist(pos,o)) + if o.strategy == 'PARALLEL' or o.strategy == 'CROSS' or o.strategy == 'PENCIL': + # for these paths sorting happens after sampling, thats why they need resample the connection + between = samplePathLow(o, lastch, ch, True) + else: + # print('addbetwee') + between = samplePathLow(o, lastch, ch, + False) # other paths either dont use sampling or are sorted before it. + if o.optimisation.use_opencamlib and o.optimisation.use_exact and ( + o.strategy == 'PARALLEL' or o.strategy == 'CROSS' or o.strategy == 'PENCIL'): + chunks_to_resample.append( + (connectedchunks[-1], connectedchunks[-1].count(), between.count())) + + connectedchunks[-1].extend(between.get_points_np()) + connectedchunks[-1].extend(ch.get_points_np()) + else: + connectedchunks.append(ch) + lastch = ch + pos = lastch.get_point(-1) + + if o.optimisation.use_opencamlib and o.optimisation.use_exact and o.strategy != 'CUTOUT' and o.strategy != 'POCKET' and o.strategy != 'WATERLINE': + await oclResampleChunks(o, chunks_to_resample, use_cached_mesh=True) + + return connectedchunks
+ + + +
+[docs] +def getClosest(o, pos, chunks): + """Find the closest chunk to a given position. + + This function iterates through a list of chunks and determines which + chunk is closest to the specified position. It checks if each chunk's + children are sorted before calculating the distance. The chunk with the + minimum distance to the given position is returned. + + Args: + o: An object representing the origin point. + pos: A position to which the closest chunk is calculated. + chunks (list): A list of chunk objects to evaluate. + + Returns: + Chunk: The closest chunk object to the specified position, or None if no valid + chunk is found. + """ + + # ch=-1 + mind = 2000 + d = 100000000000 + ch = None + for chtest in chunks: + cango = True + # here was chtest.getNext==chtest, was doing recursion error and slowing down. + for child in chtest.children: + if not child.sorted: + cango = False + break + if cango: + d = chtest.dist(pos, o) + if d < mind: + ch = chtest + mind = d + return ch
+ + + +
+[docs] +async def sortChunks(chunks, o, last_pos=None): + """Sort a list of chunks based on a specified strategy. + + This function sorts a list of chunks according to the provided options + and the current position. It utilizes a recursive approach to find the + closest chunk to the current position and adapts its distance if it has + not been sorted before. The function also handles progress updates + asynchronously and adjusts the recursion limit to accommodate deep + recursion scenarios. + + Args: + chunks (list): A list of chunk objects to be sorted. + o (object): An options object that contains sorting strategy and other parameters. + last_pos (tuple?): The last known position as a tuple of coordinates. + Defaults to None, which initializes the position to (0, 0, 0). + + Returns: + list: A sorted list of chunk objects. + """ + + if o.strategy != 'WATERLINE': + await progress_async('sorting paths') + # the getNext() function of CamPathChunk was running out of recursion limits. + sys.setrecursionlimit(100000) + sortedchunks = [] + chunks_to_resample = [] + + lastch = None + last_progress_time = time.time() + total = len(chunks) + i = len(chunks) + pos = (0, 0, 0) if last_pos is None else last_pos + while len(chunks) > 0: + if o.strategy != 'WATERLINE' and time.time()-last_progress_time > 0.1: + await progress_async("Sorting paths", 100.0*(total-len(chunks))/total) + last_progress_time = time.time() + ch = None + if len(sortedchunks) == 0 or len( + lastch.parents) == 0: # first chunk or when there are no parents -> parents come after children here... + ch = getClosest(o, pos, chunks) + elif len(lastch.parents) > 0: # looks in parents for next candidate, recursively + for parent in lastch.parents: + ch = parent.getNextClosest(o, pos) + if ch is not None: + break + if ch is None: + ch = getClosest(o, pos, chunks) + + if ch is not None: # found next chunk, append it to list + # only adaptdist the chunk if it has not been sorted before + if not ch.sorted: + ch.adaptdist(pos, o) + ch.sorted = True + # print(len(ch.parents),'children') + chunks.remove(ch) + sortedchunks.append(ch) + lastch = ch + pos = lastch.get_point(-1) + # print(i, len(chunks)) + # experimental fix for infinite loop problem + # else: + # THIS PROBLEM WASN'T HERE AT ALL. but keeping it here, it might fix the problems somwhere else:) + # can't find chunks close enough and still some chunks left + # to be sorted. For now just move the remaining chunks over to + # the sorted list. + # This fixes an infinite loop condition that occurs sometimes. + # This is a bandaid fix: need to find the root cause of this problem + # suspect it has to do with the sorted flag? + # print("no chunks found closest. Chunks not sorted: ", len(chunks)) + # sortedchunks.extend(chunks) + # chunks[:] = [] + + i -= 1 + if o.strategy == 'POCKET' and o.pocket_option == 'OUTSIDE': + sortedchunks.reverse() + + sys.setrecursionlimit(1000) + if o.strategy != 'DRILL' and o.strategy != 'OUTLINEFILL': + # THIS SHOULD AVOID ACTUALLY MOST STRATEGIES, THIS SHOULD BE DONE MANUALLY, + # BECAUSE SOME STRATEGIES GET SORTED TWICE. + sortedchunks = await connectChunksLow(sortedchunks, o) + return sortedchunks
+ + + +# most right vector from a set regarding angle.. +
+[docs] +def getVectorRight(lastv, verts): + """Get the index of the vector that is most to the right based on angle. + + This function calculates the angle between a reference vector (formed by + the last two vectors in `lastv`) and each vector in the `verts` list. It + identifies the vector that has the smallest angle with respect to the + reference vector, indicating that it is the most rightward vector in + relation to the specified direction. + + Args: + lastv (list): A list containing two vectors, where each vector is + represented as a tuple or list of coordinates. + verts (list): A list of vectors represented as tuples or lists of + coordinates. + + Returns: + int: The index of the vector in `verts` that is most to the right + based on the calculated angle. + """ + + defa = 100 + v1 = Vector(lastv[0]) + v2 = Vector(lastv[1]) + va = v2 - v1 + for i, v in enumerate(verts): + if v != lastv[0]: + vb = Vector(v) - v2 + a = va.angle_signed(Vector(vb)) + + if a < defa: + defa = a + returnvec = i + return returnvec
+ + + +
+[docs] +def cleanUpDict(ndict): + """Remove lonely points from a dictionary. + + This function iterates over the keys of the provided dictionary and + removes any entries that contain one or fewer associated values. It + continues to check for and remove "lonely" points until no more can be + found. The process is repeated until all such entries are eliminated + from the dictionary. + + Args: + ndict (dict): A dictionary where keys are associated with lists of values. + + Returns: + None: This function modifies the input dictionary in place and does not return + a value. + """ + + # now it should delete all junk first, iterate over lonely verts. + print('Removing Lonely Points') + # found_solitaires=True + # while found_solitaires: + found_solitaires = False + keys = [] + keys.extend(ndict.keys()) + removed = 0 + for k in keys: + print(k) + print(ndict[k]) + if len(ndict[k]) <= 1: + newcheck = [k] + while (len(newcheck) > 0): + v = newcheck.pop() + if len(ndict[v]) <= 1: + for v1 in ndict[v]: + newcheck.append(v) + dictRemove(ndict, v) + removed += 1 + found_solitaires = True + print(removed)
+ + + +
+[docs] +def dictRemove(dict, val): + """Remove a key and its associated values from a dictionary. + + This function takes a dictionary and a key (val) as input. It iterates + through the list of values associated with the given key and removes the + key from each of those values' lists. Finally, it removes the key itself + from the dictionary. + + Args: + dict (dict): A dictionary where the key is associated with a list of values. + val: The key to be removed from the dictionary and from the lists of its + associated values. + """ + + for v in dict[val]: + dict[v].remove(val) + dict.pop(val)
+ + + +
+[docs] +def addLoop(parentloop, start, end): + """Add a loop to a parent loop structure. + + This function recursively checks if the specified start and end values + can be added as a new loop to the parent loop. If an existing loop + encompasses the new loop, it will call itself on that loop. If no such + loop exists, it appends the new loop defined by the start and end values + to the parent loop's list of loops. + + Args: + parentloop (list): A list representing the parent loop, where the + third element is a list of child loops. + start (int): The starting value of the new loop to be added. + end (int): The ending value of the new loop to be added. + + Returns: + None: This function modifies the parentloop in place and does not + return a value. + """ + + added = False + for l in parentloop[2]: + if l[0] < start and l[1] > end: + addLoop(l, start, end) + return + parentloop[2].append([start, end, []])
+ + + +
+[docs] +def cutloops(csource, parentloop, loops): + """Cut loops from a source code segment. + + This function takes a source code segment and a parent loop defined by + its start and end indices, along with a list of nested loops. It creates + a copy of the source code segment and removes the specified nested loops + from it. The modified segment is then appended to the provided list of + loops. The function also recursively processes any nested loops found + within the parent loop. + + Args: + csource (str): The source code from which loops will be cut. + parentloop (tuple): A tuple containing the start index, end index, and a list of nested + loops. + The list of nested loops should contain tuples with start and end + indices for each loop. + loops (list): A list that will be populated with the modified source code segments + after + removing the specified loops. + + Returns: + None: This function modifies the `loops` list in place and does not return a + value. + """ + + copy = csource[parentloop[0]:parentloop[1]] + + for li in range(len(parentloop[2]) - 1, -1, -1): + l = parentloop[2][li] + # print(l) + copy = copy[:l[0] - parentloop[0]] + copy[l[1] - parentloop[0]:] + loops.append(copy) + for l in parentloop[2]: + cutloops(csource, l, loops)
+ + + +
+[docs] +def getOperationSilhouete(operation): + """Gets the silhouette for the given operation. + + This function determines the silhouette of an operation using image + thresholding techniques. It handles different geometry sources, such as + objects or images, and applies specific methods based on the type of + geometry. If the geometry source is 'OBJECT' or 'COLLECTION', it checks + whether to process curves or not. The function also considers the number + of faces in mesh objects to decide on the appropriate method for + silhouette extraction. + + Args: + operation (Operation): An object containing the necessary data + + Returns: + Silhouette: The computed silhouette for the operation. + """ + if operation.update_silhouete_tag: + image = None + objects = None + if operation.geometry_source == 'OBJECT' or operation.geometry_source == 'COLLECTION': + if not operation.onlycurves: + stype = 'OBJECTS' + else: + stype = 'CURVES' + else: + stype = 'IMAGE' + + totfaces = 0 + if stype == 'OBJECTS': + for ob in operation.objects: + if ob.type == 'MESH': + totfaces += len(ob.data.polygons) + + if (stype == 'OBJECTS' and totfaces > 200000) or stype == 'IMAGE': + print('Image Method') + samples = renderSampleImage(operation) + if stype == 'OBJECTS': + i = samples > operation.minz - 0.0000001 + # numpy.min(operation.zbuffer_image)-0.0000001# + # #the small number solves issue with totally flat meshes, which people tend to mill instead of + # proper pockets. then the minimum was also maximum, and it didn't detect contour. + else: + # this fixes another numeric imprecision. + i = samples > numpy.min(operation.zbuffer_image) + + chunks = imageToChunks(operation, i) + operation.silhouete = chunksToShapely(chunks) + # print(operation.silhouete) + # this conversion happens because we need the silh to be oriented, for milling directions. + else: + print('object method for retrieving silhouette') # + operation.silhouete = getObjectSilhouete(stype, objects=operation.objects, + use_modifiers=operation.use_modifiers) + + operation.update_silhouete_tag = False + return operation.silhouete
+ + + +
+[docs] +def getObjectSilhouete(stype, objects=None, use_modifiers=False): + """Get the silhouette of objects based on the specified type. + + This function computes the silhouette of a given set of objects in + Blender based on the specified type. It can handle both curves and mesh + objects, converting curves to polygon format and calculating the + silhouette for mesh objects. The function also considers the use of + modifiers if specified. The silhouette is generated by processing the + geometry of the objects and returning a Shapely representation of the + silhouette. + + Args: + stype (str): The type of silhouette to generate ('CURVES' or 'OBJECTS'). + objects (list?): A list of Blender objects to process. Defaults to None. + use_modifiers (bool?): Whether to apply modifiers to the objects. Defaults to False. + + Returns: + shapely.geometry.MultiPolygon: The computed silhouette as a Shapely MultiPolygon. + """ + + print("stype",stype) + if stype == 'CURVES': # curve conversion to polygon format + allchunks = [] + for ob in objects: + chunks = curveToChunks(ob) + allchunks.extend(chunks) + silhouete = chunksToShapely(allchunks) + + elif stype == 'OBJECTS': + totfaces = 0 + for ob in objects: + totfaces += len(ob.data.polygons) + + if totfaces < 20000000: # boolean polygons method originaly was 20 000 poly limit, now limitless, + t = time.time() + print('Shapely Getting Silhouette') + polys = [] + for ob in objects: + print("object",ob) + if use_modifiers: + ob = ob.evaluated_get(bpy.context.evaluated_depsgraph_get()) + m = ob.to_mesh() + else: + m = ob.data + mw = ob.matrix_world + mwi = mw.inverted() + r = ob.rotation_euler + m.calc_loop_triangles() + id = 0 + e = 0.000001 + scaleup = 100 + for tri in m.loop_triangles: + n = tri.normal.copy() + n.rotate(r) + + if tri.area > 0 and n.z != 0: # n.z>0.0 and f.area>0.0 : + s = [] + c = mw @ tri.center + c = c.xy + for vert_index in tri.vertices: + v = mw @ m.vertices[vert_index].co + s.append((v.x, v.y)) + if len(s) > 2: + # print(s) + p = spolygon.Polygon(s) + # print(dir(p)) + if p.is_valid: + # polys.append(p) + polys.append(p.buffer(e, resolution=0)) + id += 1 + ob.select_set(False) + if totfaces < 20000: + p = sops.unary_union(polys) + else: + print('Computing in Parts') + bigshapes = [] + i = 1 + part = 20000 + while i * part < totfaces: + print(i) + ar = polys[(i - 1) * part:i * part] + bigshapes.append(sops.unary_union(ar)) + i += 1 + if (i - 1) * part < totfaces: + last_ar = polys[(i - 1) * part:] + bigshapes.append(sops.unary_union(last_ar)) + print('Joining') + p = sops.unary_union(bigshapes) + + print("time:",time.time() - t) + + t = time.time() + silhouete = shapelyToMultipolygon(p) # [polygon_utils_cam.Shapely2Polygon(p)] + + return silhouete
+ + + +
+[docs] +def getAmbient(o): + """Calculate and update the ambient geometry based on the provided object. + + This function computes the ambient shape for a given object based on its + properties, such as cutter restrictions and ambient behavior. It + determines the appropriate radius and creates the ambient geometry + either from the silhouette or as a polygon defined by the object's + minimum and maximum coordinates. If a limit curve is specified, it will + also intersect the ambient shape with the limit polygon. + + Args: + o (object): An object containing properties that define the ambient behavior, + cutter restrictions, and limit curve. + + Returns: + None: The function updates the ambient property of the object in place. + """ + + if o.update_ambient_tag: + if o.ambient_cutter_restrict: # cutter stays in ambient & limit curve + m = o.cutter_diameter / 2 + else: + m = 0 + + if o.ambient_behaviour == 'AROUND': + r = o.ambient_radius - m + # in this method we need ambient from silhouete + o.ambient = getObjectOutline(r, o, True) + else: + o.ambient = spolygon.Polygon(((o.min.x + m, o.min.y + m), (o.min.x + m, o.max.y - m), + (o.max.x - m, o.max.y - m), (o.max.x - m, o.min.y + m))) + + if o.use_limit_curve: + if o.limit_curve != '': + limit_curve = bpy.data.objects[o.limit_curve] + polys = curveToShapely(limit_curve) + o.limit_poly = shapely.ops.unary_union(polys) + + if o.ambient_cutter_restrict: + o.limit_poly = o.limit_poly.buffer( + o.cutter_diameter / 2, resolution=o.optimisation.circle_detail) + o.ambient = o.ambient.intersection(o.limit_poly) + o.update_ambient_tag = False
+ + + +
+[docs] +def getObjectOutline(radius, o, Offset): + """Get the outline of a geometric object based on specified parameters. + + This function generates an outline for a given geometric object by + applying a buffer operation to its polygons. The buffer radius can be + adjusted based on the `radius` parameter, and the operation can be + offset based on the `Offset` flag. The function also considers whether + the polygons should be merged or not, depending on the properties of the + object `o`. + + Args: + radius (float): The radius for the buffer operation. + o (object): An object containing properties that influence the outline generation. + Offset (bool): A flag indicating whether to apply a positive or negative offset. + + Returns: + MultiPolygon: The resulting outline of the geometric object as a MultiPolygon. + """ + # FIXME: make this one operation independent + # circle detail, optimize, optimize thresold. + + polygons = getOperationSilhouete(o) + + i = 0 + # print('offseting polygons') + + if Offset: + offset = 1 + else: + offset = -1 + + outlines = [] + i = 0 + if o.straight: + join = 2 + else: + join = 1 + + if isinstance(polygons, list): + polygon_list = polygons + else: + polygon_list = polygons.geoms + + for p1 in polygon_list: # sort by size before this??? + # print(p1.type, len(polygons)) + i += 1 + if radius > 0: + p1 = p1.buffer(radius * offset, resolution=o.optimisation.circle_detail, + join_style=join, mitre_limit=2) + outlines.append(p1) + + # print(outlines) + if o.dont_merge: + outline = sgeometry.MultiPolygon(outlines) + else: + outline = shapely.ops.unary_union(outlines) + return outline
+ + + +
+[docs] +def addOrientationObject(o): + """Set up orientation for a milling object. + + This function creates an orientation object in the Blender scene for + 4-axis and 5-axis milling operations. It checks if an orientation object + with the specified name already exists, and if not, it adds a new empty + object of type 'ARROWS'. The function then configures the rotation locks + and initial rotation angles based on the specified machine axes and + rotary axis. + + Args: + o (object): An object containing properties such as name, + """ + name = o.name + ' orientation' + s = bpy.context.scene + if s.objects.find(name) == -1: + bpy.ops.object.empty_add(type='ARROWS', align='WORLD', location=(0, 0, 0)) + + ob = bpy.context.active_object + ob.empty_draw_size = 0.05 + ob.show_name = True + ob.name = name + ob = s.objects[name] + if o.machine_axes == '4': + + if o.rotary_axis_1 == 'X': + ob.lock_rotation = [False, True, True] + ob.rotation_euler[1] = 0 + ob.rotation_euler[2] = 0 + if o.rotary_axis_1 == 'Y': + ob.lock_rotation = [True, False, True] + ob.rotation_euler[0] = 0 + ob.rotation_euler[2] = 0 + if o.rotary_axis_1 == 'Z': + ob.lock_rotation = [True, True, False] + ob.rotation_euler[0] = 0 + ob.rotation_euler[1] = 0 + elif o.machine_axes == '5': + ob.lock_rotation = [False, False, True] + + ob.rotation_euler[2] = 0 # this will be a bit hard to rotate.....
+ + + +# def addCutterOrientationObject(o): + + +
+[docs] +def removeOrientationObject(o): + """Remove an orientation object from the current Blender scene. + + This function constructs the name of the orientation object based on the + name of the provided object and attempts to find and delete it from the + Blender scene. If the orientation object exists, it will be removed + using the `delob` function. + + Args: + o (Object): The object whose orientation object is to be removed. + """ + # not working + name = o.name + ' orientation' + if bpy.context.scene.objects.find(name) > -1: + ob = bpy.context.scene.objects[name] + delob(ob)
+ + + +
+[docs] +def addTranspMat(ob, mname, color, alpha): + """Add a transparent material to a given object. + + This function checks if a material with the specified name already + exists in the Blender data. If it does, it retrieves that material; if + not, it creates a new material with the given name and enables the use + of nodes. The function then assigns the material to the specified + object, ensuring that it is applied correctly whether the object already + has materials or not. + + Args: + ob (bpy.types.Object): The Blender object to which the material will be assigned. + mname (str): The name of the material to be added or retrieved. + color (tuple): The RGBA color value for the material (not used in this function). + alpha (float): The transparency value for the material (not used in this function). + """ + + if mname in bpy.data.materials: + mat = bpy.data.materials[mname] + else: + mat = bpy.data.materials.new(name=mname) + mat.use_nodes = True + bsdf = mat.node_tree.nodes["Principled BSDF"] + + # Assign it to object + if ob.data.materials: + ob.data.materials[0] = mat + else: + ob.data.materials.append(mat)
+ + + +
+[docs] +def addMachineAreaObject(): + """Add a machine area object to the current Blender scene. + + This function checks if a machine object named 'CAM_machine' already + exists in the current scene. If it does not exist, it creates a new cube + mesh object, applies transformations, and modifies its geometry to + represent a machine area. The function ensures that the scene's unit + settings are set to metric before creating the object and restores the + original unit settings afterward. It also configures the display + properties of the object for better visibility in the scene. The + function operates within Blender's context and utilizes various Blender + operations to create and modify the mesh. It also handles the selection + state of the active object. + """ + + s = bpy.context.scene + ao = bpy.context.active_object + if s.objects.get('CAM_machine') is not None: + o = s.objects['CAM_machine'] + else: + oldunits = s.unit_settings.system + oldLengthUnit = s.unit_settings.length_unit + # need to be in metric units when adding machine mesh object + # in order for location to work properly + s.unit_settings.system = 'METRIC' + bpy.ops.mesh.primitive_cube_add( + align='WORLD', enter_editmode=False, location=(1, 1, -1), rotation=(0, 0, 0)) + o = bpy.context.active_object + o.name = 'CAM_machine' + o.data.name = 'CAM_machine' + bpy.ops.object.transform_apply(location=True, rotation=False, scale=False) + # o.type = 'SOLID' + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.delete(type='ONLY_FACE') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE', action='TOGGLE') + bpy.ops.mesh.select_all(action='TOGGLE') + bpy.ops.mesh.subdivide(number_cuts=32, smoothness=0, quadcorner='STRAIGHT_CUT', fractal=0, + fractal_along_normal=0, seed=0) + bpy.ops.mesh.select_nth(nth=2, offset=0) + bpy.ops.mesh.delete(type='EDGE') + bpy.ops.mesh.primitive_cube_add( + align='WORLD', enter_editmode=False, location=(1, 1, -1), rotation=(0, 0, 0)) + + bpy.ops.object.editmode_toggle() + # addTranspMat(o, "violet_transparent", (0.800000, 0.530886, 0.725165), 0.1) + o.display_type = 'BOUNDS' + o.hide_render = True + o.hide_select = True + # o.select = False + s.unit_settings.system = oldunits + s.unit_settings.length_unit = oldLengthUnit + + # bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + + o.dimensions = bpy.context.scene.cam_machine.working_area + if ao is not None: + ao.select_set(True)
+ + # else: + # bpy.context.scene.objects.active = None + + +
+[docs] +def addMaterialAreaObject(): + """Add a material area object to the current Blender scene. + + This function checks if a material area object named 'CAM_material' + already exists in the current scene. If it does, it retrieves that + object; if not, it creates a new cube mesh object to serve as the + material area. The dimensions and location of the object are set based + on the current camera operation's bounds. The function also applies + transformations to ensure the object's location and dimensions are + correctly set. The created or retrieved object is configured to be non- + renderable and non-selectable in the viewport, while still being + selectable for operations. This is useful for visualizing the working + area of the camera without affecting the render output. Raises: + None + """ + + s = bpy.context.scene + operation = s.cam_operations[s.cam_active_operation] + getOperationSources(operation) + getBounds(operation) + + ao = bpy.context.active_object + if s.objects.get('CAM_material') is not None: + o = s.objects['CAM_material'] + else: + bpy.ops.mesh.primitive_cube_add( + align='WORLD', enter_editmode=False, location=(1, 1, -1), rotation=(0, 0, 0)) + o = bpy.context.active_object + o.name = 'CAM_material' + o.data.name = 'CAM_material' + bpy.ops.object.transform_apply(location=True, rotation=False, scale=False) + + # addTranspMat(o, 'blue_transparent', (0.458695, 0.794658, 0.8), 0.1) + o.display_type = 'BOUNDS' + o.hide_render = True + o.hide_select = True + o.select_set(state=True, view_layer=None) + # bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + + o.dimensions = bpy.context.scene.cam_machine.working_area + + o.dimensions = ( + operation.max.x - operation.min.x, operation.max.y - operation.min.y, operation.max.z - operation.min.z) + o.location = (operation.min.x, operation.min.y, operation.max.z) + if ao is not None: + ao.select_set(True)
+ + # else: + # bpy.context.scene.objects.active = None + + +
+[docs] +def getContainer(): + """Get or create a container object for camera objects. + + This function checks if a container object named 'CAM_OBJECTS' exists in + the current Blender scene. If it does not exist, the function creates a + new empty object of type 'PLAIN_AXES', names it 'CAM_OBJECTS', and sets + its location to the origin (0, 0, 0). The newly created container is + also hidden. If the container already exists, it simply retrieves and + returns that object. + + Returns: + bpy.types.Object: The container object for camera objects, either newly created or + existing. + """ + + s = bpy.context.scene + if s.objects.get('CAM_OBJECTS') is None: + bpy.ops.object.empty_add(type='PLAIN_AXES', align='WORLD') + container = bpy.context.active_object + container.name = 'CAM_OBJECTS' + container.location = [0, 0, 0] + container.hide = True + else: + container = s.objects['CAM_OBJECTS'] + + return container
+ + + +# progress('finished') + +# tools for voroni graphs all copied from the delaunayVoronoi addon: +
+[docs] +class Point: + def __init__(self, x, y, z): + self.x, self.y, self.z = x, y, z
+ + + +
+[docs] +def unique(L): + """Return a list of unhashable elements in L, but without duplicates. + + This function processes a list of lists, specifically designed to handle + unhashable elements. It sorts the input list and removes duplicates by + comparing the elements based on their coordinates. The function counts + the number of duplicate vertices and the number of collinear points + along the Z-axis. + + Args: + L (list): A list of lists, where each inner list represents a point + + Returns: + tuple: A tuple containing two integers: + - The first integer represents the count of duplicate vertices. + - The second integer represents the count of Z-collinear points. + """ + # For unhashable objects, you can sort the sequence and then scan from the end of the list, + # deleting duplicates as you go + nDupli = 0 + nZcolinear = 0 + # sort() brings the equal elements together; then duplicates are easy to weed out in a single pass. + L.sort() + last = L[-1] + for i in range(len(L) - 2, -1, -1): + if last[:2] == L[i][:2]: # XY coordinates compararison + if last[2] == L[i][2]: # Z coordinates compararison + nDupli += 1 # duplicates vertices + else: # Z colinear + nZcolinear += 1 + del L[i] + else: + last = L[i] + return (nDupli, + nZcolinear) # list data type is mutable,
+ + # input list will automatically update and doesn't need to be returned + + +
+[docs] +def checkEqual(lst): + return lst[1:] == lst[:-1]
+ + + +
+[docs] +def prepareIndexed(o): + """Prepare and index objects in the given collection. + + This function stores the world matrices and parent relationships of the + objects in the provided collection. It then clears the parent + relationships while maintaining their transformations, sets the + orientation of the objects based on a specified orientation object, and + finally re-establishes the parent-child relationships with the + orientation object. The function also resets the location and rotation + of the orientation object to the origin. + + Args: + o (ObjectCollection): A collection of objects to be prepared and indexed. + """ + + s = bpy.context.scene + # first store objects positions/rotations + o.matrices = [] + o.parents = [] + for ob in o.objects: + o.matrices.append(ob.matrix_world.copy()) + o.parents.append(ob.parent) + + # then rotate them + for ob in o.objects: + ob.select = True + s.objects.active = ob + bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') + + s.cursor.location = (0, 0, 0) + oriname = o.name + ' orientation' + ori = s.objects[oriname] + o.orientation_matrix = ori.matrix_world.copy() + o.rotationaxes = rotTo2axes(ori.rotation_euler, 'CA') + ori.select = True + s.objects.active = ori + # we parent all objects to the orientation object + bpy.ops.object.parent_set(type='OBJECT', keep_transform=True) + for ob in o.objects: + ob.select = False + # then we move the orientation object to 0,0 + bpy.ops.object.location_clear() + bpy.ops.object.rotation_clear() + ori.select = False + for ob in o.objects: + activate(ob) + + bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
+ + + # rot=ori.matrix_world.inverted() + # #rot.x=-rot.x + # #rot.y=-rot.y + # #rot.z=-rot.z + # rotationaxes = rotTo2axes(ori.rotation_euler,'CA') + # + # #bpy.context.space_data.pivot_point = 'CURSOR' + # #bpy.context.space_data.pivot_point = 'CURSOR' + # + # for ob in o.objects: + # ob.rotation_euler.rotate(rot) + + +
+[docs] +def cleanupIndexed(operation): + """Clean up indexed operations by updating object orientations and paths. + + This function takes an operation object and updates the orientation of a + specified object in the scene based on the provided orientation matrix. + It also sets the location and rotation of a camera path object to match + the updated orientation. Additionally, it reassigns parent-child + relationships for the objects involved in the operation and updates + their world matrices. + + Args: + operation (OperationType): An object containing the necessary data + """ + + s = bpy.context.scene + oriname = operation.name + 'orientation' + + ori = s.objects[oriname] + path = s.objects["cam_path_{}{}".format(operation.name)] + + ori.matrix_world = operation.orientation_matrix + # set correct path location + path.location = ori.location + path.rotation_euler = ori.rotation_euler + + print(ori.matrix_world, operation.orientation_matrix) + # TODO: fix this here wrong order can cause objects out of place + for i, ob in enumerate(operation.objects): + ob.parent = operation.parents[i] + for i, ob in enumerate(operation.objects): + ob.matrix_world = operation.matrices[i]
+ + + +
+[docs] +def rotTo2axes(e, axescombination): + """Converts an Orientation Object Rotation to Rotation Defined by 2 + Rotational Axes on the Machine. + + This function takes an orientation object and a specified axes + combination, and computes the angles of rotation around two axes based + on the provided orientation. It supports different axes combinations for + indexed machining. The function utilizes vector mathematics to determine + the angles of rotation and returns them as a tuple. + + Args: + e (OrientationObject): The orientation object representing the rotation. + axescombination (str): A string indicating the axes combination ('CA' or 'CB'). + + Returns: + tuple: A tuple containing two angles (float) representing the rotation + around the specified axes. + """ + v = Vector((0, 0, 1)) + v.rotate(e) + # if axes + if axescombination == 'CA': + v2d = Vector((v.x, v.y)) + # ?is this right?It should be vector defining 0 rotation + a1base = Vector((0, -1)) + if v2d.length > 0: + cangle = a1base.angle_signed(v2d) + else: + return (0, 0) + v2d = Vector((v2d.length, v.z)) + a2base = Vector((0, 1)) + aangle = a2base.angle_signed(v2d) + print('angles', cangle, aangle) + return (cangle, aangle) + + elif axescombination == 'CB': + v2d = Vector((v.x, v.y)) + # ?is this right?It should be vector defining 0 rotation + a1base = Vector((1, 0)) + if v2d.length > 0: + cangle = a1base.angle_signed(v2d) + else: + return (0, 0) + v2d = Vector((v2d.length, v.z)) + a2base = Vector((0, 1)) + + bangle = a2base.angle_signed(v2d) + + print('angles', cangle, bangle) + + return (cangle, bangle)
+ + + # v2d=((v[a[0]],v[a[1]])) + # angle1=a1base.angle(v2d)#C for ca + # print(angle1) + # if axescombination[0]=='C': + # e1=Vector((0,0,-angle1)) + # elif axescombination[0]=='A':#TODO: finish this after prototyping stage + # pass; + # v.rotate(e1) + # vbase=Vector(0,1,0) + # bangle=v.angle(vzbase) + # print(v) + # print(bangle) + + # return (angle1, angle2) + + +
+[docs] +def reload_paths(o): + """Reload the camera path data from a pickle file. + + This function retrieves the camera path data associated with the given + object `o`. It constructs a new mesh from the path vertices and updates + the object's properties with the loaded data. If a previous path mesh + exists, it is removed to avoid memory leaks. The function also handles + the creation of a new mesh object if one does not already exist in the + current scene. + + Args: + o (Object): The object for which the camera path is being + """ + + oname = "cam_path_" + o.name + s = bpy.context.scene + # for o in s.objects: + ob = None + old_pathmesh = None + if oname in s.objects: + old_pathmesh = s.objects[oname].data + ob = s.objects[oname] + + picklepath = getCachePath(o) + '.pickle' + f = open(picklepath, 'rb') + d = pickle.load(f) + f.close() + + # passed=False + # while not passed: + # try: + # f=open(picklepath,'rb') + # d=pickle.load(f) + # f.close() + # passed=True + # except: + # print('sleep') + # time.sleep(1) + + o.info.warnings = d['warnings'] + o.info.duration = d['duration'] + verts = d['path'] + + edges = [] + for a in range(0, len(verts) - 1): + edges.append((a, a + 1)) + + oname = "cam_path_" + o.name + mesh = bpy.data.meshes.new(oname) + mesh.name = oname + mesh.from_pydata(verts, edges, []) + + if oname in s.objects: + s.objects[oname].data = mesh + else: + object_utils.object_data_add(bpy.context, mesh, operator=None) + ob = bpy.context.active_object + ob.name = oname + ob = s.objects[oname] + ob.location = (0, 0, 0) + o.path_object_name = oname + o.changed = False + + if old_pathmesh is not None: + bpy.data.meshes.remove(old_pathmesh)
+ + + +# def setup_operation_preset(): +# scene = bpy.context.scene +# cam_operations = scene.cam_operations +# active_operation = scene.cam_active_operation +# try: +# o = cam_operations[active_operation] +# except IndexError: +# bpy.ops.scene.cam_operation_add() +# o = cam_operations[active_operation] +# return o + + +# Moved from init - the following code was moved here to permit the import fix +
+[docs] +USE_PROFILER = False
+ + +
+[docs] +was_hidden_dict = {}
+ + +
+[docs] +_IS_LOADING_DEFAULTS = False
+ + + +
+[docs] +def updateMachine(self, context): + """Update the machine with the given context. + + This function is responsible for updating the machine state based on the + provided context. It prints a message indicating that the update process + has started. If the global variable _IS_LOADING_DEFAULTS is not set to + True, it proceeds to add a machine area object. + + Args: + context: The context in which the machine update is being performed. + """ + + print('Update Machine') + if not _IS_LOADING_DEFAULTS: + addMachineAreaObject()
+ + + +
+[docs] +def updateMaterial(self, context): + """Update the material in the given context. + + This method is responsible for updating the material based on the + provided context. It performs necessary operations to ensure that the + material is updated correctly. Currently, it prints a message indicating + the update process and calls the `addMaterialAreaObject` function to + handle additional material area object updates. + + Args: + context: The context in which the material update is performed. + """ + + print('Update Material') + addMaterialAreaObject()
+ + + +
+[docs] +def updateOperation(self, context): + """Update the visibility and selection state of camera operations in the + scene. + + This method manages the visibility of objects associated with camera + operations based on the current active operation. If the + 'hide_all_others' flag is set to true, it hides all other objects except + for the currently active one. If the flag is false, it restores the + visibility of previously hidden objects. The method also attempts to + highlight the currently active object in the 3D view and make it the + active object in the scene. + + Args: + context (bpy.types.Context): The context containing the current scene and + """ + + scene = context.scene + ao = scene.cam_operations[scene.cam_active_operation] + operationValid(self, context) + + if ao.hide_all_others: + for _ao in scene.cam_operations: + if _ao.path_object_name in bpy.data.objects: + other_obj = bpy.data.objects[_ao.path_object_name] + current_obj = bpy.data.objects[ao.path_object_name] + if other_obj != current_obj: + other_obj.hide = True + other_obj.select = False + else: + for path_obj_name in was_hidden_dict: + print(was_hidden_dict) + if was_hidden_dict[path_obj_name]: + # Find object and make it hidde, then reset 'hidden' flag + obj = bpy.data.objects[path_obj_name] + obj.hide = True + obj.select = False + was_hidden_dict[path_obj_name] = False + + # try highlighting the object in the 3d view and make it active + bpy.ops.object.select_all(action='DESELECT') + # highlight the cutting path if it exists + try: + ob = bpy.data.objects[ao.path_object_name] + ob.select_set(state=True, view_layer=None) + # Show object if, it's was hidden + if ob.hide: + ob.hide = False + was_hidden_dict[ao.path_object_name] = True + bpy.context.scene.objects.active = ob + except Exception as e: + print(e)
+ + +# Moved from init - part 2 + + +
+[docs] +def isValid(o, context): + """Check the validity of a geometry source. + + This function verifies if the provided geometry source is valid based on + its type. It checks for three types of geometry sources: 'OBJECT', + 'COLLECTION', and 'IMAGE'. For 'OBJECT', it ensures that the object name + ends with '_cut_bridges' or exists in the Blender data objects. For + 'COLLECTION', it checks if the collection name exists and contains + objects. For 'IMAGE', it verifies if the source image name exists in the + Blender data images. + + Args: + o (object): An object containing geometry source information, including + attributes like `geometry_source`, `object_name`, `collection_name`, + and `source_image_name`. + context: The context in which the validation is performed (not used in this + function). + + Returns: + bool: True if the geometry source is valid, False otherwise. + """ + + valid = True + if o.geometry_source == 'OBJECT': + if not o.object_name.endswith('_cut_bridges'): # let empty bridge cut be valid + if o.object_name not in bpy.data.objects: + valid = False + if o.geometry_source == 'COLLECTION': + if o.collection_name not in bpy.data.collections: + valid = False + elif len(bpy.data.collections[o.collection_name].objects) == 0: + valid = False + + if o.geometry_source == 'IMAGE': + if o.source_image_name not in bpy.data.images: + valid = False + return valid
+ + + +
+[docs] +def operationValid(self, context): + """Validate the current camera operation in the given context. + + This method checks if the active camera operation is valid based on the + current scene context. It updates the operation's validity status and + provides warnings if the source object is invalid. Additionally, it + configures specific settings related to image geometry sources. + + Args: + context (Context): The context containing the scene and camera operations. + """ + + scene = context.scene + o = scene.cam_operations[scene.cam_active_operation] + o.changed = True + o.valid = isValid(o, context) + invalidmsg = "Invalid Source Object for Operation.\n" + if o.valid: + o.info.warnings = "" + else: + o.info.warnings = invalidmsg + + if o.geometry_source == 'IMAGE': + o.optimisation.use_exact = False + o.update_offsetimage_tag = True + o.update_zbufferimage_tag = True + print('validity ')
+ + + +
+[docs] +def isChainValid(chain, context): + """Check the validity of a chain of operations within a given context. + + This function verifies if all operations in the provided chain are valid + according to the current scene context. It first checks if the chain + contains any operations. If it does, it iterates through each operation + in the chain and checks if it exists in the scene's camera operations. + If an operation is not found or is deemed invalid, the function returns + a tuple indicating the failure and provides an appropriate error + message. If all operations are valid, it returns a success indication. + + Args: + chain (Chain): The chain of operations to validate. + context (Context): The context containing the scene and camera operations. + + Returns: + tuple: A tuple containing a boolean indicating validity and an error message + (if any). The first element is True if valid, otherwise False. The + second element is an error message string. + """ + + s = context.scene + if len(chain.operations) == 0: + return (False, "") + for cho in chain.operations: + found_op = None + for so in s.cam_operations: + if so.name == cho.name: + found_op = so + if found_op == None: + return (False, f"Couldn't Find Operation {cho.name}") + if isValid(found_op, context) is False: + return (False, f"Operation {found_op.name} Is Not Valid") + return (True, "")
+ + + +
+[docs] +def updateOperationValid(self, context): + updateOperation(self, context)
+ + + +# Update functions start here +
+[docs] +def updateChipload(self, context): + """Update the chipload based on feedrate, spindle RPM, and cutter + parameters. + + This function calculates the chipload using the formula: chipload = + feedrate / (spindle_rpm * cutter_flutes). It also attempts to account + for chip thinning when cutting at less than 50% cutter engagement with + cylindrical end mills by combining two formulas. The first formula + provides the nominal chipload based on standard recommendations, while + the second formula adjusts for the cutter diameter and distance between + paths. The current implementation may not yield consistent results, and + there are concerns regarding the correctness of the units used in the + calculations. Further review and refinement of this function may be + necessary to improve accuracy and reliability. + + Args: + context: The context in which the update is performed (not used in this + implementation). + + Returns: + None: This function does not return a value; it updates the chipload in place. + """ + print('Update Chipload ') + o = self + # Old chipload + o.info.chipload = (o.feedrate / (o.spindle_rpm * o.cutter_flutes)) + # New chipload with chip thining compensation. + # I have tried to combine these 2 formulas to compinsate for the phenomenon of chip thinning when cutting at less + # than 50% cutter engagement with cylindrical end mills. formula 1 Nominal Chipload is + # " feedrate mm/minute = spindle rpm x chipload x cutter diameter mm x cutter_flutes " + # formula 2 (.5*(cutter diameter mm devided by dist_between_paths)) divided by square root of + # ((cutter diameter mm devided by dist_between_paths)-1) x Nominal Chipload + # Nominal Chipload = what you find in end mill data sheats recomended chip load at %50 cutter engagment. + # I am sure there is a better way to do this. I dont get consistent result and + # I am not sure if there is something wrong with the units going into the formula, my math or my lack of + # 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))/(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)
+ + + +
+[docs] +def updateOffsetImage(self, context): + """Refresh the Offset Image Tag for re-rendering. + + This method updates the chip load and marks the offset image tag for re- + rendering. It sets the `changed` attribute to True and indicates that + the offset image tag needs to be updated. + + Args: + context: The context in which the update is performed. + """ + updateChipload(self, context) + print('Update Offset') + self.changed = True + self.update_offsetimage_tag = True
+ + + +
+[docs] +def updateZbufferImage(self, context): + """Update the Z-buffer and offset image tags for recalculation. + + This method modifies the internal state to indicate that the Z-buffer + image and offset image tags need to be updated during the calculation + process. It sets the `changed` attribute to True and marks the relevant + tags for updating. Additionally, it calls the `getOperationSources` + function to ensure that the necessary operation sources are retrieved. + + Args: + context: The context in which the update is being performed. + """ + # print('updatezbuf') + # print(self,context) + self.changed = True + self.update_zbufferimage_tag = True + self.update_offsetimage_tag = True + getOperationSources(self)
+ + + +
+[docs] +def updateStrategy(o, context): + """Update the strategy of the given object. + + This function modifies the state of the object `o` by setting its + `changed` attribute to True and printing a message indicating that the + strategy is being updated. Depending on the value of `machine_axes` and + `strategy4axis`, it either adds or removes an orientation object + associated with `o`. Finally, it calls the `updateExact` function to + perform further updates based on the provided context. + + Args: + o (object): The object whose strategy is to be updated. + context (object): The context in which the update is performed. + """ + + """""" + o.changed = True + print('Update Strategy') + if o.machine_axes == '5' or ( + o.machine_axes == '4' and o.strategy4axis == 'INDEXED'): # INDEXED 4 AXIS DOESN'T EXIST NOW... + addOrientationObject(o) + else: + removeOrientationObject(o) + updateExact(o, context)
+ + + +
+[docs] +def updateCutout(o, context): + pass
+ + + +
+[docs] +def updateExact(o, context): + """Update the state of an object for exact operations. + + This function modifies the properties of the given object `o` to + indicate that an update is required. It sets various flags related to + the object's state and checks the optimization settings. If the + optimization is set to use exact mode, it further checks the strategy + and inverse properties to determine if exact mode can be used. If not, + it disables the use of OpenCamLib. + + Args: + o (object): The object to be updated, which contains properties related + context (object): The context in which the update is being performed. + + Returns: + None: This function does not return a value. + """ + + print('Update Exact ') + o.changed = True + o.update_zbufferimage_tag = True + o.update_offsetimage_tag = True + if o.optimisation.use_exact: + if o.strategy == 'POCKET' or o.strategy == 'MEDIAL_AXIS' or o.inverse: + o.optimisation.use_opencamlib = False + print('Current Operation Cannot Use Exact Mode') + else: + o.optimisation.use_opencamlib = False
+ + + +
+[docs] +def updateOpencamlib(o, context): + """Update the OpenCAMLib settings for a given operation. + + This function modifies the properties of the provided operation object + based on its current strategy and optimization settings. If the + operation's strategy is either 'POCKET' or 'MEDIAL_AXIS', and if + OpenCAMLib is being used for optimization, it disables the use of both + exact optimization and OpenCAMLib, indicating that the current operation + cannot utilize OpenCAMLib. + + Args: + o (object): The operation object containing optimization and strategy settings. + context (object): The context in which the operation is being updated. + + Returns: + None: This function does not return any value. + """ + + print('Update OpenCAMLib ') + o.changed = True + if o.optimisation.use_opencamlib and ( + o.strategy == 'POCKET' or o.strategy == 'MEDIAL_AXIS'): + o.optimisation.use_exact = False + o.optimisation.use_opencamlib = False + print('Current Operation Cannot Use OpenCAMLib')
+ + + +
+[docs] +def updateBridges(o, context): + """Update the status of bridges. + + This function marks the bridge object as changed, indicating that an + update has occurred. It prints a message to the console for logging + purposes. The function takes in an object and a context, but the context + is not utilized within the function. + + Args: + o (object): The bridge object that needs to be updated. + context (object): Additional context for the update, not used in this function. + """ + + print('Update Bridges ') + o.changed = True
+ + + +
+[docs] +def updateRotation(o, context): + """Update the rotation of a specified object in Blender. + + This function modifies the rotation of a Blender object based on the + properties of the provided object 'o'. It checks which rotations are + enabled and applies the corresponding rotation values to the active + object in the scene. The rotation can be aligned either along the X or Y + axis, depending on the configuration of 'o'. + + Args: + o (object): An object containing rotation settings and flags. + context (object): The context in which the operation is performed. + """ + + print('Update Rotation') + if o.enable_B or o.enable_A: + print(o, o.rotation_A) + ob = bpy.data.objects[o.object_name] + ob.select_set(True) + bpy.context.view_layer.objects.active = ob + if o.A_along_x: # A parallel with X + if o.enable_A: + bpy.context.active_object.rotation_euler.x = o.rotation_A + if o.enable_B: + bpy.context.active_object.rotation_euler.y = o.rotation_B + else: # A parallel with Y + if o.enable_A: + bpy.context.active_object.rotation_euler.y = o.rotation_A + if o.enable_B: + bpy.context.active_object.rotation_euler.x = o.rotation_B
+ + + +# def updateRest(o, context): +# print('update rest ') +# # if o.use_layers: +# o.movement.parallel_step_back = False +# o.changed = True + +
+[docs] +def updateRest(o, context): + """Update the state of the object. + + This function modifies the given object by setting its 'changed' + attribute to True. It also prints a message indicating that the update + operation has been performed. + + Args: + o (object): The object to be updated. + context (object): The context in which the update is being performed. + """ + + print('Update Rest ') + o.changed = True
+ + + +# if (o.strategy == 'WATERLINE'): +# o.use_layers = True + + +
+[docs] +def getStrategyList(scene, context): + """Get a list of available strategies for operations. + + This function retrieves a predefined list of operation strategies that + can be used in the context of a 3D scene. Each strategy is represented + as a tuple containing an identifier, a user-friendly name, and a + description of the operation. The list includes various operations such + as cutouts, pockets, drilling, and more. If experimental features are + enabled in the preferences, additional experimental strategies may be + included in the returned list. + + Args: + scene: The current scene context. + context: The current context in which the operation is being performed. + + Returns: + list: A list of tuples, each containing the strategy identifier, + name, and description. + """ + + use_experimental = bpy.context.preferences.addons[__package__].preferences.experimental + items = [ + ('CUTOUT', 'Profile(Cutout)', 'Cut the silhouete with offset'), + ('POCKET', 'Pocket', 'Pocket operation'), + ('DRILL', 'Drill', 'Drill operation'), + ('PARALLEL', 'Parallel', 'Parallel lines on any angle'), + ('CROSS', 'Cross', 'Cross paths'), + ('BLOCK', 'Block', 'Block path'), + ('SPIRAL', 'Spiral', 'Spiral path'), + ('CIRCLES', 'Circles', 'Circles path'), + ('OUTLINEFILL', 'Outline Fill', + 'Detect outline and fill it with paths as pocket. Then sample these paths on the 3d surface'), + ('CARVE', 'Project curve to surface', 'Engrave the curve path to surface'), + ('WATERLINE', 'Waterline - Roughing -below zero', + 'Waterline paths - constant z below zero'), + ('CURVE', 'Curve to Path', 'Curve object gets converted directly to path'), + ('MEDIAL_AXIS', 'Medial axis', + 'Medial axis, must be used with V or ball cutter, for engraving various width shapes with a single stroke ') + ] + # if use_experimental: + # items.extend([('MEDIAL_AXIS', 'Medial axis - EXPERIMENTAL', + # 'Medial axis, must be used with V or ball cutter, for engraving various width shapes with a single stroke ')]); + # ('PENCIL', 'Pencil - EXPERIMENTAL','Pencil operation - detects negative corners in the model and mills only those.'), + # ('CRAZY', 'Crazy path - EXPERIMENTAL', 'Crazy paths - dont even think about using this!'), + # ('PROJECTED_CURVE', 'Projected curve - EXPERIMENTAL', 'project 1 curve towards other curve')]) + return items
+ + +# The following functions are temporary +# until all content in __init__.py is cleaned up + + +
+[docs] +def update_material(self, context): + addMaterialAreaObject()
+ + + +
+[docs] +def update_operation(self, context): + """Update the camera operation based on the current context. + + This function retrieves the active camera operation from the Blender + context and updates it using the `updateRest` function. It accesses the + active operation from the scene's camera operations and passes the + current context to the updating function. + + Args: + context: The context in which the operation is being updated. + """ + + # from . import updateRest + active_op = bpy.context.scene.cam_operations[bpy.context.scene.cam_active_operation] + updateRest(active_op, bpy.context)
+ + + +
+[docs] +def update_exact_mode(self, context): + """Update the exact mode of the active camera operation. + + This function retrieves the currently active camera operation from the + Blender context and updates its exact mode using the `updateExact` + function. It accesses the active operation through the `cam_operations` + list in the current scene and passes the active operation along with the + current context to the `updateExact` function. + + Args: + context: The context in which the update is performed. + """ + + # from . import updateExact + active_op = bpy.context.scene.cam_operations[bpy.context.scene.cam_active_operation] + updateExact(active_op, bpy.context)
+ + + +
+[docs] +def update_opencamlib(self, context): + """Update the OpenCamLib with the current active operation. + + This function retrieves the currently active camera operation from the + Blender context and updates the OpenCamLib accordingly. It accesses the + active operation from the scene's camera operations and passes it along + with the current context to the update function. + + Args: + context: The context in which the operation is being performed, typically + provided by + Blender's internal API. + """ + + # from . import updateOpencamlib + active_op = bpy.context.scene.cam_operations[bpy.context.scene.cam_active_operation] + updateOpencamlib(active_op, bpy.context)
+ + + +
+[docs] +def update_zbuffer_image(self, context): + """Update the Z-buffer image based on the active camera operation. + + This function retrieves the currently active camera operation from the + Blender context and updates the Z-buffer image accordingly. It accesses + the scene's camera operations and invokes the `updateZbufferImage` + function with the active operation and context. + + Args: + context (bpy.context): The current Blender 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 +
+[docs] +def check_operations_on_load(context): + """Checks for any broken computations on load and resets them. + + This function verifies the presence of necessary Blender add-ons and + installs any that are missing. It also resets any ongoing computations + in camera operations and sets the interface level to the previously used + level when loading a new file. If the add-on has been updated, it copies + the necessary presets from the source to the target directory. + Additionally, it checks for updates to the camera plugin and updates + operation presets if required. + + Args: + context: The context in which the function is executed, typically containing + information about + the current Blender environment. + """ + + addons = bpy.context.preferences.addons + + addon_prefs = bpy.context.preferences.addons[__package__].preferences + + modules = [ + # Objects & Tools + "extra_mesh_objects", + "extra_curve_objectes", + "simplify_curves_plus", + "curve_tools", + "print3d_toolbox", + # File Formats + "stl_format_legacy", + "import_autocad_dxf_format_dxf", + "export_autocad_dxf_format_dxf" + ] + + for module in modules: + if module not in addons: + try: + addons[f'bl_ext.blender_org.{module}'] + except KeyError: + bpy.ops.extensions.package_install(repo_index=0, pkg_id=module) + + 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 = addon_prefs.default_interface_level + machine_preset = addon_prefs.machine_preset = addon_prefs.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 addon_prefs.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): + """Copy a file from source to destination if it does not already exist. + + This function checks if the destination file exists. If it does not, the + function copies the source file to the destination using a high-level + file operation that preserves metadata. + + Args: + src (str): The path to the source file to be copied. + dst (str): The path to the destination where the file should be copied. + """ + + 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, + ) + + addon_prefs.just_updated = False + bpy.ops.wm.save_userpref() + + if not addon_prefs.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) + addon_prefs.op_preset_update = True
+ + + +# add pocket op for medial axis and profile cut inside to clean unremoved material +
+[docs] +def Add_Pocket(maxdepth, sname, new_cutter_diameter): + """Add a pocket operation for the medial axis and profile cut. + + This function first deselects all objects in the scene and then checks + for any existing medial pocket objects, deleting them if found. It + verifies whether a medial pocket operation already exists in the camera + operations. If it does not exist, it creates a new pocket operation with + the specified parameters. The function also modifies the selected + object's silhouette offset based on the new cutter diameter. + + Args: + maxdepth (float): The maximum depth of the pocket to be created. + sname (str): The name of the object to which the pocket will be added. + new_cutter_diameter (float): The diameter of the new cutter to be used. + """ + + 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,2) + bpy.context.active_object.name = 'medial_pocket' + m_ob = bpy.context.view_layer.objects.active + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') + m_ob.location.z = maxdepth + 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
+ + +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/version.html b/_modules/cam/version.html new file mode 100644 index 000000000..0f05073f8 --- /dev/null +++ b/_modules/cam/version.html @@ -0,0 +1,407 @@ + + + + + + + + + + cam.version — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.version

+
+[docs] +__version__=(1,0,50)
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/cam/voronoi.html b/_modules/cam/voronoi.html new file mode 100644 index 000000000..787368a96 --- /dev/null +++ b/_modules/cam/voronoi.html @@ -0,0 +1,2499 @@ + + + + + + + + + + cam.voronoi — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for cam.voronoi

+"""CNC CAM 'voronoi.py'
+
+Voronoi diagram calculator/ Delaunay triangulator
+
+- Voronoi Diagram Sweepline algorithm and C code by Steven Fortune, 1987, http://ect.bell-labs.com/who/sjf/
+- Python translation to file voronoi.py by Bill Simons, 2005, http://www.oxfish.com/
+- Additional changes for QGIS by Carson Farmer added November 2010
+- 2012 Ported to Python 3 and additional clip functions by domlysz at gmail.com
+
+Calculate Delaunay triangulation or the Voronoi polygons for a set of
+2D input points.
+
+Derived from code bearing the following notice:
+
+The author of this software is Steven Fortune.  Copyright (c) 1994 by AT&T
+Bell Laboratories.
+Permission to use, copy, modify, and distribute this software for any
+purpose without fee is hereby granted, provided that this entire notice
+is included in all copies of any software which is or includes a copy
+or modification of this software and in all copies of the supporting
+documentation for such software.
+THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
+WARRANTY.  IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY
+REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY
+OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE.
+
+Comments were incorporated from Shane O'Sullivan's translation of the
+original code into C++ (http://mapviewer.skynet.ie/voronoi.html)
+
+Steve Fortune's homepage: http://netlib.bell-labs.com/cm/cs/who/sjf/index.html
+
+
+For programmatic use two functions are available:
+
+computeVoronoiDiagram(points, xBuff, yBuff, polygonsOutput=False, formatOutput=False) :
+Takes :
+	- a list of point objects (which must have x and y fields).
+	- x and y buffer values which are the expansion percentages of the bounding box rectangle including all input points.
+	Returns :
+	- With default options :
+	  A list of 2-tuples, representing the two points of each Voronoi diagram edge.
+	  Each point contains 2-tuples which are the x,y coordinates of point.
+	  if formatOutput is True, returns :
+			- a list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices.
+			- and a list of 2-tuples (v1, v2) representing edges of the Voronoi diagram.
+			  v1 and v2 are the indices of the vertices at the end of the edge.
+	- If polygonsOutput option is True, returns :
+	  A dictionary of polygons, keys are the indices of the input points,
+	  values contains n-tuples representing the n points of each Voronoi diagram polygon.
+	  Each point contains 2-tuples which are the x,y coordinates of point.
+	  if formatOutput is True, returns :
+			- A list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices.
+			- and a dictionary of input points indices. Values contains n-tuples representing the n points of each Voronoi diagram polygon.
+			  Each tuple contains the vertex indices of the polygon vertices.
+
+computeDelaunayTriangulation(points):
+	Takes a list of point objects (which must have x and y fields).
+	Returns a list of 3-tuples: the indices of the points that form a Delaunay triangle.
+"""
+
+import math
+import sys
+
+
+[docs] +TOLERANCE = 1e-9
+ +
+[docs] +BIG_FLOAT = 1e38
+ + +if sys.version > '3': +
+[docs] + PY3 = True
+ +else: + PY3 = False + + +# ------------------------------------------------------------------ +
+[docs] +class Context(object): + def __init__(self): + """Init function.""" +
+[docs] + self.doPrint = 0
+ +
+[docs] + self.debug = 0
+ +
+[docs] + self.extent = () # tuple (xmin, xmax, ymin, ymax)
+ +
+[docs] + self.triangulate = False
+ +
+[docs] + self.vertices = [] # list of vertex 2-tuples: (x,y)
+ + # equation of line 3-tuple (a b c), for the equation of the line a*x+b*y = c +
+[docs] + self.lines = []
+ + # edge 3-tuple: (line index, vertex 1 index, vertex 2 index) if either vertex index is -1, the edge extends to infinity +
+[docs] + self.edges = []
+ +
+[docs] + self.triangles = [] # 3-tuple of vertex indices
+ +
+[docs] + self.polygons = {} # a dict of site:[edges] pairs
+ + + ########Clip functions######## +
+[docs] + def getClipEdges(self): + """Get the clipped edges based on the current extent. + + This function iterates through the edges of a geometric shape and + determines which edges are within the specified extent. It handles both + finite and infinite lines, clipping them as necessary to fit within the + defined boundaries. For finite lines, it checks if both endpoints are + within the extent, and if not, it calculates the intersection points + using the line equations. For infinite lines, it checks if at least one + endpoint is within the extent and clips accordingly. + + Returns: + list: A list of tuples, where each tuple contains two points representing the + clipped edges. + """ + xmin, xmax, ymin, ymax = self.extent + clipEdges = [] + for edge in self.edges: + equation = self.lines[edge[0]] # line equation + if edge[1] != -1 and edge[2] != -1: # finite line + x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] + x2, y2 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] + pt1, pt2 = (x1, y1), (x2, y2) + inExtentP1, inExtentP2 = self.inExtent(x1, y1), self.inExtent(x2, y2) + if inExtentP1 and inExtentP2: + clipEdges.append((pt1, pt2)) + elif inExtentP1 and not inExtentP2: + pt2 = self.clipLine(x1, y1, equation, leftDir=False) + clipEdges.append((pt1, pt2)) + elif not inExtentP1 and inExtentP2: + pt1 = self.clipLine(x2, y2, equation, leftDir=True) + clipEdges.append((pt1, pt2)) + else: # infinite line + if edge[1] != -1: + x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] + leftDir = False + else: + x1, y1 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] + leftDir = True + if self.inExtent(x1, y1): + pt1 = (x1, y1) + pt2 = self.clipLine(x1, y1, equation, leftDir) + clipEdges.append((pt1, pt2)) + return clipEdges
+ + +
+[docs] + def getClipPolygons(self, closePoly): + """Get clipped polygons based on the provided edges. + + This function processes a set of polygons defined by their edges and + vertices, clipping them according to the specified extent. It checks + whether each edge is finite or infinite and determines if the endpoints + of each edge are within the defined extent. If they are not, the + function calculates the intersection points with the extent boundaries. + The resulting clipped edges are then used to create polygons, which are + returned as a dictionary. The user can specify whether to close the + polygons or leave them open. + + Args: + closePoly (bool): A flag indicating whether to close the polygons. + + Returns: + dict: A dictionary where keys are polygon indices and values are lists of + points defining the clipped polygons. + """ + xmin, xmax, ymin, ymax = self.extent + poly = {} + for inPtsIdx, edges in self.polygons.items(): + clipEdges = [] + for edge in edges: + equation = self.lines[edge[0]] # line equation + if edge[1] != -1 and edge[2] != -1: # finite line + x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] + x2, y2 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] + pt1, pt2 = (x1, y1), (x2, y2) + inExtentP1, inExtentP2 = self.inExtent(x1, y1), self.inExtent(x2, y2) + if inExtentP1 and inExtentP2: + clipEdges.append((pt1, pt2)) + elif inExtentP1 and not inExtentP2: + pt2 = self.clipLine(x1, y1, equation, leftDir=False) + clipEdges.append((pt1, pt2)) + elif not inExtentP1 and inExtentP2: + pt1 = self.clipLine(x2, y2, equation, leftDir=True) + clipEdges.append((pt1, pt2)) + else: # infinite line + if edge[1] != -1: + x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] + leftDir = False + else: + x1, y1 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] + leftDir = True + if self.inExtent(x1, y1): + pt1 = (x1, y1) + pt2 = self.clipLine(x1, y1, equation, leftDir) + clipEdges.append((pt1, pt2)) + # create polygon definition from edges and check if polygon is completely closed + polyPts, complete = self.orderPts(clipEdges) + if not complete: + startPt = polyPts[0] + endPt = polyPts[-1] + if startPt[0] == endPt[0] or startPt[1] == endPt[1]: + # if start & end points are collinear then they are along an extent border + polyPts.append(polyPts[0]) # simple close + else: # close at extent corner + if (startPt[0] == xmin and endPt[1] == ymax) or ( + endPt[0] == xmin and startPt[1] == ymax): # upper left + polyPts.append((xmin, ymax)) # corner point + polyPts.append(polyPts[0]) # close polygon + if (startPt[0] == xmax and endPt[1] == ymax) or ( + endPt[0] == xmax and startPt[1] == ymax): # upper right + polyPts.append((xmax, ymax)) + polyPts.append(polyPts[0]) + if (startPt[0] == xmax and endPt[1] == ymin) or ( + endPt[0] == xmax and startPt[1] == ymin): # bottom right + polyPts.append((xmax, ymin)) + polyPts.append(polyPts[0]) + if (startPt[0] == xmin and endPt[1] == ymin) or ( + endPt[0] == xmin and startPt[1] == ymin): # bottom left + polyPts.append((xmin, ymin)) + polyPts.append(polyPts[0]) + if not closePoly: # unclose polygon + polyPts = polyPts[:-1] + poly[inPtsIdx] = polyPts + return poly
+ + +
+[docs] + def clipLine(self, x1, y1, equation, leftDir): + """Clip a line segment defined by its endpoints against a bounding box. + + This function calculates the intersection points of a line defined by + the given equation with the bounding box defined by the extent of the + object. Depending on the direction specified (left or right), it will + return the appropriate intersection point that lies within the bounds. + + Args: + x1 (float): The x-coordinate of the first endpoint of the line. + y1 (float): The y-coordinate of the first endpoint of the line. + equation (tuple): A tuple containing the coefficients (a, b, c) of + the line equation in the form ax + by + c = 0. + leftDir (bool): A boolean indicating the direction to clip the line. + If True, clip towards the left; otherwise, clip + towards the right. + + Returns: + tuple: The coordinates of the clipped point as (x, y). + """ + xmin, xmax, ymin, ymax = self.extent + a, b, c = equation + if b == 0: # vertical line + if leftDir: # left is bottom of vertical line + return (x1, ymax) + else: + return (x1, ymin) + elif a == 0: # horizontal line + if leftDir: + return (xmin, y1) + else: + return (xmax, y1) + else: + y2_at_xmin = (c - a * xmin) / b + y2_at_xmax = (c - a * xmax) / b + x2_at_ymin = (c - b * ymin) / a + x2_at_ymax = (c - b * ymax) / a + intersectPts = [] + if ymin <= y2_at_xmin <= ymax: # valid intersect point + intersectPts.append((xmin, y2_at_xmin)) + if ymin <= y2_at_xmax <= ymax: + intersectPts.append((xmax, y2_at_xmax)) + if xmin <= x2_at_ymin <= xmax: + intersectPts.append((x2_at_ymin, ymin)) + if xmin <= x2_at_ymax <= xmax: + intersectPts.append((x2_at_ymax, ymax)) + # delete duplicate (happens if intersect point is at extent corner) + intersectPts = set(intersectPts) + # choose target intersect point + if leftDir: + pt = min(intersectPts) # smaller x value + else: + pt = max(intersectPts) + return pt
+ + +
+[docs] + def inExtent(self, x, y): + """Check if a point is within the defined extent. + + This function determines whether the given coordinates (x, y) fall + within the boundaries defined by the extent of the object. The extent is + defined by its minimum and maximum x and y values (xmin, xmax, ymin, + ymax). The function returns True if the point is within these bounds, + and False otherwise. + + Args: + x (float): The x-coordinate of the point to check. + y (float): The y-coordinate of the point to check. + + Returns: + bool: True if the point (x, y) is within the extent, False otherwise. + """ + xmin, xmax, ymin, ymax = self.extent + return x >= xmin and x <= xmax and y >= ymin and y <= ymax
+ + +
+[docs] + def orderPts(self, edges): + """Order points to form a polygon. + + This function takes a list of edges, where each edge is represented as a + pair of points, and orders the points to create a polygon. It identifies + the starting and ending points of the polygon and ensures that the + points are connected in the correct order. If all points are duplicates, + it recognizes that the polygon is complete and handles it accordingly. + + Args: + edges (list): A list of edges, where each edge is a tuple or list containing two + points. + + Returns: + tuple: A tuple containing: + - list: The ordered list of polygon points. + - bool: A flag indicating whether the polygon is complete. + """ + poly = [] # returned polygon points list [pt1, pt2, pt3, pt4 ....] + pts = [] + # get points list + for edge in edges: + pts.extend([pt for pt in edge]) + # try to get start & end point + try: + # start and end point aren't duplicate + startPt, endPt = [pt for pt in pts if pts.count(pt) < 2] + except: # all points are duplicate --> polygon is complete --> append some or other edge points + complete = True + firstIdx = 0 + poly.append(edges[0][0]) + poly.append(edges[0][1]) + else: # incomplete --> append the first edge points + complete = False + # search first edge + for i, edge in enumerate(edges): + if startPt in edge: # find + firstIdx = i + break + poly.append(edges[firstIdx][0]) + poly.append(edges[firstIdx][1]) + if poly[0] != startPt: + poly.reverse() + # append next points in list + del edges[firstIdx] + while edges: # all points will be treated when edges list will be empty + currentPt = poly[-1] # last item + for i, edge in enumerate(edges): + if currentPt == edge[0]: + poly.append(edge[1]) + break + elif currentPt == edge[1]: + poly.append(edge[0]) + break + del edges[i] + return poly, complete
+ + +
+[docs] + def setClipBuffer(self, xpourcent, ypourcent): + """Set the clipping buffer based on percentage adjustments. + + This function modifies the clipping extent of an object by adjusting its + boundaries according to the specified percentage values for both the x + and y axes. It calculates the new minimum and maximum values for the x + and y coordinates by applying the given percentages to the current + extent. + + Args: + xpourcent (float): The percentage adjustment for the x-axis. + ypourcent (float): The percentage adjustment for the y-axis. + + Returns: + None: This function does not return a value; it modifies the + object's extent in place. + """ + xmin, xmax, ymin, ymax = self.extent + witdh = xmax - xmin + height = ymax - ymin + xmin = xmin - witdh * xpourcent / 100 + xmax = xmax + witdh * xpourcent / 100 + ymin = ymin - height * ypourcent / 100 + ymax = ymax + height * ypourcent / 100 + self.extent = xmin, xmax, ymin, ymax
+ + + # End clip functions######## + +
+[docs] + def outSite(self, s): + """Handle output for a site object. + + This function processes the output based on the current settings of the + instance. If debugging is enabled, it prints the site number and its + coordinates. If triangulation is enabled, no action is taken. If + printing is enabled, it prints the coordinates of the site. + + Args: + s (object): An object representing a site, which should have + attributes 'sitenum', 'x', and 'y'. + + Returns: + None: This function does not return a value. + """ + if self.debug: + print("site (%d) at %f %f" % (s.sitenum, s.x, s.y)) + elif self.triangulate: + pass + elif self.doPrint: + print("s %f %f" % (s.x, s.y))
+ + +
+[docs] + def outVertex(self, s): + """Add a vertex to the list of vertices. + + This function appends the coordinates of a given vertex to the internal + list of vertices. Depending on the state of the debug, triangulate, and + doPrint flags, it may also print debug information or vertex coordinates + to the console. + + Args: + s (object): An object containing the attributes `x`, `y`, and + `sitenum` which represent the coordinates and + identifier of the vertex. + + Returns: + None: This function does not return a value. + """ + self.vertices.append((s.x, s.y)) + if self.debug: + print("vertex(%d) at %f %f" % (s.sitenum, s.x, s.y)) + elif self.triangulate: + pass + elif self.doPrint: + print("v %f %f" % (s.x, s.y))
+ + +
+[docs] + def outTriple(self, s1, s2, s3): + """Add a triangle defined by three site numbers to the list of triangles. + + This function takes three site objects, extracts their site numbers, and + appends a tuple of these site numbers to the `triangles` list. If + debugging is enabled, it prints the site numbers to the console. + Additionally, if triangulation is enabled and printing is allowed, it + prints the site numbers in a formatted manner. + + Args: + s1 (Site): The first site object. + s2 (Site): The second site object. + s3 (Site): The third site object. + + Returns: + None: This function does not return a value. + """ + self.triangles.append((s1.sitenum, s2.sitenum, s3.sitenum)) + if self.debug: + print("circle through left=%d right=%d bottom=%d" % + (s1.sitenum, s2.sitenum, s3.sitenum)) + elif self.triangulate and self.doPrint: + print("%d %d %d" % (s1.sitenum, s2.sitenum, s3.sitenum))
+ + +
+[docs] + def outBisector(self, edge): + """Process and log the outbisector of a given edge. + + This function appends the parameters of the edge (a, b, c) to the lines + list and optionally prints debugging information or the parameters based + on the state of the debug and doPrint flags. The function is designed to + handle geometric edges and their properties in a computational geometry + context. + + Args: + edge (Edge): An object representing an edge with attributes + a, b, c, edgenum, and reg. + + Returns: + None: This function does not return a value. + """ + self.lines.append((edge.a, edge.b, edge.c)) + if self.debug: + print("line(%d) %gx+%gy=%g, bisecting %d %d" % ( + edge.edgenum, edge.a, edge.b, edge.c, edge.reg[0].sitenum, edge.reg[1].sitenum)) + elif self.doPrint: + print("l %f %f %f" % (edge.a, edge.b, edge.c))
+ + +
+[docs] + def outEdge(self, edge): + """Process an edge and update the associated polygons and edges. + + This function takes an edge as input and retrieves the site numbers + associated with its left and right endpoints. It then updates the + polygons dictionary to include the edge information for the regions + associated with the edge. If the regions are not already present in the + polygons dictionary, they are initialized. The function also appends the + edge information to the edges list. If triangulation is not enabled, it + prints the edge number and its associated site numbers. + + Args: + edge (Edge): An instance of the Edge class containing information + + Returns: + None: This function does not return a value. + """ + sitenumL = -1 + if edge.ep[Edge.LE] is not None: + sitenumL = edge.ep[Edge.LE].sitenum + sitenumR = -1 + if edge.ep[Edge.RE] is not None: + sitenumR = edge.ep[Edge.RE].sitenum + + # polygons dict add by CF + if edge.reg[0].sitenum not in self.polygons: + self.polygons[edge.reg[0].sitenum] = [] + if edge.reg[1].sitenum not in self.polygons: + self.polygons[edge.reg[1].sitenum] = [] + self.polygons[edge.reg[0].sitenum].append((edge.edgenum, sitenumL, sitenumR)) + self.polygons[edge.reg[1].sitenum].append((edge.edgenum, sitenumL, sitenumR)) + + self.edges.append((edge.edgenum, sitenumL, sitenumR)) + + if not self.triangulate: + if self.doPrint: + print("e %d" % edge.edgenum) + print(" %d " % sitenumL) + print("%d" % sitenumR)
+
+ + + +# ------------------------------------------------------------------ +
+[docs] +def voronoi(siteList, context): + """Generate a Voronoi diagram from a list of sites. + + This function computes the Voronoi diagram for a given list of sites. It + utilizes a sweep line algorithm to process site events and circle + events, maintaining a priority queue and edge list to manage the + geometric relationships between the sites. The function outputs the + resulting edges, vertices, and bisectors to the provided context. + + Args: + siteList (SiteList): A list of sites represented by their coordinates. + context (Context): An object that handles the output of the Voronoi diagram + elements, including sites, edges, and vertices. + + Returns: + None: This function does not return a value; it outputs results directly + to the context provided. + """ + context.extent = siteList.extent + edgeList = EdgeList(siteList.xmin, siteList.xmax, len(siteList)) + priorityQ = PriorityQueue(siteList.ymin, siteList.ymax, len(siteList)) + siteIter = siteList.iterator() + + bottomsite = siteIter.next() + context.outSite(bottomsite) + newsite = siteIter.next() + minpt = Site(-BIG_FLOAT, -BIG_FLOAT) + while True: + if not priorityQ.isEmpty(): + minpt = priorityQ.getMinPt() + + if newsite and (priorityQ.isEmpty() or newsite < minpt): + # newsite is smallest - this is a site event + context.outSite(newsite) + + # get first Halfedge to the LEFT and RIGHT of the new site + lbnd = edgeList.leftbnd(newsite) + rbnd = lbnd.right + + # if this halfedge has no edge, bot = bottom site (whatever that is) + # create a new edge that bisects + bot = lbnd.rightreg(bottomsite) + edge = Edge.bisect(bot, newsite) + context.outBisector(edge) + + # create a new Halfedge, setting its pm field to 0 and insert + # this new bisector edge between the left and right vectors in + # a linked list + bisector = Halfedge(edge, Edge.LE) + edgeList.insert(lbnd, bisector) + + # if the new bisector intersects with the left edge, remove + # the left edge's vertex, and put in the new one + p = lbnd.intersect(bisector) + if p is not None: + priorityQ.delete(lbnd) + priorityQ.insert(lbnd, p, newsite.distance(p)) + + # create a new Halfedge, setting its pm field to 1 + # insert the new Halfedge to the right of the original bisector + lbnd = bisector + bisector = Halfedge(edge, Edge.RE) + edgeList.insert(lbnd, bisector) + + # if this new bisector intersects with the right Halfedge + p = bisector.intersect(rbnd) + if p is not None: + # push the Halfedge into the ordered linked list of vertices + priorityQ.insert(bisector, p, newsite.distance(p)) + + newsite = siteIter.next() + + elif not priorityQ.isEmpty(): + # intersection is smallest - this is a vector (circle) event + + # pop the Halfedge with the lowest vector off the ordered list of + # vectors. Get the Halfedge to the left and right of the above HE + # and also the Halfedge to the right of the right HE + lbnd = priorityQ.popMinHalfedge() + llbnd = lbnd.left + rbnd = lbnd.right + rrbnd = rbnd.right + + # get the Site to the left of the left HE and to the right of + # the right HE which it bisects + bot = lbnd.leftreg(bottomsite) + top = rbnd.rightreg(bottomsite) + + # output the triple of sites, stating that a circle goes through them + mid = lbnd.rightreg(bottomsite) + context.outTriple(bot, top, mid) + + # get the vertex that caused this event and set the vertex number + # couldn't do this earlier since we didn't know when it would be processed + v = lbnd.vertex + siteList.setSiteNumber(v) + context.outVertex(v) + + # set the endpoint of the left and right Halfedge to be this vector + if lbnd.edge.setEndpoint(lbnd.pm, v): + context.outEdge(lbnd.edge) + + if rbnd.edge.setEndpoint(rbnd.pm, v): + context.outEdge(rbnd.edge) + + # delete the lowest HE, remove all vertex events to do with the + # right HE and delete the right HE + edgeList.delete(lbnd) + priorityQ.delete(rbnd) + edgeList.delete(rbnd) + + # if the site to the left of the event is higher than the Site + # to the right of it, then swap them and set 'pm' to RIGHT + pm = Edge.LE + if bot.y > top.y: + bot, top = top, bot + pm = Edge.RE + + # Create an Edge (or line) that is between the two Sites. This + # creates the formula of the line, and assigns a line number to it + edge = Edge.bisect(bot, top) + context.outBisector(edge) + + # create a HE from the edge + bisector = Halfedge(edge, pm) + + # insert the new bisector to the right of the left HE + # set one endpoint to the new edge to be the vector point 'v' + # If the site to the left of this bisector is higher than the right + # Site, then this endpoint is put in position 0; otherwise in pos 1 + edgeList.insert(llbnd, bisector) + if edge.setEndpoint(Edge.RE - pm, v): + context.outEdge(edge) + + # if left HE and the new bisector don't intersect, then delete + # the left HE, and reinsert it + p = llbnd.intersect(bisector) + if p is not None: + priorityQ.delete(llbnd) + priorityQ.insert(llbnd, p, bot.distance(p)) + + # if right HE and the new bisector don't intersect, then reinsert it + p = bisector.intersect(rrbnd) + if p is not None: + priorityQ.insert(bisector, p, bot.distance(p)) + else: + break + + he = edgeList.leftend.right + while he is not edgeList.rightend: + context.outEdge(he.edge) + he = he.right + Edge.EDGE_NUM = 0 # CF
+ + + +# ------------------------------------------------------------------ +
+[docs] +def isEqual(a, b, relativeError=TOLERANCE): + """Check if two values are nearly equal within a specified relative error. + + This function determines if the absolute difference between two values + is within a specified relative error of the larger of the two values. It + is useful for comparing floating-point numbers where precision issues + may arise. + + Args: + a (float): The first value to compare. + b (float): The second value to compare. + relativeError (float): The allowed relative error for the comparison. + + Returns: + bool: True if the values are considered nearly equal, False otherwise. + """ + # is nearly equal to within the allowed relative error + norm = max(abs(a), abs(b)) + return (norm < relativeError) or (abs(a - b) < (relativeError * norm))
+ + + +# ------------------------------------------------------------------ +
+[docs] +class Site(object): + def __init__(self, x=0.0, y=0.0, sitenum=0): + """Init function.""" +
+[docs] + self.x = x
+ +
+[docs] + self.y = y
+ +
+[docs] + self.sitenum = sitenum
+ + +
+[docs] + def dump(self): + """Dump the site information. + + This function prints the site number along with its x and y coordinates + in a formatted string. It is primarily used for debugging or logging + purposes to provide a quick overview of the site's attributes. + + Returns: + None: This function does not return any value. + """ + print("Site #%d (%g, %g)" % (self.sitenum, self.x, self.y))
+ + +
+[docs] + def __lt__(self, other): + """Compare two objects based on their coordinates. + + This method implements the less-than comparison for objects that have x + and y attributes. It first compares the y coordinates; if they are + equal, it then compares the x coordinates. The method returns True if + the current object is considered less than the other object based on + these comparisons. + + Args: + other (object): The object to compare against, which must have + x and y attributes. + + Returns: + bool: True if the current object is less than the other object, + otherwise False. + """ + if self.y < other.y: + return True + elif self.y > other.y: + return False + elif self.x < other.x: + return True + elif self.x > other.x: + return False + else: + return False
+ + +
+[docs] + def __eq__(self, other): + """Determine equality between two objects. + + This method checks if the current object is equal to another object by + comparing their 'x' and 'y' attributes. If both attributes are equal for + the two objects, it returns True; otherwise, it returns False. + + Args: + other (object): The object to compare with the current object. + + Returns: + bool: True if both objects are equal, False otherwise. + """ + if self.y == other.y and self.x == other.x: + return True
+ + +
+[docs] + def distance(self, other): + """Calculate the distance between two points in a 2D space. + + This function computes the Euclidean distance between the current point + (represented by the instance's coordinates) and another point provided + as an argument. It uses the Pythagorean theorem to calculate the + distance based on the differences in the x and y coordinates of the two + points. + + Args: + other (Point): Another point in 2D space to calculate the distance from. + + Returns: + float: The Euclidean distance between the two points. + """ + dx = self.x - other.x + dy = self.y - other.y + return math.sqrt(dx * dx + dy * dy)
+
+ + + +# ------------------------------------------------------------------ +
+[docs] +class Edge(object): +
+[docs] + LE = 0 # left end indice --> edge.ep[Edge.LE]
+ +
+[docs] + RE = 1 # right end indice
+ +
+[docs] + EDGE_NUM = 0
+ +
+[docs] + DELETED = {} # marker value
+ + + def __init__(self): + """Init function.""" +
+[docs] + self.a = 0.0 # equation of the line a*x+b*y = c
+ +
+[docs] + self.b = 0.0
+ +
+[docs] + self.c = 0.0
+ +
+[docs] + self.ep = [None, None] # end point (2 tuples of site)
+ +
+[docs] + self.reg = [None, None]
+ +
+[docs] + self.edgenum = 0
+ + +
+[docs] + def dump(self): + """Dump the current state of the object. + + This function prints the values of the object's attributes, including + the edge number, and the values of a, b, c, as well as the ep and reg + attributes. It is useful for debugging purposes to understand the + current state of the object. + + Attributes: + edgenum (int): The edge number of the object. + a (float): The value of attribute a. + b (float): The value of attribute b. + c (float): The value of attribute c. + ep: The value of the ep attribute. + reg: The value of the reg attribute. + """ + print("(#%d a=%g, b=%g, c=%g)" % (self.edgenum, self.a, self.b, self.c)) + print("ep", self.ep) + print("reg", self.reg)
+ + +
+[docs] + def setEndpoint(self, lrFlag, site): + """Set the endpoint for a given flag. + + This function assigns a site to the specified endpoint flag. It checks + if the corresponding endpoint for the opposite flag is not set to None. + If it is None, the function returns False; otherwise, it returns True. + + Args: + lrFlag (int): The flag indicating which endpoint to set. + site (str): The site to be assigned to the specified endpoint. + + Returns: + bool: True if the opposite endpoint is set, False otherwise. + """ + self.ep[lrFlag] = site + if self.ep[Edge.RE - lrFlag] is None: + return False + return True
+ + + @staticmethod +
+[docs] + def bisect(s1, s2): + """Bisect two sites to create a new edge. + + This function takes two site objects and computes the bisector edge + between them. It calculates the slope and intercept of the line that + bisects the two sites, storing the necessary parameters in a new edge + object. The edge is initialized with no endpoints, as it extends to + infinity. The function determines whether to fix x or y based on the + relative distances between the sites. + + Args: + s1 (Site): The first site to be bisected. + s2 (Site): The second site to be bisected. + + Returns: + Edge: A new edge object representing the bisector between the two sites. + """ + newedge = Edge() + newedge.reg[0] = s1 # store the sites that this edge is bisecting + newedge.reg[1] = s2 + + # to begin with, there are no endpoints on the bisector - it goes to infinity + # ep[0] and ep[1] are None + + # get the difference in x dist between the sites + dx = float(s2.x - s1.x) + dy = float(s2.y - s1.y) + adx = abs(dx) # make sure that the difference in positive + ady = abs(dy) + + # get the slope of the line + newedge.c = float(s1.x * dx + s1.y * dy + (dx * dx + dy * dy) * 0.5) + if adx > ady: + # set formula of line, with x fixed to 1 + newedge.a = 1.0 + newedge.b = dy / dx + newedge.c /= dx + else: + # set formula of line, with y fixed to 1 + newedge.b = 1.0 + newedge.a = dx / dy + newedge.c /= dy + + newedge.edgenum = Edge.EDGE_NUM + Edge.EDGE_NUM += 1 + return newedge
+
+ + + +# ------------------------------------------------------------------ +
+[docs] +class Halfedge(object): + def __init__(self, edge=None, pm=Edge.LE): + """Init function.""" +
+[docs] + self.left = None # left Halfedge in the edge list
+ +
+[docs] + self.right = None # right Halfedge in the edge list
+ +
+[docs] + self.qnext = None # priority queue linked list pointer
+ +
+[docs] + self.edge = edge # edge list Edge
+ +
+[docs] + self.pm = pm
+ +
+[docs] + self.vertex = None # Site()
+ +
+[docs] + self.ystar = BIG_FLOAT
+ + +
+[docs] + def dump(self): + """Dump the internal state of the object. + + This function prints the current values of the object's attributes, + including left, right, edge, pm, vertex, and ystar. If the vertex + attribute is present and has a dump method, it will call that method to + print the vertex's internal state. Otherwise, it will print "None" for + the vertex. + + Attributes: + left: The left halfedge associated with this object. + right: The right halfedge associated with this object. + edge: The edge associated with this object. + pm: The PM associated with this object. + vertex: The vertex associated with this object, which may have its + own dump method. + ystar: The ystar value associated with this object. + """ + print("Halfedge--------------------------") + print("Left: ", self.left) + print("Right: ", self.right) + print("Edge: ", self.edge) + print("PM: ", self.pm) + print("Vertex: "), + if self.vertex: + self.vertex.dump() + else: + print("None") + print("Ystar: ", self.ystar)
+ + +
+[docs] + def __lt__(self, other): + """Compare two objects based on their ystar and vertex attributes. + + This method implements the less-than comparison for objects. It first + compares the `ystar` attributes of the two objects. If they are equal, + it then compares the x-coordinate of their `vertex` attributes to + determine the order. + + Args: + other (YourClass): The object to compare against. + + Returns: + bool: True if the current object is less than the other object, False + otherwise. + """ + if self.ystar < other.ystar: + return True + elif self.ystar > other.ystar: + return False + elif self.vertex.x < other.vertex.x: + return True + elif self.vertex.x > other.vertex.x: + return False + else: + return False
+ + +
+[docs] + def __eq__(self, other): + """Check equality of two objects. + + This method compares the current object with another object to determine + if they are equal. It checks if the 'ystar' attribute and the 'x' + coordinate of the 'vertex' attribute are the same for both objects. + + Args: + other (object): The object to compare with the current instance. + + Returns: + bool: True if both objects are considered equal, False otherwise. + """ + if self.ystar == other.ystar and self.vertex.x == other.vertex.x: + return True
+ + +
+[docs] + def leftreg(self, default): + """Retrieve the left registration value based on the edge state. + + This function checks the state of the edge attribute. If the edge is not + set, it returns the provided default value. If the edge is set and its + property indicates a left edge (Edge.LE), it returns the left + registration value. Otherwise, it returns the right registration value. + + Args: + default: The value to return if the edge is not set. + + Returns: + The left registration value if applicable, otherwise the default value. + """ + if not self.edge: + return default + elif self.pm == Edge.LE: + return self.edge.reg[Edge.LE] + else: + return self.edge.reg[Edge.RE]
+ + +
+[docs] + def rightreg(self, default): + """Retrieve the appropriate registration value based on the edge state. + + This function checks if the current edge is set. If it is not set, it + returns the provided default value. If the edge is set and the current + state is Edge.LE, it returns the registration value associated with + Edge.RE. Otherwise, it returns the registration value associated with + Edge.LE. + + Args: + default: The value to return if there is no edge set. + + Returns: + The registration value corresponding to the current edge state or the + default value if no edge is set. + """ + if not self.edge: + return default + elif self.pm == Edge.LE: + return self.edge.reg[Edge.RE] + else: + return self.edge.reg[Edge.LE]
+ + + # returns True if p is to right of halfedge self +
+[docs] + def isPointRightOf(self, pt): + """Determine if a point is to the right of a half-edge. + + This function checks whether the given point `pt` is located to the + right of the half-edge represented by the current object. It takes into + account the position of the top site of the edge and various geometric + properties to make this determination. The function uses the edge's + parameters to evaluate the relationship between the point and the half- + edge. + + Args: + pt (Point): A point object with x and y coordinates. + + Returns: + bool: True if the point is to the right of the half-edge, False otherwise. + """ + e = self.edge + topsite = e.reg[1] + right_of_site = pt.x > topsite.x + + if right_of_site and self.pm == Edge.LE: + return True + + if not right_of_site and self.pm == Edge.RE: + return False + + if e.a == 1.0: + dyp = pt.y - topsite.y + dxp = pt.x - topsite.x + fast = 0 + if (not right_of_site and e.b < 0.0) or (right_of_site and e.b >= 0.0): + above = dyp >= e.b * dxp + fast = above + else: + above = pt.x + pt.y * e.b > e.c + if e.b < 0.0: + above = not above + if not above: + fast = 1 + if not fast: + dxs = topsite.x - (e.reg[0]).x + above = e.b * (dxp * dxp - dyp * dyp) < dxs * dyp * \ + (1.0 + 2.0 * dxp / dxs + e.b * e.b) + if e.b < 0.0: + above = not above + else: # e.b == 1.0 + yl = e.c - e.a * pt.x + t1 = pt.y - yl + t2 = pt.x - topsite.x + t3 = yl - topsite.y + above = t1 * t1 > t2 * t2 + t3 * t3 + + if self.pm == Edge.LE: + return above + else: + return not above
+ + + # -------------------------- + # create a new site where the Halfedges el1 and el2 intersect +
+[docs] + def intersect(self, other): + """Create a new site where two edges intersect. + + This function calculates the intersection point of two edges, + represented by the current instance and another instance passed as an + argument. It first checks if either edge is None, and if they belong to + the same parent region. If the edges are parallel or do not intersect, + it returns None. If an intersection point is found, it creates and + returns a new Site object at the intersection coordinates. + + Args: + other (Edge): Another edge to intersect with the current edge. + + Returns: + Site or None: A Site object representing the intersection point + if an intersection occurs; otherwise, None. + """ + e1 = self.edge + e2 = other.edge + if (e1 is None) or (e2 is None): + return None + + # if the two edges bisect the same parent return None + if e1.reg[1] is e2.reg[1]: + return None + + d = e1.a * e2.b - e1.b * e2.a + if isEqual(d, 0.0): + return None + + xint = (e1.c * e2.b - e2.c * e1.b) / d + yint = (e2.c * e1.a - e1.c * e2.a) / d + if e1.reg[1] < e2.reg[1]: + he = self + e = e1 + else: + he = other + e = e2 + + rightOfSite = xint >= e.reg[1].x + if ((rightOfSite and he.pm == Edge.LE) or + (not rightOfSite and he.pm == Edge.RE)): + return None + + # create a new site at the point of intersection - this is a new + # vector event waiting to happen + return Site(xint, yint)
+
+ + + +# ------------------------------------------------------------------ +
+[docs] +class EdgeList(object): + def __init__(self, xmin, xmax, nsites): + """Init function.""" + if xmin > xmax: + xmin, xmax = xmax, xmin +
+[docs] + self.hashsize = int(2 * math.sqrt(nsites + 4))
+ + +
+[docs] + self.xmin = xmin
+ +
+[docs] + self.deltax = float(xmax - xmin)
+ +
+[docs] + self.hash = [None] * self.hashsize
+ + +
+[docs] + self.leftend = Halfedge()
+ +
+[docs] + self.rightend = Halfedge()
+ +
+[docs] + self.leftend.right = self.rightend
+ +
+[docs] + self.rightend.left = self.leftend
+ + self.hash[0] = self.leftend + self.hash[-1] = self.rightend + +
+[docs] + def insert(self, left, he): + """Insert a node into a doubly linked list. + + This function takes a node and inserts it into the list immediately + after the specified left node. It updates the pointers of the + surrounding nodes to maintain the integrity of the doubly linked list. + + Args: + left (Node): The node after which the new node will be inserted. + he (Node): The new node to be inserted into the list. + """ + he.left = left + he.right = left.right + left.right.left = he + left.right = he
+ + +
+[docs] + def delete(self, he): + """Delete a node from a doubly linked list. + + This function updates the pointers of the neighboring nodes to remove + the specified node from the list. It also marks the node as deleted by + setting its edge attribute to Edge.DELETED. + + Args: + he (Node): The node to be deleted from the list. + """ + he.left.right = he.right + he.right.left = he.left + he.edge = Edge.DELETED
+ + + # Get entry from hash table, pruning any deleted nodes +
+[docs] + def gethash(self, b): + """Retrieve an entry from the hash table, ignoring deleted nodes. + + This function checks if the provided index is within the valid range of + the hash table. If the index is valid, it retrieves the corresponding + entry. If the entry is marked as deleted, it updates the hash table to + remove the reference to the deleted entry and returns None. + + Args: + b (int): The index in the hash table to retrieve the entry from. + + Returns: + object: The entry at the specified index, or None if the index is out of bounds + or if the entry is marked as deleted. + """ + if (b < 0 or b >= self.hashsize): + return None + he = self.hash[b] + if he is None or he.edge is not Edge.DELETED: + return he + + # Hash table points to deleted half edge. Patch as necessary. + self.hash[b] = None + return None
+ + +
+[docs] + def leftbnd(self, pt): + """Find the left boundary half-edge for a given point. + + This function computes the appropriate half-edge that is to the left of + the specified point. It utilizes a hash table to quickly locate the + half-edge that is closest to the desired position based on the + x-coordinate of the point. If the initial bucket derived from the + point's x-coordinate does not contain a valid half-edge, the function + will search adjacent buckets until it finds one. Once a half-edge is + located, it will traverse through the linked list of half-edges to find + the correct one that lies to the left of the point. + + Args: + pt (Point): A point object containing x and y coordinates. + + Returns: + HalfEdge: The half-edge that is to the left of the given point. + """ + # Use hash table to get close to desired halfedge + bucket = int(((pt.x - self.xmin) / self.deltax * self.hashsize)) + + if bucket < 0: + bucket = 0 + + if bucket >= self.hashsize: + bucket = self.hashsize - 1 + + he = self.gethash(bucket) + if (he is None): + i = 1 + while True: + he = self.gethash(bucket - i) + if (he is not None): + break + he = self.gethash(bucket + i) + if (he is not None): + break + i += 1 + + # Now search linear list of halfedges for the corect one + if (he is self.leftend) or (he is not self.rightend and he.isPointRightOf(pt)): + he = he.right + while he is not self.rightend and he.isPointRightOf(pt): + he = he.right + he = he.left + else: + he = he.left + while he is not self.leftend and not he.isPointRightOf(pt): + he = he.left + + # Update hash table and reference counts + if bucket > 0 and bucket < self.hashsize - 1: + self.hash[bucket] = he + return he
+
+ + + +# ------------------------------------------------------------------ +
+[docs] +class PriorityQueue(object): + def __init__(self, ymin, ymax, nsites): + """Init function.""" +
+[docs] + self.ymin = ymin
+ +
+[docs] + self.deltay = ymax - ymin
+ +
+[docs] + self.hashsize = int(4 * math.sqrt(nsites))
+ +
+[docs] + self.count = 0
+ +
+[docs] + self.minidx = 0
+ +
+[docs] + self.hash = []
+ + for i in range(self.hashsize): + self.hash.append(Halfedge()) + +
+[docs] + def __len__(self): + """Return the length of the object. + + This method returns the count of items in the object, which is useful + for determining how many elements are present. It is typically used to + support the built-in `len()` function. + + Returns: + int: The number of items in the object. + """ + return self.count
+ + +
+[docs] + def isEmpty(self): + """Check if the object is empty. + + This method determines whether the object contains any elements by + checking the value of the count attribute. If the count is zero, the + object is considered empty; otherwise, it is not. + + Returns: + bool: True if the object is empty, False otherwise. + """ + return self.count == 0
+ + +
+[docs] + def insert(self, he, site, offset): + """Insert a new element into the data structure. + + This function inserts a new element represented by `he` into the + appropriate position in the data structure based on its value. It + updates the `ystar` attribute of the element and links it to the next + element in the list. The function also manages the count of elements in + the structure. + + Args: + he (Element): The element to be inserted, which contains a vertex and + a y-coordinate. + site (Site): The site object that provides the y-coordinate for the + insertion. + offset (float): The offset to be added to the y-coordinate of the site. + + Returns: + None: This function does not return a value. + """ + he.vertex = site + he.ystar = site.y + offset + last = self.hash[self.getBucket(he)] + next = last.qnext + while (next is not None) and he > next: + last = next + next = last.qnext + he.qnext = last.qnext + last.qnext = he + self.count += 1
+ + +
+[docs] + def delete(self, he): + """Delete a specified element from the data structure. + + This function removes the specified element (he) from the linked list + associated with the corresponding bucket in the hash table. It traverses + the linked list until it finds the element to delete, updates the + pointers to bypass the deleted element, and decrements the count of + elements in the structure. If the element is found and deleted, its + vertex is set to None to indicate that it is no longer valid. + + Args: + he (Element): The element to be deleted from the data structure. + """ + if he.vertex is not None: + last = self.hash[self.getBucket(he)] + while last.qnext is not he: + last = last.qnext + last.qnext = he.qnext + self.count -= 1 + he.vertex = None
+ + +
+[docs] + def getBucket(self, he): + """Get the appropriate bucket index for a given value. + + This function calculates the bucket index based on the provided value + and the object's parameters. It ensures that the bucket index is within + the valid range, adjusting it if necessary. The calculation is based on + the difference between a specified value and a minimum value, scaled by + a delta value and the size of the hash table. The function also updates + the minimum index if the calculated bucket is lower than the current + minimum index. + + Args: + he: An object that contains the attribute `ystar`, which is used + in the bucket calculation. + + Returns: + int: The calculated bucket index, constrained within the valid range. + """ + bucket = int(((he.ystar - self.ymin) / self.deltay) * self.hashsize) + if bucket < 0: + bucket = 0 + if bucket >= self.hashsize: + bucket = self.hashsize - 1 + if bucket < self.minidx: + self.minidx = bucket + return bucket
+ + +
+[docs] + def getMinPt(self): + """Retrieve the minimum point from a hash table. + + This function iterates through the hash table starting from the current + minimum index and finds the next non-null entry. It then extracts the + coordinates (x, y) of the vertex associated with that entry and returns + it as a Site object. + + Returns: + Site: An object representing the minimum point with x and y coordinates. + """ + while self.hash[self.minidx].qnext is None: + self.minidx += 1 + he = self.hash[self.minidx].qnext + x = he.vertex.x + y = he.ystar + return Site(x, y)
+ + +
+[docs] + def popMinHalfedge(self): + """Remove and return the minimum half-edge from the data structure. + + This function retrieves the minimum half-edge from a hash table, updates + the necessary pointers to maintain the integrity of the data structure, + and decrements the count of half-edges. It effectively removes the + minimum half-edge while ensuring that the next half-edge in the sequence + is correctly linked. + + Returns: + HalfEdge: The minimum half-edge that was removed from the data structure. + """ + curr = self.hash[self.minidx].qnext + self.hash[self.minidx].qnext = curr.qnext + self.count -= 1 + return curr
+
+ + + +# ------------------------------------------------------------------ +
+[docs] +class SiteList(object): + def __init__(self, pointList): + """Init function.""" +
+[docs] + self.__sites = []
+ +
+[docs] + self.__sitenum = 0
+ + +
+[docs] + self.__xmin = min([pt.x for pt in pointList])
+ +
+[docs] + self.__ymin = min([pt.y for pt in pointList])
+ +
+[docs] + self.__xmax = max([pt.x for pt in pointList])
+ +
+[docs] + self.__ymax = max([pt.y for pt in pointList])
+ +
+[docs] + self.__extent = (self.__xmin, self.__xmax, self.__ymin, self.__ymax)
+ + + for i, pt in enumerate(pointList): + self.__sites.append(Site(pt.x, pt.y, i)) + self.__sites.sort() + +
+[docs] + def setSiteNumber(self, site): + """Set the site number for a given site. + + This function assigns a unique site number to the provided site object. + It updates the site object's 'sitenum' attribute with the current value + of the instance's private '__sitenum' attribute and then increments the + '__sitenum' for the next site. + + Args: + site (object): An object representing a site that has a 'sitenum' attribute. + + Returns: + None: This function does not return a value. + """ + site.sitenum = self.__sitenum + self.__sitenum += 1
+ + +
+[docs] + class Iterator(object): + def __init__(this, lst): + """Init function.""" +
+[docs] + this.generator = (s for s in lst)
+ + +
+[docs] + def __iter__(this): + """Return the iterator object itself. + + This method is part of the iterator protocol. It allows an object to be + iterable by returning the iterator object itself when the `__iter__` + method is called. This is typically used in conjunction with the + `__next__` method to iterate over the elements of the object. + + Returns: + self: The iterator object itself. + """ + return this
+ + +
+[docs] + def next(this): + """Retrieve the next item from a generator. + + This function attempts to get the next value from the provided + generator. It handles both Python 2 and Python 3 syntax for retrieving + the next item. If the generator is exhausted, it returns None instead of + raising an exception. + + Args: + this (object): An object that contains a generator attribute. + + Returns: + object: The next item from the generator, or None if the generator is exhausted. + """ + try: + if PY3: + return this.generator.__next__() + else: + return this.generator.next() + except StopIteration: + return None
+
+ + +
+[docs] + def iterator(self): + """Create an iterator for the sites. + + This function returns an iterator object that allows iteration over the + collection of sites stored in the instance. It utilizes the + SiteList.Iterator class to facilitate the iteration process. + + Returns: + Iterator: An iterator for the sites in the SiteList. + """ + return SiteList.Iterator(self.__sites)
+ + +
+[docs] + def __iter__(self): + """Iterate over the sites in the SiteList. + + This method returns an iterator for the SiteList, allowing for traversal + of the contained sites. It utilizes the internal Iterator class to + manage the iteration process. + + Returns: + Iterator: An iterator for the sites in the SiteList. + """ + return SiteList.Iterator(self.__sites)
+ + +
+[docs] + def __len__(self): + """Return the number of sites. + + This method returns the length of the internal list of sites. It is used + to determine how many sites are currently stored in the object. The + length is calculated using the built-in `len()` function on the + `__sites` attribute. + + Returns: + int: The number of sites in the object. + """ + return len(self.__sites)
+ + +
+[docs] + def _getxmin(self): + """Retrieve the minimum x-coordinate value. + + This function accesses and returns the private attribute __xmin, which + holds the minimum x-coordinate value for the object. It is typically + used in contexts where the minimum x value is needed for calculations or + comparisons. + + Returns: + float: The minimum x-coordinate value. + """ + return self.__xmin
+ + +
+[docs] + def _getymin(self): + """Retrieve the minimum y-coordinate value. + + This function returns the minimum y-coordinate value stored in the + instance variable `__ymin`. It is typically used in contexts where the + minimum y-value is needed for calculations or comparisons. + + Returns: + float: The minimum y-coordinate value. + """ + return self.__ymin
+ + +
+[docs] + def _getxmax(self): + """Retrieve the maximum x value. + + This function returns the maximum x value stored in the instance. It is + a private method intended for internal use within the class and provides + access to the __xmax attribute. + + Returns: + float: The maximum x value. + """ + return self.__xmax
+ + +
+[docs] + def _getymax(self): + """Retrieve the maximum y-coordinate value. + + This function accesses and returns the private attribute __ymax, which + represents the maximum y-coordinate value stored in the instance. + + Returns: + float: The maximum y-coordinate value. + """ + return self.__ymax
+ + +
+[docs] + def _getextent(self): + """Retrieve the extent of the object. + + This function returns the current extent of the object, which is + typically a representation of its boundaries or limits. The extent is + stored as a private attribute and can be used for various purposes such + as rendering, collision detection, or spatial analysis. + + Returns: + The extent of the object, which may be in a specific format depending + on the implementation (e.g., a tuple, list, or custom object). + """ + return self.__extent
+ + +
+[docs] + xmin = property(_getxmin)
+ +
+[docs] + ymin = property(_getymin)
+ +
+[docs] + xmax = property(_getxmax)
+ +
+[docs] + ymax = property(_getymax)
+ +
+[docs] + extent = property(_getextent)
+
+ + + +# ------------------------------------------------------------------ +
+[docs] +def computeVoronoiDiagram(points, xBuff=0, yBuff=0, polygonsOutput=False, formatOutput=False, closePoly=True): + """Compute the Voronoi diagram for a set of points. + + This function takes a list of point objects and computes the Voronoi + diagram, which partitions the plane into regions based on the distance + to the input points. The function allows for optional buffering of the + bounding box and can return various formats of the output, including + edges or polygons of the Voronoi diagram. + + Args: + points (list): A list of point objects, each having 'x' and 'y' attributes. + xBuff (float?): The expansion percentage of the bounding box in the x-direction. + Defaults to 0. + yBuff (float?): The expansion percentage of the bounding box in the y-direction. + Defaults to 0. + polygonsOutput (bool?): If True, returns polygons instead of edges. Defaults to False. + formatOutput (bool?): If True, formats the output to include vertex coordinates. Defaults to + False. + closePoly (bool?): If True, closes the polygons by repeating the first point at the end. + Defaults to True. + + Returns: + If `polygonsOutput` is False: + - list: A list of 2-tuples representing the edges of the Voronoi + diagram, + where each tuple contains the x and y coordinates of the points. + If `formatOutput` is True: + - tuple: A list of 2-tuples for vertex coordinates and a list of edges + indices. + If `polygonsOutput` is True: + - dict: A dictionary where keys are indices of input points and values + are n-tuples + representing the vertices of each Voronoi polygon. + If `formatOutput` is True: + - tuple: A list of 2-tuples for vertex coordinates and a dictionary of + polygon vertex indices. + """ + siteList = SiteList(points) + context = Context() + voronoi(siteList, context) + context.setClipBuffer(xBuff, yBuff) + if not polygonsOutput: + clipEdges = context.getClipEdges() + if formatOutput: + vertices, edgesIdx = formatEdgesOutput(clipEdges) + return vertices, edgesIdx + else: + return clipEdges + else: + clipPolygons = context.getClipPolygons(closePoly) + if formatOutput: + vertices, polyIdx = formatPolygonsOutput(clipPolygons) + return vertices, polyIdx + else: + return clipPolygons
+ + + +
+[docs] +def formatEdgesOutput(edges): + """Format edges output for a list of edges. + + This function takes a list of edges, where each edge is represented as a + tuple of points. It extracts unique points from the edges and creates a + mapping of these points to their corresponding indices. The function + then returns a list of unique points and a list of edges represented by + their indices. + + Args: + edges (list): A list of edges, where each edge is a tuple containing points. + + Returns: + tuple: A tuple containing: + - list: A list of unique points extracted from the edges. + - list: A list of edges represented by their corresponding indices. + """ + # get list of points + pts = [] + for edge in edges: + pts.extend(edge) + # get unique values + pts = set(pts) # unique values (tuples are hashable) + # get dict {values:index} + valuesIdxDict = dict(zip(pts, range(len(pts)))) + # get edges index reference + edgesIdx = [] + for edge in edges: + edgesIdx.append([valuesIdxDict[pt] for pt in edge]) + return list(pts), edgesIdx
+ + + +
+[docs] +def formatPolygonsOutput(polygons): + """Format the output of polygons into a standardized structure. + + This function takes a dictionary of polygons, where each polygon is + represented as a list of points. It extracts unique points from all + polygons and creates an index mapping for these points. The output + consists of a list of unique points and a dictionary that maps each + polygon's original indices to their corresponding indices in the unique + points list. + + Args: + polygons (dict): A dictionary where keys are polygon identifiers and values + are lists of points (tuples) representing the vertices of + the polygons. + + Returns: + tuple: A tuple containing: + - list: A list of unique points (tuples) extracted from the input + polygons. + - dict: A dictionary mapping each polygon's identifier to a list of + indices + corresponding to the unique points. + """ + # get list of points + pts = [] + for poly in polygons.values(): + pts.extend(poly) + # get unique values + pts = set(pts) # unique values (tuples are hashable) + # get dict {values:index} + valuesIdxDict = dict(zip(pts, range(len(pts)))) + # get polygons index reference + polygonsIdx = {} + for inPtsIdx, poly in polygons.items(): + polygonsIdx[inPtsIdx] = [valuesIdxDict[pt] for pt in poly] + return list(pts), polygonsIdx
+ + + +# ------------------------------------------------------------------ +
+[docs] +def computeDelaunayTriangulation(points): + """Compute the Delaunay triangulation for a set of points. + + This function takes a list of point objects, each of which must have 'x' + and 'y' fields. It computes the Delaunay triangulation and returns a + list of 3-tuples, where each tuple contains the indices of the points + that form a Delaunay triangle. The triangulation is performed using the + Voronoi diagram method. + + Args: + points (list): A list of point objects with 'x' and 'y' attributes. + + Returns: + list: A list of 3-tuples representing the indices of points that + form Delaunay triangles. + """ + siteList = SiteList(points) + context = Context() + context.triangulate = True + voronoi(siteList, context) + return context.triangles
+ + +# ----------------------------------------------------------------------------- +# def shapely_voronoi(amount): +# import random +# +# rcoord = [] +# x = 0 +# while x < self.amount: +# rcoord.append((width * random.random(), height * random.random(), 0.02 * random.random())) +# x += 1 +# +# points = MultiPoint(rcoord) +# voronoi = shapely.ops.voronoi_diagram(points, tolerance=0, edges=False) +# +# utils.shapelyToCurve('voronoi', voronoi, 0) +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 000000000..4ffd410ae --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,440 @@ + + + + + + + + + + Overview: module code — BlenderCAM 1.0.38 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

All modules for which code is available

+ + +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_sources/autoapi/cam/autoupdate/index.rst b/_sources/autoapi/cam/autoupdate/index.rst new file mode 100644 index 000000000..54052dea2 --- /dev/null +++ b/_sources/autoapi/cam/autoupdate/index.rst @@ -0,0 +1,99 @@ +cam.autoupdate +============== + +.. py:module:: cam.autoupdate + +.. autoapi-nested-parse:: + + CNC CAM 'autoupdate.py' + + Classes to check for, download and install CNC CAM updates. + + + +Classes +------- + +.. autoapisummary:: + + cam.autoupdate.UpdateChecker + cam.autoupdate.Updater + cam.autoupdate.UpdateSourceOperator + + +Module Contents +--------------- + +.. py:class:: UpdateChecker + + Bases: :py:obj:`bpy.types.Operator` + + + Check for Updates + + + .. py:attribute:: bl_idname + :value: 'render.cam_check_updates' + + + + .. py:attribute:: bl_label + :value: 'Check for Updates in CNC CAM Plugin' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + +.. py:class:: Updater + + Bases: :py:obj:`bpy.types.Operator` + + + Update to Newer Version if Possible + + + .. py:attribute:: bl_idname + :value: 'render.cam_update_now' + + + + .. py:attribute:: bl_label + :value: 'Update' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + + .. py:method:: install_zip_from_url(zip_url) + + +.. py:class:: UpdateSourceOperator + + Bases: :py:obj:`bpy.types.Operator` + + + .. py:attribute:: bl_idname + :value: 'render.cam_set_update_source' + + + + .. py:attribute:: bl_label + :value: 'Set CNC CAM Update Source' + + + + .. py:attribute:: new_source + :type: StringProperty(default='') + + + .. py:method:: execute(context) + + diff --git a/_sources/autoapi/cam/basrelief/index.rst b/_sources/autoapi/cam/basrelief/index.rst new file mode 100644 index 000000000..c136a3751 --- /dev/null +++ b/_sources/autoapi/cam/basrelief/index.rst @@ -0,0 +1,893 @@ +cam.basrelief +============= + +.. py:module:: cam.basrelief + +.. autoapi-nested-parse:: + + CNC CAM 'basrelief.py' + + Module to allow the creation of reliefs from Images or View Layers. + (https://en.wikipedia.org/wiki/Relief#Bas-relief_or_low_relief) + + + +Attributes +---------- + +.. autoapisummary:: + + cam.basrelief.EPS + cam.basrelief.PRECISION + cam.basrelief.NUMPYALG + + +Exceptions +---------- + +.. autoapisummary:: + + cam.basrelief.ReliefError + + +Classes +------- + +.. autoapisummary:: + + cam.basrelief.BasReliefsettings + cam.basrelief.BASRELIEF_Panel + cam.basrelief.DoBasRelief + cam.basrelief.ProblemAreas + + +Functions +--------- + +.. autoapisummary:: + + cam.basrelief.copy_compbuf_data + cam.basrelief.restrictbuf + cam.basrelief.prolongate + cam.basrelief.idx + cam.basrelief.smooth + cam.basrelief.calculate_defect + cam.basrelief.add_correction + cam.basrelief.solve_pde_multigrid + cam.basrelief.asolve + cam.basrelief.atimes + cam.basrelief.snrm + cam.basrelief.linbcg + cam.basrelief.numpysave + cam.basrelief.numpytoimage + cam.basrelief.imagetonumpy + cam.basrelief.tonemap + cam.basrelief.vert + cam.basrelief.buildMesh + cam.basrelief.renderScene + cam.basrelief.problemAreas + cam.basrelief.relief + cam.basrelief.get_panels + cam.basrelief.register + cam.basrelief.unregister + + +Module Contents +--------------- + +.. py:data:: EPS + :value: 1e-32 + + +.. py:data:: PRECISION + :value: 5 + + +.. py:data:: NUMPYALG + :value: False + + +.. py:function:: copy_compbuf_data(inbuf, outbuf) + +.. py:function:: restrictbuf(inbuf, outbuf) + + Restrict the resolution of an input buffer to match an output buffer. + + This function scales down the input buffer `inbuf` to fit the dimensions + of the output buffer `outbuf`. It computes the average of the + neighboring pixels in the input buffer to create a downsampled version + in the output buffer. The method used for downsampling can vary based on + the dimensions of the input and output buffers, utilizing either a + simple averaging method or a more complex numpy-based approach. + + :param inbuf: The input buffer to be downsampled, expected to be + a 2D array. + :type inbuf: numpy.ndarray + :param outbuf: The output buffer where the downsampled result will + be stored, also expected to be a 2D array. + :type outbuf: numpy.ndarray + + :returns: The function modifies `outbuf` in place. + :rtype: None + + +.. py:function:: prolongate(inbuf, outbuf) + + Prolongate an input buffer to a larger output buffer. + + This function takes an input buffer and enlarges it to fit the + dimensions of the output buffer. It uses different methods to achieve + this based on the scaling factors derived from the input and output + dimensions. The function can handle specific cases where the scaling + factors are exactly 0.5, as well as a general case that applies a + bilinear interpolation technique for resizing. + + :param inbuf: The input buffer to be enlarged, expected to be a 2D array. + :type inbuf: numpy.ndarray + :param outbuf: The output buffer where the enlarged data will be stored, + expected to be a 2D array of larger dimensions than inbuf. + :type outbuf: numpy.ndarray + + +.. py:function:: idx(r, c, cols) + +.. py:function:: smooth(U, F, linbcgiterations, planar) + + Smooth a matrix U using a filter F at a specified level. + + This function applies a smoothing operation on the input matrix U using + the filter F. It utilizes the linear Biconjugate Gradient method for the + smoothing process. The number of iterations for the linear BCG method is + specified by linbcgiterations, and the planar parameter indicates + whether the operation is to be performed in a planar manner. + + :param U: The input matrix to be smoothed. + :type U: numpy.ndarray + :param F: The filter used for smoothing. + :type F: numpy.ndarray + :param linbcgiterations: The number of iterations for the linear BCG method. + :type linbcgiterations: int + :param planar: A flag indicating whether to perform the operation in a planar manner. + :type planar: bool + + :returns: This function modifies the input matrix U in place. + :rtype: None + + +.. py:function:: calculate_defect(D, U, F) + + Calculate the defect of a grid based on the input fields. + + This function computes the defect values for a grid by comparing the + input field `F` with the values in the grid `U`. The defect is + calculated using finite difference approximations, taking into account + the neighboring values in the grid. The results are stored in the output + array `D`, which is modified in place. + + :param D: A 2D array where the defect values will be stored. + :type D: ndarray + :param U: A 2D array representing the current state of the grid. + :type U: ndarray + :param F: A 2D array representing the target field to compare against. + :type F: ndarray + + :returns: + + The function modifies the array `D` in place and does not return a + value. + :rtype: None + + +.. py:function:: add_correction(U, C) + +.. py:function:: solve_pde_multigrid(F, U, vcycleiterations, linbcgiterations, smoothiterations, mins, levels, useplanar, planar) + + Solve a partial differential equation using a multigrid method. + + This function implements a multigrid algorithm to solve a given partial + differential equation (PDE). It operates on a grid of varying + resolutions, applying smoothing and correction steps iteratively to + converge towards the solution. The algorithm consists of several key + phases: restriction of the right-hand side to coarser grids, solving on + the coarsest grid, and then interpolating corrections back to finer + grids. The process is repeated for a specified number of V-cycle + iterations. + + :param F: The right-hand side of the PDE represented as a 2D array. + :type F: numpy.ndarray + :param U: The initial guess for the solution, which will be updated in place. + :type U: numpy.ndarray + :param vcycleiterations: The number of V-cycle iterations to perform. + :type vcycleiterations: int + :param linbcgiterations: The number of iterations for the linear solver used in smoothing. + :type linbcgiterations: int + :param smoothiterations: The number of smoothing iterations to apply at each level. + :type smoothiterations: int + :param mins: Minimum grid size (not used in the current implementation). + :type mins: int + :param levels: The number of levels in the multigrid hierarchy. + :type levels: int + :param useplanar: A flag indicating whether to use planar information during the solution + process. + :type useplanar: bool + :param planar: A 2D array indicating planar information for the grid. + :type planar: numpy.ndarray + + :returns: + + The function modifies the input array U in place to contain the final + solution. + :rtype: None + + .. note:: + + The function assumes that the input arrays F and U have compatible + shapes + and that the planar array is appropriately defined for the problem + context. + + +.. py:function:: asolve(b, x) + +.. py:function:: atimes(x, res) + + Apply a discrete Laplacian operator to a 2D array. + + This function computes the discrete Laplacian of a given 2D array `x` + and stores the result in the `res` array. The Laplacian is calculated + using finite difference methods, which involve summing the values of + neighboring elements and applying specific boundary conditions for the + edges and corners of the array. + + :param x: A 2D array representing the input values. + :type x: numpy.ndarray + :param res: A 2D array where the result will be stored. It must have the same shape + as `x`. + :type res: numpy.ndarray + + :returns: The result is stored directly in the `res` array. + :rtype: None + + +.. py:function:: snrm(n, sx, itol) + + Calculate the square root of the sum of squares or the maximum absolute + value. + + This function computes a value based on the input parameters. If the + tolerance level (itol) is less than or equal to 3, it calculates the + square root of the sum of squares of the input array (sx). If the + tolerance level is greater than 3, it returns the maximum absolute value + from the input array. + + :param n: An integer parameter, though it is not used in the current + implementation. + :type n: int + :param sx: A numpy array of numeric values. + :type sx: numpy.ndarray + :param itol: An integer that determines which calculation to perform. + :type itol: int + + :returns: + + The square root of the sum of squares if itol <= 3, otherwise the + maximum absolute value. + :rtype: float + + +.. py:function:: linbcg(n, b, x, itol, tol, itmax, iter, err, rows, cols, planar) + + Solve a linear system using the Biconjugate Gradient Method. + + This function implements the Biconjugate Gradient Method as described in + Numerical Recipes in C. It iteratively refines the solution to a linear + system of equations defined by the matrix-vector product. The method is + particularly useful for large, sparse systems where direct methods are + inefficient. The function takes various parameters to control the + iteration process and convergence criteria. + + :param n: The size of the linear system. + :type n: int + :param b: The right-hand side vector of the linear system. + :type b: numpy.ndarray + :param x: The initial guess for the solution vector. + :type x: numpy.ndarray + :param itol: The type of norm to use for convergence checks. + :type itol: int + :param tol: The tolerance for convergence. + :type tol: float + :param itmax: The maximum number of iterations allowed. + :type itmax: int + :param iter: The current iteration count (should be initialized to 0). + :type iter: int + :param err: The error estimate (should be initialized). + :type err: float + :param rows: The number of rows in the matrix. + :type rows: int + :param cols: The number of columns in the matrix. + :type cols: int + :param planar: A flag indicating if the problem is planar. + :type planar: bool + + :returns: The solution is stored in the input array `x`. + :rtype: None + + +.. py:function:: numpysave(a, iname) + + Save a NumPy array as an image file in OpenEXR format. + + This function takes a NumPy array and saves it as an image file using + Blender's rendering capabilities. It configures the image settings to + use the OpenEXR format with black and white color mode and a color depth + of 32 bits. The rendered image is saved to the specified filename. + + :param a: The NumPy array to be saved as an image. + :type a: numpy.ndarray + :param iname: The filename (including path) where the image will be saved. + :type iname: str + + +.. py:function:: numpytoimage(a, iname) + + Convert a NumPy array to a Blender image. + + This function takes a NumPy array and converts it into a Blender image. + It first checks if an image with the specified name and dimensions + already exists in Blender. If it does, that image is used; otherwise, a + new image is created with the specified name and dimensions. The + function then reshapes the NumPy array to match the image format and + assigns the pixel data to the image. + + :param a: A 2D NumPy array representing the pixel data of the image. + :type a: numpy.ndarray + :param iname: The name to assign to the Blender image. + :type iname: str + + :returns: + + The Blender image created or modified with the pixel data from the NumPy + array. + :rtype: bpy.types.Image + + +.. py:function:: imagetonumpy(i) + + Convert an image to a NumPy array. + + This function takes an image object and converts its pixel data into a + NumPy array. It first retrieves the pixel data from the image, then + reshapes and rearranges it to match the image's dimensions. The + resulting array is structured such that the height and width of the + image are preserved, and the color channels are appropriately ordered. + + :param i: An image object that contains pixel data. + :type i: Image + + :returns: A 2D NumPy array representing the pixel data of the image. + :rtype: numpy.ndarray + + .. note:: + + The function optimizes performance by directly accessing pixel data + instead of using slower methods. + + +.. py:function:: tonemap(i, exponent) + + Apply tone mapping to an image array. + + This function performs tone mapping on the input image array by first + filtering out values that are excessively high, which may indicate that + the depth buffer was not written correctly. It then normalizes the + values between the minimum and maximum heights, and finally applies an + exponentiation to adjust the brightness of the image. + + :param i: A numpy array representing the image data. + :type i: numpy.ndarray + :param exponent: The exponent used for adjusting the brightness + of the normalized image. + :type exponent: float + + :returns: The function modifies the input array in place. + :rtype: None + + +.. py:function:: vert(column, row, z, XYscaling, Zscaling) + + Create a single vertex in 3D space. + + This function calculates the 3D coordinates of a vertex based on the + provided column and row values, as well as scaling factors for the X-Y + and Z dimensions. The resulting coordinates are scaled accordingly to + fit within a specified 3D space. + + :param column: The column value representing the X coordinate. + :type column: float + :param row: The row value representing the Y coordinate. + :type row: float + :param z: The Z coordinate value. + :type z: float + :param XYscaling: The scaling factor for the X and Y coordinates. + :type XYscaling: float + :param Zscaling: The scaling factor for the Z coordinate. + :type Zscaling: float + + :returns: A tuple containing the scaled X, Y, and Z coordinates. + :rtype: tuple + + +.. py:function:: buildMesh(mesh_z, br) + + Build a 3D mesh from a height map and apply transformations. + + This function constructs a 3D mesh based on the provided height map + (mesh_z) and applies various transformations such as scaling and + positioning based on the parameters defined in the br object. It first + removes any existing BasReliefMesh objects from the scene, then creates + a new mesh from the height data, and finally applies decimation if the + specified ratio is within acceptable limits. + + :param mesh_z: A 2D array representing the height values + for the mesh vertices. + :type mesh_z: numpy.ndarray + :param br: An object containing properties for width, height, + thickness, justification, and decimation ratio. + :type br: object + + +.. py:function:: renderScene(width, height, bit_diameter, passes_per_radius, make_nodes, view_layer) + + Render a scene using Blender's Cycles engine. + + This function switches the rendering engine to Cycles, sets up the + necessary nodes for depth rendering if specified, and configures the + render resolution based on the provided parameters. It ensures that the + scene is in object mode before rendering and restores the original + rendering engine after the process is complete. + + :param width: The width of the render in pixels. + :type width: int + :param height: The height of the render in pixels. + :type height: int + :param bit_diameter: The diameter used to calculate the number of passes. + :type bit_diameter: float + :param passes_per_radius: The number of passes per radius for rendering. + :type passes_per_radius: int + :param make_nodes: A flag indicating whether to create render nodes. + :type make_nodes: bool + :param view_layer: The name of the view layer to be rendered. + :type view_layer: str + + :returns: This function does not return any value. + :rtype: None + + +.. py:function:: problemAreas(br) + + Process image data to identify problem areas based on silhouette + thresholds. + + This function analyzes an image and computes gradients to detect and + recover silhouettes based on specified parameters. It utilizes various + settings from the provided `br` object to adjust the processing, + including silhouette thresholds, scaling factors, and iterations for + smoothing and recovery. The function also handles image scaling and + applies a gradient mask if specified. The resulting data is then + converted back into an image format for further use. + + :param br: An object containing various parameters for processing, including: + - use_image_source (bool): Flag to determine if a specific image source + should be used. + - source_image_name (str): Name of the source image if + `use_image_source` is True. + - silhouette_threshold (float): Threshold for silhouette detection. + - recover_silhouettes (bool): Flag to indicate if silhouettes should be + recovered. + - silhouette_scale (float): Scaling factor for silhouette recovery. + - min_gridsize (int): Minimum grid size for processing. + - smooth_iterations (int): Number of iterations for smoothing. + - vcycle_iterations (int): Number of iterations for V-cycle processing. + - linbcg_iterations (int): Number of iterations for linear BCG + processing. + - use_planar (bool): Flag to indicate if planar processing should be + used. + - gradient_scaling_mask_use (bool): Flag to indicate if a gradient + scaling mask should be used. + - gradient_scaling_mask_name (str): Name of the gradient scaling mask + image. + - depth_exponent (float): Exponent for depth adjustment. + - silhouette_exponent (int): Exponent for silhouette recovery. + - attenuation (float): Attenuation factor for processing. + :type br: object + + :returns: + + The function does not return a value but processes the image data and + saves the result. + :rtype: None + + +.. py:function:: relief(br) + + Process an image to enhance relief features. + + This function takes an input image and applies various processing + techniques to enhance the relief features based on the provided + parameters. It utilizes gradient calculations, silhouette recovery, and + optional detail enhancement through Fourier transforms. The processed + image is then used to build a mesh representation. + + :param br: An object containing various parameters for the relief processing, + including: + - use_image_source (bool): Whether to use a specified image source. + - source_image_name (str): The name of the source image. + - silhouette_threshold (float): Threshold for silhouette detection. + - recover_silhouettes (bool): Flag to indicate if silhouettes should be + recovered. + - silhouette_scale (float): Scale factor for silhouette recovery. + - min_gridsize (int): Minimum grid size for processing. + - smooth_iterations (int): Number of iterations for smoothing. + - vcycle_iterations (int): Number of iterations for V-cycle processing. + - linbcg_iterations (int): Number of iterations for linear BCG + processing. + - use_planar (bool): Flag to indicate if planar processing should be + used. + - gradient_scaling_mask_use (bool): Flag to indicate if a gradient + scaling mask should be used. + - gradient_scaling_mask_name (str): Name of the gradient scaling mask + image. + - depth_exponent (float): Exponent for depth adjustment. + - attenuation (float): Attenuation factor for the processing. + - detail_enhancement_use (bool): Flag to indicate if detail enhancement + should be applied. + - detail_enhancement_freq (float): Frequency for detail enhancement. + - detail_enhancement_amount (float): Amount of detail enhancement to + apply. + :type br: object + + :returns: + + The function processes the image and builds a mesh but does not return a + value. + :rtype: None + + :raises ReliefError: If the input image is blank or invalid. + + +.. py:class:: BasReliefsettings + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + .. py:attribute:: use_image_source + :type: BoolProperty(name='Use Image Source', description='', default=False) + + + .. py:attribute:: source_image_name + :type: StringProperty(name='Image Source', description='image source') + + + .. py:attribute:: view_layer_name + :type: StringProperty(name='View Layer Source', description='Make a bas-relief from whatever is on this view layer') + + + .. py:attribute:: bit_diameter + :type: FloatProperty(name='Diameter of Ball End in mm', description='Diameter of bit which will be used for carving', min=0.01, max=50.0, default=3.175, precision=PRECISION) + + + .. py:attribute:: pass_per_radius + :type: IntProperty(name='Passes per Radius', description='Amount of passes per radius\n(more passes, more mesh precision)', default=2, min=1, max=10) + + + .. py:attribute:: widthmm + :type: IntProperty(name='Desired Width in mm', default=200, min=5, max=4000) + + + .. py:attribute:: heightmm + :type: IntProperty(name='Desired Height in mm', default=150, min=5, max=4000) + + + .. py:attribute:: thicknessmm + :type: IntProperty(name='Thickness in mm', default=15, min=5, max=100) + + + .. py:attribute:: justifyx + :type: EnumProperty(name='X', items=[('1', 'Left', '', 0), ('-0.5', 'Centered', '', 1), ('-1', 'Right', '', 2)], default='-1') + + + .. py:attribute:: justifyy + :type: EnumProperty(name='Y', items=[('1', 'Bottom', '', 0), ('-0.5', 'Centered', '', 2), ('-1', 'Top', '', 1)], default='-1') + + + .. py:attribute:: justifyz + :type: EnumProperty(name='Z', items=[('-1', 'Below 0', '', 0), ('-0.5', 'Centered', '', 2), ('1', 'Above 0', '', 1)], default='-1') + + + .. py:attribute:: depth_exponent + :type: FloatProperty(name='Depth Exponent', description='Initial depth map is taken to this power. Higher = sharper relief', min=0.5, max=10.0, default=1.0, precision=PRECISION) + + + .. py:attribute:: silhouette_threshold + :type: FloatProperty(name='Silhouette Threshold', description='Silhouette threshold', min=1e-06, max=1.0, default=0.003, precision=PRECISION) + + + .. py:attribute:: recover_silhouettes + :type: BoolProperty(name='Recover Silhouettes', description='', default=True) + + + .. py:attribute:: silhouette_scale + :type: FloatProperty(name='Silhouette Scale', description='Silhouette scale', min=1e-06, max=5.0, default=0.3, precision=PRECISION) + + + .. py:attribute:: silhouette_exponent + :type: IntProperty(name='Silhouette Square Exponent', description='If lower, true depth distances between objects will be more visibe in the relief', default=3, min=0, max=5) + + + .. py:attribute:: attenuation + :type: FloatProperty(name='Gradient Attenuation', description='Gradient attenuation', min=1e-06, max=100.0, default=1.0, precision=PRECISION) + + + .. py:attribute:: min_gridsize + :type: IntProperty(name='Minimum Grid Size', default=16, min=2, max=512) + + + .. py:attribute:: smooth_iterations + :type: IntProperty(name='Smooth Iterations', default=1, min=1, max=64) + + + .. py:attribute:: vcycle_iterations + :type: IntProperty(name='V-Cycle Iterations', description='Set higher for planar constraint', default=2, min=1, max=128) + + + .. py:attribute:: linbcg_iterations + :type: IntProperty(name='LINBCG Iterations', description='Set lower for flatter relief, and when using planar constraint', default=5, min=1, max=64) + + + .. py:attribute:: use_planar + :type: BoolProperty(name='Use Planar Constraint', description='', default=False) + + + .. py:attribute:: gradient_scaling_mask_use + :type: BoolProperty(name='Scale Gradients with Mask', description='', default=False) + + + .. py:attribute:: decimate_ratio + :type: FloatProperty(name='Decimate Ratio', description='Simplify the mesh using the Decimate modifier. The lower the value the more simplyfied', min=0.01, max=1.0, default=0.1, precision=PRECISION) + + + .. py:attribute:: gradient_scaling_mask_name + :type: StringProperty(name='Scaling Mask Name', description='Mask name') + + + .. py:attribute:: scale_down_before_use + :type: BoolProperty(name='Scale Down Image Before Processing', description='', default=False) + + + .. py:attribute:: scale_down_before + :type: FloatProperty(name='Image Scale', description='Image scale', min=0.025, max=1.0, default=0.5, precision=PRECISION) + + + .. py:attribute:: detail_enhancement_use + :type: BoolProperty(name='Enhance Details', description='Enhance details by frequency analysis', default=False) + + + .. py:attribute:: detail_enhancement_amount + :type: FloatProperty(name='Amount', description='Image scale', min=0.025, max=1.0, default=0.5, precision=PRECISION) + + + .. py:attribute:: advanced + :type: BoolProperty(name='Advanced Options', description='Show advanced options', default=True) + + +.. py:class:: BASRELIEF_Panel + + Bases: :py:obj:`bpy.types.Panel` + + + Bas Relief Panel + + + .. py:attribute:: bl_label + :value: 'Bas Relief' + + + + .. py:attribute:: bl_idname + :value: 'WORLD_PT_BASRELIEF' + + + + .. py:attribute:: bl_space_type + :value: 'PROPERTIES' + + + + .. py:attribute:: bl_region_type + :value: 'WINDOW' + + + + .. py:attribute:: bl_context + :value: 'render' + + + + .. py:attribute:: COMPAT_ENGINES + + + .. py:method:: poll(context) + :classmethod: + + + Check if the current render engine is compatible. + + This class method checks whether the render engine specified in the + provided context is included in the list of compatible engines. It + accesses the render settings from the context and verifies if the engine + is part of the predefined compatible engines. + + :param context: The context containing the scene and render settings. + :type context: Context + + :returns: True if the render engine is compatible, False otherwise. + :rtype: bool + + + + .. py:method:: draw(context) + + Draw the user interface for the bas relief settings. + + This method constructs the layout for the bas relief settings in the + Blender user interface. It includes various properties and options that + allow users to configure the bas relief calculations, such as selecting + images, adjusting parameters, and setting justification options. The + layout is dynamically updated based on user selections, providing a + comprehensive interface for manipulating bas relief settings. + + :param context: The context in which the UI is being drawn. + :type context: bpy.context + + :returns: This method does not return any value; it modifies the layout + directly. + :rtype: None + + + +.. py:exception:: ReliefError + + Bases: :py:obj:`Exception` + + + Common base class for all non-exit exceptions. + + +.. py:class:: DoBasRelief + + Bases: :py:obj:`bpy.types.Operator` + + + Calculate Bas Relief + + + .. py:attribute:: bl_idname + :value: 'scene.calculate_bas_relief' + + + + .. py:attribute:: bl_label + :value: 'Calculate Bas Relief' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: processes + :value: [] + + + + .. py:method:: execute(context) + + Execute the relief rendering process based on the provided context. + + This function retrieves the scene and its associated bas relief + settings. It checks if an image source is being used and sets the view + layer name accordingly. The function then attempts to render the scene + and generate the relief. If any errors occur during these processes, + they are reported, and the operation is canceled. + + :param context: The context in which the function is executed. + + :returns: A dictionary indicating the result of the operation, either + :rtype: dict + + + +.. py:class:: ProblemAreas + + Bases: :py:obj:`bpy.types.Operator` + + + Find Bas Relief Problem Areas + + + .. py:attribute:: bl_idname + :value: 'scene.problemareas_bas_relief' + + + + .. py:attribute:: bl_label + :value: 'Problem Areas Bas Relief' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: processes + :value: [] + + + + .. py:method:: execute(context) + + Execute the operation related to the bas relief settings in the current + scene. + + This method retrieves the current scene from the Blender context and + accesses the bas relief settings. It then calls the `problemAreas` + function to perform operations related to those settings. The method + concludes by returning a status indicating that the operation has + finished successfully. + + :param context: The current Blender context, which provides access + :type context: bpy.context + + :returns: A dictionary with a status key indicating the operation result, + specifically {'FINISHED'}. + :rtype: dict + + + +.. py:function:: get_panels() + + Retrieve a tuple of panel settings and related components. + + This function returns a tuple containing various components related to + Bas Relief settings. The components include BasReliefsettings, + BASRELIEF_Panel, DoBasRelief, and ProblemAreas, which are likely used in + the context of a graphical user interface or a specific application + domain. + + :returns: A tuple containing the BasReliefsettings, BASRELIEF_Panel, + DoBasRelief, and ProblemAreas components. + :rtype: tuple + + +.. py:function:: register() + + Register the necessary classes and properties for the add-on. + + This function registers all the panels defined in the add-on by + iterating through the list of panels returned by the `get_panels()` + function. It also adds a new property, `basreliefsettings`, to the + `Scene` type, which is a pointer property that references the + `BasReliefsettings` class. This setup is essential for the proper + functioning of the add-on, allowing users to access and modify the + settings related to bas relief. + + +.. py:function:: unregister() + + Unregister all panels and remove basreliefsettings from the Scene type. + + This function iterates through all registered panels and unregisters + each one using Blender's utility functions. Additionally, it removes the + basreliefsettings attribute from the Scene type, ensuring that any + settings related to bas relief are no longer accessible in the current + Blender session. + + diff --git a/_sources/autoapi/cam/bridges/index.rst b/_sources/autoapi/cam/bridges/index.rst new file mode 100644 index 000000000..e9ada00bd --- /dev/null +++ b/_sources/autoapi/cam/bridges/index.rst @@ -0,0 +1,136 @@ +cam.bridges +=========== + +.. py:module:: cam.bridges + +.. autoapi-nested-parse:: + + CNC CAM 'bridges.py' © 2012 Vilem Novak + + Functions to add Bridges / Tabs to meshes or curves. + Called with Operators defined in 'ops.py' + + + +Functions +--------- + +.. autoapisummary:: + + cam.bridges.addBridge + cam.bridges.addAutoBridges + cam.bridges.getBridgesPoly + cam.bridges.useBridges + cam.bridges.auto_cut_bridge + + +Module Contents +--------------- + +.. py:function:: addBridge(x, y, rot, sizex, sizey) + + Add a bridge mesh object to the scene. + + This function creates a bridge by adding a primitive plane to the + Blender scene, adjusting its dimensions, and then converting it into a + curve. The bridge is positioned based on the provided coordinates and + rotation. The size of the bridge is determined by the `sizex` and + `sizey` parameters. + + :param x: The x-coordinate for the bridge's location. + :type x: float + :param y: The y-coordinate for the bridge's location. + :type y: float + :param rot: The rotation angle around the z-axis in radians. + :type rot: float + :param sizex: The width of the bridge. + :type sizex: float + :param sizey: The height of the bridge. + :type sizey: float + + :returns: The created bridge object. + :rtype: bpy.types.Object + + +.. py:function:: addAutoBridges(o) + + Attempt to add auto bridges as a set of curves. + + This function creates a collection of bridges based on the provided + object. It checks if a collection for bridges already exists; if not, it + creates a new one. The function then iterates through the objects in the + input object, processing curves and meshes to generate bridge + geometries. For each geometry, it calculates the necessary points and + adds bridges at various orientations based on the geometry's bounds. + + :param o: An object containing properties such as + bridges_collection_name, bridges_width, and cutter_diameter, + along with a list of objects to process. + :type o: object + + :returns: + + This function does not return a value but modifies the + Blender context by adding bridge objects to the specified + collection. + :rtype: None + + +.. py:function:: getBridgesPoly(o) + + Generate and prepare bridge polygons from a Blender object. + + This function checks if the provided object has an attribute for bridge + polygons. If not, it retrieves the bridge collection, selects all curve + objects within that collection, duplicates them, and joins them into a + single object. The resulting shape is then converted to a Shapely + geometry. The function buffers the resulting polygon to account for the + cutter diameter and prepares the boundary and polygon for further + processing. + + :param o: An object containing properties related to bridge + :type o: object + + +.. py:function:: useBridges(ch, o) + + Add bridges to chunks using a collection of bridge objects. + + This function takes a collection of bridge objects and uses the curves + within it to create bridges over the specified chunks. It calculates the + necessary points for the bridges based on the height and geometry of the + chunks and the bridge objects. The function also handles intersections + with the bridge polygon and adjusts the points accordingly. Finally, it + generates a mesh for the bridges and converts it into a curve object in + Blender. + + :param ch: The chunk object to which bridges will be added. + :type ch: Chunk + :param o: An object containing options such as bridge height, + collection name, and other parameters. + :type o: ObjectOptions + + :returns: + + The function modifies the chunk object in place and does not return a + value. + :rtype: None + + +.. py:function:: auto_cut_bridge(o) + + Automatically processes a bridge collection. + + This function retrieves a bridge collection by its name from the + provided object and checks if there are any objects within that + collection. If there are objects present, it prints "bridges" to the + console. This function is useful for managing and processing bridge + collections in a 3D environment. + + :param o: An object that contains the attribute + :type o: object + + :returns: This function does not return any value. + :rtype: None + + diff --git a/_sources/autoapi/cam/cam_chunk/index.rst b/_sources/autoapi/cam/cam_chunk/index.rst new file mode 100644 index 000000000..462ab53a8 --- /dev/null +++ b/_sources/autoapi/cam/cam_chunk/index.rst @@ -0,0 +1,269 @@ +cam.cam_chunk +============= + +.. py:module:: cam.cam_chunk + +.. autoapi-nested-parse:: + + CNC CAM 'chunk.py' © 2012 Vilem Novak + + Classes and Functions to build, store and optimize CAM path chunks. + + + +Classes +------- + +.. autoapisummary:: + + cam.cam_chunk.camPathChunkBuilder + cam.cam_chunk.camPathChunk + + +Functions +--------- + +.. autoapisummary:: + + cam.cam_chunk.Rotate_pbyp + cam.cam_chunk._internalXyDistanceTo + cam.cam_chunk.chunksCoherency + cam.cam_chunk.setChunksZ + cam.cam_chunk._optimize_internal + cam.cam_chunk.optimizeChunk + cam.cam_chunk.limitChunks + cam.cam_chunk.parentChildPoly + cam.cam_chunk.parentChildDist + cam.cam_chunk.parentChild + cam.cam_chunk.chunksToShapely + cam.cam_chunk.meshFromCurveToChunk + cam.cam_chunk.makeVisible + cam.cam_chunk.restoreVisibility + cam.cam_chunk.meshFromCurve + cam.cam_chunk.curveToChunks + cam.cam_chunk.shapelyToChunks + cam.cam_chunk.chunkToShapely + cam.cam_chunk.chunksRefine + cam.cam_chunk.chunksRefineThreshold + + +Module Contents +--------------- + +.. py:function:: Rotate_pbyp(originp, p, ang) + +.. py:function:: _internalXyDistanceTo(ourpoints, theirpoints, cutoff) + +.. py:class:: camPathChunkBuilder(inpoints=None, startpoints=None, endpoints=None, rotations=None) + + .. py:attribute:: points + + + .. py:attribute:: startpoints + + + .. py:attribute:: endpoints + + + .. py:attribute:: rotations + + + .. py:attribute:: depth + :value: None + + + + .. py:method:: to_chunk() + + +.. py:class:: camPathChunk(inpoints, startpoints=None, endpoints=None, rotations=None) + + .. py:attribute:: poly + :value: None + + + + .. py:attribute:: simppoly + :value: None + + + + .. py:attribute:: closed + :value: False + + + + .. py:attribute:: children + :value: [] + + + + .. py:attribute:: parents + :value: [] + + + + .. py:attribute:: sorted + :value: False + + + + .. py:attribute:: length + :value: 0 + + + + .. py:attribute:: zstart + :value: 0 + + + + .. py:attribute:: zend + :value: 0 + + + + .. py:method:: update_poly() + + + .. py:method:: get_point(n) + + + .. py:method:: get_points() + + + .. py:method:: get_points_np() + + + .. py:method:: set_points(points) + + + .. py:method:: count() + + + .. py:method:: copy() + + + .. py:method:: shift(x, y, z) + + + .. py:method:: setZ(z, if_bigger=False) + + + .. py:method:: offsetZ(z) + + + .. py:method:: flipX(x_centre) + + + .. py:method:: isbelowZ(z) + + + .. py:method:: clampZ(z) + + + .. py:method:: clampmaxZ(z) + + + .. py:method:: dist(pos, o) + + + .. py:method:: distStart(pos, o) + + + .. py:method:: xyDistanceWithin(other, cutoff) + + + .. py:method:: xyDistanceTo(other, cutoff=0) + + + .. py:method:: adaptdist(pos, o) + + + .. py:method:: getNextClosest(o, pos) + + + .. py:method:: getLength() + + + .. py:method:: reverse() + + + .. py:method:: pop(index) + + + .. py:method:: dedupePoints() + + + .. py:method:: insert(at_index, point, startpoint=None, endpoint=None, rotation=None) + + + .. py:method:: append(point, startpoint=None, endpoint=None, rotation=None, at_index=None) + + + .. py:method:: extend(points, startpoints=None, endpoints=None, rotations=None, at_index=None) + + + .. py:method:: clip_points(minx, maxx, miny, maxy) + + Remove Any Points Outside This Range + + + + .. py:method:: rampContour(zstart, zend, o) + + + .. py:method:: rampZigZag(zstart, zend, o) + + + .. py:method:: changePathStart(o) + + + .. py:method:: breakPathForLeadinLeadout(o) + + + .. py:method:: leadContour(o) + + +.. py:function:: chunksCoherency(chunks) + +.. py:function:: setChunksZ(chunks, z) + +.. py:function:: _optimize_internal(points, keep_points, e, protect_vertical, protect_vertical_limit) + +.. py:function:: optimizeChunk(chunk, operation) + +.. py:function:: limitChunks(chunks, o, force=False) + +.. py:function:: parentChildPoly(parents, children, o) + +.. py:function:: parentChildDist(parents, children, o, distance=None) + +.. py:function:: parentChild(parents, children, o) + +.. py:function:: chunksToShapely(chunks) + +.. py:function:: meshFromCurveToChunk(object) + +.. py:function:: makeVisible(o) + +.. py:function:: restoreVisibility(o, storage) + +.. py:function:: meshFromCurve(o, use_modifiers=False) + +.. py:function:: curveToChunks(o, use_modifiers=False) + +.. py:function:: shapelyToChunks(p, zlevel) + +.. py:function:: chunkToShapely(chunk) + +.. py:function:: chunksRefine(chunks, o) + + Add Extra Points in Between for Chunks + + +.. py:function:: chunksRefineThreshold(chunks, distance, limitdistance) + + Add Extra Points in Between for Chunks. for Medial Axis Strategy only! + + diff --git a/_sources/autoapi/cam/cam_operation/index.rst b/_sources/autoapi/cam/cam_operation/index.rst new file mode 100644 index 000000000..5d013dcaf --- /dev/null +++ b/_sources/autoapi/cam/cam_operation/index.rst @@ -0,0 +1,634 @@ +cam.cam_operation +================= + +.. py:module:: cam.cam_operation + +.. autoapi-nested-parse:: + + CNC CAM 'cam_operation.py' + + All properties of a single CAM Operation. + + + +Classes +------- + +.. autoapisummary:: + + cam.cam_operation.camOperation + + +Module Contents +--------------- + +.. py:class:: camOperation + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + .. py:attribute:: material + :type: PointerProperty(type=CAM_MATERIAL_Properties) + + + .. py:attribute:: info + :type: PointerProperty(type=CAM_INFO_Properties) + + + .. py:attribute:: optimisation + :type: PointerProperty(type=CAM_OPTIMISATION_Properties) + + + .. py:attribute:: movement + :type: PointerProperty(type=CAM_MOVEMENT_Properties) + + + .. py:attribute:: name + :type: StringProperty(name='Operation Name', default='Operation', update=updateRest) + + + .. py:attribute:: filename + :type: StringProperty(name='File Name', default='Operation', update=updateRest) + + + .. py:attribute:: auto_export + :type: BoolProperty(name='Auto Export', description='Export files immediately after path calculation', default=True) + + + .. py:attribute:: remove_redundant_points + :type: BoolProperty(name='Simplify G-code', description='Remove redundant points sharing the same angle as the start vector', default=False) + + + .. py:attribute:: simplify_tol + :type: IntProperty(name='Tolerance', description='lower number means more precise', default=50, min=1, max=1000) + + + .. py:attribute:: hide_all_others + :type: BoolProperty(name='Hide All Others', description='Hide all other tool paths except toolpath associated with selected CAM operation', default=False) + + + .. py:attribute:: parent_path_to_object + :type: BoolProperty(name='Parent Path to Object', description='Parent generated CAM path to source object', default=False) + + + .. py:attribute:: object_name + :type: StringProperty(name='Object', description='Object handled by this operation', update=updateOperationValid) + + + .. py:attribute:: collection_name + :type: StringProperty(name='Collection', description='Object collection handled by this operation', update=updateOperationValid) + + + .. py:attribute:: curve_object + :type: StringProperty(name='Curve Source', description='Curve which will be sampled along the 3D object', update=operationValid) + + + .. py:attribute:: curve_object1 + :type: StringProperty(name='Curve Target', description='Curve which will serve as attractor for the cutter when the cutter follows the curve', update=operationValid) + + + .. py:attribute:: source_image_name + :type: StringProperty(name='Image Source', description='image source', update=operationValid) + + + .. py:attribute:: geometry_source + :type: EnumProperty(name='Data Source', items=(('OBJECT', 'Object', 'a'), ('COLLECTION', 'Collection of Objects', 'a'), ('IMAGE', 'Image', 'a')), description='Geometry source', default='OBJECT', update=updateOperationValid) + + + .. py:attribute:: cutter_type + :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) + + + .. py:attribute:: cutter_object_name + :type: StringProperty(name='Cutter Object', description='Object used as custom cutter for this operation', update=updateZbufferImage) + + + .. py:attribute:: machine_axes + :type: 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) + + + .. py:attribute:: strategy + :type: EnumProperty(name='Strategy', items=getStrategyList, description='Strategy', update=updateStrategy) + + + .. py:attribute:: strategy4axis + :type: 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) + + + .. py:attribute:: strategy5axis + :type: 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) + + + .. py:attribute:: rotary_axis_1 + :type: EnumProperty(name='Rotary Axis', items=(('X', 'X', ''), ('Y', 'Y', ''), ('Z', 'Z', '')), description='Around which axis rotates the first rotary axis', default='X', update=updateStrategy) + + + .. py:attribute:: rotary_axis_2 + :type: 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) + + + .. py:attribute:: skin + :type: 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) + + + .. py:attribute:: inverse + :type: BoolProperty(name='Inverse Milling', description='Male to female model conversion', default=False, update=updateOffsetImage) + + + .. py:attribute:: array + :type: BoolProperty(name='Use Array', description='Create a repetitive array for producing the same thing many times', default=False, update=updateRest) + + + .. py:attribute:: array_x_count + :type: IntProperty(name='X Count', description='X count', default=1, min=1, max=32000, update=updateRest) + + + .. py:attribute:: array_y_count + :type: IntProperty(name='Y Count', description='Y count', default=1, min=1, max=32000, update=updateRest) + + + .. py:attribute:: array_x_distance + :type: FloatProperty(name='X Distance', description='Distance between operation origins', min=1e-05, max=1.0, default=0.01, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: array_y_distance + :type: FloatProperty(name='Y Distance', description='Distance between operation origins', min=1e-05, max=1.0, default=0.01, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: pocket_option + :type: EnumProperty(name='Start Position', items=(('INSIDE', 'Inside', 'a'), ('OUTSIDE', 'Outside', 'a')), description='Pocket starting position', default='INSIDE', update=updateRest) + + + .. py:attribute:: pocketType + :type: EnumProperty(name='pocket type', items=(('PERIMETER', 'Perimeter', 'a'), ('PARALLEL', 'Parallel', 'a')), description='Type of pocket', default='PERIMETER', update=updateRest) + + + .. py:attribute:: parallelPocketAngle + :type: FloatProperty(name='Parallel Pocket Angle', description='Angle for parallel pocket', min=-180, max=180.0, default=45.0, precision=constants.PRECISION, update=updateRest) + + + .. py:attribute:: parallelPocketCrosshatch + :type: BoolProperty(name='Crosshatch #', description='Crosshatch X finish', default=False, update=updateRest) + + + .. py:attribute:: parallelPocketContour + :type: BoolProperty(name='Contour Finish', description='Contour path finish', default=False, update=updateRest) + + + .. py:attribute:: pocketToCurve + :type: BoolProperty(name='Pocket to Curve', description='Generates a curve instead of a path', default=False, update=updateRest) + + + .. py:attribute:: cut_type + :type: EnumProperty(name='Cut', items=(('OUTSIDE', 'Outside', 'a'), ('INSIDE', 'Inside', 'a'), ('ONLINE', 'On Line', 'a')), description='Type of cutter used', default='OUTSIDE', update=updateRest) + + + .. py:attribute:: outlines_count + :type: IntProperty(name='Outlines Count', description='Outlines count', default=1, min=1, max=32, update=updateCutout) + + + .. py:attribute:: straight + :type: BoolProperty(name='Overshoot Style', description='Use overshoot cutout instead of conventional rounded', default=True, update=updateRest) + + + .. py:attribute:: cutter_id + :type: IntProperty(name='Tool Number', description='For machines which support tool change based on tool id', min=0, max=10000, default=1, update=updateRest) + + + .. py:attribute:: cutter_diameter + :type: FloatProperty(name='Cutter Diameter', description='Cutter diameter = 2x cutter radius', min=1e-06, max=10, default=0.003, precision=constants.PRECISION, unit='LENGTH', update=updateOffsetImage) + + + .. py:attribute:: cylcone_diameter + :type: FloatProperty(name='Bottom Diameter', description='Bottom diameter', min=1e-06, max=10, default=0.003, precision=constants.PRECISION, unit='LENGTH', update=updateOffsetImage) + + + .. py:attribute:: cutter_length + :type: 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) + + + .. py:attribute:: cutter_flutes + :type: IntProperty(name='Cutter Flutes', description='Cutter flutes', min=1, max=20, default=2, update=updateChipload) + + + .. py:attribute:: cutter_tip_angle + :type: 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) + + + .. py:attribute:: ball_radius + :type: FloatProperty(name='Ball Radius', description='Radius of', min=0.0, max=0.035, default=0.001, unit='LENGTH', precision=constants.PRECISION, update=updateOffsetImage) + + + .. py:attribute:: bull_corner_radius + :type: 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) + + + .. py:attribute:: cutter_description + :type: StringProperty(name='Tool Description', default='', update=updateOffsetImage) + + + .. py:attribute:: Laser_on + :type: StringProperty(name='Laser ON String', default='M68 E0 Q100') + + + .. py:attribute:: Laser_off + :type: StringProperty(name='Laser OFF String', default='M68 E0 Q0') + + + .. py:attribute:: Laser_cmd + :type: StringProperty(name='Laser Command', default='M68 E0 Q') + + + .. py:attribute:: Laser_delay + :type: FloatProperty(name='Laser ON Delay', description='Time after fast move to turn on laser and let machine stabilize', default=0.2) + + + .. py:attribute:: Plasma_on + :type: StringProperty(name='Plasma ON String', default='M03') + + + .. py:attribute:: Plasma_off + :type: StringProperty(name='Plasma OFF String', default='M05') + + + .. py:attribute:: Plasma_delay + :type: FloatProperty(name='Plasma ON Delay', description='Time after fast move to turn on Plasma and let machine stabilize', default=0.1) + + + .. py:attribute:: Plasma_dwell + :type: FloatProperty(name='Plasma Dwell Time', description='Time to dwell and warm up the torch', default=0.0) + + + .. py:attribute:: dist_between_paths + :type: FloatProperty(name='Distance Between Toolpaths', default=0.001, min=1e-05, max=32, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: dist_along_paths + :type: FloatProperty(name='Distance Along Toolpaths', default=0.0002, min=1e-05, max=32, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: parallel_angle + :type: FloatProperty(name='Angle of Paths', default=0, min=-360, max=360, precision=0, subtype='ANGLE', unit='ROTATION', update=updateRest) + + + .. py:attribute:: old_rotation_A + :type: 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) + + + .. py:attribute:: old_rotation_B + :type: 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) + + + .. py:attribute:: rotation_A + :type: 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) + + + .. py:attribute:: enable_A + :type: BoolProperty(name='Enable A Axis', description='Rotate A axis', default=False, update=updateRotation) + + + .. py:attribute:: A_along_x + :type: BoolProperty(name='A Along X ', description='A Parallel to X', default=True, update=updateRest) + + + .. py:attribute:: rotation_B + :type: 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) + + + .. py:attribute:: enable_B + :type: BoolProperty(name='Enable B Axis', description='Rotate B axis', default=False, update=updateRotation) + + + .. py:attribute:: carve_depth + :type: FloatProperty(name='Carve Depth', default=0.001, min=-0.1, max=32, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: drill_type + :type: EnumProperty(name='Holes On', items=(('MIDDLE_SYMETRIC', 'Middle of Symmetric 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) + + + .. py:attribute:: slice_detail + :type: FloatProperty(name='Distance Between Slices', default=0.001, min=1e-05, max=32, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: waterline_fill + :type: BoolProperty(name='Fill Areas Between Slices', description='Fill areas between slices in waterline mode', default=True, update=updateRest) + + + .. py:attribute:: waterline_project + :type: BoolProperty(name='Project Paths - Not Recomended', description='Project paths in areas between slices', default=True, update=updateRest) + + + .. py:attribute:: use_layers + :type: BoolProperty(name='Use Layers', description='Use layers for roughing', default=True, update=updateRest) + + + .. py:attribute:: stepdown + :type: FloatProperty(name='', description='Layer height', default=0.01, min=1e-05, max=32, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: lead_in + :type: FloatProperty(name='Lead-in Radius', description='Lead in radius for torch or laser to turn off', min=0.0, max=1, default=0.0, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: lead_out + :type: FloatProperty(name='Lead-out Radius', description='Lead out radius for torch or laser to turn off', min=0.0, max=1, default=0.0, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: profile_start + :type: IntProperty(name='Start Point', description='Start point offset', min=0, default=0, update=updateRest) + + + .. py:attribute:: minz + :type: FloatProperty(name='Operation Depth End', default=-0.01, min=-3, max=3, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: minz_from + :type: EnumProperty(name='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) + + + .. py:attribute:: start_type + :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) + + + .. py:attribute:: maxz + :type: FloatProperty(name='Operation Depth Start', description='operation starting depth', default=0, min=-3, max=10, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: first_down + :type: BoolProperty(name='First Down', description='First go down on a contour, then go to the next one', default=False, update=update_operation) + + + .. py:attribute:: source_image_scale_z + :type: FloatProperty(name='Image Source Depth Scale', default=0.01, min=-1, max=1, precision=constants.PRECISION, unit='LENGTH', update=updateZbufferImage) + + + .. py:attribute:: source_image_size_x + :type: FloatProperty(name='Image Source X Size', default=0.1, min=-10, max=10, precision=constants.PRECISION, unit='LENGTH', update=updateZbufferImage) + + + .. py:attribute:: source_image_offset + :type: FloatVectorProperty(name='Image Offset', default=(0, 0, 0), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ', update=updateZbufferImage) + + + .. py:attribute:: source_image_crop + :type: 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) + + + .. py:attribute:: source_image_crop_start_x + :type: FloatProperty(name='Crop Start X', default=0, min=0, max=100, precision=constants.PRECISION, subtype='PERCENTAGE', update=updateZbufferImage) + + + .. py:attribute:: source_image_crop_start_y + :type: FloatProperty(name='Crop Start Y', default=0, min=0, max=100, precision=constants.PRECISION, subtype='PERCENTAGE', update=updateZbufferImage) + + + .. py:attribute:: source_image_crop_end_x + :type: FloatProperty(name='Crop End X', default=100, min=0, max=100, precision=constants.PRECISION, subtype='PERCENTAGE', update=updateZbufferImage) + + + .. py:attribute:: source_image_crop_end_y + :type: FloatProperty(name='Crop End Y', default=100, min=0, max=100, precision=constants.PRECISION, subtype='PERCENTAGE', update=updateZbufferImage) + + + .. py:attribute:: ambient_behaviour + :type: EnumProperty(name='Ambient', items=(('ALL', 'All', 'a'), ('AROUND', 'Around', 'a')), description='Handling ambient surfaces', default='ALL', update=updateZbufferImage) + + + .. py:attribute:: ambient_radius + :type: 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) + + + .. py:attribute:: use_limit_curve + :type: BoolProperty(name='Use Limit Curve', description='A curve limits the operation area', default=False, update=updateRest) + + + .. py:attribute:: ambient_cutter_restrict + :type: 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) + + + .. py:attribute:: limit_curve + :type: StringProperty(name='Limit Curve', description='Curve used to limit the area of the operation', update=updateRest) + + + .. py:attribute:: feedrate + :type: FloatProperty(name='Feedrate', description='Feedrate in units per minute', min=5e-05, max=50.0, default=1.0, precision=constants.PRECISION, unit='LENGTH', update=updateChipload) + + + .. py:attribute:: plunge_feedrate + :type: FloatProperty(name='Plunge Speed', description='% of feedrate', min=0.1, max=100.0, default=50.0, precision=1, subtype='PERCENTAGE', update=updateRest) + + + .. py:attribute:: plunge_angle + :type: FloatProperty(name='Plunge Angle', description='What angle is already considered to plunge', default=pi / 6, min=0, max=pi * 0.5, precision=0, subtype='ANGLE', unit='ROTATION', update=updateRest) + + + .. py:attribute:: spindle_rpm + :type: FloatProperty(name='Spindle RPM', description='Spindle speed ', min=0, max=60000, default=12000, update=updateChipload) + + + .. py:attribute:: do_simulation_feedrate + :type: BoolProperty(name='Adjust Feedrates with Simulation EXPERIMENTAL', description='Adjust feedrates with simulation', default=False, update=updateRest) + + + .. py:attribute:: dont_merge + :type: BoolProperty(name="Don't Merge Outlines when Cutting", description='this is usefull when you want to cut around everything', default=False, update=updateRest) + + + .. py:attribute:: pencil_threshold + :type: FloatProperty(name='Pencil Threshold', default=2e-05, min=1e-08, max=1, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: crazy_threshold1 + :type: FloatProperty(name='Min Engagement', default=0.02, min=1e-08, max=100, precision=constants.PRECISION, update=updateRest) + + + .. py:attribute:: crazy_threshold5 + :type: FloatProperty(name='Optimal Engagement', default=0.3, min=1e-08, max=100, precision=constants.PRECISION, update=updateRest) + + + .. py:attribute:: crazy_threshold2 + :type: FloatProperty(name='Max Engagement', default=0.5, min=1e-08, max=100, precision=constants.PRECISION, update=updateRest) + + + .. py:attribute:: crazy_threshold3 + :type: FloatProperty(name='Max Angle', default=2, min=1e-08, max=100, precision=constants.PRECISION, update=updateRest) + + + .. py:attribute:: crazy_threshold4 + :type: FloatProperty(name='Test Angle Step', default=0.05, min=1e-08, max=100, precision=constants.PRECISION, update=updateRest) + + + .. py:attribute:: add_pocket_for_medial + :type: BoolProperty(name='Add Pocket Operation', description='Clean unremoved material after medial axis', default=True, update=updateRest) + + + .. py:attribute:: add_mesh_for_medial + :type: BoolProperty(name='Add Medial mesh', description='Medial operation returns mesh for editing and further processing', default=False, update=updateRest) + + + .. py:attribute:: medial_axis_threshold + :type: FloatProperty(name='Long Vector Threshold', default=0.001, min=1e-08, max=100, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: medial_axis_subdivision + :type: FloatProperty(name='Fine Subdivision', default=0.0002, min=1e-08, max=100, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: use_bridges + :type: BoolProperty(name='Use Bridges / Tabs', description='Use bridges in cutout', default=False, update=updateBridges) + + + .. py:attribute:: bridges_width + :type: FloatProperty(name='Bridge / Tab Width', default=0.002, unit='LENGTH', precision=constants.PRECISION, update=updateBridges) + + + .. py:attribute:: bridges_height + :type: FloatProperty(name='Bridge / Tab Height', description='Height from the bottom of the cutting operation', default=0.0005, unit='LENGTH', precision=constants.PRECISION, update=updateBridges) + + + .. py:attribute:: bridges_collection_name + :type: StringProperty(name='Bridges / Tabs Collection', description='Collection of curves used as bridges', update=operationValid) + + + .. py:attribute:: use_bridge_modifiers + :type: BoolProperty(name='Use Bridge / Tab Modifiers', description='Include bridge curve modifiers using render level when calculating operation, does not effect original bridge data', default=True, update=updateBridges) + + + .. py:attribute:: use_modifiers + :type: BoolProperty(name='Use Mesh Modifiers', description='Include mesh modifiers using render level when calculating operation, does not effect original mesh', default=True, update=operationValid) + + + .. py:attribute:: min + :type: FloatVectorProperty(name='Operation Minimum', default=(0, 0, 0), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ') + + + .. py:attribute:: max + :type: FloatVectorProperty(name='Operation Maximum', default=(0, 0, 0), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ') + + + .. py:attribute:: output_header + :type: BoolProperty(name='Output G-code Header', description='Output user defined G-code command header at start of operation', default=False) + + + .. py:attribute:: gcode_header + :type: StringProperty(name='G-code Header', description='G-code commands at start of operation. Use ; for line breaks', default='G53 G0') + + + .. py:attribute:: enable_dust + :type: BoolProperty(name='Dust Collector', description='Output user defined g-code command header at start of operation', default=False) + + + .. py:attribute:: gcode_start_dust_cmd + :type: StringProperty(name='Start Dust Collector', description='Commands to start dust collection. Use ; for line breaks', default='M100') + + + .. py:attribute:: gcode_stop_dust_cmd + :type: StringProperty(name='Stop Dust Collector', description='Command to stop dust collection. Use ; for line breaks', default='M101') + + + .. py:attribute:: enable_hold + :type: BoolProperty(name='Hold Down', description='Output hold down command at start of operation', default=False) + + + .. py:attribute:: gcode_start_hold_cmd + :type: StringProperty(name='G-code Header', description='G-code commands at start of operation. Use ; for line breaks', default='M102') + + + .. py:attribute:: gcode_stop_hold_cmd + :type: StringProperty(name='G-code Header', description='G-code commands at end operation. Use ; for line breaks', default='M103') + + + .. py:attribute:: enable_mist + :type: BoolProperty(name='Mist', description='Mist command at start of operation', default=False) + + + .. py:attribute:: gcode_start_mist_cmd + :type: StringProperty(name='Start Mist', description='Command to start mist. Use ; for line breaks', default='M104') + + + .. py:attribute:: gcode_stop_mist_cmd + :type: StringProperty(name='Stop Mist', description='Command to stop mist. Use ; for line breaks', default='M105') + + + .. py:attribute:: output_trailer + :type: BoolProperty(name='Output G-code Trailer', description='Output user defined g-code command trailer at end of operation', default=False) + + + .. py:attribute:: gcode_trailer + :type: StringProperty(name='G-code Trailer', description='G-code commands at end of operation. Use ; for line breaks', default='M02') + + + .. py:attribute:: offset_image + + + .. py:attribute:: zbuffer_image + + + .. py:attribute:: silhouete + + + .. py:attribute:: ambient + + + .. py:attribute:: operation_limit + + + .. py:attribute:: borderwidth + :value: 50 + + + + .. py:attribute:: object + :value: None + + + + .. py:attribute:: path_object_name + :type: StringProperty(name='Path Object', description='Actual CNC path') + + + .. py:attribute:: changed + :type: BoolProperty(name='True if any of the Operation Settings has Changed', description='Mark for update', default=False) + + + .. py:attribute:: update_zbufferimage_tag + :type: BoolProperty(name='Mark Z-Buffer Image for Update', description='Mark for update', default=True) + + + .. py:attribute:: update_offsetimage_tag + :type: BoolProperty(name='Mark Offset Image for Update', description='Mark for update', default=True) + + + .. py:attribute:: update_silhouete_tag + :type: BoolProperty(name='Mark Silhouette Image for Update', description='Mark for update', default=True) + + + .. py:attribute:: update_ambient_tag + :type: BoolProperty(name='Mark Ambient Polygon for Update', description='Mark for update', default=True) + + + .. py:attribute:: update_bullet_collision_tag + :type: BoolProperty(name='Mark Bullet Collision World for Update', description='Mark for update', default=True) + + + .. py:attribute:: valid + :type: BoolProperty(name='Valid', description='True if operation is ok for calculation', default=True) + + + .. py:attribute:: changedata + :type: StringProperty(name='Changedata', description='change data for checking if stuff changed.') + + + .. py:attribute:: computing + :type: BoolProperty(name='Computing Right Now', description='', default=False) + + + .. py:attribute:: pid + :type: IntProperty(name='Process Id', description='Background process id', default=-1) + + + .. py:attribute:: outtext + :type: StringProperty(name='Outtext', description='outtext', default='') + + diff --git a/_sources/autoapi/cam/chain/index.rst b/_sources/autoapi/cam/chain/index.rst new file mode 100644 index 000000000..a67b39184 --- /dev/null +++ b/_sources/autoapi/cam/chain/index.rst @@ -0,0 +1,72 @@ +cam.chain +========= + +.. py:module:: cam.chain + +.. autoapi-nested-parse:: + + CNC CAM 'chain.py' + + All properties of a CAM Chain (a series of Operations), and the Chain's Operation reference. + + + +Classes +------- + +.. autoapisummary:: + + cam.chain.opReference + cam.chain.camChain + + +Module Contents +--------------- + +.. py:class:: opReference + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + .. py:attribute:: name + :type: StringProperty(name='Operation Name', default='Operation') + + + .. py:attribute:: computing + :value: False + + + +.. py:class:: camChain + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + .. py:attribute:: index + :type: IntProperty(name='Index', description='Index in the hard-defined camChains', default=-1) + + + .. py:attribute:: active_operation + :type: IntProperty(name='Active Operation', description='Active operation in chain', default=-1) + + + .. py:attribute:: name + :type: StringProperty(name='Chain Name', default='Chain') + + + .. py:attribute:: filename + :type: StringProperty(name='File Name', default='Chain') + + + .. py:attribute:: valid + :type: BoolProperty(name='Valid', description='True if whole chain is ok for calculation', default=True) + + + .. py:attribute:: computing + :type: BoolProperty(name='Computing Right Now', description='', default=False) + + + .. py:attribute:: operations + :type: CollectionProperty(type=opReference) + + diff --git a/_sources/autoapi/cam/collision/index.rst b/_sources/autoapi/cam/collision/index.rst new file mode 100644 index 000000000..8c75beee5 --- /dev/null +++ b/_sources/autoapi/cam/collision/index.rst @@ -0,0 +1,161 @@ +cam.collision +============= + +.. py:module:: cam.collision + +.. autoapi-nested-parse:: + + CNC CAM 'collision.py' © 2012 Vilem Novak + + Functions for Bullet and Cutter collision checks. + + + +Functions +--------- + +.. autoapisummary:: + + cam.collision.getCutterBullet + cam.collision.subdivideLongEdges + cam.collision.prepareBulletCollision + cam.collision.cleanupBulletCollision + cam.collision.getSampleBullet + cam.collision.getSampleBulletNAxis + + +Module Contents +--------------- + +.. py:function:: getCutterBullet(o) + + Create a cutter for Rigidbody simulation collisions. + + This function generates a 3D cutter object based on the specified cutter + type and parameters. It supports various cutter types including 'END', + 'BALLNOSE', 'VCARVE', 'CYLCONE', 'BALLCONE', and 'CUSTOM'. The function + also applies rigid body physics to the created cutter for realistic + simulation in Blender. + + :param o: An object containing properties such as cutter_type, cutter_diameter, + cutter_tip_angle, ball_radius, and cutter_object_name. + :type o: object + + :returns: The created cutter object with rigid body properties applied. + :rtype: bpy.types.Object + + +.. py:function:: subdivideLongEdges(ob, threshold) + + Subdivide edges of a mesh object that exceed a specified length. + + This function iteratively checks the edges of a given mesh object and + subdivides those that are longer than a specified threshold. The process + involves toggling the edit mode of the object, selecting the long edges, + and applying a subdivision operation. The function continues to + subdivide until no edges exceed the threshold. + + :param ob: The Blender object containing the mesh to be + subdivided. + :type ob: bpy.types.Object + :param threshold: The length threshold above which edges will be + subdivided. + :type threshold: float + + +.. py:function:: prepareBulletCollision(o) + + Prepares all objects needed for sampling with Bullet collision. + + This function sets up the Bullet physics simulation by preparing the + specified objects for collision detection. It begins by cleaning up any + existing rigid bodies that are not part of the 'machine' object. Then, + it duplicates the collision objects, converts them to mesh if they are + curves or fonts, and applies necessary modifiers. The function also + handles the subdivision of long edges and configures the rigid body + properties for each object. Finally, it scales the 'machine' objects to + the simulation scale and steps through the simulation frames to ensure + that all objects are up to date. + + :param o: An object containing properties and settings for + :type o: Object + + +.. py:function:: cleanupBulletCollision(o) + + Clean up bullet collision objects in the scene. + + This function checks for the presence of a 'machine' object in the + Blender scene and removes any rigid body objects that are not part of + the 'machine'. If the 'machine' object is present, it scales the machine + objects up to the simulation scale and adjusts their locations + accordingly. + + :param o: An object that may be used in the cleanup process (specific usage not + detailed). + + :returns: This function does not return a value. + :rtype: None + + +.. py:function:: getSampleBullet(cutter, x, y, radius, startz, endz) + + Perform a collision test for a 3-axis milling cutter. + + This function simplifies the collision detection process compared to a + full 3D test. It utilizes the Blender Python API to perform a convex + sweep test on the cutter's position within a specified 3D space. The + function checks for collisions between the cutter and other objects in + the scene, adjusting for the cutter's radius to determine the effective + position of the cutter tip. + + :param cutter: The milling cutter object used for the collision test. + :type cutter: object + :param x: The x-coordinate of the cutter's position. + :type x: float + :param y: The y-coordinate of the cutter's position. + :type y: float + :param radius: The radius of the cutter, used to adjust the collision detection. + :type radius: float + :param startz: The starting z-coordinate for the collision test. + :type startz: float + :param endz: The ending z-coordinate for the collision test. + :type endz: float + + :returns: + + The adjusted z-coordinate of the cutter tip if a collision is detected; + otherwise, returns a value 10 units below the specified endz. + :rtype: float + + +.. py:function:: getSampleBulletNAxis(cutter, startpoint, endpoint, rotation, cutter_compensation) + + Perform a fully 3D collision test for N-Axis milling. + + This function computes the collision detection between a cutter and a + specified path in a 3D space. It takes into account the cutter's + rotation and compensation to accurately determine if a collision occurs + during the milling process. The function uses Bullet physics for the + collision detection and returns the adjusted position of the cutter if a + collision is detected. + + :param cutter: The cutter object used in the milling operation. + :type cutter: object + :param startpoint: The starting point of the milling path. + :type startpoint: Vector + :param endpoint: The ending point of the milling path. + :type endpoint: Vector + :param rotation: The rotation applied to the cutter. + :type rotation: Euler + :param cutter_compensation: The compensation factor for the cutter's position. + :type cutter_compensation: float + + :returns: + + The adjusted position of the cutter if a collision is + detected; + otherwise, returns None. + :rtype: Vector or None + + diff --git a/_sources/autoapi/cam/constants/index.rst b/_sources/autoapi/cam/constants/index.rst new file mode 100644 index 000000000..0f78ae601 --- /dev/null +++ b/_sources/autoapi/cam/constants/index.rst @@ -0,0 +1,51 @@ +cam.constants +============= + +.. py:module:: cam.constants + +.. autoapi-nested-parse:: + + CNC CAM 'constants.py' + + Package to store all constants of CNC CAM. + + + +Attributes +---------- + +.. autoapisummary:: + + cam.constants.PRECISION + cam.constants.CHIPLOAD_PRECISION + cam.constants.MAX_OPERATION_TIME + cam.constants.G64_INCOMPATIBLE_MACHINES + cam.constants.BULLET_SCALE + cam.constants.CUTTER_OFFSET + + +Module Contents +--------------- + +.. py:data:: PRECISION + :value: 5 + + +.. py:data:: CHIPLOAD_PRECISION + :value: 10 + + +.. py:data:: MAX_OPERATION_TIME + :value: 3200000000 + + +.. py:data:: G64_INCOMPATIBLE_MACHINES + :value: ['GRBL'] + + +.. py:data:: BULLET_SCALE + :value: 10000 + + +.. py:data:: CUTTER_OFFSET + diff --git a/_sources/autoapi/cam/curvecamcreate/index.rst b/_sources/autoapi/cam/curvecamcreate/index.rst new file mode 100644 index 000000000..aec275dfc --- /dev/null +++ b/_sources/autoapi/cam/curvecamcreate/index.rst @@ -0,0 +1,846 @@ +cam.curvecamcreate +================== + +.. py:module:: cam.curvecamcreate + +.. autoapi-nested-parse:: + + CNC CAM 'curvecamcreate.py' © 2021, 2022 Alain Pelletier + + Operators to create a number of predefined curve objects. + + + +Classes +------- + +.. autoapisummary:: + + cam.curvecamcreate.CamCurveHatch + cam.curvecamcreate.CamCurvePlate + cam.curvecamcreate.CamCurveFlatCone + cam.curvecamcreate.CamCurveMortise + cam.curvecamcreate.CamCurveInterlock + cam.curvecamcreate.CamCurveDrawer + cam.curvecamcreate.CamCurvePuzzle + cam.curvecamcreate.CamCurveGear + + +Functions +--------- + +.. autoapisummary:: + + cam.curvecamcreate.generate_crosshatch + + +Module Contents +--------------- + +.. py:function:: generate_crosshatch(context, angle, distance, offset, pocket_shape, join, ob=None) + + Execute the crosshatch generation process based on the provided context. + + :param context: The Blender context containing the active object. + :type context: bpy.context + :param angle: The angle for rotating the crosshatch pattern. + :type angle: float + :param distance: The distance between crosshatch lines. + :type distance: float + :param offset: The offset for the bounds or hull. + :type offset: float + :param pocket_shape: Determines whether to use bounds, hull, or pocket. + :type pocket_shape: str + + :returns: The resulting intersection geometry of the crosshatch. + :rtype: shapely.geometry.MultiLineString + + +.. py:class:: CamCurveHatch + + Bases: :py:obj:`bpy.types.Operator` + + + Perform Hatch Operation on Single or Multiple Curves + + + .. py:attribute:: bl_idname + :value: 'object.curve_hatch' + + + + .. py:attribute:: bl_label + :value: 'CrossHatch Curve' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: angle + :type: FloatProperty(default=0, min=-pi / 2, max=pi / 2, precision=4, subtype='ANGLE') + + + .. py:attribute:: distance + :type: FloatProperty(default=0.003, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: offset + :type: FloatProperty(default=0, min=-1.0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: pocket_shape + :type: EnumProperty(name='Pocket Shape', items=(('BOUNDS', 'Bounds Rectangle', 'Uses a bounding rectangle'), ('HULL', 'Convex Hull', 'Uses a convex hull'), ('POCKET', 'Pocket', 'Uses the pocket shape')), description='Type of pocket shape', default='POCKET') + + + .. py:attribute:: contour + :type: BoolProperty(name='Contour Curve', default=False) + + + .. py:attribute:: xhatch + :type: BoolProperty(name='Crosshatch #', default=False) + + + .. py:attribute:: contour_separate + :type: BoolProperty(name='Contour Separate', default=False) + + + .. py:attribute:: straight + :type: BoolProperty(name='Overshoot Style', description='Use overshoot cutout instead of conventional rounded', default=True) + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: draw(context) + + Draw the layout properties for the given context. + + + + .. py:method:: execute(context) + + +.. py:class:: CamCurvePlate + + Bases: :py:obj:`bpy.types.Operator` + + + Perform Generates Rounded Plate with Mounting Holes + + + .. py:attribute:: bl_idname + :value: 'object.curve_plate' + + + + .. py:attribute:: bl_label + :value: 'Sign Plate' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: radius + :type: FloatProperty(name='Corner Radius', default=0.025, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: width + :type: FloatProperty(name='Width of Plate', default=0.3048, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: height + :type: FloatProperty(name='Height of Plate', default=0.457, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: hole_diameter + :type: FloatProperty(name='Hole Diameter', default=0.01, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: hole_tolerance + :type: FloatProperty(name='Hole V Tolerance', default=0.005, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: hole_vdist + :type: FloatProperty(name='Hole Vert Distance', default=0.4, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: hole_hdist + :type: FloatProperty(name='Hole Horiz Distance', default=0, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: hole_hamount + :type: IntProperty(name='Hole Horiz Amount', default=1, min=0, max=50) + + + .. py:attribute:: resolution + :type: IntProperty(name='Spline Resolution', default=50, min=3, max=150) + + + .. py:attribute:: plate_type + :type: EnumProperty(name='Type Plate', items=(('ROUNDED', 'Rounded corner', 'Makes a rounded corner plate'), ('COVE', 'Cove corner', 'Makes a plate with circles cut in each corner '), ('BEVEL', 'Bevel corner', 'Makes a plate with beveled corners '), ('OVAL', 'Elipse', 'Makes an oval plate')), description='Type of Plate', default='ROUNDED') + + + .. py:method:: draw(context) + + Draw the UI layout for plate properties. + + This method creates a user interface layout for configuring various + properties of a plate, including its type, dimensions, hole + specifications, and resolution. It dynamically adds properties to the + layout based on the selected plate type, allowing users to input + relevant parameters. + + :param context: The context in which the UI is being drawn. + + + + .. py:method:: execute(context) + + Execute the creation of a plate based on specified parameters. + + This function generates a plate shape in Blender based on the defined + attributes such as width, height, radius, and plate type. It supports + different plate types including rounded, oval, cove, and bevel. The + function also handles the creation of holes in the plate if specified. + It utilizes Blender's curve operations to create the geometry and + applies various transformations to achieve the desired shape. + + :param context: The Blender context in which the operation is performed. + :type context: bpy.context + + :returns: + + A dictionary indicating the result of the operation, typically + {'FINISHED'} if successful. + :rtype: dict + + + +.. py:class:: CamCurveFlatCone + + Bases: :py:obj:`bpy.types.Operator` + + + Generates cone from flat stock + + + .. py:attribute:: bl_idname + :value: 'object.curve_flat_cone' + + + + .. py:attribute:: bl_label + :value: 'Cone Flat Calculator' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: small_d + :type: FloatProperty(name='Small Diameter', default=0.025, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: large_d + :type: FloatProperty(name='Large Diameter', default=0.3048, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: height + :type: FloatProperty(name='Height of Cone', default=0.457, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: tab + :type: FloatProperty(name='Tab Witdh', default=0.01, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: intake + :type: FloatProperty(name='Intake Diameter', default=0, min=0, max=0.2, precision=4, unit='LENGTH') + + + .. py:attribute:: intake_skew + :type: FloatProperty(name='Intake Skew', default=1, min=0.1, max=4) + + + .. py:attribute:: resolution + :type: IntProperty(name='Resolution', default=12, min=5, max=200) + + + .. py:method:: execute(context) + + Execute the construction of a geometric shape in Blender. + + This method performs a series of operations to create a geometric shape + based on specified dimensions and parameters. It calculates various + dimensions needed for the shape, including height and angles, and then + uses Blender's operations to create segments, rectangles, and ellipses. + The function also handles the positioning and rotation of these shapes + within the 3D space of Blender. + + :param context: The context in which the operation is executed, typically containing + information about the current + scene and active objects in Blender. + + :returns: + + A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: CamCurveMortise + + Bases: :py:obj:`bpy.types.Operator` + + + Generates Mortise Along a Curve + + + .. py:attribute:: bl_idname + :value: 'object.curve_mortise' + + + + .. py:attribute:: bl_label + :value: 'Mortise' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: finger_size + :type: BoolProperty(name='Kurf Bending only', default=False) + + + .. py:attribute:: min_finger_size + :type: FloatProperty(name='Minimum Finger Size', default=0.0025, min=0.001, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_tolerance + :type: FloatProperty(name='Finger Play Room', default=4.5e-05, min=0, max=0.003, precision=4, unit='LENGTH') + + + .. py:attribute:: plate_thickness + :type: FloatProperty(name='Drawer Plate Thickness', default=0.00477, min=0.001, max=3.0, unit='LENGTH') + + + .. py:attribute:: side_height + :type: FloatProperty(name='Side Height', default=0.05, min=0.001, max=3.0, unit='LENGTH') + + + .. py:attribute:: flex_pocket + :type: FloatProperty(name='Flex Pocket', default=0.004, min=0.0, max=1.0, unit='LENGTH') + + + .. py:attribute:: top_bottom + :type: BoolProperty(name='Side Top & Bottom Fingers', default=True) + + + .. py:attribute:: opencurve + :type: BoolProperty(name='OpenCurve', default=False) + + + .. py:attribute:: adaptive + :type: FloatProperty(name='Adaptive Angle Threshold', default=0.0, min=0.0, max=2, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: double_adaptive + :type: BoolProperty(name='Double Adaptive Pockets', default=False) + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the joinery process based on the provided context. + + This function performs a series of operations to duplicate the active + object, convert it to a mesh, and then process its geometry to create + joinery features. It extracts vertex coordinates, converts them into a + LineString data structure, and applies either variable or fixed finger + joinery based on the specified parameters. The function also handles the + creation of flexible sides and pockets if required. + + :param context: The context in which the operation is executed. + :type context: bpy.context + + :returns: A dictionary indicating the completion status of the operation. + :rtype: dict + + + +.. py:class:: CamCurveInterlock + + Bases: :py:obj:`bpy.types.Operator` + + + Generates Interlock Along a Curve + + + .. py:attribute:: bl_idname + :value: 'object.curve_interlock' + + + + .. py:attribute:: bl_label + :value: 'Interlock' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: finger_size + :type: FloatProperty(name='Finger Size', default=0.015, min=0.005, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_tolerance + :type: FloatProperty(name='Finger Play Room', default=4.5e-05, min=0, max=0.003, precision=4, unit='LENGTH') + + + .. py:attribute:: plate_thickness + :type: FloatProperty(name='Plate Thickness', default=0.00477, min=0.001, max=3.0, unit='LENGTH') + + + .. py:attribute:: opencurve + :type: BoolProperty(name='OpenCurve', default=False) + + + .. py:attribute:: interlock_type + :type: EnumProperty(name='Type of Interlock', items=(('TWIST', 'Twist', 'Interlock requires 1/4 turn twist'), ('GROOVE', 'Groove', 'Simple sliding groove'), ('PUZZLE', 'Puzzle Interlock', 'Puzzle good for flat joints')), description='Type of interlock', default='GROOVE') + + + .. py:attribute:: finger_amount + :type: IntProperty(name='Finger Amount', default=2, min=1, max=100) + + + .. py:attribute:: tangent_angle + :type: FloatProperty(name='Tangent Deviation', default=0.0, min=0.0, max=2, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: fixed_angle + :type: FloatProperty(name='Fixed Angle', default=0.0, min=0.0, max=2, subtype='ANGLE', unit='ROTATION') + + + .. py:method:: execute(context) + + Execute the joinery operation based on the selected objects in the + context. + + This function checks the selected objects in the provided context and + performs different operations depending on the type of the active + object. If the active object is a curve or font and there are selected + objects, it duplicates the object, converts it to a mesh, and processes + its vertices to create a LineString representation. The function then + calculates lengths and applies distributed interlock joinery based on + the specified parameters. If no valid objects are selected, it defaults + to a single interlock operation at the cursor's location. + + :param context: The context containing selected objects and active object. + :type context: bpy.context + + :returns: A dictionary indicating the operation's completion status. + :rtype: dict + + + +.. py:class:: CamCurveDrawer + + Bases: :py:obj:`bpy.types.Operator` + + + Generates Drawers + + + .. py:attribute:: bl_idname + :value: 'object.curve_drawer' + + + + .. py:attribute:: bl_label + :value: 'Drawer' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: depth + :type: FloatProperty(name='Drawer Depth', default=0.2, min=0, max=1.0, precision=4, unit='LENGTH') + + + .. py:attribute:: width + :type: FloatProperty(name='Drawer Width', default=0.125, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: height + :type: FloatProperty(name='Drawer Height', default=0.07, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_size + :type: FloatProperty(name='Maximum Finger Size', default=0.015, min=0.005, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_tolerance + :type: FloatProperty(name='Finger Play Room', default=4.5e-05, min=0, max=0.003, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_inset + :type: FloatProperty(name='Finger Inset', default=0.0, min=0.0, max=0.01, precision=4, unit='LENGTH') + + + .. py:attribute:: drawer_plate_thickness + :type: FloatProperty(name='Drawer Plate Thickness', default=0.00477, min=0.001, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: drawer_hole_diameter + :type: FloatProperty(name='Drawer Hole Diameter', default=0.02, min=1e-05, max=0.5, precision=4, unit='LENGTH') + + + .. py:attribute:: drawer_hole_offset + :type: FloatProperty(name='Drawer Hole Offset', default=0.0, min=-0.5, max=0.5, precision=4, unit='LENGTH') + + + .. py:attribute:: overcut + :type: BoolProperty(name='Add Overcut', default=False) + + + .. py:attribute:: overcut_diameter + :type: FloatProperty(name='Overcut Tool Diameter', default=0.003175, min=-0.001, max=0.5, precision=4, unit='LENGTH') + + + .. py:method:: draw(context) + + Draw the user interface properties for the object. + + This method is responsible for rendering the layout of various + properties related to the object's dimensions and specifications. It + adds properties such as depth, width, height, finger size, finger + tolerance, finger inset, drawer plate thickness, drawer hole diameter, + drawer hole offset, and overcut diameter to the layout. The overcut + diameter property is only added if the overcut option is enabled. + + :param context: The context in which the drawing occurs, typically containing + information about the current state and environment. + + + + .. py:method:: execute(context) + + Execute the drawer creation process in Blender. + + This method orchestrates the creation of a drawer by calculating the + necessary dimensions for the finger joints, creating the base plate, and + generating the drawer components such as the back, front, sides, and + bottom. It utilizes various helper functions to perform operations like + boolean differences and transformations to achieve the desired geometry. + The method also handles the placement of the drawer components in the 3D + space. + + :param context: The Blender context that provides access to the current scene and + objects. + :type context: bpy.context + + :returns: + + A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: CamCurvePuzzle + + Bases: :py:obj:`bpy.types.Operator` + + + Generates Puzzle Joints and Interlocks + + + .. py:attribute:: bl_idname + :value: 'object.curve_puzzle' + + + + .. py:attribute:: bl_label + :value: 'Puzzle Joints' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: diameter + :type: FloatProperty(name='Tool Diameter', default=0.003175, min=0.001, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_tolerance + :type: FloatProperty(name='Finger Play Room', default=5e-05, min=0, max=0.003, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_amount + :type: IntProperty(name='Finger Amount', default=1, min=0, max=100) + + + .. py:attribute:: stem_size + :type: IntProperty(name='Size of the Stem', default=2, min=1, max=200) + + + .. py:attribute:: width + :type: FloatProperty(name='Width', default=0.1, min=0.005, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: height + :type: FloatProperty(name='Height or Thickness', default=0.025, min=0.005, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: angle + :type: FloatProperty(name='Angle A', default=pi / 4, min=-10, max=10, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: angleb + :type: FloatProperty(name='Angle B', default=pi / 4, min=-10, max=10, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: radius + :type: FloatProperty(name='Arc Radius', default=0.025, min=0.005, max=5, precision=4, unit='LENGTH') + + + .. py:attribute:: interlock_type + :type: EnumProperty(name='Type of Shape', items=(('JOINT', 'Joint', 'Puzzle Joint interlock'), ('BAR', 'Bar', 'Bar interlock'), ('ARC', 'Arc', 'Arc interlock'), ('MULTIANGLE', 'Multi angle', 'Multi angle joint'), ('CURVEBAR', 'Arc Bar', 'Arc Bar interlock'), ('CURVEBARCURVE', 'Arc Bar Arc', 'Arc Bar Arc interlock'), ('CURVET', 'T curve', 'T curve interlock'), ('T', 'T Bar', 'T Bar interlock'), ('CORNER', 'Corner Bar', 'Corner Bar interlock'), ('TILE', 'Tile', 'Tile interlock'), ('OPENCURVE', 'Open Curve', 'Corner Bar interlock')), description='Type of interlock', default='CURVET') + + + .. py:attribute:: gender + :type: EnumProperty(name='Type Gender', items=(('MF', 'Male-Receptacle', 'Male and receptacle'), ('F', 'Receptacle only', 'Receptacle'), ('M', 'Male only', 'Male')), description='Type of interlock', default='MF') + + + .. py:attribute:: base_gender + :type: EnumProperty(name='Base Gender', items=(('MF', 'Male - Receptacle', 'Male - Receptacle'), ('F', 'Receptacle', 'Receptacle'), ('M', 'Male', 'Male')), description='Type of interlock', default='M') + + + .. py:attribute:: multiangle_gender + :type: EnumProperty(name='Multiangle Gender', items=(('MMF', 'Male Male Receptacle', 'M M F'), ('MFF', 'Male Receptacle Receptacle', 'M F F')), description='Type of interlock', default='MFF') + + + .. py:attribute:: mitre + :type: BoolProperty(name='Add Mitres', default=False) + + + .. py:attribute:: twist_lock + :type: BoolProperty(name='Add TwistLock', default=False) + + + .. py:attribute:: twist_thick + :type: FloatProperty(name='Twist Thickness', default=0.0047, min=0.001, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: twist_percent + :type: FloatProperty(name='Twist Neck', default=0.3, min=0.1, max=0.9, precision=4) + + + .. py:attribute:: twist_keep + :type: BoolProperty(name='Keep Twist Holes', default=False) + + + .. py:attribute:: twist_line + :type: BoolProperty(name='Add Twist to Bar', default=False) + + + .. py:attribute:: twist_line_amount + :type: IntProperty(name='Amount of Separators', default=2, min=1, max=600) + + + .. py:attribute:: twist_separator + :type: BoolProperty(name='Add Twist Separator', default=False) + + + .. py:attribute:: twist_separator_amount + :type: IntProperty(name='Amount of Separators', default=2, min=2, max=600) + + + .. py:attribute:: twist_separator_spacing + :type: FloatProperty(name='Separator Spacing', default=0.025, min=-0.004, max=1.0, precision=4, unit='LENGTH') + + + .. py:attribute:: twist_separator_edge_distance + :type: FloatProperty(name='Separator Edge Distance', default=0.01, min=0.0005, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: tile_x_amount + :type: IntProperty(name='Amount of X Fingers', default=2, min=1, max=600) + + + .. py:attribute:: tile_y_amount + :type: IntProperty(name='Amount of Y Fingers', default=2, min=1, max=600) + + + .. py:attribute:: interlock_amount + :type: IntProperty(name='Interlock Amount on Curve', default=2, min=0, max=200) + + + .. py:attribute:: overcut + :type: BoolProperty(name='Add Overcut', default=False) + + + .. py:attribute:: overcut_diameter + :type: FloatProperty(name='Overcut Tool Diameter', default=0.003175, min=-0.001, max=0.5, precision=4, unit='LENGTH') + + + .. py:method:: draw(context) + + Draws the user interface layout for interlock type properties. + + This method is responsible for creating and displaying the layout of + various properties related to different interlock types in the user + interface. It dynamically adjusts the layout based on the selected + interlock type, allowing users to input relevant parameters such as + dimensions, tolerances, and other characteristics specific to the chosen + interlock type. + + :param context: The context in which the layout is being drawn, typically + provided by the user interface framework. + + :returns: + + This method does not return any value; it modifies the layout + directly. + :rtype: None + + + + .. py:method:: execute(context) + + Execute the puzzle joinery process based on the provided context. + + This method processes the selected objects in the given context to + perform various types of puzzle joinery operations. It first checks if + there are any selected objects and if the active object is a curve. If + so, it duplicates the object, applies transformations, and converts it + to a mesh. The method then extracts vertex coordinates and performs + different joinery operations based on the specified interlock type. + Supported interlock types include 'FINGER', 'JOINT', 'BAR', 'ARC', + 'CURVEBARCURVE', 'CURVEBAR', 'MULTIANGLE', 'T', 'CURVET', 'CORNER', + 'TILE', and 'OPENCURVE'. + + :param context: The context containing selected objects and the active object. + :type context: Context + + :returns: A dictionary indicating the completion status of the operation. + :rtype: dict + + + +.. py:class:: CamCurveGear + + Bases: :py:obj:`bpy.types.Operator` + + + Generates Involute Gears // version 1.1 by Leemon Baird, 2011, Leemon@Leemon.com + http://www.thingiverse.com/thing:5505 + + + .. py:attribute:: bl_idname + :value: 'object.curve_gear' + + + + .. py:attribute:: bl_label + :value: 'Gears' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: tooth_spacing + :type: FloatProperty(name='Distance per Tooth', default=0.01, min=0.001, max=1.0, precision=4, unit='LENGTH') + + + .. py:attribute:: tooth_amount + :type: IntProperty(name='Amount of Teeth', default=7, min=4) + + + .. py:attribute:: spoke_amount + :type: IntProperty(name='Amount of Spokes', default=4, min=0) + + + .. py:attribute:: hole_diameter + :type: FloatProperty(name='Hole Diameter', default=0.003175, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: rim_size + :type: FloatProperty(name='Rim Size', default=0.003175, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: hub_diameter + :type: FloatProperty(name='Hub Diameter', default=0.005, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: pressure_angle + :type: FloatProperty(name='Pressure Angle', default=radians(20), min=0.001, max=pi / 2, precision=4, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: clearance + :type: FloatProperty(name='Clearance', default=0.0, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: backlash + :type: FloatProperty(name='Backlash', default=0.0, min=0.0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: rack_height + :type: FloatProperty(name='Rack Height', default=0.012, min=0.001, max=1, precision=4, unit='LENGTH') + + + .. py:attribute:: rack_tooth_per_hole + :type: IntProperty(name='Teeth per Mounting Hole', default=7, min=2) + + + .. py:attribute:: gear_type + :type: EnumProperty(name='Type of Gear', items=(('PINION', 'Pinion', 'Circular Gear'), ('RACK', 'Rack', 'Straight Rack')), description='Type of gear', default='PINION') + + + .. py:method:: draw(context) + + Draw the user interface properties for gear settings. + + This method sets up the layout for various gear parameters based on the + selected gear type. It dynamically adds properties to the layout for + different gear types, allowing users to input specific values for gear + design. The properties include gear type, tooth spacing, tooth amount, + hole diameter, pressure angle, and backlash. Additional properties are + displayed if the gear type is 'PINION' or 'RACK'. + + :param context: The context in which the layout is being drawn. + + + + .. py:method:: execute(context) + + Execute the gear generation process based on the specified gear type. + + This method checks the type of gear to be generated (either 'PINION' or + 'RACK') and calls the appropriate function from the `involute_gear` + module to create the gear or rack with the specified parameters. The + parameters include tooth spacing, number of teeth, hole diameter, + pressure angle, clearance, backlash, rim size, hub diameter, and spoke + amount for pinion gears, and additional parameters for rack gears. + + :param context: The context in which the execution is taking place. + + :returns: + + A dictionary indicating that the operation has finished with a key + 'FINISHED'. + :rtype: dict + + + diff --git a/_sources/autoapi/cam/curvecamequation/index.rst b/_sources/autoapi/cam/curvecamequation/index.rst new file mode 100644 index 000000000..c93e557d3 --- /dev/null +++ b/_sources/autoapi/cam/curvecamequation/index.rst @@ -0,0 +1,280 @@ +cam.curvecamequation +==================== + +.. py:module:: cam.curvecamequation + +.. autoapi-nested-parse:: + + CNC CAM 'curvecamequation.py' © 2021, 2022 Alain Pelletier + + Operators to create a number of geometric shapes with curves. + + + +Classes +------- + +.. autoapisummary:: + + cam.curvecamequation.CamSineCurve + cam.curvecamequation.CamLissajousCurve + cam.curvecamequation.CamHypotrochoidCurve + cam.curvecamequation.CamCustomCurve + + +Functions +--------- + +.. autoapisummary:: + + cam.curvecamequation.triangle + cam.curvecamequation.ssine + + +Module Contents +--------------- + +.. py:class:: CamSineCurve + + Bases: :py:obj:`bpy.types.Operator` + + + Object Sine + + + .. py:attribute:: bl_idname + :value: 'object.sine' + + + + .. py:attribute:: bl_label + :value: 'Periodic Wave' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: axis + :type: EnumProperty(name='Displacement Axis', items=(('XY', 'Y to displace X axis', 'Y constant; X sine displacement'), ('YX', 'X to displace Y axis', 'X constant; Y sine displacement'), ('ZX', 'X to displace Z axis', 'X constant; Y sine displacement'), ('ZY', 'Y to displace Z axis', 'X constant; Y sine displacement')), default='ZX') + + + .. py:attribute:: wave + :type: EnumProperty(name='Wave', items=(('sine', 'Sine Wave', 'Sine Wave'), ('triangle', 'Triangle Wave', 'triangle wave'), ('cycloid', 'Cycloid', 'Sine wave rectification'), ('invcycloid', 'Inverse Cycloid', 'Sine wave rectification')), default='sine') + + + .. py:attribute:: amplitude + :type: FloatProperty(name='Amplitude', default=0.01, min=0, max=10, precision=4, unit='LENGTH') + + + .. py:attribute:: period + :type: FloatProperty(name='Period', default=0.5, min=0.001, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: beatperiod + :type: FloatProperty(name='Beat Period Offset', default=0.0, min=0.0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: shift + :type: FloatProperty(name='Phase Shift', default=0, min=-360, max=360, precision=4, unit='ROTATION') + + + .. py:attribute:: offset + :type: FloatProperty(name='Offset', default=0, min=-1.0, max=1, precision=4, unit='LENGTH') + + + .. py:attribute:: iteration + :type: IntProperty(name='Iteration', default=100, min=50, max=2000) + + + .. py:attribute:: maxt + :type: FloatProperty(name='Wave Ends at X', default=0.5, min=-3.0, max=3, precision=4, unit='LENGTH') + + + .. py:attribute:: mint + :type: FloatProperty(name='Wave Starts at X', default=0, min=-3.0, max=3, precision=4, unit='LENGTH') + + + .. py:attribute:: wave_distance + :type: FloatProperty(name='Distance Between Multiple Waves', default=0.0, min=0.0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: wave_angle_offset + :type: FloatProperty(name='Angle Offset for Multiple Waves', default=pi / 2, min=-200 * pi, max=200 * pi, precision=4, unit='ROTATION') + + + .. py:attribute:: wave_amount + :type: IntProperty(name='Amount of Multiple Waves', default=1, min=1, max=2000) + + + .. py:method:: execute(context) + + +.. py:class:: CamLissajousCurve + + Bases: :py:obj:`bpy.types.Operator` + + + Lissajous + + + .. py:attribute:: bl_idname + :value: 'object.lissajous' + + + + .. py:attribute:: bl_label + :value: 'Lissajous Figure' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: amplitude_A + :type: FloatProperty(name='Amplitude A', default=0.1, min=0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: waveA + :type: EnumProperty(name='Wave X', items=(('sine', 'Sine Wave', 'Sine Wave'), ('triangle', 'Triangle Wave', 'triangle wave')), default='sine') + + + .. py:attribute:: amplitude_B + :type: FloatProperty(name='Amplitude B', default=0.1, min=0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: waveB + :type: EnumProperty(name='Wave Y', items=(('sine', 'Sine Wave', 'Sine Wave'), ('triangle', 'Triangle Wave', 'triangle wave')), default='sine') + + + .. py:attribute:: period_A + :type: FloatProperty(name='Period A', default=1.1, min=0.001, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: period_B + :type: FloatProperty(name='Period B', default=1.0, min=0.001, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: period_Z + :type: FloatProperty(name='Period Z', default=1.0, min=0.001, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: amplitude_Z + :type: FloatProperty(name='Amplitude Z', default=0.0, min=0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: shift + :type: FloatProperty(name='Phase Shift', default=0, min=-360, max=360, precision=4, unit='ROTATION') + + + .. py:attribute:: iteration + :type: IntProperty(name='Iteration', default=500, min=50, max=10000) + + + .. py:attribute:: maxt + :type: FloatProperty(name='Wave Ends at X', default=11, min=-3.0, max=1000000, precision=4, unit='LENGTH') + + + .. py:attribute:: mint + :type: FloatProperty(name='Wave Starts at X', default=0, min=-10.0, max=3, precision=4, unit='LENGTH') + + + .. py:method:: execute(context) + + +.. py:class:: CamHypotrochoidCurve + + Bases: :py:obj:`bpy.types.Operator` + + + Hypotrochoid + + + .. py:attribute:: bl_idname + :value: 'object.hypotrochoid' + + + + .. py:attribute:: bl_label + :value: 'Spirograph Type Figure' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: typecurve + :type: EnumProperty(name='Type of Curve', items=(('hypo', 'Hypotrochoid', 'Inside ring'), ('epi', 'Epicycloid', 'Outside inner ring'))) + + + .. py:attribute:: R + :type: FloatProperty(name='Big Circle Radius', default=0.25, min=0.001, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: r + :type: FloatProperty(name='Small Circle Radius', default=0.18, min=0.0001, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: d + :type: FloatProperty(name='Distance from Center of Interior Circle', default=0.05, min=0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: dip + :type: FloatProperty(name='Variable Depth from Center', default=0.0, min=-100, max=100, precision=4) + + + .. py:method:: execute(context) + + +.. py:class:: CamCustomCurve + + Bases: :py:obj:`bpy.types.Operator` + + + Object Custom Curve + + + .. py:attribute:: bl_idname + :value: 'object.customcurve' + + + + .. py:attribute:: bl_label + :value: 'Custom Curve' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: xstring + :type: StringProperty(name='X Equation', description='Equation x=F(t)', default='t') + + + .. py:attribute:: ystring + :type: StringProperty(name='Y Equation', description='Equation y=F(t)', default='0') + + + .. py:attribute:: zstring + :type: StringProperty(name='Z Equation', description='Equation z=F(t)', default='0.05*sin(2*pi*4*t)') + + + .. py:attribute:: iteration + :type: IntProperty(name='Iteration', default=100, min=50, max=2000) + + + .. py:attribute:: maxt + :type: FloatProperty(name='Wave Ends at X', default=0.5, min=-3.0, max=10, precision=4, unit='LENGTH') + + + .. py:attribute:: mint + :type: FloatProperty(name='Wave Starts at X', default=0, min=-3.0, max=3, precision=4, unit='LENGTH') + + + .. py:method:: execute(context) + + +.. py:function:: triangle(i, T, A) + +.. py:function:: ssine(A, T, dc_offset=0, phase_shift=0) + diff --git a/_sources/autoapi/cam/curvecamtools/index.rst b/_sources/autoapi/cam/curvecamtools/index.rst new file mode 100644 index 000000000..cd2e46c60 --- /dev/null +++ b/_sources/autoapi/cam/curvecamtools/index.rst @@ -0,0 +1,432 @@ +cam.curvecamtools +================= + +.. py:module:: cam.curvecamtools + +.. autoapi-nested-parse:: + + CNC CAM 'curvecamtools.py' © 2012 Vilem Novak, 2021 Alain Pelletier + + Operators that perform various functions on existing curves. + + + +Classes +------- + +.. autoapisummary:: + + cam.curvecamtools.CamCurveBoolean + cam.curvecamtools.CamCurveConvexHull + cam.curvecamtools.CamCurveIntarsion + cam.curvecamtools.CamCurveOvercuts + cam.curvecamtools.CamCurveOvercutsB + cam.curvecamtools.CamCurveRemoveDoubles + cam.curvecamtools.CamMeshGetPockets + cam.curvecamtools.CamOffsetSilhouete + cam.curvecamtools.CamObjectSilhouete + + +Module Contents +--------------- + +.. py:class:: CamCurveBoolean + + Bases: :py:obj:`bpy.types.Operator` + + + Perform Boolean Operation on Two or More Curves + + + .. py:attribute:: bl_idname + :value: 'object.curve_boolean' + + + + .. py:attribute:: bl_label + :value: 'Curve Boolean' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: boolean_type + :type: EnumProperty(name='Type', items=(('UNION', 'Union', ''), ('DIFFERENCE', 'Difference', ''), ('INTERSECT', 'Intersect', '')), description='Boolean type', default='UNION') + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + + .. py:method:: invoke(context, event) + + +.. py:class:: CamCurveConvexHull + + Bases: :py:obj:`bpy.types.Operator` + + + Perform Hull Operation on Single or Multiple Curves + + + .. py:attribute:: bl_idname + :value: 'object.convex_hull' + + + + .. py:attribute:: bl_label + :value: 'Convex Hull' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + +.. py:class:: CamCurveIntarsion + + Bases: :py:obj:`bpy.types.Operator` + + + Makes Curve Cuttable Both Inside and Outside, for Intarsion and Joints + + + .. py:attribute:: bl_idname + :value: 'object.curve_intarsion' + + + + .. py:attribute:: bl_label + :value: 'Intarsion' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: diameter + :type: FloatProperty(name='Cutter Diameter', default=0.001, min=0, max=0.025, precision=4, unit='LENGTH') + + + .. py:attribute:: tolerance + :type: FloatProperty(name='Cutout Tolerance', default=0.0001, min=0, max=0.005, precision=4, unit='LENGTH') + + + .. py:attribute:: backlight + :type: FloatProperty(name='Backlight Seat', default=0.0, min=0, max=0.01, precision=4, unit='LENGTH') + + + .. py:attribute:: perimeter_cut + :type: FloatProperty(name='Perimeter Cut Offset', default=0.0, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: base_thickness + :type: FloatProperty(name='Base Material Thickness', default=0.0, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: intarsion_thickness + :type: FloatProperty(name='Intarsion Material Thickness', default=0.0, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: backlight_depth_from_top + :type: FloatProperty(name='Backlight Well Depth', default=0.0, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + + .. py:method:: invoke(context, event) + + +.. py:class:: CamCurveOvercuts + + Bases: :py:obj:`bpy.types.Operator` + + + Adds Overcuts for Slots + + + .. py:attribute:: bl_idname + :value: 'object.curve_overcuts' + + + + .. py:attribute:: bl_label + :value: 'Add Overcuts - A' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: diameter + :type: FloatProperty(name='Diameter', default=0.003175, min=0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: threshold + :type: FloatProperty(name='Threshold', default=pi / 2 * 0.99, min=-3.14, max=3.14, precision=4, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: do_outer + :type: BoolProperty(name='Outer Polygons', default=True) + + + .. py:attribute:: invert + :type: BoolProperty(name='Invert', default=False) + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + + .. py:method:: invoke(context, event) + + +.. py:class:: CamCurveOvercutsB + + Bases: :py:obj:`bpy.types.Operator` + + + Adds Overcuts for Slots + + + .. py:attribute:: bl_idname + :value: 'object.curve_overcuts_b' + + + + .. py:attribute:: bl_label + :value: 'Add Overcuts - B' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: diameter + :type: FloatProperty(name='Tool Diameter', default=0.003175, description='Tool bit diameter used in cut operation', min=0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: style + :type: EnumProperty(name='Style', items=(('OPEDGE', 'opposite edge', 'place corner overcuts on opposite edges'), ('DOGBONE', 'Dog-bone / Corner Point', 'place overcuts at center of corners'), ('TBONE', 'T-bone', 'place corner overcuts on the same edge')), default='DOGBONE', description='style of overcut to use') + + + .. py:attribute:: threshold + :type: FloatProperty(name='Max Inside Angle', default=pi / 2, min=-3.14, max=3.14, description='The maximum angle to be considered as an inside corner', precision=4, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: do_outer + :type: BoolProperty(name='Include Outer Curve', description='Include the outer curve if there are curves inside', default=True) + + + .. py:attribute:: do_invert + :type: BoolProperty(name='Invert', description='invert overcut operation on all curves', default=True) + + + .. py:attribute:: otherEdge + :type: BoolProperty(name='Other Edge', description='change to the other edge for the overcut to be on', default=False) + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + + .. py:method:: invoke(context, event) + + +.. py:class:: CamCurveRemoveDoubles + + Bases: :py:obj:`bpy.types.Operator` + + + Curve Remove Doubles + + + .. py:attribute:: bl_idname + :value: 'object.curve_remove_doubles' + + + + .. py:attribute:: bl_label + :value: 'Remove Curve Doubles' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: merge_distance + :type: FloatProperty(name='Merge distance', default=0.0001, min=0, max=0.01) + + + .. py:attribute:: keep_bezier + :type: BoolProperty(name='Keep bezier', default=False) + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + + .. py:method:: draw(context) + + + .. py:method:: invoke(context, event) + + +.. py:class:: CamMeshGetPockets + + Bases: :py:obj:`bpy.types.Operator` + + + Detect Pockets in a Mesh and Extract Them as Curves + + + .. py:attribute:: bl_idname + :value: 'object.mesh_get_pockets' + + + + .. py:attribute:: bl_label + :value: 'Get Pocket Surfaces' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: threshold + :type: FloatProperty(name='Horizontal Threshold', description='How horizontal the surface must be for a pocket: 1.0 perfectly flat, 0.0 is any orientation', default=0.99, min=0, max=1.0, precision=4) + + + .. py:attribute:: zlimit + :type: FloatProperty(name='Z Limit', description='Maximum z height considered for pocket operation, default is 0.0', default=0.0, min=-1000.0, max=1000.0, precision=4, unit='LENGTH') + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + +.. py:class:: CamOffsetSilhouete + + Bases: :py:obj:`bpy.types.Operator` + + + Curve Offset Operation + + + .. py:attribute:: bl_idname + :value: 'object.silhouete_offset' + + + + .. py:attribute:: bl_label + :value: 'Silhouette & Offset' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: offset + :type: FloatProperty(name='Offset', default=0.003, min=-100, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: mitrelimit + :type: FloatProperty(name='Mitre Limit', default=2, min=1e-08, max=20, precision=4, unit='LENGTH') + + + .. py:attribute:: style + :type: EnumProperty(name='Corner Type', items=(('1', 'Round', ''), ('2', 'Mitre', ''), ('3', 'Bevel', ''))) + + + .. py:attribute:: caps + :type: EnumProperty(name='Cap Type', items=(('round', 'Round', ''), ('square', 'Square', ''), ('flat', 'Flat', ''))) + + + .. py:attribute:: align + :type: EnumProperty(name='Alignment', items=(('worldxy', 'World XY', ''), ('bottom', 'Base Bottom', ''), ('top', 'Base Top', ''))) + + + .. py:attribute:: opentype + :type: EnumProperty(name='Curve Type', items=(('dilate', 'Dilate open curve', ''), ('leaveopen', 'Leave curve open', ''), ('closecurve', 'Close curve', '')), default='closecurve') + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: isStraight(geom) + + + .. py:method:: execute(context) + + + .. py:method:: draw(context) + + + .. py:method:: invoke(context, event) + + +.. py:class:: CamObjectSilhouete + + Bases: :py:obj:`bpy.types.Operator` + + + Object Silhouette + + + .. py:attribute:: bl_idname + :value: 'object.silhouete' + + + + .. py:attribute:: bl_label + :value: 'Object Silhouette' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + diff --git a/_sources/autoapi/cam/engine/index.rst b/_sources/autoapi/cam/engine/index.rst new file mode 100644 index 000000000..f3f0ee6c0 --- /dev/null +++ b/_sources/autoapi/cam/engine/index.rst @@ -0,0 +1,68 @@ +cam.engine +========== + +.. py:module:: cam.engine + +.. autoapi-nested-parse:: + + CNCCAM 'engine.py' + + Engine definition, options and panels. + + + +Classes +------- + +.. autoapisummary:: + + cam.engine.CNCCAM_ENGINE + + +Functions +--------- + +.. autoapisummary:: + + cam.engine.get_panels + + +Module Contents +--------------- + +.. py:class:: CNCCAM_ENGINE + + Bases: :py:obj:`bpy.types.RenderEngine` + + + .. py:attribute:: bl_idname + :value: 'CNCCAM_RENDER' + + + + .. py:attribute:: bl_label + :value: 'CNC CAM' + + + + .. py:attribute:: bl_use_eevee_viewport + :value: True + + + +.. py:function:: get_panels() + + Retrieve a list of panels for the Blender UI. + + This function compiles a list of UI panels that are compatible with the + Blender rendering engine. It excludes certain predefined panels that are + not relevant for the current context. The function checks all subclasses + of the `bpy.types.Panel` and includes those that have the + `COMPAT_ENGINES` attribute set to include 'BLENDER_RENDER', provided + they are not in the exclusion list. + + :returns: A list of panel classes that are compatible with the + Blender rendering engine, excluding specified panels. + :rtype: list + + diff --git a/_sources/autoapi/cam/exception/index.rst b/_sources/autoapi/cam/exception/index.rst new file mode 100644 index 000000000..fc461ab54 --- /dev/null +++ b/_sources/autoapi/cam/exception/index.rst @@ -0,0 +1,32 @@ +cam.exception +============= + +.. py:module:: cam.exception + +.. autoapi-nested-parse:: + + CNC CAM 'exception.py' + + Generic CAM Exception class. + + + +Exceptions +---------- + +.. autoapisummary:: + + cam.exception.CamException + + +Module Contents +--------------- + +.. py:exception:: CamException + + Bases: :py:obj:`Exception` + + + Common base class for all non-exit exceptions. + + diff --git a/_sources/autoapi/cam/gcodeimportparser/index.rst b/_sources/autoapi/cam/gcodeimportparser/index.rst new file mode 100644 index 000000000..1ca67c630 --- /dev/null +++ b/_sources/autoapi/cam/gcodeimportparser/index.rst @@ -0,0 +1,454 @@ +cam.gcodeimportparser +===================== + +.. py:module:: cam.gcodeimportparser + +.. autoapi-nested-parse:: + + CNC CAM 'gcodeimportparser.py' + + Code modified from YAGV (Yet Another G-code Viewer) - https://github.com/jonathanwin/yagv + No license terms found in YAGV repo, will assume GNU release + + + +Attributes +---------- + +.. autoapisummary:: + + cam.gcodeimportparser.path + + +Classes +------- + +.. autoapisummary:: + + cam.gcodeimportparser.GcodeParser + cam.gcodeimportparser.GcodeModel + cam.gcodeimportparser.Segment + cam.gcodeimportparser.Layer + + +Functions +--------- + +.. autoapisummary:: + + cam.gcodeimportparser.import_gcode + cam.gcodeimportparser.segments_to_meshdata + cam.gcodeimportparser.obj_from_pydata + + +Module Contents +--------------- + +.. py:function:: import_gcode(context, filepath) + + Import G-code data into the scene. + + This function reads G-code from a specified file and processes it + according to the settings defined in the context. It utilizes the + GcodeParser to parse the file and classify segments of the model. + Depending on the options set in the scene, it may subdivide the model + and draw it with or without layer splitting. The time taken for the + import process is printed to the console. + + :param context: The context containing the scene and tool settings. + :type context: Context + :param filepath: The path to the G-code file to be imported. + :type filepath: str + + :returns: + + A dictionary indicating the import status, typically + {'FINISHED'}. + :rtype: dict + + +.. py:function:: segments_to_meshdata(segments) + + Convert a list of segments into mesh data consisting of vertices and + edges. + + This function processes a list of segment objects, extracting the + coordinates of vertices and defining edges based on the styles of the + segments. It identifies when to add vertices and edges based on whether + the segments are in 'extrude' or 'travel' styles. The resulting mesh + data can be used for 3D modeling or rendering applications. + + :param segments: A list of segment objects, each containing 'style' and + 'coords' attributes. + :type segments: list + + :returns: + + A tuple containing two elements: + - list: A list of vertices, where each vertex is represented as a + list of coordinates [X, Y, Z]. + - list: A list of edges, where each edge is represented as a list + of indices corresponding to the vertices. + :rtype: tuple + + +.. py:function:: obj_from_pydata(name, verts, edges=None, close=True, collection_name=None) + + Create a Blender object from provided vertex and edge data. + + This function generates a mesh object in Blender using the specified + vertices and edges. If edges are not provided, it automatically creates + a chain of edges connecting the vertices. The function also allows for + the option to close the mesh by connecting the last vertex back to the + first. Additionally, it can place the created object into a specified + collection within the Blender scene. The object is scaled down to a + smaller size for better visibility in the Blender environment. + + :param name: The name of the object to be created. + :type name: str + :param verts: A list of vertex coordinates, where each vertex is represented as a + tuple of (x, y, z). + :type verts: list + :param edges: A list of edges defined by pairs of vertex indices. Defaults to None. + :type edges: list? + :param close: Whether to close the mesh by connecting the last vertex to the first. + Defaults to True. + :type close: bool? + :param collection_name: The name of the collection to which the object should be added. Defaults + to None. + :type collection_name: str? + + :returns: + + The function does not return a value; it creates an object in the + Blender scene. + :rtype: None + + +.. py:class:: GcodeParser + + .. py:attribute:: comment + :value: '' + + + + .. py:attribute:: model + + + .. py:method:: parseFile(path) + + Parse a G-code file and update the model. + + This function reads a G-code file line by line, increments a line + counter for each line, and processes each line using the `parseLine` + method. The function assumes that the file is well-formed and that each + line can be parsed without errors. After processing all lines, it + returns the updated model. + + :param path: The file path to the G-code file to be parsed. + :type path: str + + :returns: The updated model after parsing the G-code file. + :rtype: model + + + + .. py:method:: parseLine() + + Parse a line of G-code and execute the corresponding command. + + This method processes a line of G-code by stripping comments, cleaning + the command, and identifying the command code and its arguments. It + handles specific G-code commands and invokes the appropriate parsing + method if available. If the command is unsupported, it prints an error + message. The method also manages tool numbers and coordinates based on + the parsed command. + + + + .. py:method:: parseArgs(args) + + Parse command-line arguments into a dictionary. + + This function takes a string of arguments, splits it into individual + components, and maps each component's first character to its + corresponding numeric value. If a numeric value cannot be converted from + the string, it defaults to 1. The resulting dictionary contains the + first characters as keys and their associated numeric values as values. + + :param args: A string of space-separated arguments, where each argument + consists of a letter followed by a numeric value. + :type args: str + + :returns: A dictionary mapping each letter to its corresponding numeric value. + :rtype: dict + + + + .. py:method:: parse_G1(args, type='G1') + + + .. py:method:: parse_G0(args, type='G0') + + + .. py:method:: parse_G90(args) + + + .. py:method:: parse_G91(args) + + + .. py:method:: parse_G92(args) + + + .. py:method:: warn(msg) + + + .. py:method:: error(msg) + + Log an error message and raise an exception. + + This method prints an error message to the console, including the line + number, the provided message, and the text associated with the error. + After logging the error, it raises a generic Exception with the same + message format. + + :param msg: The error message to be logged. + :type msg: str + + :raises Exception: Always raises an Exception with the formatted error message. + + + +.. py:class:: GcodeModel(parser) + + .. py:attribute:: parser + + + .. py:attribute:: relative + + + .. py:attribute:: offset + + + .. py:attribute:: isRelative + :value: False + + + + .. py:attribute:: color + :value: [0, 0, 0, 0, 0, 0, 0, 0] + + + + .. py:attribute:: toolnumber + :value: 0 + + + + .. py:attribute:: segments + :value: [] + + + + .. py:attribute:: layers + :value: [] + + + + .. py:method:: do_G1(args, type) + + Perform a rapid or controlled movement based on the provided arguments. + + This method updates the current coordinates based on the input + arguments, either in relative or absolute terms. It constructs a segment + representing the movement and adds it to the model if there are changes + in the XYZ coordinates. The function handles unknown axes by issuing a + warning and ensures that the segment is only added if there are actual + changes in position. + + :param args: A dictionary containing movement parameters for each axis. + :type args: dict + :param type: The type of movement (e.g., 'G0' for rapid move, 'G1' for controlled + move). + :type type: str + + + + .. py:method:: do_G92(args) + + Set the current position of the axes without moving. + + This method updates the current coordinates for the specified axes based + on the provided arguments. If no axes are mentioned, it sets all axes + (X, Y, Z) to zero. The method adjusts the offset values by transferring + the difference between the relative and specified values for each axis. + If an unknown axis is provided, a warning is issued. + + :param args: A dictionary containing axis names as keys + (e.g., 'X', 'Y', 'Z') and their corresponding + position values as float. + :type args: dict + + + + .. py:method:: do_M163(args) + + Update the color settings for a specific segment based on given + parameters. + + This method modifies the color attributes of an object by updating the + CMYKW values for a specified segment. It first creates a new list from + the existing color attribute to avoid reference issues. The method then + extracts the index and weight from the provided arguments and updates + the color list accordingly. Additionally, it retrieves RGB values from + the last comment and applies them to the color list. + + :param args: A dictionary containing the parameters for the operation. + - 'S' (int): The index of the segment to update. + - 'P' (float): The weight to set for the CMYKW color component. + :type args: dict + + :returns: This method does not return a value; it modifies the object's state. + :rtype: None + + + + .. py:method:: setRelative(isRelative) + + + .. py:method:: addSegment(segment) + + + .. py:method:: warn(msg) + + + .. py:method:: error(msg) + + + .. py:method:: classifySegments() + + Classify segments into layers based on their coordinates and extrusion + style. + + This method processes a list of segments, determining their extrusion + style (travel, retract, restore, or extrude) based on the movement of + the coordinates and the state of the extruder. It organizes the segments + into layers, which are used for later rendering. The classification is + based on changes in the Z-coordinate and the extruder's position. The + function initializes the coordinates and iterates through each segment, + checking for movements in the X, Y, and Z directions. It identifies when + a new layer begins based on changes in the Z-coordinate and the + extruder's state. Segments are then grouped into layers for further + processing. Raises: None + + + + .. py:method:: subdivide(subd_threshold) + + Subdivide segments based on a specified threshold. + + This method processes a list of segments and subdivides them into + smaller segments if the distance between consecutive segments exceeds + the given threshold. The subdivision is performed by interpolating + points between the original segment's coordinates, ensuring that the + resulting segments maintain the original order and properties. This is + particularly useful for manipulating attributes such as color and + continuous deformation in graphical representations. + + :param subd_threshold: The distance threshold for subdividing segments. + Segments with a distance greater than this value + will be subdivided. + :type subd_threshold: float + + :returns: The method modifies the instance's segments attribute in place. + :rtype: None + + + + .. py:method:: draw(split_layers=False) + + Draws a mesh from segments and layers. + + This function creates a Blender curve and vertex information in a text + file, which includes coordinates, style, and color. If the + `split_layers` parameter is set to True, it processes each layer + individually, generating vertices and edges for each layer. If False, it + processes the segments as a whole. + + :param split_layers: A flag indicating whether to split the drawing into + separate layers or not. + :type split_layers: bool + + + +.. py:class:: Segment(type, coords, color, toolnumber, lineNb, line) + + .. py:attribute:: type + + + .. py:attribute:: coords + + + .. py:attribute:: color + + + .. py:attribute:: toolnumber + + + .. py:attribute:: lineNb + + + .. py:attribute:: line + + + .. py:attribute:: style + :value: None + + + + .. py:attribute:: layerIdx + :value: None + + + + .. py:method:: __str__() + + Return a string representation of the object. + + This method constructs a string that includes the coordinates, line + number, style, layer index, and color of the object. It formats these + attributes into a readable string format for easier debugging and + logging. + + :returns: A formatted string representing the object's attributes. + :rtype: str + + + +.. py:class:: Layer(Z) + + .. py:attribute:: Z + + + .. py:attribute:: segments + :value: [] + + + + .. py:attribute:: distance + :value: None + + + + .. py:attribute:: extrudate + :value: None + + + + .. py:method:: __str__() + + +.. py:data:: path + :value: 'test.gcode' + + diff --git a/_sources/autoapi/cam/gcodepath/index.rst b/_sources/autoapi/cam/gcodepath/index.rst new file mode 100644 index 000000000..daa14670e --- /dev/null +++ b/_sources/autoapi/cam/gcodepath/index.rst @@ -0,0 +1,196 @@ +cam.gcodepath +============= + +.. py:module:: cam.gcodepath + +.. autoapi-nested-parse:: + + CNC CAM 'gcodepath.py' © 2012 Vilem Novak + + Generate and Export G-Code based on scene, machine, chain, operation and path settings. + + + +Functions +--------- + +.. autoapisummary:: + + cam.gcodepath.pointonline + cam.gcodepath.exportGcodePath + cam.gcodepath.getPath + cam.gcodepath.getChangeData + cam.gcodepath.checkMemoryLimit + cam.gcodepath.getPath3axis + cam.gcodepath.getPath4axis + + +Module Contents +--------------- + +.. py:function:: pointonline(a, b, c, tolerence) + + Determine if the angle between two vectors is within a specified + tolerance. + + This function checks if the angle formed by two vectors, defined by + points `b` and `c` relative to point `a`, is less than or equal to a + given tolerance. It converts the points into vectors, calculates the dot + product, and then computes the angle between them using the arccosine + function. If the angle exceeds the specified tolerance, the function + returns False; otherwise, it returns True. + + :param a: The origin point as a vector. + :type a: numpy.ndarray + :param b: The first point as a vector. + :type b: numpy.ndarray + :param c: The second point as a vector. + :type c: numpy.ndarray + :param tolerence: The maximum allowable angle (in degrees) between the vectors. + :type tolerence: float + + :returns: + + True if the angle between vectors b and c is within the specified + tolerance, + False otherwise. + :rtype: bool + + +.. py:function:: exportGcodePath(filename, vertslist, operations) + + Exports G-code using the Heeks NC Adopted Library. + + This function generates G-code from a list of vertices and operations + specified by the user. It handles various post-processor settings based + on the machine configuration and can split the output into multiple + files if the total number of operations exceeds a specified limit. The + G-code is tailored for different machine types and includes options for + tool changes, spindle control, and various movement commands. + + :param filename: The name of the file to which the G-code will be exported. + :type filename: str + :param vertslist: A list of mesh objects containing vertex data. + :type vertslist: list + :param operations: A list of operations to be performed, each containing + specific parameters for G-code generation. + :type operations: list + + :returns: This function does not return a value; it writes the G-code to a file. + :rtype: None + + +.. py:function:: getPath(context, operation) + :async: + + + Calculate the path for a given operation in a specified context. + + This function performs various calculations to determine the path based + on the operation's parameters and context. It checks for changes in the + operation's data and updates relevant tags accordingly. Depending on the + number of machine axes specified in the operation, it calls different + functions to handle 3-axis, 4-axis, or 5-axis operations. Additionally, + if automatic export is enabled, it exports the generated G-code path. + + :param context: The context in which the operation is being performed. + :param operation: An object representing the operation with various + attributes such as machine_axes, strategy, and + auto_export. + + +.. py:function:: getChangeData(o) + + Check if object properties have changed to determine if image updates + are needed. + + This function inspects the properties of objects specified by the input + parameter to see if any changes have occurred. It concatenates the + location, rotation, and dimensions of the relevant objects into a single + string, which can be used to determine if an image update is necessary + based on changes in the object's state. + + :param o: An object containing properties that specify the geometry source + and relevant object or collection names. + :type o: object + + :returns: + + A string representation of the location, rotation, and dimensions of + the specified objects. + :rtype: str + + +.. py:function:: checkMemoryLimit(o) + + Check and adjust the memory limit for an object. + + This function calculates the resolution of an object based on its + dimensions and the specified pixel size. If the calculated resolution + exceeds the defined memory limit, it adjusts the pixel size accordingly + to reduce the resolution. A warning message is appended to the object's + info if the pixel size is modified. + + :param o: An object containing properties such as max, min, optimisation, and + info. + :type o: object + + :returns: + + This function modifies the object's properties in place and does not + return a value. + :rtype: None + + +.. py:function:: getPath3axis(context, operation) + :async: + + + Generate a machining path based on the specified operation strategy. + + This function evaluates the provided operation's strategy and generates + the corresponding machining path. It supports various strategies such as + 'CUTOUT', 'CURVE', 'PROJECTED_CURVE', 'POCKET', and others. Depending on + the strategy, it performs specific calculations and manipulations on the + input data to create a path that can be used for machining operations. + The function handles different strategies by calling appropriate methods + from the `strategy` module and processes the path samples accordingly. + It also manages the generation of chunks, which represent segments of + the machining path, and applies any necessary transformations based on + the operation's parameters. + + :param context: The Blender context containing scene information. + :type context: bpy.context + :param operation: An object representing the machining operation, + which includes strategy and other relevant parameters. + :type operation: Operation + + :returns: This function does not return a value but modifies the state of + the operation and context directly. + :rtype: None + + +.. py:function:: getPath4axis(context, operation) + :async: + + + Generate a path for a specified axis based on the given operation. + + This function retrieves the bounds of the operation and checks the + strategy associated with the axis. If the strategy is one of the + specified types ('PARALLELR', 'PARALLEL', 'HELIX', 'CROSS'), it + generates path samples and processes them into chunks for meshing. The + function utilizes various helper functions to achieve this, including + obtaining layers and sampling chunks. + + :param context: The context in which the operation is executed. + :param operation: An object that contains the strategy and other + necessary parameters for generating the path. + + :returns: + + This function does not return a value but modifies + the state of the operation by processing chunks for meshing. + :rtype: None + + diff --git a/_sources/autoapi/cam/image_utils/index.rst b/_sources/autoapi/cam/image_utils/index.rst new file mode 100644 index 000000000..e17109711 --- /dev/null +++ b/_sources/autoapi/cam/image_utils/index.rst @@ -0,0 +1,564 @@ +cam.image_utils +=============== + +.. py:module:: cam.image_utils + +.. autoapi-nested-parse:: + + CNC CAM 'image_utils.py' © 2012 Vilem Novak + + Functions to render, save, convert and analyze image data. + + + +Functions +--------- + +.. autoapisummary:: + + cam.image_utils.numpysave + cam.image_utils.getCircle + cam.image_utils.getCircleBinary + cam.image_utils.numpytoimage + cam.image_utils.imagetonumpy + cam.image_utils._offset_inner_loop + cam.image_utils.offsetArea + cam.image_utils.dilateAr + cam.image_utils.getOffsetImageCavities + cam.image_utils.imageEdgeSearch_online + cam.image_utils.crazyPath + cam.image_utils.buildStroke + cam.image_utils.testStroke + cam.image_utils.applyStroke + cam.image_utils.testStrokeBinary + cam.image_utils.crazyStrokeImage + cam.image_utils.crazyStrokeImageBinary + cam.image_utils.imageToChunks + cam.image_utils.imageToShapely + cam.image_utils.getSampleImage + cam.image_utils.getResolution + cam.image_utils._backup_render_settings + cam.image_utils._restore_render_settings + cam.image_utils.renderSampleImage + cam.image_utils.prepareArea + cam.image_utils.getCutterArray + + +Module Contents +--------------- + +.. py:function:: numpysave(a, iname) + + Save a NumPy array as an image file in OpenEXR format. + + This function converts a NumPy array into an image and saves it using + Blender's rendering capabilities. It sets the image format to OpenEXR + with black and white color mode and a color depth of 32 bits. The image + is saved to the specified filename. + + :param a: The NumPy array to be converted and saved as an image. + :type a: numpy.ndarray + :param iname: The file path where the image will be saved. + :type iname: str + + +.. py:function:: getCircle(r, z) + + Generate a 2D array representing a circle. + + This function creates a 2D NumPy array filled with a specified value for + points that fall within a circle of a given radius. The circle is + centered in the array, and the function uses the Euclidean distance to + determine which points are inside the circle. The resulting array has + dimensions that are twice the radius, ensuring that the entire circle + fits within the array. + + :param r: The radius of the circle. + :type r: int + :param z: The value to fill the points inside the circle. + :type z: float + + :returns: A 2D array where points inside the circle are filled + with the value `z`, and points outside are filled with -10. + :rtype: numpy.ndarray + + +.. py:function:: getCircleBinary(r) + + Generate a binary representation of a circle in a 2D grid. + + This function creates a 2D boolean array where the elements inside a + circle of radius `r` are set to `True`, and the elements outside the + circle are set to `False`. The circle is centered in the middle of the + array, which has dimensions of (2*r, 2*r). The function iterates over + each point in the grid and checks if it lies within the specified + radius. + + :param r: The radius of the circle. + :type r: int + + :returns: A 2D boolean array representing the circle. + :rtype: numpy.ndarray + + +.. py:function:: numpytoimage(a, iname) + + Convert a NumPy array to a Blender image. + + This function takes a NumPy array and converts it into a Blender image. + It first checks if an image with the specified name and dimensions + already exists in Blender. If it does not exist, a new image is created + with the specified name and dimensions. The pixel data from the NumPy + array is then reshaped and assigned to the image's pixel buffer. + + :param a: A 2D NumPy array representing the image data. + :type a: numpy.ndarray + :param iname: The name to assign to the created or found image. + :type iname: str + + :returns: The Blender image object that was created or found. + :rtype: bpy.types.Image + + +.. py:function:: imagetonumpy(i) + + Convert a Blender image to a NumPy array. + + This function takes a Blender image object and converts its pixel data + into a NumPy array. It retrieves the pixel data, reshapes it, and swaps + the axes to match the expected format for further processing. The + function also measures the time taken for the conversion and prints it + to the console. + + :param i: A Blender image object containing pixel data. + :type i: Image + + :returns: A 2D NumPy array representing the image pixels. + :rtype: numpy.ndarray + + +.. py:function:: _offset_inner_loop(y1, y2, cutterArrayNan, cwidth, sourceArray, width, height, comparearea) + + Offset the inner loop for processing a specified area in a 2D array. + + This function iterates over a specified range of rows and columns in a + 2D array, calculating the maximum value from a source array combined + with a cutter array for each position in the defined area. The results + are stored in the comparearea array, which is updated with the maximum + values found. + + :param y1: The starting index for the row iteration. + :type y1: int + :param y2: The ending index for the row iteration. + :type y2: int + :param cutterArrayNan: A 2D array used for modifying the source array. + :type cutterArrayNan: numpy.ndarray + :param cwidth: The width of the area to consider for the maximum calculation. + :type cwidth: int + :param sourceArray: The source 2D array from which maximum values are derived. + :type sourceArray: numpy.ndarray + :param width: The width of the source array. + :type width: int + :param height: The height of the source array. + :type height: int + :param comparearea: A 2D array where the calculated maximum values are stored. + :type comparearea: numpy.ndarray + + :returns: + + This function modifies the comparearea in place and does not return a + value. + :rtype: None + + +.. py:function:: offsetArea(o, samples) + :async: + + + Offsets the whole image with the cutter and skin offsets. + + This function modifies the offset image based on the provided cutter and + skin offsets. It calculates the dimensions of the source and cutter + arrays, initializes an offset image, and processes the image in + segments. The function handles the inversion of the source array if + specified and updates the offset image accordingly. Progress is reported + asynchronously during processing. + + :param o: An object containing properties such as `update_offsetimage_tag`, + `min`, `max`, `inverse`, and `offset_image`. + :param samples: A 2D array representing the source image data. + :type samples: numpy.ndarray + + :returns: The updated offset image after applying the cutter and skin offsets. + :rtype: numpy.ndarray + + +.. py:function:: dilateAr(ar, cycles) + + Dilate a binary array using a specified number of cycles. + + This function performs a dilation operation on a 2D binary array. For + each cycle, it updates the array by applying a logical OR operation + between the current array and its neighboring elements. The dilation + effect expands the boundaries of the foreground (True) pixels in the + binary array. + + :param ar: A 2D binary array (numpy array) where + dilation will be applied. + :type ar: numpy.ndarray + :param cycles: The number of dilation cycles to perform. + :type cycles: int + + :returns: + + The function modifies the input array in place and does not + return a value. + :rtype: None + + +.. py:function:: getOffsetImageCavities(o, i) + + Detects areas in the offset image which are 'cavities' due to curvature + changes. + + This function analyzes the input image to identify regions where the + curvature changes, indicating the presence of cavities. It computes + vertical and horizontal differences in pixel values to detect edges and + applies a threshold to filter out insignificant changes. The resulting + areas are then processed to remove any chunks that do not meet the + minimum criteria for cavity detection. The function returns a list of + valid chunks that represent the detected cavities. + + :param o: An object containing parameters and thresholds for the detection + process. + :param i: A 2D array representing the image data to be analyzed. + :type i: numpy.ndarray + + :returns: A list of detected chunks representing the cavities in the image. + :rtype: list + + +.. py:function:: imageEdgeSearch_online(o, ar, zimage) + + Search for edges in an image using a pencil strategy. + + This function implements an edge detection algorithm that simulates a + pencil-like movement across the image represented by a 2D array. It + identifies white pixels and builds chunks of points based on the + detected edges. The algorithm iteratively explores possible directions + to find and track the edges until a specified condition is met, such as + exhausting the available white pixels or reaching a maximum number of + tests. + + :param o: An object containing parameters such as min, max coordinates, cutter + diameter, + border width, and optimisation settings. + :type o: object + :param ar: A 2D array representing the image where edge detection is to be + performed. + :type ar: numpy.ndarray + :param zimage: A 2D array representing the z-coordinates corresponding to the image. + :type zimage: numpy.ndarray + + :returns: A list of chunks representing the detected edges in the image. + :rtype: list + + +.. py:function:: crazyPath(o) + :async: + + + Execute a greedy adaptive algorithm for path planning. + + This function prepares an area based on the provided object `o`, + calculates the dimensions of the area, and initializes a mill image and + cutter array. The dimensions are determined by the maximum and minimum + coordinates of the object, adjusted by the simulation detail and border + width. The function is currently a stub and requires further + implementation. + + :param o: An object containing properties such as max, min, optimisation, and + borderwidth. + :type o: object + + :returns: This function does not return a value. + :rtype: None + + +.. py:function:: buildStroke(start, end, cutterArray) + + Build a stroke array based on start and end points. + + This function generates a 2D stroke array that represents a stroke from + a starting point to an ending point. It calculates the length of the + stroke and creates a grid that is filled based on the positions defined + by the start and end coordinates. The function uses a cutter array to + determine how the stroke interacts with the grid. + + :param start: A tuple representing the starting coordinates (x, y, z). + :type start: tuple + :param end: A tuple representing the ending coordinates (x, y, z). + :type end: tuple + :param cutterArray: An object that contains size information used to modify + the stroke array. + + :returns: + + A 2D array representing the stroke, filled with + calculated values based on the input parameters. + :rtype: numpy.ndarray + + +.. py:function:: testStroke() + +.. py:function:: applyStroke() + +.. py:function:: testStrokeBinary(img, stroke) + +.. py:function:: crazyStrokeImage(o) + + Generate a toolpath for a milling operation using a crazy stroke + strategy. + + This function computes a path for a milling cutter based on the provided + parameters and the offset image. It utilizes a circular cutter + representation and evaluates potential cutting positions based on + various thresholds. The algorithm iteratively tests different angles and + lengths for the cutter's movement until the desired cutting area is + achieved or the maximum number of tests is reached. + + :param o: An object containing parameters such as cutter diameter, + optimization settings, movement type, and thresholds for + determining cutting effectiveness. + :type o: object + + :returns: + + A list of chunks representing the computed toolpath for the milling + operation. + :rtype: list + + +.. py:function:: crazyStrokeImageBinary(o, ar, avoidar) + + Perform a milling operation using a binary image representation. + + This function implements a strategy for milling by navigating through a + binary image. It starts from a defined point and attempts to move in + various directions, evaluating the cutter load to determine the + appropriate path. The algorithm continues until it either exhausts the + available pixels to cut or reaches a predefined limit on the number of + tests. The function modifies the input array to represent the areas that + have been milled and returns the generated path as a list of chunks. + + :param o: An object containing parameters for the milling operation, including + cutter diameter, thresholds, and movement type. + :type o: object + :param ar: A 2D binary array representing the image to be milled. + :type ar: numpy.ndarray + :param avoidar: A 2D binary array indicating areas to avoid during milling. + :type avoidar: numpy.ndarray + + :returns: + + A list of chunks representing the path taken during the milling + operation. + :rtype: list + + +.. py:function:: imageToChunks(o, image, with_border=False) + + Convert an image into chunks based on detected edges. + + This function processes a given image to identify edges and convert them + into polychunks, which are essentially collections of connected edge + segments. It utilizes the properties of the input object `o` to + determine the boundaries and size of the chunks. The function can + optionally include borders in the edge detection process. The output is + a list of chunks that represent the detected polygons in the image. + + :param o: An object containing properties such as min, max, borderwidth, + and optimisation settings. + :type o: object + :param image: A 2D array representing the image to be processed, + expected to be in a format compatible with uint8. + :type image: numpy.ndarray + :param with_border: A flag indicating whether to include borders + in the edge detection. Defaults to False. + :type with_border: bool? + + :returns: + + A list of chunks, where each chunk is represented as a collection of + points that outline the detected edges in the image. + :rtype: list + + +.. py:function:: imageToShapely(o, i, with_border=False) + + Convert an image to Shapely polygons. + + This function takes an image and converts it into a series of Shapely + polygon objects. It first processes the image into chunks and then + transforms those chunks into polygon geometries. The `with_border` + parameter allows for the inclusion of borders in the resulting polygons. + + :param o: The input image to be processed. + :param i: Additional input parameters for processing the image. + :param with_border: A flag indicating whether to include + borders in the resulting polygons. Defaults to False. + :type with_border: bool + + :returns: + + A list of Shapely polygon objects created from the + image chunks. + :rtype: list + + +.. py:function:: getSampleImage(s, sarray, minz) + + Get a sample image value from a 2D array based on given coordinates. + + This function retrieves a value from a 2D array by performing bilinear + interpolation based on the provided coordinates. It checks if the + coordinates are within the bounds of the array and calculates the + interpolated value accordingly. If the coordinates are out of bounds, it + returns -10. + + :param s: A tuple containing the x and y coordinates (float). + :type s: tuple + :param sarray: A 2D array from which to sample the image values. + :type sarray: numpy.ndarray + :param minz: A minimum threshold value (not used in the current implementation). + :type minz: float + + :returns: + + The interpolated value from the 2D array, or -10 if the coordinates are + out of bounds. + :rtype: float + + +.. py:function:: getResolution(o) + + Calculate the resolution based on the dimensions of an object. + + This function computes the resolution in both x and y directions by + determining the width and height of the object, adjusting for pixel size + and border width. The resolution is calculated by dividing the + dimensions by the pixel size and adding twice the border width to each + dimension. + + :param o: An object with attributes `max`, `min`, `optimisation`, + and `borderwidth`. The `max` and `min` attributes should + have `x` and `y` properties representing the coordinates, + while `optimisation` should have a `pixsize` attribute. + :type o: object + + :returns: + + This function does not return a value; it performs calculations + to determine resolution. + :rtype: None + + +.. py:function:: _backup_render_settings(pairs) + + Backup the render settings of Blender objects. + + This function iterates over a list of pairs consisting of owners and + their corresponding structure names. It retrieves the properties of each + structure and stores them in a backup list. If the structure is a + Blender object, it saves all its properties that do not start with an + underscore. For simple values, it directly appends them to the + properties list. This is useful for preserving render settings that + Blender does not allow direct access to during rendering. + + :param pairs: A list of tuples where each tuple contains an owner and a structure + name. + :type pairs: list + + :returns: + + A list containing the backed-up properties of the specified Blender + objects. + :rtype: list + + +.. py:function:: _restore_render_settings(pairs, properties) + + Restore render settings for a given owner and structure. + + This function takes pairs of owners and structure names along with their + corresponding properties. It iterates through these pairs, retrieves the + appropriate object from the owner using the structure name, and sets the + properties on the object. If the object is an instance of + `bpy.types.bpy_struct`, it updates its attributes; otherwise, it + directly sets the value on the owner. + + :param pairs: A list of tuples where each tuple contains an owner and a structure + name. + :type pairs: list + :param properties: A list of dictionaries containing property names and their corresponding + values. + :type properties: list + + +.. py:function:: renderSampleImage(o) + + Render a sample image based on the provided object settings. + + This function generates a Z-buffer image for a given object by either + rendering it from scratch or loading an existing image from the cache. + It handles different geometry sources and applies various settings to + ensure the image is rendered correctly. The function also manages backup + and restoration of render settings to maintain the scene's integrity + during the rendering process. + + :param o: An object containing various properties and settings + :type o: object + + :returns: The generated or loaded Z-buffer image as a NumPy array. + :rtype: numpy.ndarray + + +.. py:function:: prepareArea(o) + :async: + + + Prepare the area for rendering by processing the offset image. + + This function handles the preparation of the area by rendering a sample + image and managing the offset image based on the provided options. It + checks if the offset image needs to be updated and loads it if + necessary. If the inverse option is set, it adjusts the samples + accordingly before calling the offsetArea function. Finally, it saves + the processed offset image. + + :param o: An object containing various properties and methods + required for preparing the area, including flags for + updating the offset image and rendering options. + :type o: object + + +.. py:function:: getCutterArray(operation, pixsize) + + Generate a cutter array based on the specified operation and pixel size. + + This function calculates a 2D array representing the cutter shape based + on the cutter type defined in the operation object. The cutter can be of + various types such as 'END', 'BALL', 'VCARVE', 'CYLCONE', 'BALLCONE', or + 'CUSTOM'. The function uses geometric calculations to fill the array + with appropriate values based on the cutter's dimensions and properties. + + :param operation: An object containing properties of the cutter, including + cutter type, diameter, tip angle, and other relevant parameters. + :type operation: object + :param pixsize: The size of each pixel in the generated cutter array. + :type pixsize: float + + :returns: A 2D array filled with values representing the cutter shape. + :rtype: numpy.ndarray + + diff --git a/_sources/autoapi/cam/index.rst b/_sources/autoapi/cam/index.rst new file mode 100644 index 000000000..b4190280b --- /dev/null +++ b/_sources/autoapi/cam/index.rst @@ -0,0 +1,4041 @@ +cam +=== + +.. py:module:: cam + +.. autoapi-nested-parse:: + + CNC CAM '__init__.py' © 2012 Vilem Novak + + Import Modules, Register and Unregister Classes + + + +Submodules +---------- + +.. toctree:: + :maxdepth: 1 + + /autoapi/cam/autoupdate/index + /autoapi/cam/basrelief/index + /autoapi/cam/bridges/index + /autoapi/cam/cam_chunk/index + /autoapi/cam/cam_operation/index + /autoapi/cam/chain/index + /autoapi/cam/collision/index + /autoapi/cam/constants/index + /autoapi/cam/curvecamcreate/index + /autoapi/cam/curvecamequation/index + /autoapi/cam/curvecamtools/index + /autoapi/cam/engine/index + /autoapi/cam/exception/index + /autoapi/cam/gcodeimportparser/index + /autoapi/cam/gcodepath/index + /autoapi/cam/image_utils/index + /autoapi/cam/involute_gear/index + /autoapi/cam/joinery/index + /autoapi/cam/machine_settings/index + /autoapi/cam/numba_wrapper/index + /autoapi/cam/ops/index + /autoapi/cam/pack/index + /autoapi/cam/parametric/index + /autoapi/cam/pattern/index + /autoapi/cam/polygon_utils_cam/index + /autoapi/cam/preset_managers/index + /autoapi/cam/puzzle_joinery/index + /autoapi/cam/simple/index + /autoapi/cam/simulation/index + /autoapi/cam/slice/index + /autoapi/cam/strategy/index + /autoapi/cam/testing/index + /autoapi/cam/ui/index + /autoapi/cam/utils/index + /autoapi/cam/version/index + /autoapi/cam/voronoi/index + + +Attributes +---------- + +.. autoapisummary:: + + cam.classes + + +Classes +------- + +.. autoapisummary:: + + cam.UpdateChecker + cam.Updater + cam.UpdateSourceOperator + cam.camOperation + cam.camChain + cam.opReference + cam.CamCurveDrawer + cam.CamCurveFlatCone + cam.CamCurveGear + cam.CamCurveHatch + cam.CamCurveInterlock + cam.CamCurveMortise + cam.CamCurvePlate + cam.CamCurvePuzzle + cam.CamCustomCurve + cam.CamHypotrochoidCurve + cam.CamLissajousCurve + cam.CamSineCurve + cam.CamCurveBoolean + cam.CamCurveConvexHull + cam.CamCurveIntarsion + cam.CamCurveOvercuts + cam.CamCurveOvercutsB + cam.CamCurveRemoveDoubles + cam.CamMeshGetPockets + cam.CamOffsetSilhouete + cam.CamObjectSilhouete + cam.CNCCAM_ENGINE + cam.machineSettings + cam.CalculatePath + cam.CamBridgesAdd + cam.CamChainAdd + cam.CamChainRemove + cam.CamChainOperationAdd + cam.CamChainOperationRemove + cam.CamChainOperationUp + cam.CamChainOperationDown + cam.CamOperationAdd + cam.CamOperationCopy + cam.CamOperationRemove + cam.CamOperationMove + cam.CamOrientationAdd + cam.CamPackObjects + cam.CamSliceObjects + cam.CAMSimulate + cam.CAMSimulateChain + cam.KillPathsBackground + cam.PathsAll + cam.PathsBackground + cam.PathsChain + cam.PathExport + cam.PathExportChain + cam.PackObjectsSettings + cam.AddPresetCamCutter + cam.AddPresetCamMachine + cam.AddPresetCamOperation + cam.CAM_CUTTER_MT_presets + cam.CAM_MACHINE_MT_presets + cam.CAM_OPERATION_MT_presets + cam.SliceObjectsSettings + cam.CustomPanel + cam.import_settings + cam.VIEW3D_PT_tools_curvetools + cam.VIEW3D_PT_tools_create + cam.WM_OT_gcode_import + + +Functions +--------- + +.. autoapisummary:: + + cam.get_panels + cam.timer_update + cam.check_operations_on_load + cam.updateOperation + cam.register + cam.unregister + + +Package Contents +---------------- + +.. py:class:: UpdateChecker + + Bases: :py:obj:`bpy.types.Operator` + + + Check for Updates + + + .. py:attribute:: bl_idname + :value: 'render.cam_check_updates' + + + + .. py:attribute:: bl_label + :value: 'Check for Updates in CNC CAM Plugin' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + +.. py:class:: Updater + + Bases: :py:obj:`bpy.types.Operator` + + + Update to Newer Version if Possible + + + .. py:attribute:: bl_idname + :value: 'render.cam_update_now' + + + + .. py:attribute:: bl_label + :value: 'Update' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + + .. py:method:: install_zip_from_url(zip_url) + + +.. py:class:: UpdateSourceOperator + + Bases: :py:obj:`bpy.types.Operator` + + + .. py:attribute:: bl_idname + :value: 'render.cam_set_update_source' + + + + .. py:attribute:: bl_label + :value: 'Set CNC CAM Update Source' + + + + .. py:attribute:: new_source + :type: StringProperty(default='') + + + .. py:method:: execute(context) + + +.. py:class:: camOperation + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + .. py:attribute:: material + :type: PointerProperty(type=CAM_MATERIAL_Properties) + + + .. py:attribute:: info + :type: PointerProperty(type=CAM_INFO_Properties) + + + .. py:attribute:: optimisation + :type: PointerProperty(type=CAM_OPTIMISATION_Properties) + + + .. py:attribute:: movement + :type: PointerProperty(type=CAM_MOVEMENT_Properties) + + + .. py:attribute:: name + :type: StringProperty(name='Operation Name', default='Operation', update=updateRest) + + + .. py:attribute:: filename + :type: StringProperty(name='File Name', default='Operation', update=updateRest) + + + .. py:attribute:: auto_export + :type: BoolProperty(name='Auto Export', description='Export files immediately after path calculation', default=True) + + + .. py:attribute:: remove_redundant_points + :type: BoolProperty(name='Simplify G-code', description='Remove redundant points sharing the same angle as the start vector', default=False) + + + .. py:attribute:: simplify_tol + :type: IntProperty(name='Tolerance', description='lower number means more precise', default=50, min=1, max=1000) + + + .. py:attribute:: hide_all_others + :type: BoolProperty(name='Hide All Others', description='Hide all other tool paths except toolpath associated with selected CAM operation', default=False) + + + .. py:attribute:: parent_path_to_object + :type: BoolProperty(name='Parent Path to Object', description='Parent generated CAM path to source object', default=False) + + + .. py:attribute:: object_name + :type: StringProperty(name='Object', description='Object handled by this operation', update=updateOperationValid) + + + .. py:attribute:: collection_name + :type: StringProperty(name='Collection', description='Object collection handled by this operation', update=updateOperationValid) + + + .. py:attribute:: curve_object + :type: StringProperty(name='Curve Source', description='Curve which will be sampled along the 3D object', update=operationValid) + + + .. py:attribute:: curve_object1 + :type: StringProperty(name='Curve Target', description='Curve which will serve as attractor for the cutter when the cutter follows the curve', update=operationValid) + + + .. py:attribute:: source_image_name + :type: StringProperty(name='Image Source', description='image source', update=operationValid) + + + .. py:attribute:: geometry_source + :type: EnumProperty(name='Data Source', items=(('OBJECT', 'Object', 'a'), ('COLLECTION', 'Collection of Objects', 'a'), ('IMAGE', 'Image', 'a')), description='Geometry source', default='OBJECT', update=updateOperationValid) + + + .. py:attribute:: cutter_type + :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) + + + .. py:attribute:: cutter_object_name + :type: StringProperty(name='Cutter Object', description='Object used as custom cutter for this operation', update=updateZbufferImage) + + + .. py:attribute:: machine_axes + :type: 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) + + + .. py:attribute:: strategy + :type: EnumProperty(name='Strategy', items=getStrategyList, description='Strategy', update=updateStrategy) + + + .. py:attribute:: strategy4axis + :type: 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) + + + .. py:attribute:: strategy5axis + :type: 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) + + + .. py:attribute:: rotary_axis_1 + :type: EnumProperty(name='Rotary Axis', items=(('X', 'X', ''), ('Y', 'Y', ''), ('Z', 'Z', '')), description='Around which axis rotates the first rotary axis', default='X', update=updateStrategy) + + + .. py:attribute:: rotary_axis_2 + :type: 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) + + + .. py:attribute:: skin + :type: 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) + + + .. py:attribute:: inverse + :type: BoolProperty(name='Inverse Milling', description='Male to female model conversion', default=False, update=updateOffsetImage) + + + .. py:attribute:: array + :type: BoolProperty(name='Use Array', description='Create a repetitive array for producing the same thing many times', default=False, update=updateRest) + + + .. py:attribute:: array_x_count + :type: IntProperty(name='X Count', description='X count', default=1, min=1, max=32000, update=updateRest) + + + .. py:attribute:: array_y_count + :type: IntProperty(name='Y Count', description='Y count', default=1, min=1, max=32000, update=updateRest) + + + .. py:attribute:: array_x_distance + :type: FloatProperty(name='X Distance', description='Distance between operation origins', min=1e-05, max=1.0, default=0.01, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: array_y_distance + :type: FloatProperty(name='Y Distance', description='Distance between operation origins', min=1e-05, max=1.0, default=0.01, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: pocket_option + :type: EnumProperty(name='Start Position', items=(('INSIDE', 'Inside', 'a'), ('OUTSIDE', 'Outside', 'a')), description='Pocket starting position', default='INSIDE', update=updateRest) + + + .. py:attribute:: pocketType + :type: EnumProperty(name='pocket type', items=(('PERIMETER', 'Perimeter', 'a'), ('PARALLEL', 'Parallel', 'a')), description='Type of pocket', default='PERIMETER', update=updateRest) + + + .. py:attribute:: parallelPocketAngle + :type: FloatProperty(name='Parallel Pocket Angle', description='Angle for parallel pocket', min=-180, max=180.0, default=45.0, precision=constants.PRECISION, update=updateRest) + + + .. py:attribute:: parallelPocketCrosshatch + :type: BoolProperty(name='Crosshatch #', description='Crosshatch X finish', default=False, update=updateRest) + + + .. py:attribute:: parallelPocketContour + :type: BoolProperty(name='Contour Finish', description='Contour path finish', default=False, update=updateRest) + + + .. py:attribute:: pocketToCurve + :type: BoolProperty(name='Pocket to Curve', description='Generates a curve instead of a path', default=False, update=updateRest) + + + .. py:attribute:: cut_type + :type: EnumProperty(name='Cut', items=(('OUTSIDE', 'Outside', 'a'), ('INSIDE', 'Inside', 'a'), ('ONLINE', 'On Line', 'a')), description='Type of cutter used', default='OUTSIDE', update=updateRest) + + + .. py:attribute:: outlines_count + :type: IntProperty(name='Outlines Count', description='Outlines count', default=1, min=1, max=32, update=updateCutout) + + + .. py:attribute:: straight + :type: BoolProperty(name='Overshoot Style', description='Use overshoot cutout instead of conventional rounded', default=True, update=updateRest) + + + .. py:attribute:: cutter_id + :type: IntProperty(name='Tool Number', description='For machines which support tool change based on tool id', min=0, max=10000, default=1, update=updateRest) + + + .. py:attribute:: cutter_diameter + :type: FloatProperty(name='Cutter Diameter', description='Cutter diameter = 2x cutter radius', min=1e-06, max=10, default=0.003, precision=constants.PRECISION, unit='LENGTH', update=updateOffsetImage) + + + .. py:attribute:: cylcone_diameter + :type: FloatProperty(name='Bottom Diameter', description='Bottom diameter', min=1e-06, max=10, default=0.003, precision=constants.PRECISION, unit='LENGTH', update=updateOffsetImage) + + + .. py:attribute:: cutter_length + :type: 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) + + + .. py:attribute:: cutter_flutes + :type: IntProperty(name='Cutter Flutes', description='Cutter flutes', min=1, max=20, default=2, update=updateChipload) + + + .. py:attribute:: cutter_tip_angle + :type: 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) + + + .. py:attribute:: ball_radius + :type: FloatProperty(name='Ball Radius', description='Radius of', min=0.0, max=0.035, default=0.001, unit='LENGTH', precision=constants.PRECISION, update=updateOffsetImage) + + + .. py:attribute:: bull_corner_radius + :type: 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) + + + .. py:attribute:: cutter_description + :type: StringProperty(name='Tool Description', default='', update=updateOffsetImage) + + + .. py:attribute:: Laser_on + :type: StringProperty(name='Laser ON String', default='M68 E0 Q100') + + + .. py:attribute:: Laser_off + :type: StringProperty(name='Laser OFF String', default='M68 E0 Q0') + + + .. py:attribute:: Laser_cmd + :type: StringProperty(name='Laser Command', default='M68 E0 Q') + + + .. py:attribute:: Laser_delay + :type: FloatProperty(name='Laser ON Delay', description='Time after fast move to turn on laser and let machine stabilize', default=0.2) + + + .. py:attribute:: Plasma_on + :type: StringProperty(name='Plasma ON String', default='M03') + + + .. py:attribute:: Plasma_off + :type: StringProperty(name='Plasma OFF String', default='M05') + + + .. py:attribute:: Plasma_delay + :type: FloatProperty(name='Plasma ON Delay', description='Time after fast move to turn on Plasma and let machine stabilize', default=0.1) + + + .. py:attribute:: Plasma_dwell + :type: FloatProperty(name='Plasma Dwell Time', description='Time to dwell and warm up the torch', default=0.0) + + + .. py:attribute:: dist_between_paths + :type: FloatProperty(name='Distance Between Toolpaths', default=0.001, min=1e-05, max=32, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: dist_along_paths + :type: FloatProperty(name='Distance Along Toolpaths', default=0.0002, min=1e-05, max=32, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: parallel_angle + :type: FloatProperty(name='Angle of Paths', default=0, min=-360, max=360, precision=0, subtype='ANGLE', unit='ROTATION', update=updateRest) + + + .. py:attribute:: old_rotation_A + :type: 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) + + + .. py:attribute:: old_rotation_B + :type: 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) + + + .. py:attribute:: rotation_A + :type: 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) + + + .. py:attribute:: enable_A + :type: BoolProperty(name='Enable A Axis', description='Rotate A axis', default=False, update=updateRotation) + + + .. py:attribute:: A_along_x + :type: BoolProperty(name='A Along X ', description='A Parallel to X', default=True, update=updateRest) + + + .. py:attribute:: rotation_B + :type: 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) + + + .. py:attribute:: enable_B + :type: BoolProperty(name='Enable B Axis', description='Rotate B axis', default=False, update=updateRotation) + + + .. py:attribute:: carve_depth + :type: FloatProperty(name='Carve Depth', default=0.001, min=-0.1, max=32, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: drill_type + :type: EnumProperty(name='Holes On', items=(('MIDDLE_SYMETRIC', 'Middle of Symmetric 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) + + + .. py:attribute:: slice_detail + :type: FloatProperty(name='Distance Between Slices', default=0.001, min=1e-05, max=32, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: waterline_fill + :type: BoolProperty(name='Fill Areas Between Slices', description='Fill areas between slices in waterline mode', default=True, update=updateRest) + + + .. py:attribute:: waterline_project + :type: BoolProperty(name='Project Paths - Not Recomended', description='Project paths in areas between slices', default=True, update=updateRest) + + + .. py:attribute:: use_layers + :type: BoolProperty(name='Use Layers', description='Use layers for roughing', default=True, update=updateRest) + + + .. py:attribute:: stepdown + :type: FloatProperty(name='', description='Layer height', default=0.01, min=1e-05, max=32, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: lead_in + :type: FloatProperty(name='Lead-in Radius', description='Lead in radius for torch or laser to turn off', min=0.0, max=1, default=0.0, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: lead_out + :type: FloatProperty(name='Lead-out Radius', description='Lead out radius for torch or laser to turn off', min=0.0, max=1, default=0.0, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: profile_start + :type: IntProperty(name='Start Point', description='Start point offset', min=0, default=0, update=updateRest) + + + .. py:attribute:: minz + :type: FloatProperty(name='Operation Depth End', default=-0.01, min=-3, max=3, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: minz_from + :type: EnumProperty(name='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) + + + .. py:attribute:: start_type + :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) + + + .. py:attribute:: maxz + :type: FloatProperty(name='Operation Depth Start', description='operation starting depth', default=0, min=-3, max=10, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: first_down + :type: BoolProperty(name='First Down', description='First go down on a contour, then go to the next one', default=False, update=update_operation) + + + .. py:attribute:: source_image_scale_z + :type: FloatProperty(name='Image Source Depth Scale', default=0.01, min=-1, max=1, precision=constants.PRECISION, unit='LENGTH', update=updateZbufferImage) + + + .. py:attribute:: source_image_size_x + :type: FloatProperty(name='Image Source X Size', default=0.1, min=-10, max=10, precision=constants.PRECISION, unit='LENGTH', update=updateZbufferImage) + + + .. py:attribute:: source_image_offset + :type: FloatVectorProperty(name='Image Offset', default=(0, 0, 0), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ', update=updateZbufferImage) + + + .. py:attribute:: source_image_crop + :type: 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) + + + .. py:attribute:: source_image_crop_start_x + :type: FloatProperty(name='Crop Start X', default=0, min=0, max=100, precision=constants.PRECISION, subtype='PERCENTAGE', update=updateZbufferImage) + + + .. py:attribute:: source_image_crop_start_y + :type: FloatProperty(name='Crop Start Y', default=0, min=0, max=100, precision=constants.PRECISION, subtype='PERCENTAGE', update=updateZbufferImage) + + + .. py:attribute:: source_image_crop_end_x + :type: FloatProperty(name='Crop End X', default=100, min=0, max=100, precision=constants.PRECISION, subtype='PERCENTAGE', update=updateZbufferImage) + + + .. py:attribute:: source_image_crop_end_y + :type: FloatProperty(name='Crop End Y', default=100, min=0, max=100, precision=constants.PRECISION, subtype='PERCENTAGE', update=updateZbufferImage) + + + .. py:attribute:: ambient_behaviour + :type: EnumProperty(name='Ambient', items=(('ALL', 'All', 'a'), ('AROUND', 'Around', 'a')), description='Handling ambient surfaces', default='ALL', update=updateZbufferImage) + + + .. py:attribute:: ambient_radius + :type: 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) + + + .. py:attribute:: use_limit_curve + :type: BoolProperty(name='Use Limit Curve', description='A curve limits the operation area', default=False, update=updateRest) + + + .. py:attribute:: ambient_cutter_restrict + :type: 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) + + + .. py:attribute:: limit_curve + :type: StringProperty(name='Limit Curve', description='Curve used to limit the area of the operation', update=updateRest) + + + .. py:attribute:: feedrate + :type: FloatProperty(name='Feedrate', description='Feedrate in units per minute', min=5e-05, max=50.0, default=1.0, precision=constants.PRECISION, unit='LENGTH', update=updateChipload) + + + .. py:attribute:: plunge_feedrate + :type: FloatProperty(name='Plunge Speed', description='% of feedrate', min=0.1, max=100.0, default=50.0, precision=1, subtype='PERCENTAGE', update=updateRest) + + + .. py:attribute:: plunge_angle + :type: FloatProperty(name='Plunge Angle', description='What angle is already considered to plunge', default=pi / 6, min=0, max=pi * 0.5, precision=0, subtype='ANGLE', unit='ROTATION', update=updateRest) + + + .. py:attribute:: spindle_rpm + :type: FloatProperty(name='Spindle RPM', description='Spindle speed ', min=0, max=60000, default=12000, update=updateChipload) + + + .. py:attribute:: do_simulation_feedrate + :type: BoolProperty(name='Adjust Feedrates with Simulation EXPERIMENTAL', description='Adjust feedrates with simulation', default=False, update=updateRest) + + + .. py:attribute:: dont_merge + :type: BoolProperty(name="Don't Merge Outlines when Cutting", description='this is usefull when you want to cut around everything', default=False, update=updateRest) + + + .. py:attribute:: pencil_threshold + :type: FloatProperty(name='Pencil Threshold', default=2e-05, min=1e-08, max=1, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: crazy_threshold1 + :type: FloatProperty(name='Min Engagement', default=0.02, min=1e-08, max=100, precision=constants.PRECISION, update=updateRest) + + + .. py:attribute:: crazy_threshold5 + :type: FloatProperty(name='Optimal Engagement', default=0.3, min=1e-08, max=100, precision=constants.PRECISION, update=updateRest) + + + .. py:attribute:: crazy_threshold2 + :type: FloatProperty(name='Max Engagement', default=0.5, min=1e-08, max=100, precision=constants.PRECISION, update=updateRest) + + + .. py:attribute:: crazy_threshold3 + :type: FloatProperty(name='Max Angle', default=2, min=1e-08, max=100, precision=constants.PRECISION, update=updateRest) + + + .. py:attribute:: crazy_threshold4 + :type: FloatProperty(name='Test Angle Step', default=0.05, min=1e-08, max=100, precision=constants.PRECISION, update=updateRest) + + + .. py:attribute:: add_pocket_for_medial + :type: BoolProperty(name='Add Pocket Operation', description='Clean unremoved material after medial axis', default=True, update=updateRest) + + + .. py:attribute:: add_mesh_for_medial + :type: BoolProperty(name='Add Medial mesh', description='Medial operation returns mesh for editing and further processing', default=False, update=updateRest) + + + .. py:attribute:: medial_axis_threshold + :type: FloatProperty(name='Long Vector Threshold', default=0.001, min=1e-08, max=100, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: medial_axis_subdivision + :type: FloatProperty(name='Fine Subdivision', default=0.0002, min=1e-08, max=100, precision=constants.PRECISION, unit='LENGTH', update=updateRest) + + + .. py:attribute:: use_bridges + :type: BoolProperty(name='Use Bridges / Tabs', description='Use bridges in cutout', default=False, update=updateBridges) + + + .. py:attribute:: bridges_width + :type: FloatProperty(name='Bridge / Tab Width', default=0.002, unit='LENGTH', precision=constants.PRECISION, update=updateBridges) + + + .. py:attribute:: bridges_height + :type: FloatProperty(name='Bridge / Tab Height', description='Height from the bottom of the cutting operation', default=0.0005, unit='LENGTH', precision=constants.PRECISION, update=updateBridges) + + + .. py:attribute:: bridges_collection_name + :type: StringProperty(name='Bridges / Tabs Collection', description='Collection of curves used as bridges', update=operationValid) + + + .. py:attribute:: use_bridge_modifiers + :type: BoolProperty(name='Use Bridge / Tab Modifiers', description='Include bridge curve modifiers using render level when calculating operation, does not effect original bridge data', default=True, update=updateBridges) + + + .. py:attribute:: use_modifiers + :type: BoolProperty(name='Use Mesh Modifiers', description='Include mesh modifiers using render level when calculating operation, does not effect original mesh', default=True, update=operationValid) + + + .. py:attribute:: min + :type: FloatVectorProperty(name='Operation Minimum', default=(0, 0, 0), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ') + + + .. py:attribute:: max + :type: FloatVectorProperty(name='Operation Maximum', default=(0, 0, 0), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ') + + + .. py:attribute:: output_header + :type: BoolProperty(name='Output G-code Header', description='Output user defined G-code command header at start of operation', default=False) + + + .. py:attribute:: gcode_header + :type: StringProperty(name='G-code Header', description='G-code commands at start of operation. Use ; for line breaks', default='G53 G0') + + + .. py:attribute:: enable_dust + :type: BoolProperty(name='Dust Collector', description='Output user defined g-code command header at start of operation', default=False) + + + .. py:attribute:: gcode_start_dust_cmd + :type: StringProperty(name='Start Dust Collector', description='Commands to start dust collection. Use ; for line breaks', default='M100') + + + .. py:attribute:: gcode_stop_dust_cmd + :type: StringProperty(name='Stop Dust Collector', description='Command to stop dust collection. Use ; for line breaks', default='M101') + + + .. py:attribute:: enable_hold + :type: BoolProperty(name='Hold Down', description='Output hold down command at start of operation', default=False) + + + .. py:attribute:: gcode_start_hold_cmd + :type: StringProperty(name='G-code Header', description='G-code commands at start of operation. Use ; for line breaks', default='M102') + + + .. py:attribute:: gcode_stop_hold_cmd + :type: StringProperty(name='G-code Header', description='G-code commands at end operation. Use ; for line breaks', default='M103') + + + .. py:attribute:: enable_mist + :type: BoolProperty(name='Mist', description='Mist command at start of operation', default=False) + + + .. py:attribute:: gcode_start_mist_cmd + :type: StringProperty(name='Start Mist', description='Command to start mist. Use ; for line breaks', default='M104') + + + .. py:attribute:: gcode_stop_mist_cmd + :type: StringProperty(name='Stop Mist', description='Command to stop mist. Use ; for line breaks', default='M105') + + + .. py:attribute:: output_trailer + :type: BoolProperty(name='Output G-code Trailer', description='Output user defined g-code command trailer at end of operation', default=False) + + + .. py:attribute:: gcode_trailer + :type: StringProperty(name='G-code Trailer', description='G-code commands at end of operation. Use ; for line breaks', default='M02') + + + .. py:attribute:: offset_image + + + .. py:attribute:: zbuffer_image + + + .. py:attribute:: silhouete + + + .. py:attribute:: ambient + + + .. py:attribute:: operation_limit + + + .. py:attribute:: borderwidth + :value: 50 + + + + .. py:attribute:: object + :value: None + + + + .. py:attribute:: path_object_name + :type: StringProperty(name='Path Object', description='Actual CNC path') + + + .. py:attribute:: changed + :type: BoolProperty(name='True if any of the Operation Settings has Changed', description='Mark for update', default=False) + + + .. py:attribute:: update_zbufferimage_tag + :type: BoolProperty(name='Mark Z-Buffer Image for Update', description='Mark for update', default=True) + + + .. py:attribute:: update_offsetimage_tag + :type: BoolProperty(name='Mark Offset Image for Update', description='Mark for update', default=True) + + + .. py:attribute:: update_silhouete_tag + :type: BoolProperty(name='Mark Silhouette Image for Update', description='Mark for update', default=True) + + + .. py:attribute:: update_ambient_tag + :type: BoolProperty(name='Mark Ambient Polygon for Update', description='Mark for update', default=True) + + + .. py:attribute:: update_bullet_collision_tag + :type: BoolProperty(name='Mark Bullet Collision World for Update', description='Mark for update', default=True) + + + .. py:attribute:: valid + :type: BoolProperty(name='Valid', description='True if operation is ok for calculation', default=True) + + + .. py:attribute:: changedata + :type: StringProperty(name='Changedata', description='change data for checking if stuff changed.') + + + .. py:attribute:: computing + :type: BoolProperty(name='Computing Right Now', description='', default=False) + + + .. py:attribute:: pid + :type: IntProperty(name='Process Id', description='Background process id', default=-1) + + + .. py:attribute:: outtext + :type: StringProperty(name='Outtext', description='outtext', default='') + + +.. py:class:: camChain + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + .. py:attribute:: index + :type: IntProperty(name='Index', description='Index in the hard-defined camChains', default=-1) + + + .. py:attribute:: active_operation + :type: IntProperty(name='Active Operation', description='Active operation in chain', default=-1) + + + .. py:attribute:: name + :type: StringProperty(name='Chain Name', default='Chain') + + + .. py:attribute:: filename + :type: StringProperty(name='File Name', default='Chain') + + + .. py:attribute:: valid + :type: BoolProperty(name='Valid', description='True if whole chain is ok for calculation', default=True) + + + .. py:attribute:: computing + :type: BoolProperty(name='Computing Right Now', description='', default=False) + + + .. py:attribute:: operations + :type: CollectionProperty(type=opReference) + + +.. py:class:: opReference + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + .. py:attribute:: name + :type: StringProperty(name='Operation Name', default='Operation') + + + .. py:attribute:: computing + :value: False + + + +.. py:class:: CamCurveDrawer + + Bases: :py:obj:`bpy.types.Operator` + + + Generates Drawers + + + .. py:attribute:: bl_idname + :value: 'object.curve_drawer' + + + + .. py:attribute:: bl_label + :value: 'Drawer' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: depth + :type: FloatProperty(name='Drawer Depth', default=0.2, min=0, max=1.0, precision=4, unit='LENGTH') + + + .. py:attribute:: width + :type: FloatProperty(name='Drawer Width', default=0.125, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: height + :type: FloatProperty(name='Drawer Height', default=0.07, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_size + :type: FloatProperty(name='Maximum Finger Size', default=0.015, min=0.005, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_tolerance + :type: FloatProperty(name='Finger Play Room', default=4.5e-05, min=0, max=0.003, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_inset + :type: FloatProperty(name='Finger Inset', default=0.0, min=0.0, max=0.01, precision=4, unit='LENGTH') + + + .. py:attribute:: drawer_plate_thickness + :type: FloatProperty(name='Drawer Plate Thickness', default=0.00477, min=0.001, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: drawer_hole_diameter + :type: FloatProperty(name='Drawer Hole Diameter', default=0.02, min=1e-05, max=0.5, precision=4, unit='LENGTH') + + + .. py:attribute:: drawer_hole_offset + :type: FloatProperty(name='Drawer Hole Offset', default=0.0, min=-0.5, max=0.5, precision=4, unit='LENGTH') + + + .. py:attribute:: overcut + :type: BoolProperty(name='Add Overcut', default=False) + + + .. py:attribute:: overcut_diameter + :type: FloatProperty(name='Overcut Tool Diameter', default=0.003175, min=-0.001, max=0.5, precision=4, unit='LENGTH') + + + .. py:method:: draw(context) + + Draw the user interface properties for the object. + + This method is responsible for rendering the layout of various + properties related to the object's dimensions and specifications. It + adds properties such as depth, width, height, finger size, finger + tolerance, finger inset, drawer plate thickness, drawer hole diameter, + drawer hole offset, and overcut diameter to the layout. The overcut + diameter property is only added if the overcut option is enabled. + + :param context: The context in which the drawing occurs, typically containing + information about the current state and environment. + + + + .. py:method:: execute(context) + + Execute the drawer creation process in Blender. + + This method orchestrates the creation of a drawer by calculating the + necessary dimensions for the finger joints, creating the base plate, and + generating the drawer components such as the back, front, sides, and + bottom. It utilizes various helper functions to perform operations like + boolean differences and transformations to achieve the desired geometry. + The method also handles the placement of the drawer components in the 3D + space. + + :param context: The Blender context that provides access to the current scene and + objects. + :type context: bpy.context + + :returns: + + A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: CamCurveFlatCone + + Bases: :py:obj:`bpy.types.Operator` + + + Generates cone from flat stock + + + .. py:attribute:: bl_idname + :value: 'object.curve_flat_cone' + + + + .. py:attribute:: bl_label + :value: 'Cone Flat Calculator' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: small_d + :type: FloatProperty(name='Small Diameter', default=0.025, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: large_d + :type: FloatProperty(name='Large Diameter', default=0.3048, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: height + :type: FloatProperty(name='Height of Cone', default=0.457, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: tab + :type: FloatProperty(name='Tab Witdh', default=0.01, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: intake + :type: FloatProperty(name='Intake Diameter', default=0, min=0, max=0.2, precision=4, unit='LENGTH') + + + .. py:attribute:: intake_skew + :type: FloatProperty(name='Intake Skew', default=1, min=0.1, max=4) + + + .. py:attribute:: resolution + :type: IntProperty(name='Resolution', default=12, min=5, max=200) + + + .. py:method:: execute(context) + + Execute the construction of a geometric shape in Blender. + + This method performs a series of operations to create a geometric shape + based on specified dimensions and parameters. It calculates various + dimensions needed for the shape, including height and angles, and then + uses Blender's operations to create segments, rectangles, and ellipses. + The function also handles the positioning and rotation of these shapes + within the 3D space of Blender. + + :param context: The context in which the operation is executed, typically containing + information about the current + scene and active objects in Blender. + + :returns: + + A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: CamCurveGear + + Bases: :py:obj:`bpy.types.Operator` + + + Generates Involute Gears // version 1.1 by Leemon Baird, 2011, Leemon@Leemon.com + http://www.thingiverse.com/thing:5505 + + + .. py:attribute:: bl_idname + :value: 'object.curve_gear' + + + + .. py:attribute:: bl_label + :value: 'Gears' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: tooth_spacing + :type: FloatProperty(name='Distance per Tooth', default=0.01, min=0.001, max=1.0, precision=4, unit='LENGTH') + + + .. py:attribute:: tooth_amount + :type: IntProperty(name='Amount of Teeth', default=7, min=4) + + + .. py:attribute:: spoke_amount + :type: IntProperty(name='Amount of Spokes', default=4, min=0) + + + .. py:attribute:: hole_diameter + :type: FloatProperty(name='Hole Diameter', default=0.003175, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: rim_size + :type: FloatProperty(name='Rim Size', default=0.003175, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: hub_diameter + :type: FloatProperty(name='Hub Diameter', default=0.005, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: pressure_angle + :type: FloatProperty(name='Pressure Angle', default=radians(20), min=0.001, max=pi / 2, precision=4, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: clearance + :type: FloatProperty(name='Clearance', default=0.0, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: backlash + :type: FloatProperty(name='Backlash', default=0.0, min=0.0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: rack_height + :type: FloatProperty(name='Rack Height', default=0.012, min=0.001, max=1, precision=4, unit='LENGTH') + + + .. py:attribute:: rack_tooth_per_hole + :type: IntProperty(name='Teeth per Mounting Hole', default=7, min=2) + + + .. py:attribute:: gear_type + :type: EnumProperty(name='Type of Gear', items=(('PINION', 'Pinion', 'Circular Gear'), ('RACK', 'Rack', 'Straight Rack')), description='Type of gear', default='PINION') + + + .. py:method:: draw(context) + + Draw the user interface properties for gear settings. + + This method sets up the layout for various gear parameters based on the + selected gear type. It dynamically adds properties to the layout for + different gear types, allowing users to input specific values for gear + design. The properties include gear type, tooth spacing, tooth amount, + hole diameter, pressure angle, and backlash. Additional properties are + displayed if the gear type is 'PINION' or 'RACK'. + + :param context: The context in which the layout is being drawn. + + + + .. py:method:: execute(context) + + Execute the gear generation process based on the specified gear type. + + This method checks the type of gear to be generated (either 'PINION' or + 'RACK') and calls the appropriate function from the `involute_gear` + module to create the gear or rack with the specified parameters. The + parameters include tooth spacing, number of teeth, hole diameter, + pressure angle, clearance, backlash, rim size, hub diameter, and spoke + amount for pinion gears, and additional parameters for rack gears. + + :param context: The context in which the execution is taking place. + + :returns: + + A dictionary indicating that the operation has finished with a key + 'FINISHED'. + :rtype: dict + + + +.. py:class:: CamCurveHatch + + Bases: :py:obj:`bpy.types.Operator` + + + Perform Hatch Operation on Single or Multiple Curves + + + .. py:attribute:: bl_idname + :value: 'object.curve_hatch' + + + + .. py:attribute:: bl_label + :value: 'CrossHatch Curve' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: angle + :type: FloatProperty(default=0, min=-pi / 2, max=pi / 2, precision=4, subtype='ANGLE') + + + .. py:attribute:: distance + :type: FloatProperty(default=0.003, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: offset + :type: FloatProperty(default=0, min=-1.0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: pocket_shape + :type: EnumProperty(name='Pocket Shape', items=(('BOUNDS', 'Bounds Rectangle', 'Uses a bounding rectangle'), ('HULL', 'Convex Hull', 'Uses a convex hull'), ('POCKET', 'Pocket', 'Uses the pocket shape')), description='Type of pocket shape', default='POCKET') + + + .. py:attribute:: contour + :type: BoolProperty(name='Contour Curve', default=False) + + + .. py:attribute:: xhatch + :type: BoolProperty(name='Crosshatch #', default=False) + + + .. py:attribute:: contour_separate + :type: BoolProperty(name='Contour Separate', default=False) + + + .. py:attribute:: straight + :type: BoolProperty(name='Overshoot Style', description='Use overshoot cutout instead of conventional rounded', default=True) + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: draw(context) + + Draw the layout properties for the given context. + + + + .. py:method:: execute(context) + + +.. py:class:: CamCurveInterlock + + Bases: :py:obj:`bpy.types.Operator` + + + Generates Interlock Along a Curve + + + .. py:attribute:: bl_idname + :value: 'object.curve_interlock' + + + + .. py:attribute:: bl_label + :value: 'Interlock' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: finger_size + :type: FloatProperty(name='Finger Size', default=0.015, min=0.005, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_tolerance + :type: FloatProperty(name='Finger Play Room', default=4.5e-05, min=0, max=0.003, precision=4, unit='LENGTH') + + + .. py:attribute:: plate_thickness + :type: FloatProperty(name='Plate Thickness', default=0.00477, min=0.001, max=3.0, unit='LENGTH') + + + .. py:attribute:: opencurve + :type: BoolProperty(name='OpenCurve', default=False) + + + .. py:attribute:: interlock_type + :type: EnumProperty(name='Type of Interlock', items=(('TWIST', 'Twist', 'Interlock requires 1/4 turn twist'), ('GROOVE', 'Groove', 'Simple sliding groove'), ('PUZZLE', 'Puzzle Interlock', 'Puzzle good for flat joints')), description='Type of interlock', default='GROOVE') + + + .. py:attribute:: finger_amount + :type: IntProperty(name='Finger Amount', default=2, min=1, max=100) + + + .. py:attribute:: tangent_angle + :type: FloatProperty(name='Tangent Deviation', default=0.0, min=0.0, max=2, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: fixed_angle + :type: FloatProperty(name='Fixed Angle', default=0.0, min=0.0, max=2, subtype='ANGLE', unit='ROTATION') + + + .. py:method:: execute(context) + + Execute the joinery operation based on the selected objects in the + context. + + This function checks the selected objects in the provided context and + performs different operations depending on the type of the active + object. If the active object is a curve or font and there are selected + objects, it duplicates the object, converts it to a mesh, and processes + its vertices to create a LineString representation. The function then + calculates lengths and applies distributed interlock joinery based on + the specified parameters. If no valid objects are selected, it defaults + to a single interlock operation at the cursor's location. + + :param context: The context containing selected objects and active object. + :type context: bpy.context + + :returns: A dictionary indicating the operation's completion status. + :rtype: dict + + + +.. py:class:: CamCurveMortise + + Bases: :py:obj:`bpy.types.Operator` + + + Generates Mortise Along a Curve + + + .. py:attribute:: bl_idname + :value: 'object.curve_mortise' + + + + .. py:attribute:: bl_label + :value: 'Mortise' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: finger_size + :type: BoolProperty(name='Kurf Bending only', default=False) + + + .. py:attribute:: min_finger_size + :type: FloatProperty(name='Minimum Finger Size', default=0.0025, min=0.001, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_tolerance + :type: FloatProperty(name='Finger Play Room', default=4.5e-05, min=0, max=0.003, precision=4, unit='LENGTH') + + + .. py:attribute:: plate_thickness + :type: FloatProperty(name='Drawer Plate Thickness', default=0.00477, min=0.001, max=3.0, unit='LENGTH') + + + .. py:attribute:: side_height + :type: FloatProperty(name='Side Height', default=0.05, min=0.001, max=3.0, unit='LENGTH') + + + .. py:attribute:: flex_pocket + :type: FloatProperty(name='Flex Pocket', default=0.004, min=0.0, max=1.0, unit='LENGTH') + + + .. py:attribute:: top_bottom + :type: BoolProperty(name='Side Top & Bottom Fingers', default=True) + + + .. py:attribute:: opencurve + :type: BoolProperty(name='OpenCurve', default=False) + + + .. py:attribute:: adaptive + :type: FloatProperty(name='Adaptive Angle Threshold', default=0.0, min=0.0, max=2, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: double_adaptive + :type: BoolProperty(name='Double Adaptive Pockets', default=False) + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the joinery process based on the provided context. + + This function performs a series of operations to duplicate the active + object, convert it to a mesh, and then process its geometry to create + joinery features. It extracts vertex coordinates, converts them into a + LineString data structure, and applies either variable or fixed finger + joinery based on the specified parameters. The function also handles the + creation of flexible sides and pockets if required. + + :param context: The context in which the operation is executed. + :type context: bpy.context + + :returns: A dictionary indicating the completion status of the operation. + :rtype: dict + + + +.. py:class:: CamCurvePlate + + Bases: :py:obj:`bpy.types.Operator` + + + Perform Generates Rounded Plate with Mounting Holes + + + .. py:attribute:: bl_idname + :value: 'object.curve_plate' + + + + .. py:attribute:: bl_label + :value: 'Sign Plate' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: radius + :type: FloatProperty(name='Corner Radius', default=0.025, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: width + :type: FloatProperty(name='Width of Plate', default=0.3048, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: height + :type: FloatProperty(name='Height of Plate', default=0.457, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: hole_diameter + :type: FloatProperty(name='Hole Diameter', default=0.01, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: hole_tolerance + :type: FloatProperty(name='Hole V Tolerance', default=0.005, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: hole_vdist + :type: FloatProperty(name='Hole Vert Distance', default=0.4, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: hole_hdist + :type: FloatProperty(name='Hole Horiz Distance', default=0, min=0, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: hole_hamount + :type: IntProperty(name='Hole Horiz Amount', default=1, min=0, max=50) + + + .. py:attribute:: resolution + :type: IntProperty(name='Spline Resolution', default=50, min=3, max=150) + + + .. py:attribute:: plate_type + :type: EnumProperty(name='Type Plate', items=(('ROUNDED', 'Rounded corner', 'Makes a rounded corner plate'), ('COVE', 'Cove corner', 'Makes a plate with circles cut in each corner '), ('BEVEL', 'Bevel corner', 'Makes a plate with beveled corners '), ('OVAL', 'Elipse', 'Makes an oval plate')), description='Type of Plate', default='ROUNDED') + + + .. py:method:: draw(context) + + Draw the UI layout for plate properties. + + This method creates a user interface layout for configuring various + properties of a plate, including its type, dimensions, hole + specifications, and resolution. It dynamically adds properties to the + layout based on the selected plate type, allowing users to input + relevant parameters. + + :param context: The context in which the UI is being drawn. + + + + .. py:method:: execute(context) + + Execute the creation of a plate based on specified parameters. + + This function generates a plate shape in Blender based on the defined + attributes such as width, height, radius, and plate type. It supports + different plate types including rounded, oval, cove, and bevel. The + function also handles the creation of holes in the plate if specified. + It utilizes Blender's curve operations to create the geometry and + applies various transformations to achieve the desired shape. + + :param context: The Blender context in which the operation is performed. + :type context: bpy.context + + :returns: + + A dictionary indicating the result of the operation, typically + {'FINISHED'} if successful. + :rtype: dict + + + +.. py:class:: CamCurvePuzzle + + Bases: :py:obj:`bpy.types.Operator` + + + Generates Puzzle Joints and Interlocks + + + .. py:attribute:: bl_idname + :value: 'object.curve_puzzle' + + + + .. py:attribute:: bl_label + :value: 'Puzzle Joints' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: diameter + :type: FloatProperty(name='Tool Diameter', default=0.003175, min=0.001, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_tolerance + :type: FloatProperty(name='Finger Play Room', default=5e-05, min=0, max=0.003, precision=4, unit='LENGTH') + + + .. py:attribute:: finger_amount + :type: IntProperty(name='Finger Amount', default=1, min=0, max=100) + + + .. py:attribute:: stem_size + :type: IntProperty(name='Size of the Stem', default=2, min=1, max=200) + + + .. py:attribute:: width + :type: FloatProperty(name='Width', default=0.1, min=0.005, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: height + :type: FloatProperty(name='Height or Thickness', default=0.025, min=0.005, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: angle + :type: FloatProperty(name='Angle A', default=pi / 4, min=-10, max=10, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: angleb + :type: FloatProperty(name='Angle B', default=pi / 4, min=-10, max=10, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: radius + :type: FloatProperty(name='Arc Radius', default=0.025, min=0.005, max=5, precision=4, unit='LENGTH') + + + .. py:attribute:: interlock_type + :type: EnumProperty(name='Type of Shape', items=(('JOINT', 'Joint', 'Puzzle Joint interlock'), ('BAR', 'Bar', 'Bar interlock'), ('ARC', 'Arc', 'Arc interlock'), ('MULTIANGLE', 'Multi angle', 'Multi angle joint'), ('CURVEBAR', 'Arc Bar', 'Arc Bar interlock'), ('CURVEBARCURVE', 'Arc Bar Arc', 'Arc Bar Arc interlock'), ('CURVET', 'T curve', 'T curve interlock'), ('T', 'T Bar', 'T Bar interlock'), ('CORNER', 'Corner Bar', 'Corner Bar interlock'), ('TILE', 'Tile', 'Tile interlock'), ('OPENCURVE', 'Open Curve', 'Corner Bar interlock')), description='Type of interlock', default='CURVET') + + + .. py:attribute:: gender + :type: EnumProperty(name='Type Gender', items=(('MF', 'Male-Receptacle', 'Male and receptacle'), ('F', 'Receptacle only', 'Receptacle'), ('M', 'Male only', 'Male')), description='Type of interlock', default='MF') + + + .. py:attribute:: base_gender + :type: EnumProperty(name='Base Gender', items=(('MF', 'Male - Receptacle', 'Male - Receptacle'), ('F', 'Receptacle', 'Receptacle'), ('M', 'Male', 'Male')), description='Type of interlock', default='M') + + + .. py:attribute:: multiangle_gender + :type: EnumProperty(name='Multiangle Gender', items=(('MMF', 'Male Male Receptacle', 'M M F'), ('MFF', 'Male Receptacle Receptacle', 'M F F')), description='Type of interlock', default='MFF') + + + .. py:attribute:: mitre + :type: BoolProperty(name='Add Mitres', default=False) + + + .. py:attribute:: twist_lock + :type: BoolProperty(name='Add TwistLock', default=False) + + + .. py:attribute:: twist_thick + :type: FloatProperty(name='Twist Thickness', default=0.0047, min=0.001, max=3.0, precision=4, unit='LENGTH') + + + .. py:attribute:: twist_percent + :type: FloatProperty(name='Twist Neck', default=0.3, min=0.1, max=0.9, precision=4) + + + .. py:attribute:: twist_keep + :type: BoolProperty(name='Keep Twist Holes', default=False) + + + .. py:attribute:: twist_line + :type: BoolProperty(name='Add Twist to Bar', default=False) + + + .. py:attribute:: twist_line_amount + :type: IntProperty(name='Amount of Separators', default=2, min=1, max=600) + + + .. py:attribute:: twist_separator + :type: BoolProperty(name='Add Twist Separator', default=False) + + + .. py:attribute:: twist_separator_amount + :type: IntProperty(name='Amount of Separators', default=2, min=2, max=600) + + + .. py:attribute:: twist_separator_spacing + :type: FloatProperty(name='Separator Spacing', default=0.025, min=-0.004, max=1.0, precision=4, unit='LENGTH') + + + .. py:attribute:: twist_separator_edge_distance + :type: FloatProperty(name='Separator Edge Distance', default=0.01, min=0.0005, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: tile_x_amount + :type: IntProperty(name='Amount of X Fingers', default=2, min=1, max=600) + + + .. py:attribute:: tile_y_amount + :type: IntProperty(name='Amount of Y Fingers', default=2, min=1, max=600) + + + .. py:attribute:: interlock_amount + :type: IntProperty(name='Interlock Amount on Curve', default=2, min=0, max=200) + + + .. py:attribute:: overcut + :type: BoolProperty(name='Add Overcut', default=False) + + + .. py:attribute:: overcut_diameter + :type: FloatProperty(name='Overcut Tool Diameter', default=0.003175, min=-0.001, max=0.5, precision=4, unit='LENGTH') + + + .. py:method:: draw(context) + + Draws the user interface layout for interlock type properties. + + This method is responsible for creating and displaying the layout of + various properties related to different interlock types in the user + interface. It dynamically adjusts the layout based on the selected + interlock type, allowing users to input relevant parameters such as + dimensions, tolerances, and other characteristics specific to the chosen + interlock type. + + :param context: The context in which the layout is being drawn, typically + provided by the user interface framework. + + :returns: + + This method does not return any value; it modifies the layout + directly. + :rtype: None + + + + .. py:method:: execute(context) + + Execute the puzzle joinery process based on the provided context. + + This method processes the selected objects in the given context to + perform various types of puzzle joinery operations. It first checks if + there are any selected objects and if the active object is a curve. If + so, it duplicates the object, applies transformations, and converts it + to a mesh. The method then extracts vertex coordinates and performs + different joinery operations based on the specified interlock type. + Supported interlock types include 'FINGER', 'JOINT', 'BAR', 'ARC', + 'CURVEBARCURVE', 'CURVEBAR', 'MULTIANGLE', 'T', 'CURVET', 'CORNER', + 'TILE', and 'OPENCURVE'. + + :param context: The context containing selected objects and the active object. + :type context: Context + + :returns: A dictionary indicating the completion status of the operation. + :rtype: dict + + + +.. py:class:: CamCustomCurve + + Bases: :py:obj:`bpy.types.Operator` + + + Object Custom Curve + + + .. py:attribute:: bl_idname + :value: 'object.customcurve' + + + + .. py:attribute:: bl_label + :value: 'Custom Curve' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: xstring + :type: StringProperty(name='X Equation', description='Equation x=F(t)', default='t') + + + .. py:attribute:: ystring + :type: StringProperty(name='Y Equation', description='Equation y=F(t)', default='0') + + + .. py:attribute:: zstring + :type: StringProperty(name='Z Equation', description='Equation z=F(t)', default='0.05*sin(2*pi*4*t)') + + + .. py:attribute:: iteration + :type: IntProperty(name='Iteration', default=100, min=50, max=2000) + + + .. py:attribute:: maxt + :type: FloatProperty(name='Wave Ends at X', default=0.5, min=-3.0, max=10, precision=4, unit='LENGTH') + + + .. py:attribute:: mint + :type: FloatProperty(name='Wave Starts at X', default=0, min=-3.0, max=3, precision=4, unit='LENGTH') + + + .. py:method:: execute(context) + + +.. py:class:: CamHypotrochoidCurve + + Bases: :py:obj:`bpy.types.Operator` + + + Hypotrochoid + + + .. py:attribute:: bl_idname + :value: 'object.hypotrochoid' + + + + .. py:attribute:: bl_label + :value: 'Spirograph Type Figure' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: typecurve + :type: EnumProperty(name='Type of Curve', items=(('hypo', 'Hypotrochoid', 'Inside ring'), ('epi', 'Epicycloid', 'Outside inner ring'))) + + + .. py:attribute:: R + :type: FloatProperty(name='Big Circle Radius', default=0.25, min=0.001, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: r + :type: FloatProperty(name='Small Circle Radius', default=0.18, min=0.0001, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: d + :type: FloatProperty(name='Distance from Center of Interior Circle', default=0.05, min=0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: dip + :type: FloatProperty(name='Variable Depth from Center', default=0.0, min=-100, max=100, precision=4) + + + .. py:method:: execute(context) + + +.. py:class:: CamLissajousCurve + + Bases: :py:obj:`bpy.types.Operator` + + + Lissajous + + + .. py:attribute:: bl_idname + :value: 'object.lissajous' + + + + .. py:attribute:: bl_label + :value: 'Lissajous Figure' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: amplitude_A + :type: FloatProperty(name='Amplitude A', default=0.1, min=0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: waveA + :type: EnumProperty(name='Wave X', items=(('sine', 'Sine Wave', 'Sine Wave'), ('triangle', 'Triangle Wave', 'triangle wave')), default='sine') + + + .. py:attribute:: amplitude_B + :type: FloatProperty(name='Amplitude B', default=0.1, min=0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: waveB + :type: EnumProperty(name='Wave Y', items=(('sine', 'Sine Wave', 'Sine Wave'), ('triangle', 'Triangle Wave', 'triangle wave')), default='sine') + + + .. py:attribute:: period_A + :type: FloatProperty(name='Period A', default=1.1, min=0.001, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: period_B + :type: FloatProperty(name='Period B', default=1.0, min=0.001, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: period_Z + :type: FloatProperty(name='Period Z', default=1.0, min=0.001, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: amplitude_Z + :type: FloatProperty(name='Amplitude Z', default=0.0, min=0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: shift + :type: FloatProperty(name='Phase Shift', default=0, min=-360, max=360, precision=4, unit='ROTATION') + + + .. py:attribute:: iteration + :type: IntProperty(name='Iteration', default=500, min=50, max=10000) + + + .. py:attribute:: maxt + :type: FloatProperty(name='Wave Ends at X', default=11, min=-3.0, max=1000000, precision=4, unit='LENGTH') + + + .. py:attribute:: mint + :type: FloatProperty(name='Wave Starts at X', default=0, min=-10.0, max=3, precision=4, unit='LENGTH') + + + .. py:method:: execute(context) + + +.. py:class:: CamSineCurve + + Bases: :py:obj:`bpy.types.Operator` + + + Object Sine + + + .. py:attribute:: bl_idname + :value: 'object.sine' + + + + .. py:attribute:: bl_label + :value: 'Periodic Wave' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: axis + :type: EnumProperty(name='Displacement Axis', items=(('XY', 'Y to displace X axis', 'Y constant; X sine displacement'), ('YX', 'X to displace Y axis', 'X constant; Y sine displacement'), ('ZX', 'X to displace Z axis', 'X constant; Y sine displacement'), ('ZY', 'Y to displace Z axis', 'X constant; Y sine displacement')), default='ZX') + + + .. py:attribute:: wave + :type: EnumProperty(name='Wave', items=(('sine', 'Sine Wave', 'Sine Wave'), ('triangle', 'Triangle Wave', 'triangle wave'), ('cycloid', 'Cycloid', 'Sine wave rectification'), ('invcycloid', 'Inverse Cycloid', 'Sine wave rectification')), default='sine') + + + .. py:attribute:: amplitude + :type: FloatProperty(name='Amplitude', default=0.01, min=0, max=10, precision=4, unit='LENGTH') + + + .. py:attribute:: period + :type: FloatProperty(name='Period', default=0.5, min=0.001, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: beatperiod + :type: FloatProperty(name='Beat Period Offset', default=0.0, min=0.0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: shift + :type: FloatProperty(name='Phase Shift', default=0, min=-360, max=360, precision=4, unit='ROTATION') + + + .. py:attribute:: offset + :type: FloatProperty(name='Offset', default=0, min=-1.0, max=1, precision=4, unit='LENGTH') + + + .. py:attribute:: iteration + :type: IntProperty(name='Iteration', default=100, min=50, max=2000) + + + .. py:attribute:: maxt + :type: FloatProperty(name='Wave Ends at X', default=0.5, min=-3.0, max=3, precision=4, unit='LENGTH') + + + .. py:attribute:: mint + :type: FloatProperty(name='Wave Starts at X', default=0, min=-3.0, max=3, precision=4, unit='LENGTH') + + + .. py:attribute:: wave_distance + :type: FloatProperty(name='Distance Between Multiple Waves', default=0.0, min=0.0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: wave_angle_offset + :type: FloatProperty(name='Angle Offset for Multiple Waves', default=pi / 2, min=-200 * pi, max=200 * pi, precision=4, unit='ROTATION') + + + .. py:attribute:: wave_amount + :type: IntProperty(name='Amount of Multiple Waves', default=1, min=1, max=2000) + + + .. py:method:: execute(context) + + +.. py:class:: CamCurveBoolean + + Bases: :py:obj:`bpy.types.Operator` + + + Perform Boolean Operation on Two or More Curves + + + .. py:attribute:: bl_idname + :value: 'object.curve_boolean' + + + + .. py:attribute:: bl_label + :value: 'Curve Boolean' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: boolean_type + :type: EnumProperty(name='Type', items=(('UNION', 'Union', ''), ('DIFFERENCE', 'Difference', ''), ('INTERSECT', 'Intersect', '')), description='Boolean type', default='UNION') + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + + .. py:method:: invoke(context, event) + + +.. py:class:: CamCurveConvexHull + + Bases: :py:obj:`bpy.types.Operator` + + + Perform Hull Operation on Single or Multiple Curves + + + .. py:attribute:: bl_idname + :value: 'object.convex_hull' + + + + .. py:attribute:: bl_label + :value: 'Convex Hull' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + +.. py:class:: CamCurveIntarsion + + Bases: :py:obj:`bpy.types.Operator` + + + Makes Curve Cuttable Both Inside and Outside, for Intarsion and Joints + + + .. py:attribute:: bl_idname + :value: 'object.curve_intarsion' + + + + .. py:attribute:: bl_label + :value: 'Intarsion' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: diameter + :type: FloatProperty(name='Cutter Diameter', default=0.001, min=0, max=0.025, precision=4, unit='LENGTH') + + + .. py:attribute:: tolerance + :type: FloatProperty(name='Cutout Tolerance', default=0.0001, min=0, max=0.005, precision=4, unit='LENGTH') + + + .. py:attribute:: backlight + :type: FloatProperty(name='Backlight Seat', default=0.0, min=0, max=0.01, precision=4, unit='LENGTH') + + + .. py:attribute:: perimeter_cut + :type: FloatProperty(name='Perimeter Cut Offset', default=0.0, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: base_thickness + :type: FloatProperty(name='Base Material Thickness', default=0.0, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: intarsion_thickness + :type: FloatProperty(name='Intarsion Material Thickness', default=0.0, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:attribute:: backlight_depth_from_top + :type: FloatProperty(name='Backlight Well Depth', default=0.0, min=0, max=0.1, precision=4, unit='LENGTH') + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + + .. py:method:: invoke(context, event) + + +.. py:class:: CamCurveOvercuts + + Bases: :py:obj:`bpy.types.Operator` + + + Adds Overcuts for Slots + + + .. py:attribute:: bl_idname + :value: 'object.curve_overcuts' + + + + .. py:attribute:: bl_label + :value: 'Add Overcuts - A' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: diameter + :type: FloatProperty(name='Diameter', default=0.003175, min=0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: threshold + :type: FloatProperty(name='Threshold', default=pi / 2 * 0.99, min=-3.14, max=3.14, precision=4, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: do_outer + :type: BoolProperty(name='Outer Polygons', default=True) + + + .. py:attribute:: invert + :type: BoolProperty(name='Invert', default=False) + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + + .. py:method:: invoke(context, event) + + +.. py:class:: CamCurveOvercutsB + + Bases: :py:obj:`bpy.types.Operator` + + + Adds Overcuts for Slots + + + .. py:attribute:: bl_idname + :value: 'object.curve_overcuts_b' + + + + .. py:attribute:: bl_label + :value: 'Add Overcuts - B' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: diameter + :type: FloatProperty(name='Tool Diameter', default=0.003175, description='Tool bit diameter used in cut operation', min=0, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: style + :type: EnumProperty(name='Style', items=(('OPEDGE', 'opposite edge', 'place corner overcuts on opposite edges'), ('DOGBONE', 'Dog-bone / Corner Point', 'place overcuts at center of corners'), ('TBONE', 'T-bone', 'place corner overcuts on the same edge')), default='DOGBONE', description='style of overcut to use') + + + .. py:attribute:: threshold + :type: FloatProperty(name='Max Inside Angle', default=pi / 2, min=-3.14, max=3.14, description='The maximum angle to be considered as an inside corner', precision=4, subtype='ANGLE', unit='ROTATION') + + + .. py:attribute:: do_outer + :type: BoolProperty(name='Include Outer Curve', description='Include the outer curve if there are curves inside', default=True) + + + .. py:attribute:: do_invert + :type: BoolProperty(name='Invert', description='invert overcut operation on all curves', default=True) + + + .. py:attribute:: otherEdge + :type: BoolProperty(name='Other Edge', description='change to the other edge for the overcut to be on', default=False) + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + + .. py:method:: invoke(context, event) + + +.. py:class:: CamCurveRemoveDoubles + + Bases: :py:obj:`bpy.types.Operator` + + + Curve Remove Doubles + + + .. py:attribute:: bl_idname + :value: 'object.curve_remove_doubles' + + + + .. py:attribute:: bl_label + :value: 'Remove Curve Doubles' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: merge_distance + :type: FloatProperty(name='Merge distance', default=0.0001, min=0, max=0.01) + + + .. py:attribute:: keep_bezier + :type: BoolProperty(name='Keep bezier', default=False) + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + + .. py:method:: draw(context) + + + .. py:method:: invoke(context, event) + + +.. py:class:: CamMeshGetPockets + + Bases: :py:obj:`bpy.types.Operator` + + + Detect Pockets in a Mesh and Extract Them as Curves + + + .. py:attribute:: bl_idname + :value: 'object.mesh_get_pockets' + + + + .. py:attribute:: bl_label + :value: 'Get Pocket Surfaces' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: threshold + :type: FloatProperty(name='Horizontal Threshold', description='How horizontal the surface must be for a pocket: 1.0 perfectly flat, 0.0 is any orientation', default=0.99, min=0, max=1.0, precision=4) + + + .. py:attribute:: zlimit + :type: FloatProperty(name='Z Limit', description='Maximum z height considered for pocket operation, default is 0.0', default=0.0, min=-1000.0, max=1000.0, precision=4, unit='LENGTH') + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + +.. py:class:: CamOffsetSilhouete + + Bases: :py:obj:`bpy.types.Operator` + + + Curve Offset Operation + + + .. py:attribute:: bl_idname + :value: 'object.silhouete_offset' + + + + .. py:attribute:: bl_label + :value: 'Silhouette & Offset' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: offset + :type: FloatProperty(name='Offset', default=0.003, min=-100, max=100, precision=4, unit='LENGTH') + + + .. py:attribute:: mitrelimit + :type: FloatProperty(name='Mitre Limit', default=2, min=1e-08, max=20, precision=4, unit='LENGTH') + + + .. py:attribute:: style + :type: EnumProperty(name='Corner Type', items=(('1', 'Round', ''), ('2', 'Mitre', ''), ('3', 'Bevel', ''))) + + + .. py:attribute:: caps + :type: EnumProperty(name='Cap Type', items=(('round', 'Round', ''), ('square', 'Square', ''), ('flat', 'Flat', ''))) + + + .. py:attribute:: align + :type: EnumProperty(name='Alignment', items=(('worldxy', 'World XY', ''), ('bottom', 'Base Bottom', ''), ('top', 'Base Top', ''))) + + + .. py:attribute:: opentype + :type: EnumProperty(name='Curve Type', items=(('dilate', 'Dilate open curve', ''), ('leaveopen', 'Leave curve open', ''), ('closecurve', 'Close curve', '')), default='closecurve') + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: isStraight(geom) + + + .. py:method:: execute(context) + + + .. py:method:: draw(context) + + + .. py:method:: invoke(context, event) + + +.. py:class:: CamObjectSilhouete + + Bases: :py:obj:`bpy.types.Operator` + + + Object Silhouette + + + .. py:attribute:: bl_idname + :value: 'object.silhouete' + + + + .. py:attribute:: bl_label + :value: 'Object Silhouette' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + +.. py:class:: CNCCAM_ENGINE + + Bases: :py:obj:`bpy.types.RenderEngine` + + + .. py:attribute:: bl_idname + :value: 'CNCCAM_RENDER' + + + + .. py:attribute:: bl_label + :value: 'CNC CAM' + + + + .. py:attribute:: bl_use_eevee_viewport + :value: True + + + +.. py:function:: get_panels() + + Retrieve a list of panels for the Blender UI. + + This function compiles a list of UI panels that are compatible with the + Blender rendering engine. It excludes certain predefined panels that are + not relevant for the current context. The function checks all subclasses + of the `bpy.types.Panel` and includes those that have the + `COMPAT_ENGINES` attribute set to include 'BLENDER_RENDER', provided + they are not in the exclusion list. + + :returns: A list of panel classes that are compatible with the + Blender rendering engine, excluding specified panels. + :rtype: list + + +.. py:class:: machineSettings + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + stores all data for machines + + + .. py:attribute:: post_processor + :type: 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') + + + .. py:attribute:: use_position_definitions + :type: BoolProperty(name='Use Position Definitions', description='Define own positions for op start, toolchange, ending position', default=False) + + + .. py:attribute:: starting_position + :type: FloatVectorProperty(name='Start Position', default=(0, 0, 0), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ', update=updateMachine) + + + .. py:attribute:: mtc_position + :type: FloatVectorProperty(name='MTC Position', default=(0, 0, 0), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ', update=updateMachine) + + + .. py:attribute:: ending_position + :type: FloatVectorProperty(name='End Position', default=(0, 0, 0), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ', update=updateMachine) + + + .. py:attribute:: working_area + :type: FloatVectorProperty(name='Work Area', default=(0.5, 0.5, 0.1), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ', update=updateMachine) + + + .. py:attribute:: feedrate_min + :type: FloatProperty(name='Feedrate Minimum /min', default=0.0, min=1e-05, max=320000, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: feedrate_max + :type: FloatProperty(name='Feedrate Maximum /min', default=2, min=1e-05, max=320000, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: feedrate_default + :type: FloatProperty(name='Feedrate Default /min', default=1.5, min=1e-05, max=320000, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: hourly_rate + :type: FloatProperty(name='Price per Hour', default=100, min=0.005, precision=2) + + + .. py:attribute:: spindle_min + :type: FloatProperty(name='Spindle Speed Minimum RPM', default=5000, min=1e-05, max=320000, precision=1) + + + .. py:attribute:: spindle_max + :type: FloatProperty(name='Spindle Speed Maximum RPM', default=30000, min=1e-05, max=320000, precision=1) + + + .. py:attribute:: spindle_default + :type: FloatProperty(name='Spindle Speed Default RPM', default=15000, min=1e-05, max=320000, precision=1) + + + .. py:attribute:: spindle_start_time + :type: FloatProperty(name='Spindle Start Delay Seconds', description='Wait for the spindle to start spinning before starting the feeds , in seconds', default=0, min=0.0, max=320000, precision=1) + + + .. py:attribute:: axis4 + :type: BoolProperty(name='#4th Axis', description='Machine has 4th axis', default=0) + + + .. py:attribute:: axis5 + :type: BoolProperty(name='#5th Axis', description='Machine has 5th axis', default=0) + + + .. py:attribute:: eval_splitting + :type: BoolProperty(name='Split Files', description='Split gcode file with large number of operations', default=True) + + + .. py:attribute:: split_limit + :type: IntProperty(name='Operations per File', description='Split files with larger number of operations than this', min=1000, max=20000000, default=800000) + + + .. py:attribute:: collet_size + :type: FloatProperty(name='#Collet Size', description='Collet size for collision detection', default=33, min=1e-05, max=320000, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: output_block_numbers + :type: BoolProperty(name='Output Block Numbers', description='Output block numbers ie N10 at start of line', default=False) + + + .. py:attribute:: start_block_number + :type: IntProperty(name='Start Block Number', description='The starting block number ie 10', default=10) + + + .. py:attribute:: block_number_increment + :type: IntProperty(name='Block Number Increment', description='How much the block number should increment for the next line', default=10) + + + .. py:attribute:: output_tool_definitions + :type: BoolProperty(name='Output Tool Definitions', description='Output tool definitions', default=True) + + + .. py:attribute:: output_tool_change + :type: BoolProperty(name='Output Tool Change Commands', description='Output tool change commands ie: Tn M06', default=True) + + + .. py:attribute:: output_g43_on_tool_change + :type: BoolProperty(name='Output G43 on Tool Change', description='Output G43 on tool change line', default=False) + + +.. py:class:: CalculatePath + + Bases: :py:obj:`bpy.types.Operator`, :py:obj:`cam.async_op.AsyncOperatorMixin` + + + Calculate CAM Paths + + + .. py:attribute:: bl_idname + :value: 'object.calculate_cam_path' + + + + .. py:attribute:: bl_label + :value: 'Calculate CAM Paths' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + Check if the current camera operation is valid. + + This method checks the active camera operation in the given context and + determines if it is valid. It retrieves the active operation from the + scene's camera operations and validates it using the `isValid` function. + If the operation is valid, it returns True; otherwise, it returns False. + + :param context: The context containing the scene and camera operations. + :type context: Context + + :returns: True if the active camera operation is valid, False otherwise. + :rtype: bool + + + + .. py:method:: execute_async(context) + :async: + + + Execute an asynchronous calculation of a path. + + This method performs an asynchronous operation to calculate a path based + on the provided context. It awaits the result of the calculation and + prints the success status along with the return value. The return value + can be used for further processing or analysis. + + :param context: The context in which the path calculation is to be executed. + :type context: Any + + :returns: The result of the path calculation. + :rtype: Any + + + +.. py:class:: CamBridgesAdd + + Bases: :py:obj:`bpy.types.Operator` + + + Add Bridge Objects to Curve + + + .. py:attribute:: bl_idname + :value: 'scene.cam_bridges_add' + + + + .. py:attribute:: bl_label + :value: 'Add Bridges / Tabs' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera operation in the given context. + + This function retrieves the active camera operation from the current + scene and adds automatic bridges to it. It is typically called within + the context of a Blender operator to perform specific actions related to + camera operations. + + :param context: The context in which the operation is executed. + + :returns: A dictionary indicating the result of the operation, typically + containing the key 'FINISHED' to signify successful completion. + :rtype: dict + + + +.. py:class:: CamChainAdd + + Bases: :py:obj:`bpy.types.Operator` + + + Add New CAM Chain + + + .. py:attribute:: bl_idname + :value: 'scene.cam_chain_add' + + + + .. py:attribute:: bl_label + :value: 'Add New CAM Chain' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera chain creation in the given context. + + This function adds a new camera chain to the current scene in Blender. + It updates the active camera chain index and assigns a name and filename + to the newly created chain. The function is intended to be called within + a Blender operator context. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the operation's completion status, + specifically returning {'FINISHED'} upon successful execution. + :rtype: dict + + + +.. py:class:: CamChainRemove + + Bases: :py:obj:`bpy.types.Operator` + + + Remove CAM Chain + + + .. py:attribute:: bl_idname + :value: 'scene.cam_chain_remove' + + + + .. py:attribute:: bl_label + :value: 'Remove CAM Chain' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera chain removal process. + + This function removes the currently active camera chain from the scene + and decrements the active camera chain index if it is greater than zero. + It modifies the Blender context to reflect these changes. + + :param context: The context in which the function is executed. + + :returns: + + A dictionary indicating the status of the operation, + specifically {'FINISHED'} upon successful execution. + :rtype: dict + + + +.. py:class:: CamChainOperationAdd + + Bases: :py:obj:`bpy.types.Operator` + + + Add Operation to Chain + + + .. py:attribute:: bl_idname + :value: 'scene.cam_chain_operation_add' + + + + .. py:attribute:: bl_label + :value: 'Add Operation to Chain' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute an operation in the active camera chain. + + This function retrieves the active camera chain from the current scene + and adds a new operation to it. It increments the active operation index + and assigns the name of the currently selected camera operation to the + newly added operation. This is typically used in the context of managing + camera operations in a 3D environment. + + :param context: The context in which the operation is executed. + + :returns: A dictionary indicating the execution status, typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: CamChainOperationRemove + + Bases: :py:obj:`bpy.types.Operator` + + + Remove Operation from Chain + + + .. py:attribute:: bl_idname + :value: 'scene.cam_chain_operation_remove' + + + + .. py:attribute:: bl_label + :value: 'Remove Operation from Chain' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the operation to remove the active operation from the camera + chain. + + This method accesses the current scene and retrieves the active camera + chain. It then removes the currently active operation from that chain + and adjusts the index of the active operation accordingly. If the active + operation index becomes negative, it resets it to zero to ensure it + remains within valid bounds. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the execution status, typically + containing {'FINISHED'} upon successful completion. + :rtype: dict + + + +.. py:class:: CamChainOperationUp + + Bases: :py:obj:`bpy.types.Operator` + + + Add Operation to Chain + + + .. py:attribute:: bl_idname + :value: 'scene.cam_chain_operation_up' + + + + .. py:attribute:: bl_label + :value: 'Add Operation to Chain' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the operation to move the active camera operation in the chain. + + This function retrieves the current scene and the active camera chain. + If there is an active operation (i.e., its index is greater than 0), it + moves the operation one step up in the chain by adjusting the indices + accordingly. After moving the operation, it updates the active operation + index to reflect the change. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the result of the operation, + specifically returning {'FINISHED'} upon successful execution. + :rtype: dict + + + +.. py:class:: CamChainOperationDown + + Bases: :py:obj:`bpy.types.Operator` + + + Add Operation to Chain + + + .. py:attribute:: bl_idname + :value: 'scene.cam_chain_operation_down' + + + + .. py:attribute:: bl_label + :value: 'Add Operation to Chain' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the operation to move the active camera operation in the chain. + + This function retrieves the current scene and the active camera chain. + It checks if the active operation can be moved down in the list of + operations. If so, it moves the active operation one position down and + updates the active operation index accordingly. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the result of the operation, + specifically {'FINISHED'} when the operation completes successfully. + :rtype: dict + + + +.. py:class:: CamOperationAdd + + Bases: :py:obj:`bpy.types.Operator` + + + Add New CAM Operation + + + .. py:attribute:: bl_idname + :value: 'scene.cam_operation_add' + + + + .. py:attribute:: bl_label + :value: 'Add New CAM Operation' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera operation based on the active object in the scene. + + This method retrieves the active object from the Blender context and + performs operations related to camera settings. It checks if an object + is selected and retrieves its bounding box dimensions. If no object is + found, it reports an error and cancels the operation. If an object is + present, it adds a new camera operation to the scene, sets its + properties, and ensures that a machine area object is present. + + :param context: The context in which the operation is executed. + + + +.. py:class:: CamOperationCopy + + Bases: :py:obj:`bpy.types.Operator` + + + Copy CAM Operation + + + .. py:attribute:: bl_idname + :value: 'scene.cam_operation_copy' + + + + .. py:attribute:: bl_label + :value: 'Copy Active CAM Operation' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera operation in the given context. + + This method handles the execution of camera operations within the + Blender scene. It first checks if there are any camera operations + available. If not, it returns a cancellation status. If there are + operations, it copies the active operation, increments the active + operation index, and updates the name and filename of the new operation. + The function also ensures that the new operation's name is unique by + appending a copy suffix or incrementing a numeric suffix. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the status of the operation, + either {'CANCELLED'} if no operations are available or + {'FINISHED'} if the operation was successfully executed. + :rtype: dict + + + +.. py:class:: CamOperationRemove + + Bases: :py:obj:`bpy.types.Operator` + + + Remove CAM Operation + + + .. py:attribute:: bl_idname + :value: 'scene.cam_operation_remove' + + + + .. py:attribute:: bl_label + :value: 'Remove CAM Operation' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera operation in the given context. + + This function performs the active camera operation by deleting the + associated object from the scene. It checks if there are any camera + operations available and handles the deletion of the active operation's + object. If the active operation is removed, it updates the active + operation index accordingly. Additionally, it manages a dictionary that + tracks hidden objects. + + :param context: The Blender context containing the scene and operations. + :type context: bpy.context + + :returns: + + A dictionary indicating the result of the operation, either + {'CANCELLED'} if no operations are available or {'FINISHED'} if the + operation was successfully executed. + :rtype: dict + + + +.. py:class:: CamOperationMove + + Bases: :py:obj:`bpy.types.Operator` + + + Move CAM Operation + + + .. py:attribute:: bl_idname + :value: 'scene.cam_operation_move' + + + + .. py:attribute:: bl_label + :value: 'Move CAM Operation in List' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: direction + :type: EnumProperty(name='Direction', items=(('UP', 'Up', ''), ('DOWN', 'Down', '')), description='Direction', default='DOWN') + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute a camera operation based on the specified direction. + + This method modifies the active camera operation in the Blender context + based on the direction specified. If the direction is 'UP', it moves the + active operation up in the list, provided it is not already at the top. + Conversely, if the direction is not 'UP', it moves the active operation + down in the list, as long as it is not at the bottom. The method updates + the active operation index accordingly. + + :param context: The context in which the operation is executed. + + :returns: A dictionary indicating the operation has finished, with + the key 'FINISHED'. + :rtype: dict + + + +.. py:class:: CamOrientationAdd + + Bases: :py:obj:`bpy.types.Operator` + + + Add Orientation to CAM Operation, for Multiaxis Operations + + + .. py:attribute:: bl_idname + :value: 'scene.cam_orientation_add' + + + + .. py:attribute:: bl_label + :value: 'Add Orientation' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera orientation operation in Blender. + + This function retrieves the active camera operation from the current + scene, creates an empty object to represent the camera orientation, and + adds it to a specified group. The empty object is named based on the + operation's name and the current count of objects in the group. The size + of the empty object is set to a predefined value for visibility. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the operation's completion status, + typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: CamPackObjects + + Bases: :py:obj:`bpy.types.Operator` + + + Calculate All CAM Paths + + + .. py:attribute:: bl_idname + :value: 'object.cam_pack_objects' + + + + .. py:attribute:: bl_label + :value: 'Pack Curves on Sheet' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + Execute the operation in the given context. + + This function sets the Blender object mode to 'OBJECT', retrieves the + currently selected objects, and calls the `packCurves` function from the + `pack` module. It is typically used to finalize operations on selected + objects in Blender. + + :param context: The context in which the operation is executed. + + :returns: A dictionary indicating the completion status of the operation. + :rtype: dict + + + + .. py:method:: draw(context) + + +.. py:class:: CamSliceObjects + + Bases: :py:obj:`bpy.types.Operator` + + + Slice a Mesh Object Horizontally + + + .. py:attribute:: bl_idname + :value: 'object.cam_slice_objects' + + + + .. py:attribute:: bl_label + :value: 'Slice Object - Useful for Lasercut Puzzles etc' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + Execute the slicing operation on the active Blender object. + + This function retrieves the currently active object in the Blender + context and performs a slicing operation on it using the `sliceObject` + function from the `cam` module. The operation is intended to modify the + object based on the slicing logic defined in the external module. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the result of the operation, + typically containing the key 'FINISHED' upon successful execution. + :rtype: dict + + + + .. py:method:: draw(context) + + +.. py:class:: CAMSimulate + + Bases: :py:obj:`bpy.types.Operator`, :py:obj:`cam.async_op.AsyncOperatorMixin` + + + Simulate CAM Operation + This Is Performed by: Creating an Image, Painting Z Depth of the Brush Subtractively. + Works only for Some Operations, Can Not Be Used for 4-5 Axis. + + + .. py:attribute:: bl_idname + :value: 'object.cam_simulate' + + + + .. py:attribute:: bl_label + :value: 'CAM Simulation' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: operation + :type: StringProperty(name='Operation', description='Specify the operation to calculate', default='Operation') + + + .. py:method:: execute_async(context) + :async: + + + Execute an asynchronous simulation operation based on the active camera + operation. + + This method retrieves the current scene and the active camera operation. + It constructs the operation name and checks if the corresponding object + exists in the Blender data. If it does, it attempts to run the + simulation asynchronously. If the simulation is cancelled, it returns a + cancellation status. If the object does not exist, it reports an error + and returns a finished status. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the status of the operation, either + {'CANCELLED'} or {'FINISHED'}. + :rtype: dict + + + + .. py:method:: draw(context) + + Draws the user interface for selecting camera operations. + + This method creates a layout element in the user interface that allows + users to search and select a specific camera operation from a list of + available operations defined in the current scene. It utilizes the + Blender Python API to integrate with the UI. + + :param context: The context in which the drawing occurs, typically + provided by Blender's UI system. + + + +.. py:class:: CAMSimulateChain + + Bases: :py:obj:`bpy.types.Operator`, :py:obj:`cam.async_op.AsyncOperatorMixin` + + + Simulate CAM Chain, Compared to Single Op Simulation Just Writes Into One Image and Thus Enables + to See how Ops Work Together. + + + .. py:attribute:: bl_idname + :value: 'object.cam_simulate_chain' + + + + .. py:attribute:: bl_label + :value: 'CAM Simulation' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + Check the validity of the active camera chain in the scene. + + This method retrieves the currently active camera chain from the scene's + camera chains and checks its validity using the `isChainValid` function. + It returns a boolean indicating whether the active camera chain is + valid. + + :param context: The context containing the scene and its properties. + :type context: object + + :returns: True if the active camera chain is valid, False otherwise. + :rtype: bool + + + + .. py:attribute:: operation + :type: StringProperty(name='Operation', description='Specify the operation to calculate', default='Operation') + + + .. py:method:: execute_async(context) + :async: + + + Execute an asynchronous simulation for a specified camera chain. + + This method retrieves the active camera chain from the current Blender + scene and determines the operations associated with that chain. It + checks if all operations are valid and can be simulated. If valid, it + proceeds to execute the simulation asynchronously. If any operation is + invalid, it logs a message and returns a finished status without + performing the simulation. + + :param context: The context in which the operation is executed. + + :returns: A dictionary indicating the status of the operation, either + operation completed successfully. + :rtype: dict + + + + .. py:method:: draw(context) + + Draw the user interface for selecting camera operations. + + This function creates a user interface element that allows the user to + search and select a specific camera operation from a list of available + operations in the current scene. It utilizes the Blender Python API to + create a property search layout. + + :param context: The context in which the drawing occurs, typically containing + information about the current scene and UI elements. + + + +.. py:class:: KillPathsBackground + + Bases: :py:obj:`bpy.types.Operator` + + + Remove CAM Path Processes in Background. + + + .. py:attribute:: bl_idname + :value: 'object.kill_calculate_cam_paths_background' + + + + .. py:attribute:: bl_label + :value: 'Kill Background Computation of an Operation' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + Execute the camera operation in the given context. + + This method retrieves the active camera operation from the scene and + checks if there are any ongoing processes related to camera path + calculations. If such processes exist and match the current operation, + they are terminated. The method then marks the operation as not + computing and returns a status indicating that the execution has + finished. + + :param context: The context in which the operation is executed. + + :returns: A dictionary with a status key indicating the result of the execution. + :rtype: dict + + + +.. py:class:: PathsAll + + Bases: :py:obj:`bpy.types.Operator` + + + Calculate All CAM Paths + + + .. py:attribute:: bl_idname + :value: 'object.calculate_cam_paths_all' + + + + .. py:attribute:: bl_label + :value: 'Calculate All CAM Paths' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + Execute camera operations in the current Blender context. + + This function iterates through the camera operations defined in the + current scene and executes the background calculation for each + operation. It sets the active camera operation index and prints the name + of each operation being processed. This is typically used in a Blender + add-on or script to automate camera path calculations. + + :param context: The current Blender context. + :type context: bpy.context + + :returns: + + A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + :rtype: dict + + + + .. py:method:: draw(context) + + Draws the user interface elements for the operation selection. + + This method utilizes the Blender layout system to create a property + search interface for selecting operations related to camera + functionalities. It links the current instance's operation property to + the available camera operations defined in the Blender scene. + + :param context: The context in which the drawing occurs, + :type context: bpy.context + + + +.. py:class:: PathsBackground + + Bases: :py:obj:`bpy.types.Operator` + + + Calculate CAM Paths in Background. File Has to Be Saved Before. + + + .. py:attribute:: bl_idname + :value: 'object.calculate_cam_paths_background' + + + + .. py:attribute:: bl_label + :value: 'Calculate CAM Paths in Background' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + Execute the camera operation in the background. + + This method initiates a background process to perform camera operations + based on the current scene and active camera operation. It sets up the + necessary paths for the script and starts a subprocess to handle the + camera computations. Additionally, it manages threading to ensure that + the main thread remains responsive while the background operation is + executed. + + :param context: The context in which the operation is executed. + + :returns: A dictionary indicating the completion status of the operation. + :rtype: dict + + + +.. py:class:: PathsChain + + Bases: :py:obj:`bpy.types.Operator`, :py:obj:`cam.async_op.AsyncOperatorMixin` + + + Calculate a Chain and Export the G-code Alltogether. + + + .. py:attribute:: bl_idname + :value: 'object.calculate_cam_paths_chain' + + + + .. py:attribute:: bl_label + :value: 'Calculate CAM Paths in Current Chain and Export Chain G-code' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + Check the validity of the active camera chain in the given context. + + This method retrieves the active camera chain from the scene and checks + its validity using the `isChainValid` function. It returns a boolean + value indicating whether the camera chain is valid or not. + + :param context: The context containing the scene and camera chain information. + :type context: Context + + :returns: True if the active camera chain is valid, False otherwise. + :rtype: bool + + + + .. py:method:: execute_async(context) + :async: + + + Execute asynchronous operations for camera path calculations. + + This method sets the object mode for the Blender scene and processes a + series of camera operations defined in the active camera chain. It + reports the progress of each operation and handles any exceptions that + may occur during the path calculation. After successful calculations, it + exports the resulting mesh data to a specified G-code file. + + :param context: The Blender context containing scene and + :type context: bpy.context + + :returns: A dictionary indicating the result of the operation, + typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: PathExport + + Bases: :py:obj:`bpy.types.Operator` + + + Export G-code. Can Be Used only when the Path Object Is Present + + + .. py:attribute:: bl_idname + :value: 'object.cam_export' + + + + .. py:attribute:: bl_label + :value: 'Export Operation G-code' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + Execute the camera operation and export the G-code path. + + This method retrieves the active camera operation from the current scene + and exports the corresponding G-code path to a specified filename. It + prints the filename and relevant operation details to the console for + debugging purposes. The G-code path is generated based on the camera + path data associated with the active operation. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: PathExportChain + + Bases: :py:obj:`bpy.types.Operator` + + + Calculate a Chain and Export the G-code Together. + + + .. py:attribute:: bl_idname + :value: 'object.cam_export_paths_chain' + + + + .. py:attribute:: bl_label + :value: 'Export CAM Paths in Current Chain as G-code' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + Check the validity of the active camera chain in the given context. + + This method retrieves the currently active camera chain from the scene + context and checks its validity using the `isChainValid` function. It + returns a boolean indicating whether the active camera chain is valid or + not. + + :param context: The context containing the scene and camera chain information. + :type context: object + + :returns: True if the active camera chain is valid, False otherwise. + :rtype: bool + + + + .. py:method:: execute(context) + + Execute the camera path export process. + + This function retrieves the active camera chain from the current scene + and gathers the mesh data associated with the operations of that chain. + It then exports the G-code path using the specified filename and the + collected mesh data. The function is designed to be called within the + context of a Blender operator. + + :param context: The context in which the operator is executed. + :type context: bpy.context + + :returns: + + A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + :rtype: dict + + + +.. py:function:: timer_update(context) + + Monitor background processes related to camera path calculations. + + This function checks the status of background processes that are + responsible for calculating camera paths. It retrieves the current + processes and monitors their state. If a process has finished, it + updates the corresponding camera operation and reloads the necessary + paths. If the process is still running, it restarts the associated + thread to continue monitoring. + + :param context: The context in which the function is called, typically + containing information about the current scene and operations. + + +.. py:class:: PackObjectsSettings + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + stores all data for machines + + + .. py:attribute:: sheet_fill_direction + :type: 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') + + + .. py:attribute:: sheet_x + :type: FloatProperty(name='X Size', description='Sheet size', min=0.001, max=10, default=0.5, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: sheet_y + :type: FloatProperty(name='Y Size', description='Sheet size', min=0.001, max=10, default=0.5, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: distance + :type: 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') + + + .. py:attribute:: tolerance + :type: 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') + + + .. py:attribute:: rotate + :type: BoolProperty(name='Enable Rotation', description='Enable rotation of elements', default=True) + + + .. py:attribute:: rotate_angle + :type: 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') + + +.. py:class:: AddPresetCamCutter + + Bases: :py:obj:`bl_operators.presets.AddPresetBase`, :py:obj:`bpy.types.Operator` + + + Add a Cutter Preset + + + .. py:attribute:: bl_idname + :value: 'render.cam_preset_cutter_add' + + + + .. py:attribute:: bl_label + :value: 'Add Cutter Preset' + + + + .. py:attribute:: preset_menu + :value: 'CAM_CUTTER_MT_presets' + + + + .. py:attribute:: preset_defines + :value: ['d = bpy.context.scene.cam_operations[bpy.context.scene.cam_active_operation]'] + + + + .. py:attribute:: preset_values + :value: ['d.cutter_id', 'd.cutter_type', 'd.cutter_diameter', 'd.cutter_length', 'd.cutter_flutes',... + + + + .. py:attribute:: preset_subdir + :value: 'cam_cutters' + + + +.. py:class:: AddPresetCamMachine + + Bases: :py:obj:`bl_operators.presets.AddPresetBase`, :py:obj:`bpy.types.Operator` + + + Add a Cam Machine Preset + + + .. py:attribute:: bl_idname + :value: 'render.cam_preset_machine_add' + + + + .. py:attribute:: bl_label + :value: 'Add Machine Preset' + + + + .. py:attribute:: preset_menu + :value: 'CAM_MACHINE_MT_presets' + + + + .. py:attribute:: preset_defines + :value: ['d = bpy.context.scene.cam_machine', 's = bpy.context.scene.unit_settings'] + + + + .. py:attribute:: preset_values + :value: ['d.post_processor', 's.system', 'd.use_position_definitions', 'd.starting_position',... + + + + .. py:attribute:: preset_subdir + :value: 'cam_machines' + + + +.. py:class:: AddPresetCamOperation + + Bases: :py:obj:`bl_operators.presets.AddPresetBase`, :py:obj:`bpy.types.Operator` + + + Add an Operation Preset + + + .. py:attribute:: bl_idname + :value: 'render.cam_preset_operation_add' + + + + .. py:attribute:: bl_label + :value: 'Add Operation Preset' + + + + .. py:attribute:: preset_menu + :value: 'CAM_OPERATION_MT_presets' + + + + .. py:attribute:: preset_defines + :value: ['from pathlib import Path', 'bpy.ops.scene.cam_operation_add()', 'scene = bpy.context.scene',... + + + + .. py:attribute:: preset_values + :value: ['o.info.duration', 'o.info.chipload', 'o.info.warnings', 'o.material.estimate_from_model',... + + + + .. py:attribute:: preset_subdir + :value: 'cam_operations' + + + +.. py:class:: CAM_CUTTER_MT_presets + + Bases: :py:obj:`bpy.types.Menu` + + + .. py:attribute:: bl_label + :value: 'Cutter Presets' + + + + .. py:attribute:: preset_subdir + :value: 'cam_cutters' + + + + .. py:attribute:: preset_operator + :value: 'script.execute_preset' + + + + .. py:attribute:: draw + + +.. py:class:: CAM_MACHINE_MT_presets + + Bases: :py:obj:`bpy.types.Menu` + + + .. py:attribute:: bl_label + :value: 'Machine Presets' + + + + .. py:attribute:: preset_subdir + :value: 'cam_machines' + + + + .. py:attribute:: preset_operator + :value: 'script.execute_preset' + + + + .. py:attribute:: draw + + + .. py:method:: post_cb(context) + :classmethod: + + + +.. py:class:: CAM_OPERATION_MT_presets + + Bases: :py:obj:`bpy.types.Menu` + + + .. py:attribute:: bl_label + :value: 'Operation Presets' + + + + .. py:attribute:: preset_subdir + :value: 'cam_operations' + + + + .. py:attribute:: preset_operator + :value: 'script.execute_preset' + + + + .. py:attribute:: draw + + +.. py:class:: SliceObjectsSettings + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + Stores All Data for Machines + + + .. py:attribute:: slice_distance + :type: 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') + + + .. py:attribute:: slice_above0 + :type: BoolProperty(name='Slice Above 0', description='only slice model above 0', default=False) + + + .. py:attribute:: slice_3d + :type: BoolProperty(name='3D Slice', description='For 3D carving', default=False) + + + .. py:attribute:: indexes + :type: BoolProperty(name='Add Indexes', description='Adds index text of layer + index', default=True) + + +.. py:class:: CustomPanel + + Bases: :py:obj:`bpy.types.Panel` + + + .. py:attribute:: bl_space_type + :value: 'VIEW_3D' + + + + .. py:attribute:: bl_region_type + :value: 'TOOLS' + + + + .. py:attribute:: bl_context + :value: 'objectmode' + + + + .. py:attribute:: bl_label + :value: 'Import G-code' + + + + .. py:attribute:: bl_idname + :value: 'OBJECT_PT_importgcode' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: draw(context) + + +.. py:class:: import_settings + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + .. py:attribute:: split_layers + :type: BoolProperty(name='Split Layers', description='Save every layer as single Objects in Collection', default=False) + + + .. py:attribute:: subdivide + :type: BoolProperty(name='Subdivide', description="Only Subdivide gcode segments that are bigger than 'Segment length' ", default=False) + + + .. py:attribute:: output + :type: EnumProperty(name='Output Type', items=(('mesh', 'Mesh', 'Make a mesh output'), ('curve', 'Curve', 'Make curve output')), default='curve') + + + .. py:attribute:: max_segment_size + :type: FloatProperty(name='', description='Only Segments bigger than this value get subdivided', default=0.001, min=0.0001, max=1.0, unit='LENGTH') + + +.. py:class:: VIEW3D_PT_tools_curvetools + + Bases: :py:obj:`bpy.types.Panel` + + + .. py:attribute:: bl_space_type + :value: 'VIEW_3D' + + + + .. py:attribute:: bl_region_type + :value: 'TOOLS' + + + + .. py:attribute:: bl_context + :value: 'objectmode' + + + + .. py:attribute:: bl_label + :value: 'Curve CAM Tools' + + + + .. py:method:: draw(context) + + +.. py:class:: VIEW3D_PT_tools_create + + Bases: :py:obj:`bpy.types.Panel` + + + .. py:attribute:: bl_space_type + :value: 'VIEW_3D' + + + + .. py:attribute:: bl_region_type + :value: 'TOOLS' + + + + .. py:attribute:: bl_context + :value: 'objectmode' + + + + .. py:attribute:: bl_label + :value: 'Curve CAM Creators' + + + + .. py:attribute:: bl_option + :value: 'DEFAULT_CLOSED' + + + + .. py:method:: draw(context) + + +.. py:class:: WM_OT_gcode_import + + Bases: :py:obj:`bpy.types.Operator`, :py:obj:`bpy_extras.io_utils.ImportHelper` + + + Import G-code, Travel Lines Don't Get Drawn + + + .. py:attribute:: bl_idname + :value: 'wm.gcode_import' + + + + .. py:attribute:: bl_label + :value: 'Import G-code' + + + + .. py:attribute:: filename_ext + :value: '.txt' + + + + .. py:attribute:: filter_glob + :type: StringProperty(default='*.*', options={'HIDDEN'}, maxlen=255) + + + .. py:method:: execute(context) + + +.. py:function:: check_operations_on_load(context) + + Checks for any broken computations on load and resets them. + + This function verifies the presence of necessary Blender add-ons and + installs any that are missing. It also resets any ongoing computations + in camera operations and sets the interface level to the previously used + level when loading a new file. If the add-on has been updated, it copies + the necessary presets from the source to the target directory. + Additionally, it checks for updates to the camera plugin and updates + operation presets if required. + + :param context: The context in which the function is executed, typically containing + information about + the current Blender environment. + + +.. py:function:: updateOperation(self, context) + + Update the visibility and selection state of camera operations in the + scene. + + This method manages the visibility of objects associated with camera + operations based on the current active operation. If the + 'hide_all_others' flag is set to true, it hides all other objects except + for the currently active one. If the flag is false, it restores the + visibility of previously hidden objects. The method also attempts to + highlight the currently active object in the 3D view and make it the + active object in the scene. + + :param context: The context containing the current scene and + :type context: bpy.types.Context + + +.. py:data:: classes + +.. py:function:: register() -> None + +.. py:function:: unregister() -> None + diff --git a/_sources/autoapi/cam/involute_gear/index.rst b/_sources/autoapi/cam/involute_gear/index.rst new file mode 100644 index 000000000..2850a6902 --- /dev/null +++ b/_sources/autoapi/cam/involute_gear/index.rst @@ -0,0 +1,164 @@ +cam.involute_gear +================= + +.. py:module:: cam.involute_gear + +.. autoapi-nested-parse:: + + CNC CAM 'involute_gear.py' Ported by Alain Pelletier Jan 2022 + + from: + Public Domain Parametric Involute Spur Gear (and involute helical gear and involute rack) + version 1.1 + by Leemon Baird, 2011, Leemon@Leemon.com + http:www.thingiverse.com/thing:5505 + + This file is public domain. Use it for any purpose, including commercial + applications. Attribution would be nice, but is not required. There is + no warranty of any kind, including its correctness, usefulness, or safety. + + This is parameterized involute spur (or helical) gear. It is much simpler and less powerful than + others on Thingiverse. But it is public domain. I implemented it from scratch from the + descriptions and equations on Wikipedia and the web, using Mathematica for calculations and testing, + and I now release it into the public domain. + + http:en.wikipedia.org/wiki/Involute_gear + http:en.wikipedia.org/wiki/Gear + http:en.wikipedia.org/wiki/List_of_gear_nomenclature + http:gtrebaol.free.fr/doc/catia/spur_gear.html + http:www.cs.cmu.edu/~rapidproto/mechanisms/chpt7.html + + The module gear() gives an involute spur gear, with reasonable defaults for all the parameters. + Normally, you should just choose the first 4 parameters, and let the rest be default values. + The module gear() gives a gear in the XY plane, centered on the origin, with one tooth centered on + the positive Y axis. The various functions below it take the same parameters, and return various + measurements for the gear. The most important is pitch_radius, which tells how far apart to space + gears that are meshing, and adendum_radius, which gives the size of the region filled by the gear. + A gear has a "pitch circle", which is an invisible circle that cuts through the middle of each + tooth (though not the exact center). In order for two gears to mesh, their pitch circles should + just touch. So the distance between their centers should be pitch_radius() for one, plus pitch_radius() + for the other, which gives the radii of their pitch circles. + + In order for two gears to mesh, they must have the same mm_per_tooth and pressure_angle parameters. + mm_per_tooth gives the number of millimeters of arc around the pitch circle covered by one tooth and one + space between teeth. The pitch angle controls how flat or bulged the sides of the teeth are. Common + values include 14.5 degrees and 20 degrees, and occasionally 25. Though I've seen 28 recommended for + plastic gears. Larger numbers bulge out more, giving stronger teeth, so 28 degrees is the default here. + + The ratio of number_of_teeth for two meshing gears gives how many times one will make a full + revolution when the the other makes one full revolution. If the two numbers are coprime (i.e. + are not both divisible by the same number greater than 1), then every tooth on one gear + will meet every tooth on the other, for more even wear. So coprime numbers of teeth are good. + + The module rack() gives a rack, which is a bar with teeth. A rack can mesh with any + gear that has the same mm_per_tooth and pressure_angle. + + Some terminology: + The outline of a gear is a smooth circle (the "pitch circle") which has mountains and valleys + added so it is toothed. So there is an inner circle (the "root circle") that touches the + base of all the teeth, an outer circle that touches the tips of all the teeth, + and the invisible pitch circle in between them. There is also a "base circle", which can be smaller than + all three of the others, which controls the shape of the teeth. The side of each tooth lies on the path + that the end of a string would follow if it were wrapped tightly around the base circle, then slowly unwound. + That shape is an "involute", which gives this type of gear its name. + + An involute spur gear, with reasonable defaults for all the parameters. + Normally, you should just choose the first 4 parameters, and let the rest be default values. + Meshing gears must match in mm_per_tooth, pressure_angle, and twist, + and be separated by the sum of their pitch radii, which can be found with pitch_radius(). + + + +Functions +--------- + +.. autoapisummary:: + + cam.involute_gear.gear_polar + cam.involute_gear.gear_iang + cam.involute_gear.gear_q7 + cam.involute_gear.gear_q6 + cam.involute_gear.gear + cam.involute_gear.rack + + +Module Contents +--------------- + +.. py:function:: gear_polar(r, theta) + +.. py:function:: gear_iang(r1, r2) + +.. py:function:: gear_q7(f, r, b, r2, t, s) + +.. py:function:: gear_q6(b, s, t, d) + +.. py:function:: 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) + + Generate a 3D gear model based on specified parameters. + + This function creates a 3D representation of a gear using the provided + parameters such as the circular pitch, number of teeth, hole diameter, + pressure angle, clearance, backlash, rim size, hub diameter, and the + number of spokes. The gear is constructed by calculating various radii + and angles based on the input parameters and then using geometric + operations to form the final shape. The resulting gear is named + according to its specifications. + + :param mm_per_tooth: The circular pitch of the gear in millimeters (default is 0.003). + :type mm_per_tooth: float + :param number_of_teeth: The total number of teeth on the gear (default is 5). + :type number_of_teeth: int + :param hole_diameter: The diameter of the central hole in millimeters (default is 0.003175). + :type hole_diameter: float + :param pressure_angle: The angle that controls the shape of the tooth sides in radians (default + is 0.3488). + :type pressure_angle: float + :param clearance: The gap between the top of a tooth and the bottom of a valley on a + meshing gear in millimeters (default is 0.0). + :type clearance: float + :param backlash: The gap between two meshing teeth along the circumference of the pitch + circle in millimeters (default is 0.0). + :type backlash: float + :param rim_size: The size of the rim around the gear in millimeters (default is 0.0005). + :type rim_size: float + :param hub_diameter: The diameter of the hub in millimeters (default is 0.006). + :type hub_diameter: float + :param spokes: The number of spokes on the gear (default is 4). + :type spokes: int + + :returns: + + This function does not return a value but modifies the Blender scene to + include the generated gear model. + :rtype: None + + +.. py:function:: 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) + + Generate a rack gear profile based on specified parameters. + + This function creates a rack gear by calculating the geometry based on + the provided parameters such as millimeters per tooth, number of teeth, + height, pressure angle, backlash, hole diameter, and teeth per hole. It + constructs the gear shape using the Shapely library and duplicates the + tooth to create the full rack. If a hole diameter is specified, it also + creates holes along the rack. The resulting gear is named based on the + input parameters. + + :param mm_per_tooth: The distance in millimeters for each tooth. Default is 0.01. + :type mm_per_tooth: float + :param number_of_teeth: The total number of teeth on the rack. Default is 11. + :type number_of_teeth: int + :param height: The height of the rack. Default is 0.012. + :type height: float + :param pressure_angle: The pressure angle in radians. Default is 0.3488. + :type pressure_angle: float + :param backlash: The backlash distance in millimeters. Default is 0.0. + :type backlash: float + :param hole_diameter: The diameter of the holes in millimeters. Default is 0.003175. + :type hole_diameter: float + :param tooth_per_hole: The number of teeth per hole. Default is 4. + :type tooth_per_hole: int + + diff --git a/_sources/autoapi/cam/joinery/index.rst b/_sources/autoapi/cam/joinery/index.rst new file mode 100644 index 000000000..9d3abb60c --- /dev/null +++ b/_sources/autoapi/cam/joinery/index.rst @@ -0,0 +1,421 @@ +cam.joinery +=========== + +.. py:module:: cam.joinery + +.. autoapi-nested-parse:: + + CNC CAM 'joinery.py' © 2021 Alain Pelletier + + Functions to create various woodworking joints - mortise, finger etc. + + + +Functions +--------- + +.. autoapisummary:: + + cam.joinery.finger_amount + cam.joinery.mortise + cam.joinery.interlock_groove + cam.joinery.interlock_twist + cam.joinery.twist_line + cam.joinery.twist_separator_slot + cam.joinery.interlock_twist_separator + cam.joinery.horizontal_finger + cam.joinery.vertical_finger + cam.joinery.finger_pair + cam.joinery.create_base_plate + cam.joinery.make_flex_pocket + cam.joinery.make_variable_flex_pocket + cam.joinery.create_flex_side + cam.joinery.angle + cam.joinery.angle_difference + cam.joinery.fixed_finger + cam.joinery.find_slope + cam.joinery.slope_array + cam.joinery.dslope_array + cam.joinery.variable_finger + cam.joinery.single_interlock + cam.joinery.distributed_interlock + + +Module Contents +--------------- + +.. py:function:: finger_amount(space, size) + + Calculates the amount of fingers needed from the available space vs the size of the finger + + :param space: available distance to cover + :type space: float + :param size: size of the finger + :type size: float + + +.. py:function:: mortise(length, thickness, finger_play, cx=0, cy=0, rotation=0) + + Generates a mortise of length, thickness and finger_play tolerance + cx and cy are the center position and rotation is the angle + + :param length: length of the mortise + :type length: float + :param thickness: thickness of material + :type thickness: float + :param finger_play: tolerance for good fit + :type finger_play: float + :param cx: coordinate for x center of the finger + :type cx: float + :param cy: coordinate for y center of the finger + :type cy: float + :param rotation: angle of rotation + :type rotation: float + + +.. py:function:: interlock_groove(length, thickness, finger_play, cx=0, cy=0, rotation=0) + + Generates an interlocking groove. + + :param length: Length of groove + :type length: float + :param thickness: thickness of groove + :type thickness: float + :param finger_play: tolerance for proper fit + :type finger_play: float + :param cx: center offset x + :type cx: float + :param cy: center offset y + :type cy: float + :param rotation: angle of rotation + :type rotation: float + + +.. py:function:: interlock_twist(length, thickness, finger_play, cx=0, cy=0, rotation=0, percentage=0.5) + + Generates an interlocking twist. + + :param length: Length of groove + :type length: float + :param thickness: thickness of groove + :type thickness: float + :param finger_play: tolerance for proper fit + :type finger_play: float + :param cx: center offset x + :type cx: float + :param cy: center offset y + :type cy: float + :param rotation: angle of rotation + :type rotation: float + :param percentage: percentage amount the twist will take (between 0 and 1) + :type percentage: float + + +.. py:function:: twist_line(length, thickness, finger_play, percentage, amount, distance, center=True) + + Generates a multiple interlocking twist. + + :param length: Length of groove + :type length: float + :param thickness: thickness of groove + :type thickness: float + :param finger_play: tolerance for proper fit + :type finger_play: float + :param percentage: percentage amount the twist will take (between 0 and 1) + :type percentage: float + :param amount: amount of twists generated + :type amount: int + :param distance: distance between twists + :type distance: float + :param center: center or not from origin + :type center: bool + + +.. py:function:: twist_separator_slot(length, thickness, finger_play=5e-05, percentage=0.5) + + Generates a slot for interlocking twist separator. + + :param length: Length of slot + :type length: float + :param thickness: thickness of slot + :type thickness: float + :param finger_play: tolerance for proper fit + :type finger_play: float + :param percentage: percentage amount the twist will take (between 0 and 1) + :type percentage: float + + +.. py:function:: interlock_twist_separator(length, thickness, amount, spacing, edge_distance, finger_play=5e-05, percentage=0.5, start='rounded', end='rounded') + + Generates a interlocking twist separator. + + :param length: Length of separator + :type length: float + :param thickness: thickness of separator + :type thickness: float + :param amount: quantity of separation grooves + :type amount: int + :param spacing: distance between slots + :type spacing: float + :param edge_distance: distance of the first slots close to the edge + :type edge_distance: float + :param finger_play: tolerance for proper fit + :type finger_play: float + :param percentage: percentage amount the twist will take (between 0 and 1) + :type percentage: float + :param start: type of start wanted (rounded, flat or other) not implemented + :type start: string + :param start: type of end wanted (rounded, flat or other) not implemented + :type start: string + + +.. py:function:: horizontal_finger(length, thickness, finger_play, amount, center=True) + + Generates an interlocking horizontal finger pair _wfa and _wfb. + + _wfa is centered at 0,0 + _wfb is _wfa offset by one length + + :param length: Length of mortise + :type length: float + :param thickness: thickness of material + :type thickness: float + :param amount: quantity of fingers + :type amount: int + :param finger_play: tolerance for proper fit + :type finger_play: float + :param center: centered of not + :type center: bool + + +.. py:function:: vertical_finger(length, thickness, finger_play, amount) + + Generates an interlocking horizontal finger pair _vfa and _vfb. + + _vfa is starts at 0,0 + _vfb is _vfa offset by one length + + :param length: Length of mortise + :type length: float + :param thickness: thickness of material + :type thickness: float + :param amount: quantity of fingers + :type amount: int + :param finger_play: tolerance for proper fit + :type finger_play: float + + +.. py:function:: finger_pair(name, dx=0, dy=0) + + Creates a duplicate set of fingers. + + :param name: name of original finger + :type name: str + :param dx: x offset + :type dx: float + :param dy: y offset + :type dy: float + + +.. py:function:: create_base_plate(height, width, depth) + + Creates blank plates for a box. + + :param height: height size for box + :type height: float + :param width: width size for box + :type width: float + :param depth: depth size for box + :type depth: float + + +.. py:function:: make_flex_pocket(length, height, finger_thick, finger_width, pocket_width) + + creates pockets using mortise function for kerf bending + + :param length: Length of pocket + :type length: float + :param height: height of pocket + :type height: float + :param finger_thick: thickness of finger + :type finger_thick: float + :param finger_width: width of finger + :type finger_width: float + :param pocket_width: width of pocket + :type pocket_width: float + + +.. py:function:: make_variable_flex_pocket(height, finger_thick, pocket_width, locations) + + creates pockets pocket using mortise function for kerf bending + + :param height: height of the side + :type height: float + :param finger_thick: thickness of the finger + :type finger_thick: float + :param pocket_width: width of pocket + :type pocket_width: float + :param locations: coordinates for pocket + :type locations: tuple + + +.. py:function:: create_flex_side(length, height, finger_thick, top_bottom=False) + + crates a flex side for mortise on curve. Assumes the base fingers were created and exist + + :param length: length of curve + :type length: float + :param height: height of side + :type height: float + :param finger_thick: finger thickness or thickness of material + :type finger_thick: float + :param top_bottom: fingers on top and bottom if true, just on bottom if false + :type top_bottom: bool + + +.. py:function:: angle(a, b) + + returns angle of a vector + + :param a: point a x,y coordinates + :type a: tuple + :param b: point b x,y coordinates + :type b: tuple + + +.. py:function:: angle_difference(a, b, c) + + returns the difference between two lines with three points + + :param a: point a x,y coordinates + :type a: tuple + :param b: point b x,y coordinates + :type b: tuple + :param c: point c x,y coordinates + :type c: tuple + + +.. py:function:: fixed_finger(loop, loop_length, finger_size, finger_thick, finger_tolerance, base=False) + + distributes mortises of a fixed distance. Dynamically changes the finger tolerance with the angle differences + + :param loop: takes in a shapely shape + :type loop: list of tuples + :param loop_length: length of loop + :type loop_length: float + :param finger_size: size of the mortise + :type finger_size: float + :param finger_thick: thickness of the material + :type finger_thick: float + :param finger_tolerance: minimum finger tolerance + :type finger_tolerance: float + :param base: if base exists, it will join with it + :type base: bool + + +.. py:function:: find_slope(p1, p2) + + returns slope of a vector + + :param p1: point 1 x,y coordinates + :type p1: tuple + :param p2: point 2 x,y coordinates + :type p2: tuple + + +.. py:function:: slope_array(loop) + + Returns an array of slopes from loop coordinates. + + :param loop: list of coordinates for a curve + :type loop: list of tuples + + +.. py:function:: dslope_array(loop, resolution=0.001) + + Returns a double derivative array or slope of the slope + + :param loop: list of coordinates for a curve + :type loop: list of tuples + :param resolution: granular resolution of the array + :type resolution: float + + +.. py:function:: variable_finger(loop, loop_length, min_finger, finger_size, finger_thick, finger_tolerance, adaptive, base=False, double_adaptive=False) + + Distributes mortises of a fixed distance. Dynamically changes the finger tolerance with the angle differences + + :param loop: takes in a shapely shape + :type loop: list of tuples + :param loop_length: length of loop + :type loop_length: float + :param finger_size: size of the mortise + :type finger_size: float + :param finger_thick: thickness of the material + :type finger_thick: float + :param min_finger: minimum finger size + :type min_finger: float + :param finger_tolerance: minimum finger tolerance + :type finger_tolerance: float + :param adaptive: angle threshold to reduce finger size + :type adaptive: float + :param base: join with base if true + :type base: bool + :param double_adaptive: uses double adaptive algorithm if true + :type double_adaptive: bool + + +.. py:function:: single_interlock(finger_depth, finger_thick, finger_tolerance, x, y, groove_angle, type, amount=1, twist_percentage=0.5) + + Generates a single interlock at coodinate x,y. + + :param finger_depth: depth of finger + :type finger_depth: float + :param finger_thick: thickness of finger + :type finger_thick: float + :param finger_tolerance: tolerance for proper fit + :type finger_tolerance: float + :param x: offset x + :type x: float + :param y: offset y + :type y: float + :param groove_angle: angle of rotation + :type groove_angle: float + :param type: GROOVE, TWIST, PUZZLE are the valid choices + :type type: str + :param twist_percentage: percentage of thickness for twist (not used in puzzle or groove) + + +.. py:function:: distributed_interlock(loop, loop_length, finger_depth, finger_thick, finger_tolerance, finger_amount, tangent=0, fixed_angle=0, start=0.01, end=0.01, closed=True, type='GROOVE', twist_percentage=0.5) + + Distributes interlocking joints of a fixed amount. + Dynamically changes the finger tolerance with the angle differences + + :param loop: coordinates curve + :type loop: list of tuples + :param loop_length: length of the curve + :type loop_length: float + :param finger_depth: depth of the mortise + :type finger_depth: float + :param finger_thick: + :type finger_thick: float + :param finger_tolerance: minimum finger tolerance + :type finger_tolerance: float + :param finger_amount: quantity of fingers + :type finger_amount: int + :param tangent: + :type tangent: int + :param fixed_angle: 0 will be variable, desired angle for the finger + :type fixed_angle: float + :param closed: False:open curve - True:closed curved + :type closed: bool + :param twist_percentage = portion of twist finger which is the stem: + :type twist_percentage = portion of twist finger which is the stem: for twist joint only + :param type: GROOVE, TWIST, PUZZLE are the valid choices + :type type: str + :param start: start distance from first point + :type start: float + :param end: end distance from last point + :type end: float + + diff --git a/_sources/autoapi/cam/machine_settings/index.rst b/_sources/autoapi/cam/machine_settings/index.rst new file mode 100644 index 000000000..527623238 --- /dev/null +++ b/_sources/autoapi/cam/machine_settings/index.rst @@ -0,0 +1,132 @@ +cam.machine_settings +==================== + +.. py:module:: cam.machine_settings + +.. autoapi-nested-parse:: + + CNC CAM 'machine_settings.py' + + All CAM machine properties. + + + +Classes +------- + +.. autoapisummary:: + + cam.machine_settings.machineSettings + + +Module Contents +--------------- + +.. py:class:: machineSettings + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + stores all data for machines + + + .. py:attribute:: post_processor + :type: 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') + + + .. py:attribute:: use_position_definitions + :type: BoolProperty(name='Use Position Definitions', description='Define own positions for op start, toolchange, ending position', default=False) + + + .. py:attribute:: starting_position + :type: FloatVectorProperty(name='Start Position', default=(0, 0, 0), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ', update=updateMachine) + + + .. py:attribute:: mtc_position + :type: FloatVectorProperty(name='MTC Position', default=(0, 0, 0), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ', update=updateMachine) + + + .. py:attribute:: ending_position + :type: FloatVectorProperty(name='End Position', default=(0, 0, 0), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ', update=updateMachine) + + + .. py:attribute:: working_area + :type: FloatVectorProperty(name='Work Area', default=(0.5, 0.5, 0.1), unit='LENGTH', precision=constants.PRECISION, subtype='XYZ', update=updateMachine) + + + .. py:attribute:: feedrate_min + :type: FloatProperty(name='Feedrate Minimum /min', default=0.0, min=1e-05, max=320000, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: feedrate_max + :type: FloatProperty(name='Feedrate Maximum /min', default=2, min=1e-05, max=320000, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: feedrate_default + :type: FloatProperty(name='Feedrate Default /min', default=1.5, min=1e-05, max=320000, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: hourly_rate + :type: FloatProperty(name='Price per Hour', default=100, min=0.005, precision=2) + + + .. py:attribute:: spindle_min + :type: FloatProperty(name='Spindle Speed Minimum RPM', default=5000, min=1e-05, max=320000, precision=1) + + + .. py:attribute:: spindle_max + :type: FloatProperty(name='Spindle Speed Maximum RPM', default=30000, min=1e-05, max=320000, precision=1) + + + .. py:attribute:: spindle_default + :type: FloatProperty(name='Spindle Speed Default RPM', default=15000, min=1e-05, max=320000, precision=1) + + + .. py:attribute:: spindle_start_time + :type: FloatProperty(name='Spindle Start Delay Seconds', description='Wait for the spindle to start spinning before starting the feeds , in seconds', default=0, min=0.0, max=320000, precision=1) + + + .. py:attribute:: axis4 + :type: BoolProperty(name='#4th Axis', description='Machine has 4th axis', default=0) + + + .. py:attribute:: axis5 + :type: BoolProperty(name='#5th Axis', description='Machine has 5th axis', default=0) + + + .. py:attribute:: eval_splitting + :type: BoolProperty(name='Split Files', description='Split gcode file with large number of operations', default=True) + + + .. py:attribute:: split_limit + :type: IntProperty(name='Operations per File', description='Split files with larger number of operations than this', min=1000, max=20000000, default=800000) + + + .. py:attribute:: collet_size + :type: FloatProperty(name='#Collet Size', description='Collet size for collision detection', default=33, min=1e-05, max=320000, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: output_block_numbers + :type: BoolProperty(name='Output Block Numbers', description='Output block numbers ie N10 at start of line', default=False) + + + .. py:attribute:: start_block_number + :type: IntProperty(name='Start Block Number', description='The starting block number ie 10', default=10) + + + .. py:attribute:: block_number_increment + :type: IntProperty(name='Block Number Increment', description='How much the block number should increment for the next line', default=10) + + + .. py:attribute:: output_tool_definitions + :type: BoolProperty(name='Output Tool Definitions', description='Output tool definitions', default=True) + + + .. py:attribute:: output_tool_change + :type: BoolProperty(name='Output Tool Change Commands', description='Output tool change commands ie: Tn M06', default=True) + + + .. py:attribute:: output_g43_on_tool_change + :type: BoolProperty(name='Output G43 on Tool Change', description='Output G43 on tool change line', default=False) + + diff --git a/_sources/autoapi/cam/numba_wrapper/index.rst b/_sources/autoapi/cam/numba_wrapper/index.rst new file mode 100644 index 000000000..bf540c764 --- /dev/null +++ b/_sources/autoapi/cam/numba_wrapper/index.rst @@ -0,0 +1,26 @@ +cam.numba_wrapper +================= + +.. py:module:: cam.numba_wrapper + +.. autoapi-nested-parse:: + + CNC CAM 'numba_wrapper.py' + + Patch to ensure functions will run if numba is unavailable. + + + +Functions +--------- + +.. autoapisummary:: + + cam.numba_wrapper.jit + + +Module Contents +--------------- + +.. py:function:: jit(f=None, *args, **kwargs) + diff --git a/_sources/autoapi/cam/ops/index.rst b/_sources/autoapi/cam/ops/index.rst new file mode 100644 index 000000000..01346bb7a --- /dev/null +++ b/_sources/autoapi/cam/ops/index.rst @@ -0,0 +1,1302 @@ +cam.ops +======= + +.. py:module:: cam.ops + +.. autoapi-nested-parse:: + + CNC CAM 'ops.py' © 2012 Vilem Novak + + Blender Operator definitions are in this file. + They mostly call the functions from 'utils.py' + + + +Classes +------- + +.. autoapisummary:: + + cam.ops.threadCom + cam.ops.PathsBackground + cam.ops.KillPathsBackground + cam.ops.CalculatePath + cam.ops.PathsAll + cam.ops.CamPackObjects + cam.ops.CamSliceObjects + cam.ops.PathsChain + cam.ops.PathExportChain + cam.ops.PathExport + cam.ops.CAMSimulate + cam.ops.CAMSimulateChain + cam.ops.CamChainAdd + cam.ops.CamChainRemove + cam.ops.CamChainOperationAdd + cam.ops.CamChainOperationUp + cam.ops.CamChainOperationDown + cam.ops.CamChainOperationRemove + cam.ops.CamOperationAdd + cam.ops.CamOperationCopy + cam.ops.CamOperationRemove + cam.ops.CamOperationMove + cam.ops.CamOrientationAdd + cam.ops.CamBridgesAdd + + +Functions +--------- + +.. autoapisummary:: + + cam.ops.threadread + cam.ops.timer_update + cam.ops._calc_path + cam.ops.getChainOperations + cam.ops.fixUnits + + +Module Contents +--------------- + +.. py:class:: threadCom(o, proc) + + .. py:attribute:: opname + + + .. py:attribute:: outtext + :value: '' + + + + .. py:attribute:: proc + + + .. py:attribute:: lasttext + :value: '' + + + +.. py:function:: threadread(tcom) + + Reads the standard output of a background process in a non-blocking + manner. + + This function reads a line from the standard output of a background + process associated with the provided `tcom` object. It searches for a + specific substring that indicates progress information, and if found, + extracts that information and assigns it to the `outtext` attribute of + the `tcom` object. This allows for real-time monitoring of the + background process's output without blocking the main thread. + + :param tcom: An object that has a `proc` attribute with a `stdout` + stream from which to read the output. + :type tcom: object + + :returns: + + This function does not return a value; it modifies the `tcom` + object in place. + :rtype: None + + +.. py:function:: timer_update(context) + + Monitor background processes related to camera path calculations. + + This function checks the status of background processes that are + responsible for calculating camera paths. It retrieves the current + processes and monitors their state. If a process has finished, it + updates the corresponding camera operation and reloads the necessary + paths. If the process is still running, it restarts the associated + thread to continue monitoring. + + :param context: The context in which the function is called, typically + containing information about the current scene and operations. + + +.. py:class:: PathsBackground + + Bases: :py:obj:`bpy.types.Operator` + + + Calculate CAM Paths in Background. File Has to Be Saved Before. + + + .. py:attribute:: bl_idname + :value: 'object.calculate_cam_paths_background' + + + + .. py:attribute:: bl_label + :value: 'Calculate CAM Paths in Background' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + Execute the camera operation in the background. + + This method initiates a background process to perform camera operations + based on the current scene and active camera operation. It sets up the + necessary paths for the script and starts a subprocess to handle the + camera computations. Additionally, it manages threading to ensure that + the main thread remains responsive while the background operation is + executed. + + :param context: The context in which the operation is executed. + + :returns: A dictionary indicating the completion status of the operation. + :rtype: dict + + + +.. py:class:: KillPathsBackground + + Bases: :py:obj:`bpy.types.Operator` + + + Remove CAM Path Processes in Background. + + + .. py:attribute:: bl_idname + :value: 'object.kill_calculate_cam_paths_background' + + + + .. py:attribute:: bl_label + :value: 'Kill Background Computation of an Operation' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + Execute the camera operation in the given context. + + This method retrieves the active camera operation from the scene and + checks if there are any ongoing processes related to camera path + calculations. If such processes exist and match the current operation, + they are terminated. The method then marks the operation as not + computing and returns a status indicating that the execution has + finished. + + :param context: The context in which the operation is executed. + + :returns: A dictionary with a status key indicating the result of the execution. + :rtype: dict + + + +.. py:function:: _calc_path(operator, context) + :async: + + + Calculate the path for a given operator and context. + + This function processes the current scene's camera operations based on + the specified operator and context. It handles different geometry + sources, checks for valid operation parameters, and manages the + visibility of objects and collections. The function also retrieves the + path using an asynchronous operation and handles any exceptions that may + arise during this process. If the operation is invalid or if certain + conditions are not met, appropriate error messages are reported to the + operator. + + :param operator: The operator that initiated the path calculation. + :type operator: bpy.types.Operator + :param context: The context in which the operation is executed. + :type context: bpy.types.Context + + :returns: + + A tuple indicating the status of the operation. + Returns {'FINISHED', True} if successful, + {'FINISHED', False} if there was an error, + or {'CANCELLED', False} if the operation was cancelled. + :rtype: tuple + + +.. py:class:: CalculatePath + + Bases: :py:obj:`bpy.types.Operator`, :py:obj:`cam.async_op.AsyncOperatorMixin` + + + Calculate CAM Paths + + + .. py:attribute:: bl_idname + :value: 'object.calculate_cam_path' + + + + .. py:attribute:: bl_label + :value: 'Calculate CAM Paths' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + Check if the current camera operation is valid. + + This method checks the active camera operation in the given context and + determines if it is valid. It retrieves the active operation from the + scene's camera operations and validates it using the `isValid` function. + If the operation is valid, it returns True; otherwise, it returns False. + + :param context: The context containing the scene and camera operations. + :type context: Context + + :returns: True if the active camera operation is valid, False otherwise. + :rtype: bool + + + + .. py:method:: execute_async(context) + :async: + + + Execute an asynchronous calculation of a path. + + This method performs an asynchronous operation to calculate a path based + on the provided context. It awaits the result of the calculation and + prints the success status along with the return value. The return value + can be used for further processing or analysis. + + :param context: The context in which the path calculation is to be executed. + :type context: Any + + :returns: The result of the path calculation. + :rtype: Any + + + +.. py:class:: PathsAll + + Bases: :py:obj:`bpy.types.Operator` + + + Calculate All CAM Paths + + + .. py:attribute:: bl_idname + :value: 'object.calculate_cam_paths_all' + + + + .. py:attribute:: bl_label + :value: 'Calculate All CAM Paths' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + Execute camera operations in the current Blender context. + + This function iterates through the camera operations defined in the + current scene and executes the background calculation for each + operation. It sets the active camera operation index and prints the name + of each operation being processed. This is typically used in a Blender + add-on or script to automate camera path calculations. + + :param context: The current Blender context. + :type context: bpy.context + + :returns: + + A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + :rtype: dict + + + + .. py:method:: draw(context) + + Draws the user interface elements for the operation selection. + + This method utilizes the Blender layout system to create a property + search interface for selecting operations related to camera + functionalities. It links the current instance's operation property to + the available camera operations defined in the Blender scene. + + :param context: The context in which the drawing occurs, + :type context: bpy.context + + + +.. py:class:: CamPackObjects + + Bases: :py:obj:`bpy.types.Operator` + + + Calculate All CAM Paths + + + .. py:attribute:: bl_idname + :value: 'object.cam_pack_objects' + + + + .. py:attribute:: bl_label + :value: 'Pack Curves on Sheet' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + Execute the operation in the given context. + + This function sets the Blender object mode to 'OBJECT', retrieves the + currently selected objects, and calls the `packCurves` function from the + `pack` module. It is typically used to finalize operations on selected + objects in Blender. + + :param context: The context in which the operation is executed. + + :returns: A dictionary indicating the completion status of the operation. + :rtype: dict + + + + .. py:method:: draw(context) + + +.. py:class:: CamSliceObjects + + Bases: :py:obj:`bpy.types.Operator` + + + Slice a Mesh Object Horizontally + + + .. py:attribute:: bl_idname + :value: 'object.cam_slice_objects' + + + + .. py:attribute:: bl_label + :value: 'Slice Object - Useful for Lasercut Puzzles etc' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + Execute the slicing operation on the active Blender object. + + This function retrieves the currently active object in the Blender + context and performs a slicing operation on it using the `sliceObject` + function from the `cam` module. The operation is intended to modify the + object based on the slicing logic defined in the external module. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the result of the operation, + typically containing the key 'FINISHED' upon successful execution. + :rtype: dict + + + + .. py:method:: draw(context) + + +.. py:function:: getChainOperations(chain) + + Return chain operations associated with a given chain object. + + This function iterates through the operations of the provided chain + object and retrieves the corresponding operations from the current + scene's camera operations in Blender. Due to limitations in Blender, + chain objects cannot store operations directly, so this function serves + to extract and return the relevant operations for further processing. + + :param chain: The chain object from which to retrieve operations. + :type chain: object + + :returns: A list of operations associated with the given chain object. + :rtype: list + + +.. py:class:: PathsChain + + Bases: :py:obj:`bpy.types.Operator`, :py:obj:`cam.async_op.AsyncOperatorMixin` + + + Calculate a Chain and Export the G-code Alltogether. + + + .. py:attribute:: bl_idname + :value: 'object.calculate_cam_paths_chain' + + + + .. py:attribute:: bl_label + :value: 'Calculate CAM Paths in Current Chain and Export Chain G-code' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + Check the validity of the active camera chain in the given context. + + This method retrieves the active camera chain from the scene and checks + its validity using the `isChainValid` function. It returns a boolean + value indicating whether the camera chain is valid or not. + + :param context: The context containing the scene and camera chain information. + :type context: Context + + :returns: True if the active camera chain is valid, False otherwise. + :rtype: bool + + + + .. py:method:: execute_async(context) + :async: + + + Execute asynchronous operations for camera path calculations. + + This method sets the object mode for the Blender scene and processes a + series of camera operations defined in the active camera chain. It + reports the progress of each operation and handles any exceptions that + may occur during the path calculation. After successful calculations, it + exports the resulting mesh data to a specified G-code file. + + :param context: The Blender context containing scene and + :type context: bpy.context + + :returns: A dictionary indicating the result of the operation, + typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: PathExportChain + + Bases: :py:obj:`bpy.types.Operator` + + + Calculate a Chain and Export the G-code Together. + + + .. py:attribute:: bl_idname + :value: 'object.cam_export_paths_chain' + + + + .. py:attribute:: bl_label + :value: 'Export CAM Paths in Current Chain as G-code' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + Check the validity of the active camera chain in the given context. + + This method retrieves the currently active camera chain from the scene + context and checks its validity using the `isChainValid` function. It + returns a boolean indicating whether the active camera chain is valid or + not. + + :param context: The context containing the scene and camera chain information. + :type context: object + + :returns: True if the active camera chain is valid, False otherwise. + :rtype: bool + + + + .. py:method:: execute(context) + + Execute the camera path export process. + + This function retrieves the active camera chain from the current scene + and gathers the mesh data associated with the operations of that chain. + It then exports the G-code path using the specified filename and the + collected mesh data. The function is designed to be called within the + context of a Blender operator. + + :param context: The context in which the operator is executed. + :type context: bpy.context + + :returns: + + A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: PathExport + + Bases: :py:obj:`bpy.types.Operator` + + + Export G-code. Can Be Used only when the Path Object Is Present + + + .. py:attribute:: bl_idname + :value: 'object.cam_export' + + + + .. py:attribute:: bl_label + :value: 'Export Operation G-code' + + + + .. py:attribute:: bl_options + + + .. py:method:: execute(context) + + Execute the camera operation and export the G-code path. + + This method retrieves the active camera operation from the current scene + and exports the corresponding G-code path to a specified filename. It + prints the filename and relevant operation details to the console for + debugging purposes. The G-code path is generated based on the camera + path data associated with the active operation. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the completion status of the operation, + typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: CAMSimulate + + Bases: :py:obj:`bpy.types.Operator`, :py:obj:`cam.async_op.AsyncOperatorMixin` + + + Simulate CAM Operation + This Is Performed by: Creating an Image, Painting Z Depth of the Brush Subtractively. + Works only for Some Operations, Can Not Be Used for 4-5 Axis. + + + .. py:attribute:: bl_idname + :value: 'object.cam_simulate' + + + + .. py:attribute:: bl_label + :value: 'CAM Simulation' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: operation + :type: StringProperty(name='Operation', description='Specify the operation to calculate', default='Operation') + + + .. py:method:: execute_async(context) + :async: + + + Execute an asynchronous simulation operation based on the active camera + operation. + + This method retrieves the current scene and the active camera operation. + It constructs the operation name and checks if the corresponding object + exists in the Blender data. If it does, it attempts to run the + simulation asynchronously. If the simulation is cancelled, it returns a + cancellation status. If the object does not exist, it reports an error + and returns a finished status. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the status of the operation, either + {'CANCELLED'} or {'FINISHED'}. + :rtype: dict + + + + .. py:method:: draw(context) + + Draws the user interface for selecting camera operations. + + This method creates a layout element in the user interface that allows + users to search and select a specific camera operation from a list of + available operations defined in the current scene. It utilizes the + Blender Python API to integrate with the UI. + + :param context: The context in which the drawing occurs, typically + provided by Blender's UI system. + + + +.. py:class:: CAMSimulateChain + + Bases: :py:obj:`bpy.types.Operator`, :py:obj:`cam.async_op.AsyncOperatorMixin` + + + Simulate CAM Chain, Compared to Single Op Simulation Just Writes Into One Image and Thus Enables + to See how Ops Work Together. + + + .. py:attribute:: bl_idname + :value: 'object.cam_simulate_chain' + + + + .. py:attribute:: bl_label + :value: 'CAM Simulation' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + Check the validity of the active camera chain in the scene. + + This method retrieves the currently active camera chain from the scene's + camera chains and checks its validity using the `isChainValid` function. + It returns a boolean indicating whether the active camera chain is + valid. + + :param context: The context containing the scene and its properties. + :type context: object + + :returns: True if the active camera chain is valid, False otherwise. + :rtype: bool + + + + .. py:attribute:: operation + :type: StringProperty(name='Operation', description='Specify the operation to calculate', default='Operation') + + + .. py:method:: execute_async(context) + :async: + + + Execute an asynchronous simulation for a specified camera chain. + + This method retrieves the active camera chain from the current Blender + scene and determines the operations associated with that chain. It + checks if all operations are valid and can be simulated. If valid, it + proceeds to execute the simulation asynchronously. If any operation is + invalid, it logs a message and returns a finished status without + performing the simulation. + + :param context: The context in which the operation is executed. + + :returns: A dictionary indicating the status of the operation, either + operation completed successfully. + :rtype: dict + + + + .. py:method:: draw(context) + + Draw the user interface for selecting camera operations. + + This function creates a user interface element that allows the user to + search and select a specific camera operation from a list of available + operations in the current scene. It utilizes the Blender Python API to + create a property search layout. + + :param context: The context in which the drawing occurs, typically containing + information about the current scene and UI elements. + + + +.. py:class:: CamChainAdd + + Bases: :py:obj:`bpy.types.Operator` + + + Add New CAM Chain + + + .. py:attribute:: bl_idname + :value: 'scene.cam_chain_add' + + + + .. py:attribute:: bl_label + :value: 'Add New CAM Chain' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera chain creation in the given context. + + This function adds a new camera chain to the current scene in Blender. + It updates the active camera chain index and assigns a name and filename + to the newly created chain. The function is intended to be called within + a Blender operator context. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the operation's completion status, + specifically returning {'FINISHED'} upon successful execution. + :rtype: dict + + + +.. py:class:: CamChainRemove + + Bases: :py:obj:`bpy.types.Operator` + + + Remove CAM Chain + + + .. py:attribute:: bl_idname + :value: 'scene.cam_chain_remove' + + + + .. py:attribute:: bl_label + :value: 'Remove CAM Chain' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera chain removal process. + + This function removes the currently active camera chain from the scene + and decrements the active camera chain index if it is greater than zero. + It modifies the Blender context to reflect these changes. + + :param context: The context in which the function is executed. + + :returns: + + A dictionary indicating the status of the operation, + specifically {'FINISHED'} upon successful execution. + :rtype: dict + + + +.. py:class:: CamChainOperationAdd + + Bases: :py:obj:`bpy.types.Operator` + + + Add Operation to Chain + + + .. py:attribute:: bl_idname + :value: 'scene.cam_chain_operation_add' + + + + .. py:attribute:: bl_label + :value: 'Add Operation to Chain' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute an operation in the active camera chain. + + This function retrieves the active camera chain from the current scene + and adds a new operation to it. It increments the active operation index + and assigns the name of the currently selected camera operation to the + newly added operation. This is typically used in the context of managing + camera operations in a 3D environment. + + :param context: The context in which the operation is executed. + + :returns: A dictionary indicating the execution status, typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: CamChainOperationUp + + Bases: :py:obj:`bpy.types.Operator` + + + Add Operation to Chain + + + .. py:attribute:: bl_idname + :value: 'scene.cam_chain_operation_up' + + + + .. py:attribute:: bl_label + :value: 'Add Operation to Chain' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the operation to move the active camera operation in the chain. + + This function retrieves the current scene and the active camera chain. + If there is an active operation (i.e., its index is greater than 0), it + moves the operation one step up in the chain by adjusting the indices + accordingly. After moving the operation, it updates the active operation + index to reflect the change. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the result of the operation, + specifically returning {'FINISHED'} upon successful execution. + :rtype: dict + + + +.. py:class:: CamChainOperationDown + + Bases: :py:obj:`bpy.types.Operator` + + + Add Operation to Chain + + + .. py:attribute:: bl_idname + :value: 'scene.cam_chain_operation_down' + + + + .. py:attribute:: bl_label + :value: 'Add Operation to Chain' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the operation to move the active camera operation in the chain. + + This function retrieves the current scene and the active camera chain. + It checks if the active operation can be moved down in the list of + operations. If so, it moves the active operation one position down and + updates the active operation index accordingly. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the result of the operation, + specifically {'FINISHED'} when the operation completes successfully. + :rtype: dict + + + +.. py:class:: CamChainOperationRemove + + Bases: :py:obj:`bpy.types.Operator` + + + Remove Operation from Chain + + + .. py:attribute:: bl_idname + :value: 'scene.cam_chain_operation_remove' + + + + .. py:attribute:: bl_label + :value: 'Remove Operation from Chain' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the operation to remove the active operation from the camera + chain. + + This method accesses the current scene and retrieves the active camera + chain. It then removes the currently active operation from that chain + and adjusts the index of the active operation accordingly. If the active + operation index becomes negative, it resets it to zero to ensure it + remains within valid bounds. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the execution status, typically + containing {'FINISHED'} upon successful completion. + :rtype: dict + + + +.. py:function:: fixUnits() + + Set up units for CNC CAM. + + This function configures the unit settings for the current Blender + scene. It sets the rotation system to degrees and the scale length to + 1.0, ensuring that the units are appropriately configured for use within + CNC CAM. + + +.. py:class:: CamOperationAdd + + Bases: :py:obj:`bpy.types.Operator` + + + Add New CAM Operation + + + .. py:attribute:: bl_idname + :value: 'scene.cam_operation_add' + + + + .. py:attribute:: bl_label + :value: 'Add New CAM Operation' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera operation based on the active object in the scene. + + This method retrieves the active object from the Blender context and + performs operations related to camera settings. It checks if an object + is selected and retrieves its bounding box dimensions. If no object is + found, it reports an error and cancels the operation. If an object is + present, it adds a new camera operation to the scene, sets its + properties, and ensures that a machine area object is present. + + :param context: The context in which the operation is executed. + + + +.. py:class:: CamOperationCopy + + Bases: :py:obj:`bpy.types.Operator` + + + Copy CAM Operation + + + .. py:attribute:: bl_idname + :value: 'scene.cam_operation_copy' + + + + .. py:attribute:: bl_label + :value: 'Copy Active CAM Operation' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera operation in the given context. + + This method handles the execution of camera operations within the + Blender scene. It first checks if there are any camera operations + available. If not, it returns a cancellation status. If there are + operations, it copies the active operation, increments the active + operation index, and updates the name and filename of the new operation. + The function also ensures that the new operation's name is unique by + appending a copy suffix or incrementing a numeric suffix. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the status of the operation, + either {'CANCELLED'} if no operations are available or + {'FINISHED'} if the operation was successfully executed. + :rtype: dict + + + +.. py:class:: CamOperationRemove + + Bases: :py:obj:`bpy.types.Operator` + + + Remove CAM Operation + + + .. py:attribute:: bl_idname + :value: 'scene.cam_operation_remove' + + + + .. py:attribute:: bl_label + :value: 'Remove CAM Operation' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera operation in the given context. + + This function performs the active camera operation by deleting the + associated object from the scene. It checks if there are any camera + operations available and handles the deletion of the active operation's + object. If the active operation is removed, it updates the active + operation index accordingly. Additionally, it manages a dictionary that + tracks hidden objects. + + :param context: The Blender context containing the scene and operations. + :type context: bpy.context + + :returns: + + A dictionary indicating the result of the operation, either + {'CANCELLED'} if no operations are available or {'FINISHED'} if the + operation was successfully executed. + :rtype: dict + + + +.. py:class:: CamOperationMove + + Bases: :py:obj:`bpy.types.Operator` + + + Move CAM Operation + + + .. py:attribute:: bl_idname + :value: 'scene.cam_operation_move' + + + + .. py:attribute:: bl_label + :value: 'Move CAM Operation in List' + + + + .. py:attribute:: bl_options + + + .. py:attribute:: direction + :type: EnumProperty(name='Direction', items=(('UP', 'Up', ''), ('DOWN', 'Down', '')), description='Direction', default='DOWN') + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute a camera operation based on the specified direction. + + This method modifies the active camera operation in the Blender context + based on the direction specified. If the direction is 'UP', it moves the + active operation up in the list, provided it is not already at the top. + Conversely, if the direction is not 'UP', it moves the active operation + down in the list, as long as it is not at the bottom. The method updates + the active operation index accordingly. + + :param context: The context in which the operation is executed. + + :returns: A dictionary indicating the operation has finished, with + the key 'FINISHED'. + :rtype: dict + + + +.. py:class:: CamOrientationAdd + + Bases: :py:obj:`bpy.types.Operator` + + + Add Orientation to CAM Operation, for Multiaxis Operations + + + .. py:attribute:: bl_idname + :value: 'scene.cam_orientation_add' + + + + .. py:attribute:: bl_label + :value: 'Add Orientation' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera orientation operation in Blender. + + This function retrieves the active camera operation from the current + scene, creates an empty object to represent the camera orientation, and + adds it to a specified group. The empty object is named based on the + operation's name and the current count of objects in the group. The size + of the empty object is set to a predefined value for visibility. + + :param context: The context in which the operation is executed. + + :returns: + + A dictionary indicating the operation's completion status, + typically {'FINISHED'}. + :rtype: dict + + + +.. py:class:: CamBridgesAdd + + Bases: :py:obj:`bpy.types.Operator` + + + Add Bridge Objects to Curve + + + .. py:attribute:: bl_idname + :value: 'scene.cam_bridges_add' + + + + .. py:attribute:: bl_label + :value: 'Add Bridges / Tabs' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: execute(context) + + Execute the camera operation in the given context. + + This function retrieves the active camera operation from the current + scene and adds automatic bridges to it. It is typically called within + the context of a Blender operator to perform specific actions related to + camera operations. + + :param context: The context in which the operation is executed. + + :returns: A dictionary indicating the result of the operation, typically + containing the key 'FINISHED' to signify successful completion. + :rtype: dict + + + diff --git a/_sources/autoapi/cam/pack/index.rst b/_sources/autoapi/cam/pack/index.rst new file mode 100644 index 000000000..ba22cf764 --- /dev/null +++ b/_sources/autoapi/cam/pack/index.rst @@ -0,0 +1,115 @@ +cam.pack +======== + +.. py:module:: cam.pack + +.. autoapi-nested-parse:: + + CNC CAM 'pack.py' © 2012 Vilem Novak + + Takes all selected curves, converts them to polygons, offsets them by the pre-set margin + then chooses a starting location possibly inside the already occupied area and moves and rotates the + polygon out of the occupied area if one or more positions are found where the poly doesn't overlap, + it is placed and added to the occupied area - allpoly + Very slow and STUPID, a collision algorithm would be much much faster... + + + +Classes +------- + +.. autoapisummary:: + + cam.pack.PackObjectsSettings + + +Functions +--------- + +.. autoapisummary:: + + cam.pack.srotate + cam.pack.packCurves + + +Module Contents +--------------- + +.. py:function:: srotate(s, r, x, y) + + Rotate a polygon's coordinates around a specified point. + + This function takes a polygon and rotates its exterior coordinates + around a given point (x, y) by a specified angle (r) in radians. It uses + the Euler rotation to compute the new coordinates for each point in the + polygon's exterior. The resulting coordinates are then used to create a + new polygon. + + :param s: The polygon to be rotated. + :type s: shapely.geometry.Polygon + :param r: The angle of rotation in radians. + :type r: float + :param x: The x-coordinate of the point around which to rotate. + :type x: float + :param y: The y-coordinate of the point around which to rotate. + :type y: float + + :returns: A new polygon with the rotated coordinates. + :rtype: shapely.geometry.Polygon + + +.. py:function:: packCurves() + + Pack selected curves into a defined area based on specified settings. + + This function organizes selected curve objects in Blender by packing + them into a specified area defined by the camera pack settings. It + calculates the optimal positions for each curve while considering + parameters such as sheet size, fill direction, distance, tolerance, and + rotation. The function utilizes geometric operations to ensure that the + curves do not overlap and fit within the defined boundaries. The packed + curves are then transformed and their properties are updated + accordingly. The function performs the following steps: 1. Activates + speedup features if available. 2. Retrieves packing settings from the + current scene. 3. Processes each selected object to create polygons from + curves. 4. Attempts to place each polygon within the defined area while + avoiding overlaps and respecting the specified fill direction. 5. + Outputs the final arrangement of polygons. + + +.. py:class:: PackObjectsSettings + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + stores all data for machines + + + .. py:attribute:: sheet_fill_direction + :type: 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') + + + .. py:attribute:: sheet_x + :type: FloatProperty(name='X Size', description='Sheet size', min=0.001, max=10, default=0.5, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: sheet_y + :type: FloatProperty(name='Y Size', description='Sheet size', min=0.001, max=10, default=0.5, precision=constants.PRECISION, unit='LENGTH') + + + .. py:attribute:: distance + :type: 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') + + + .. py:attribute:: tolerance + :type: 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') + + + .. py:attribute:: rotate + :type: BoolProperty(name='Enable Rotation', description='Enable rotation of elements', default=True) + + + .. py:attribute:: rotate_angle + :type: 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/_sources/autoapi/cam/parametric/index.rst b/_sources/autoapi/cam/parametric/index.rst new file mode 100644 index 000000000..bd1ac1986 --- /dev/null +++ b/_sources/autoapi/cam/parametric/index.rst @@ -0,0 +1,120 @@ +cam.parametric +============== + +.. py:module:: cam.parametric + +.. autoapi-nested-parse:: + + CNC CAM 'parametric.py' © 2019 Devon (Gorialis) R + + MIT License + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + + Summary: + Create a Blender curve from a 3D parametric function. + This allows for a 3D plot to be made of the function, which can be converted into a mesh. + + I have documented the inner workings here, but if you're not interested and just want to + suit this to your own function, scroll down to the bottom and edit the `f(t)` function and + the iteration count to your liking. + + This code has been checked to work on Blender 2.92. + + + +Functions +--------- + +.. autoapisummary:: + + cam.parametric.derive_bezier_handles + cam.parametric.create_parametric_curve + cam.parametric.make_edge_loops + + +Module Contents +--------------- + +.. py:function:: derive_bezier_handles(a, b, c, d, tb, tc) + + Derives bezier handles by using the start and end of the curve with 2 intermediate + points to use for interpolation. + + :param a: + The start point. + :param b: + The first mid-point, located at `tb` on the bezier segment, where 0 < `tb` < 1. + :param c: + The second mid-point, located at `tc` on the bezier segment, where 0 < `tc` < 1. + :param d: + The end point. + :param tb: + The position of the first point in the bezier segment. + :param tc: + The position of the second point in the bezier segment. + :return: + A tuple of the two intermediate handles, that is, the right handle of the start point + and the left handle of the end point. + + +.. py:function:: create_parametric_curve(function, *args, min: float = 0.0, max: float = 1.0, use_cubic: bool = True, iterations: int = 8, resolution_u: int = 10, **kwargs) + + Creates a Blender bezier curve object from a parametric function. + This "plots" the function in 3D space from `min <= t <= max`. + + :param function: + The function to plot as a Blender curve. + + This function should take in a float value of `t` and return a 3-item tuple or list + of the X, Y and Z coordinates at that point: + `function(t) -> (x, y, z)` + + `t` is plotted according to `min <= t <= max`, but if `use_cubic` is enabled, this function + needs to be able to take values less than `min` and greater than `max`. + :param *args: + Additional positional arguments to be passed to the plotting function. + These are not required. + :param use_cubic: + Whether or not to calculate the cubic bezier handles as to create smoother splines. + Turning this off reduces calculation time and memory usage, but produces more jagged + splines with sharp edges. + :param iterations: + The 'subdivisions' of the parametric to plot. + Setting this higher produces more accurate curves but increases calculation time and + memory usage. + :param resolution_u: + The preview surface resolution in the U direction of the bezier curve. + Setting this to a higher value produces smoother curves in rendering, and increases the + number of vertices the curve will get if converted into a mesh (e.g. for edge looping) + :param **kwargs: + Additional keyword arguments to be passed to the plotting function. + These are not required. + :return: + The new Blender object. + + +.. py:function:: make_edge_loops(*objects) + + Turns a set of Curve objects into meshes, creates vertex groups, + and merges them into a set of edge loops. + + :param *objects: + Positional arguments for each object to be converted and merged. + + diff --git a/_sources/autoapi/cam/pattern/index.rst b/_sources/autoapi/cam/pattern/index.rst new file mode 100644 index 000000000..37bb7cf30 --- /dev/null +++ b/_sources/autoapi/cam/pattern/index.rst @@ -0,0 +1,91 @@ +cam.pattern +=========== + +.. py:module:: cam.pattern + +.. autoapi-nested-parse:: + + CNC CAM 'pattern.py' © 2012 Vilem Novak + + Functions to read CAM path patterns and return CAM path chunks. + + + +Functions +--------- + +.. autoapisummary:: + + cam.pattern.getPathPatternParallel + cam.pattern.getPathPattern + cam.pattern.getPathPattern4axis + + +Module Contents +--------------- + +.. py:function:: getPathPatternParallel(o, angle) + + Generate path chunks for parallel movement based on object dimensions + and angle. + + This function calculates a series of path chunks for a given object, + taking into account its dimensions and the specified angle. It utilizes + both a traditional method and an alternative algorithm (currently + disabled) to generate these paths. The paths are constructed by + iterating over calculated vectors and applying transformations based on + the object's properties. The resulting path chunks can be used for + various movement types, including conventional and climb movements. + + :param o: An object containing properties such as dimensions and movement type. + :type o: object + :param angle: The angle to rotate the path generation. + :type angle: float + + :returns: + + A list of path chunks generated based on the object's dimensions and + angle. + :rtype: list + + +.. py:function:: getPathPattern(operation) + + Generate a path pattern based on the specified operation strategy. + + This function constructs a path pattern for a given operation by + analyzing its parameters and applying different strategies such as + 'PARALLEL', 'CROSS', 'BLOCK', 'SPIRAL', 'CIRCLES', and 'OUTLINEFILL'. + Each strategy dictates how the path is built, utilizing various + geometric calculations and conditions to ensure the path adheres to the + specified operational constraints. The function also handles the + orientation and direction of the path based on the movement settings + provided in the operation. + + :param operation: An object containing parameters for path generation, + including strategy, movement type, and geometric bounds. + :type operation: object + + :returns: A list of path chunks representing the generated path pattern. + :rtype: list + + +.. py:function:: getPathPattern4axis(operation) + + Generate path patterns for a specified operation along a rotary axis. + + This function constructs a series of path chunks based on the provided + operation's parameters, including the rotary axis, strategy, and + dimensions. It calculates the necessary angles and positions for the + cutter based on the specified strategy (PARALLELR, PARALLEL, or HELIX) + and generates the corresponding path chunks for machining operations. + + :param operation: An object containing parameters for the machining operation, + including min and max coordinates, rotary axis configuration, + distance settings, and movement strategy. + :type operation: object + + :returns: A list of path chunks generated for the specified operation. + :rtype: list + + diff --git a/_sources/autoapi/cam/polygon_utils_cam/index.rst b/_sources/autoapi/cam/polygon_utils_cam/index.rst new file mode 100644 index 000000000..6e606b5db --- /dev/null +++ b/_sources/autoapi/cam/polygon_utils_cam/index.rst @@ -0,0 +1,141 @@ +cam.polygon_utils_cam +===================== + +.. py:module:: cam.polygon_utils_cam + +.. autoapi-nested-parse:: + + CNC CAM 'polygon_utils_cam.py' © 2012 Vilem Novak + + Functions to handle shapely operations and conversions - curve, coords, polygon + + + +Attributes +---------- + +.. autoapisummary:: + + cam.polygon_utils_cam.SHAPELY + + +Functions +--------- + +.. autoapisummary:: + + cam.polygon_utils_cam.Circle + cam.polygon_utils_cam.shapelyRemoveDoubles + cam.polygon_utils_cam.shapelyToMultipolygon + cam.polygon_utils_cam.shapelyToCoords + cam.polygon_utils_cam.shapelyToCurve + + +Module Contents +--------------- + +.. py:data:: SHAPELY + :value: True + + +.. py:function:: Circle(r, np) + + Generate a circle defined by a given radius and number of points. + + This function creates a polygon representing a circle by generating a + list of points based on the specified radius and the number of points + (np). It uses vector rotation to calculate the coordinates of each point + around the circle. The resulting points are then used to create a + polygon object. + + :param r: The radius of the circle. + :type r: float + :param np: The number of points to generate around the circle. + :type np: int + + :returns: A polygon object representing the circle. + :rtype: spolygon.Polygon + + +.. py:function:: shapelyRemoveDoubles(p, optimize_threshold) + + Remove duplicate points from the boundary of a shape. + + This function simplifies the boundary of a given shape by removing + duplicate points using the Ramer-Douglas-Peucker algorithm. It iterates + through each contour of the shape, applies the simplification, and adds + the resulting contours to a new shape. The optimization threshold can be + adjusted to control the level of simplification. + + :param p: The shape object containing boundaries to be simplified. + :type p: Shape + :param optimize_threshold: A threshold value that influences the + simplification process. + :type optimize_threshold: float + + :returns: A new shape object with simplified boundaries. + :rtype: Shape + + +.. py:function:: shapelyToMultipolygon(anydata) + + Convert a Shapely geometry to a MultiPolygon. + + This function takes a Shapely geometry object and converts it to a + MultiPolygon. If the input geometry is already a MultiPolygon, it + returns it as is. If the input is a Polygon and not empty, it wraps the + Polygon in a MultiPolygon. If the input is an empty Polygon, it returns + an empty MultiPolygon. For any other geometry type, it prints a message + indicating that the conversion was aborted and returns an empty + MultiPolygon. + + :param anydata: A Shapely geometry object + :type anydata: shapely.geometry.base.BaseGeometry + + :returns: A MultiPolygon representation of the input + geometry. + :rtype: shapely.geometry.MultiPolygon + + +.. py:function:: shapelyToCoords(anydata) + + Convert a Shapely geometry object to a list of coordinates. + + This function takes a Shapely geometry object and extracts its + coordinates based on the geometry type. It handles various types of + geometries including Polygon, MultiPolygon, LineString, MultiLineString, + and GeometryCollection. If the geometry is empty or of type MultiPoint, + it returns an empty list. The coordinates are returned in a nested list + format, where each sublist corresponds to the exterior or interior + coordinates of the geometries. + + :param anydata: A Shapely geometry object + :type anydata: shapely.geometry.base.BaseGeometry + + :returns: A list of coordinates extracted from the input geometry. + The structure of the list depends on the geometry type. + :rtype: list + + +.. py:function:: shapelyToCurve(name, p, z, cyclic=True) + + Create a 3D curve object in Blender from a Shapely geometry. + + This function takes a Shapely geometry and converts it into a 3D curve + object in Blender. It extracts the coordinates from the Shapely geometry + and creates a new curve object with the specified name. The curve is + created in the 3D space at the given z-coordinate, with a default weight + for the points. + + :param name: The name of the curve object to be created. + :type name: str + :param p: A Shapely geometry object from which to extract + coordinates. + :type p: shapely.geometry + :param z: The z-coordinate for all points of the curve. + :type z: float + + :returns: The newly created curve object in Blender. + :rtype: bpy.types.Object + + diff --git a/_sources/autoapi/cam/preset_managers/index.rst b/_sources/autoapi/cam/preset_managers/index.rst new file mode 100644 index 000000000..3e0c5c1a3 --- /dev/null +++ b/_sources/autoapi/cam/preset_managers/index.rst @@ -0,0 +1,217 @@ +cam.preset_managers +=================== + +.. py:module:: cam.preset_managers + +.. autoapi-nested-parse:: + + CNC CAM 'preset_managers.py' + + Operators and Menus for CAM Machine, Cutter and Operation Presets. + + + +Classes +------- + +.. autoapisummary:: + + cam.preset_managers.CAM_CUTTER_MT_presets + cam.preset_managers.CAM_MACHINE_MT_presets + cam.preset_managers.AddPresetCamCutter + cam.preset_managers.CAM_OPERATION_MT_presets + cam.preset_managers.AddPresetCamOperation + cam.preset_managers.AddPresetCamMachine + + +Module Contents +--------------- + +.. py:class:: CAM_CUTTER_MT_presets + + Bases: :py:obj:`bpy.types.Menu` + + + .. py:attribute:: bl_label + :value: 'Cutter Presets' + + + + .. py:attribute:: preset_subdir + :value: 'cam_cutters' + + + + .. py:attribute:: preset_operator + :value: 'script.execute_preset' + + + + .. py:attribute:: draw + + +.. py:class:: CAM_MACHINE_MT_presets + + Bases: :py:obj:`bpy.types.Menu` + + + .. py:attribute:: bl_label + :value: 'Machine Presets' + + + + .. py:attribute:: preset_subdir + :value: 'cam_machines' + + + + .. py:attribute:: preset_operator + :value: 'script.execute_preset' + + + + .. py:attribute:: draw + + + .. py:method:: post_cb(context) + :classmethod: + + + +.. py:class:: AddPresetCamCutter + + Bases: :py:obj:`bl_operators.presets.AddPresetBase`, :py:obj:`bpy.types.Operator` + + + Add a Cutter Preset + + + .. py:attribute:: bl_idname + :value: 'render.cam_preset_cutter_add' + + + + .. py:attribute:: bl_label + :value: 'Add Cutter Preset' + + + + .. py:attribute:: preset_menu + :value: 'CAM_CUTTER_MT_presets' + + + + .. py:attribute:: preset_defines + :value: ['d = bpy.context.scene.cam_operations[bpy.context.scene.cam_active_operation]'] + + + + .. py:attribute:: preset_values + :value: ['d.cutter_id', 'd.cutter_type', 'd.cutter_diameter', 'd.cutter_length', 'd.cutter_flutes',... + + + + .. py:attribute:: preset_subdir + :value: 'cam_cutters' + + + +.. py:class:: CAM_OPERATION_MT_presets + + Bases: :py:obj:`bpy.types.Menu` + + + .. py:attribute:: bl_label + :value: 'Operation Presets' + + + + .. py:attribute:: preset_subdir + :value: 'cam_operations' + + + + .. py:attribute:: preset_operator + :value: 'script.execute_preset' + + + + .. py:attribute:: draw + + +.. py:class:: AddPresetCamOperation + + Bases: :py:obj:`bl_operators.presets.AddPresetBase`, :py:obj:`bpy.types.Operator` + + + Add an Operation Preset + + + .. py:attribute:: bl_idname + :value: 'render.cam_preset_operation_add' + + + + .. py:attribute:: bl_label + :value: 'Add Operation Preset' + + + + .. py:attribute:: preset_menu + :value: 'CAM_OPERATION_MT_presets' + + + + .. py:attribute:: preset_defines + :value: ['from pathlib import Path', 'bpy.ops.scene.cam_operation_add()', 'scene = bpy.context.scene',... + + + + .. py:attribute:: preset_values + :value: ['o.info.duration', 'o.info.chipload', 'o.info.warnings', 'o.material.estimate_from_model',... + + + + .. py:attribute:: preset_subdir + :value: 'cam_operations' + + + +.. py:class:: AddPresetCamMachine + + Bases: :py:obj:`bl_operators.presets.AddPresetBase`, :py:obj:`bpy.types.Operator` + + + Add a Cam Machine Preset + + + .. py:attribute:: bl_idname + :value: 'render.cam_preset_machine_add' + + + + .. py:attribute:: bl_label + :value: 'Add Machine Preset' + + + + .. py:attribute:: preset_menu + :value: 'CAM_MACHINE_MT_presets' + + + + .. py:attribute:: preset_defines + :value: ['d = bpy.context.scene.cam_machine', 's = bpy.context.scene.unit_settings'] + + + + .. py:attribute:: preset_values + :value: ['d.post_processor', 's.system', 'd.use_position_definitions', 'd.starting_position',... + + + + .. py:attribute:: preset_subdir + :value: 'cam_machines' + + + diff --git a/_sources/autoapi/cam/puzzle_joinery/index.rst b/_sources/autoapi/cam/puzzle_joinery/index.rst new file mode 100644 index 000000000..e515ac201 --- /dev/null +++ b/_sources/autoapi/cam/puzzle_joinery/index.rst @@ -0,0 +1,606 @@ +cam.puzzle_joinery +================== + +.. py:module:: cam.puzzle_joinery + +.. autoapi-nested-parse:: + + CNC CAM 'puzzle_joinery.py' © 2021 Alain Pelletier + + Functions to add various puzzle joints as curves. + + + +Attributes +---------- + +.. autoapisummary:: + + cam.puzzle_joinery.DT + + +Functions +--------- + +.. autoapisummary:: + + cam.puzzle_joinery.finger + cam.puzzle_joinery.fingers + cam.puzzle_joinery.twistf + cam.puzzle_joinery.twistm + cam.puzzle_joinery.bar + cam.puzzle_joinery.arc + cam.puzzle_joinery.arcbararc + cam.puzzle_joinery.arcbar + cam.puzzle_joinery.multiangle + cam.puzzle_joinery.t + cam.puzzle_joinery.curved_t + cam.puzzle_joinery.mitre + cam.puzzle_joinery.open_curve + cam.puzzle_joinery.tile + + +Module Contents +--------------- + +.. py:data:: DT + :value: 1.025 + + +.. py:function:: finger(diameter, stem=2) + + Create a joint shape based on the specified diameter and stem. + + This function generates a 3D joint shape using Blender's curve + operations. It calculates the dimensions of a rectangle and an ellipse + based on the provided diameter and stem parameters. The function then + creates these shapes, duplicates and mirrors them, and performs boolean + operations to form the final joint shape. The resulting object is named + and cleaned up to ensure no overlapping vertices remain. + + :param diameter: The diameter of the tool for joint creation. + :type diameter: float + :param stem: The amount of radius the stem or neck of the joint will have. Defaults + to 2. + :type stem: float? + + :returns: This function does not return any value. + :rtype: None + + +.. py:function:: fingers(diameter, inside, amount=1, stem=1) + + Create a specified number of fingers for a joint tool. + + This function generates a set of fingers based on the provided diameter + and tolerance values. It calculates the necessary translations for + positioning the fingers and duplicates them if more than one is + required. Additionally, it creates a receptacle using a silhouette + offset from the fingers, allowing for precise joint creation. + + :param diameter: The diameter of the tool used for joint creation. + :type diameter: float + :param inside: The tolerance in the joint receptacle. + :type inside: float + :param amount: The number of fingers to create. Defaults to 1. + :type amount: int? + :param stem: The amount of radius the stem or neck of the joint will have. Defaults + to 1. + :type stem: float? + + +.. py:function:: twistf(name, length, diameter, tolerance, twist, tneck, tthick, twist_keep=False) + + Add a twist lock to a receptacle. + + This function modifies the receptacle by adding a twist lock feature if + the `twist` parameter is set to True. It performs several operations + including interlocking the twist, rotating the object, and moving it to + the correct position. If `twist_keep` is True, it duplicates the twist + lock for further modifications. The function utilizes parameters such as + length, diameter, tolerance, and thickness to accurately create the + twist lock. + + :param name: The name of the receptacle to be modified. + :type name: str + :param length: The length of the receptacle. + :type length: float + :param diameter: The diameter of the receptacle. + :type diameter: float + :param tolerance: The tolerance value for the twist lock. + :type tolerance: float + :param twist: A flag indicating whether to add a twist lock. + :type twist: bool + :param tneck: The neck thickness for the twist lock. + :type tneck: float + :param tthick: The thickness of the twist lock. + :type tthick: float + :param twist_keep: A flag indicating whether to keep the twist + lock after duplication. Defaults to False. + :type twist_keep: bool? + + +.. py:function:: twistm(name, length, diameter, tolerance, twist, tneck, tthick, angle, twist_keep=False, x=0, y=0) + + Add a twist lock to a male connector. + + This function modifies the geometry of a male connector by adding a + twist lock feature. It utilizes various parameters to determine the + dimensions and positioning of the twist lock. If the `twist_keep` + parameter is set to True, it duplicates the twist lock for further + modifications. The function also allows for adjustments in position + through the `x` and `y` parameters. + + :param name: The name of the connector to be modified. + :type name: str + :param length: The length of the connector. + :type length: float + :param diameter: The diameter of the connector. + :type diameter: float + :param tolerance: The tolerance level for the twist lock. + :type tolerance: float + :param twist: A flag indicating whether to add a twist lock. + :type twist: bool + :param tneck: The neck thickness for the twist lock. + :type tneck: float + :param tthick: The thickness of the twist lock. + :type tthick: float + :param angle: The angle at which to rotate the twist lock. + :type angle: float + :param twist_keep: A flag indicating whether to keep the twist lock duplicate. Defaults to + False. + :type twist_keep: bool? + :param x: The x-coordinate for positioning. Defaults to 0. + :type x: float? + :param y: The y-coordinate for positioning. Defaults to 0. + :type y: float? + + :returns: + + This function modifies the state of the connector but does not return a + value. + :rtype: None + + +.. py:function:: bar(width, thick, diameter, tolerance, amount=0, stem=1, twist=False, tneck=0.5, tthick=0.01, twist_keep=False, twist_line=False, twist_line_amount=2, which='MF') + + Create a bar with specified dimensions and joint features. + + This function generates a bar with customizable parameters such as + width, thickness, and joint characteristics. It can automatically + determine the number of fingers in the joint if the amount is set to + zero. The function also supports various options for twisting and neck + dimensions, allowing for flexible design of the bar according to the + specified parameters. The resulting bar can be manipulated further based + on the provided options. + + :param width: The length of the bar. + :type width: float + :param thick: The thickness of the bar. + :type thick: float + :param diameter: The diameter of the tool used for joint creation. + :type diameter: float + :param tolerance: The tolerance in the joint. + :type tolerance: float + :param amount: The number of fingers in the joint; 0 means auto-generate. Defaults to + 0. + :type amount: int? + :param stem: The radius of the stem or neck of the joint. Defaults to 1. + :type stem: float? + :param twist: Whether to add a twist lock. Defaults to False. + :type twist: bool? + :param tneck: The percentage the twist neck will have compared to thickness. Defaults + to 0.5. + :type tneck: float? + :param tthick: The thickness of the twist material. Defaults to 0.01. + :type tthick: float? + :param twist_keep: Whether to keep the twist feature. Defaults to False. + :type twist_keep: bool? + :param twist_line: Whether to add a twist line. Defaults to False. + :type twist_line: bool? + :param twist_line_amount: The amount for the twist line. Defaults to 2. + :type twist_line_amount: int? + :param which: Specifies the type of joint; options are 'M', 'F', 'MF', 'MM', 'FF'. + Defaults to 'MF'. + :type which: str? + + :returns: + + This function does not return a value but modifies the state of the 3D + model in Blender. + :rtype: None + + +.. py:function:: arc(radius, thick, angle, diameter, tolerance, amount=0, stem=1, twist=False, tneck=0.5, tthick=0.01, twist_keep=False, which='MF') + + Generate an arc with specified parameters. + + This function creates a 3D arc based on the provided radius, thickness, + angle, and other parameters. It handles the generation of fingers for + the joint and applies twisting features if specified. The function also + manages the orientation and positioning of the generated arc in a 3D + space. + + :param radius: The radius of the curve. + :type radius: float + :param thick: The thickness of the bar. + :type thick: float + :param angle: The angle of the arc (must not be zero). + :type angle: float + :param diameter: The diameter of the tool for joint creation. + :type diameter: float + :param tolerance: Tolerance in the joint. + :type tolerance: float + :param amount: The amount of fingers in the joint; 0 means auto-generate. Defaults to + 0. + :type amount: int? + :param stem: The amount of radius the stem or neck of the joint will have. Defaults + to 1. + :type stem: float? + :param twist: Whether to add a twist lock. Defaults to False. + :type twist: bool? + :param tneck: Percentage the twist neck will have compared to thickness. Defaults to + 0.5. + :type tneck: float? + :param tthick: Thickness of the twist material. Defaults to 0.01. + :type tthick: float? + :param twist_keep: Whether to keep the twist. Defaults to False. + :type twist_keep: bool? + :param which: Specifies which joint to generate ('M', 'F', 'MF'). Defaults to 'MF'. + :type which: str? + + :returns: + + This function does not return a value but modifies the 3D scene + directly. + :rtype: None + + +.. py:function:: arcbararc(length, radius, thick, angle, angleb, diameter, tolerance, amount=0, stem=1, twist=False, tneck=0.5, tthick=0.01, which='MF', twist_keep=False, twist_line=False, twist_line_amount=2) + + Generate an arc bar joint with specified parameters. + + This function creates a joint consisting of male and female sections + based on the provided parameters. It adjusts the length to account for + the radius and thickness, generates a base rectangle, and then + constructs the male and/or female sections as specified. Additionally, + it can create a twist lock feature if required. The function utilizes + Blender's bpy operations to manipulate 3D objects. + + :param length: The total width of the segments including 2 * radius and thickness. + :type length: float + :param radius: The radius of the curve. + :type radius: float + :param thick: The thickness of the bar. + :type thick: float + :param angle: The angle of the female part. + :type angle: float + :param angleb: The angle of the male part. + :type angleb: float + :param diameter: The diameter of the tool for joint creation. + :type diameter: float + :param tolerance: Tolerance in the joint. + :type tolerance: float + :param amount: The number of fingers in the joint; 0 means auto-generate. Defaults to + 0. + :type amount: int? + :param stem: The amount of radius the stem or neck of the joint will have. Defaults + to 1. + :type stem: float? + :param twist: Whether to add a twist lock feature. Defaults to False. + :type twist: bool? + :param tneck: Percentage the twist neck will have compared to thickness. Defaults to + 0.5. + :type tneck: float? + :param tthick: Thickness of the twist material. Defaults to 0.01. + :type tthick: float? + :param which: Specifies which joint to generate ('M', 'F', or 'MF'). Defaults to 'MF'. + :type which: str? + :param twist_keep: Whether to keep the twist after creation. Defaults to False. + :type twist_keep: bool? + :param twist_line: Whether to create a twist line feature. Defaults to False. + :type twist_line: bool? + :param twist_line_amount: Amount for the twist line feature. Defaults to 2. + :type twist_line_amount: int? + + :returns: + + This function does not return a value but modifies the Blender scene + directly. + :rtype: None + + +.. py:function:: arcbar(length, radius, thick, angle, diameter, tolerance, amount=0, stem=1, twist=False, tneck=0.5, tthick=0.01, twist_keep=False, which='MF', twist_line=False, twist_line_amount=2) + + Generate an arc bar joint based on specified parameters. + + This function constructs an arc bar joint by generating male and female + sections according to the specified parameters such as length, radius, + thickness, and joint type. The function adjusts the length to account + for the radius and thickness of the bar and creates the appropriate + geometric shapes for the joint. It also includes options for twisting + and adjusting the neck thickness of the joint. + + :param length: The total width of the segments including 2 * radius and thickness. + :type length: float + :param radius: The radius of the curve. + :type radius: float + :param thick: The thickness of the bar. + :type thick: float + :param angle: The angle of the female part. + :type angle: float + :param diameter: The diameter of the tool for joint creation. + :type diameter: float + :param tolerance: Tolerance in the joint. + :type tolerance: float + :param amount: The number of fingers in the joint; 0 means auto-generate. Defaults to + 0. + :type amount: int? + :param stem: The amount of radius the stem or neck of the joint will have. Defaults + to 1. + :type stem: float? + :param twist: Whether to add a twist lock. Defaults to False. + :type twist: bool? + :param tneck: Percentage the twist neck will have compared to thickness. Defaults to + 0.5. + :type tneck: float? + :param tthick: Thickness of the twist material. Defaults to 0.01. + :type tthick: float? + :param twist_keep: Whether to keep the twist. Defaults to False. + :type twist_keep: bool? + :param which: Specifies which joint to generate ('M', 'F', 'MF'). Defaults to 'MF'. + :type which: str? + :param twist_line: Whether to include a twist line. Defaults to False. + :type twist_line: bool? + :param twist_line_amount: Amount of twist line. Defaults to 2. + :type twist_line_amount: int? + + +.. py:function:: multiangle(radius, thick, angle, diameter, tolerance, amount=0, stem=1, twist=False, tneck=0.5, tthick=0.01, combination='MFF') + + Generate a multi-angle joint based on specified parameters. + + This function creates a multi-angle joint by generating various + geometric shapes using the provided parameters such as radius, + thickness, angle, diameter, and tolerance. It utilizes Blender's + operations to create and manipulate curves, resulting in a joint that + can be customized with different combinations of male and female parts. + The function also allows for automatic generation of the number of + fingers in the joint and includes options for twisting and neck + dimensions. + + :param radius: The radius of the curve. + :type radius: float + :param thick: The thickness of the bar. + :type thick: float + :param angle: The angle of the female part. + :type angle: float + :param diameter: The diameter of the tool for joint creation. + :type diameter: float + :param tolerance: Tolerance in the joint. + :type tolerance: float + :param amount: The amount of fingers in the joint; 0 means auto-generate. Defaults to + 0. + :type amount: int? + :param stem: The amount of radius the stem or neck of the joint will have. Defaults + to 1. + :type stem: float? + :param twist: Indicates if a twist lock addition is required. Defaults to False. + :type twist: bool? + :param tneck: Percentage the twist neck will have compared to thickness. Defaults to + 0.5. + :type tneck: float? + :param tthick: Thickness of the twist material. Defaults to 0.01. + :type tthick: float? + :param combination: Specifies which joint to generate ('M', 'F', 'MF', 'MFF', 'MMF'). + Defaults to 'MFF'. + :type combination: str? + + :returns: + + This function does not return a value but performs operations in + Blender. + :rtype: None + + +.. py:function:: t(length, thick, diameter, tolerance, amount=0, stem=1, twist=False, tneck=0.5, tthick=0.01, combination='MF', base_gender='M', corner=False) + + Generate a 3D model based on specified parameters. + + This function creates a 3D model by manipulating geometric shapes based + on the provided parameters. It handles different combinations of shapes + and orientations based on the specified gender and corner options. The + function utilizes several helper functions to perform operations such as + moving, duplicating, and uniting shapes to form the final model. + + :param length: The length of the model. + :type length: float + :param thick: The thickness of the model. + :type thick: float + :param diameter: The diameter of the model. + :type diameter: float + :param tolerance: The tolerance level for the model dimensions. + :type tolerance: float + :param amount: The amount of material to use. Defaults to 0. + :type amount: int? + :param stem: The stem value for the model. Defaults to 1. + :type stem: int? + :param twist: Whether to apply a twist to the model. Defaults to False. + :type twist: bool? + :param tneck: The neck thickness. Defaults to 0.5. + :type tneck: float? + :param tthick: The thickness for the neck. Defaults to 0.01. + :type tthick: float? + :param combination: The combination type ('MF', 'F', 'M'). Defaults to 'MF'. + :type combination: str? + :param base_gender: The base gender for the model ('M' or 'F'). Defaults to 'M'. + :type base_gender: str? + :param corner: Whether to apply corner adjustments. Defaults to False. + :type corner: bool? + + :returns: + + This function does not return a value but modifies the 3D model + directly. + :rtype: None + + +.. py:function:: curved_t(length, thick, radius, diameter, tolerance, amount=0, stem=1, twist=False, tneck=0.5, tthick=0.01, combination='MF', base_gender='M') + + Create a curved shape based on specified parameters. + + This function generates a 3D curved shape using the provided dimensions + and characteristics. It utilizes the `bar` and `arc` functions to create + the desired geometry and applies transformations such as mirroring and + union operations to achieve the final shape. The function also allows + for customization based on the gender specification, which influences + the shape's design. + + :param length: The length of the bar. + :type length: float + :param thick: The thickness of the bar. + :type thick: float + :param radius: The radius of the arc. + :type radius: float + :param diameter: The diameter used in arc creation. + :type diameter: float + :param tolerance: The tolerance level for the shape. + :type tolerance: float + :param amount: The amount parameter for the shape generation. Defaults to 0. + :type amount: int? + :param stem: The stem parameter for the shape generation. Defaults to 1. + :type stem: int? + :param twist: A flag indicating whether to apply a twist to the shape. Defaults to + False. + :type twist: bool? + :param tneck: The neck thickness parameter. Defaults to 0.5. + :type tneck: float? + :param tthick: The thickness parameter for the neck. Defaults to 0.01. + :type tthick: float? + :param combination: The combination type for the shape. Defaults to 'MF'. + :type combination: str? + :param base_gender: The base gender for the shape design. Defaults to 'M'. + :type base_gender: str? + + :returns: + + This function does not return a value but modifies the 3D model in the + environment. + :rtype: None + + +.. py:function:: mitre(length, thick, angle, angleb, diameter, tolerance, amount=0, stem=1, twist=False, tneck=0.5, tthick=0.01, which='MF') + + Generate a mitre joint based on specified parameters. + + This function creates a 3D representation of a mitre joint using + Blender's bpy.ops.curve.simple operations. It generates a base rectangle + and cutout shapes, then constructs male and female sections of the joint + based on the provided angles and dimensions. The function allows for + customization of various parameters such as thickness, diameter, + tolerance, and the number of fingers in the joint. The resulting joint + can be either male, female, or a combination of both. + + :param length: The total width of the segments including 2 * radius and thickness. + :type length: float + :param thick: The thickness of the bar. + :type thick: float + :param angle: The angle of the female part. + :type angle: float + :param angleb: The angle of the male part. + :type angleb: float + :param diameter: The diameter of the tool for joint creation. + :type diameter: float + :param tolerance: Tolerance in the joint. + :type tolerance: float + :param amount: Amount of fingers in the joint; 0 means auto-generate. Defaults to 0. + :type amount: int? + :param stem: Amount of radius the stem or neck of the joint will have. Defaults to 1. + :type stem: float? + :param twist: Indicates if a twist lock addition is required. Defaults to False. + :type twist: bool? + :param tneck: Percentage the twist neck will have compared to thickness. Defaults to + 0.5. + :type tneck: float? + :param tthick: Thickness of the twist material. Defaults to 0.01. + :type tthick: float? + :param which: Specifies which joint to generate ('M', 'F', 'MF'). Defaults to 'MF'. + :type which: str? + + +.. py:function:: open_curve(line, thick, diameter, tolerance, amount=0, stem=1, twist=False, t_neck=0.5, t_thick=0.01, twist_amount=1, which='MF', twist_keep=False) + + Open a curve and add puzzle connectors with optional twist lock + connectors. + + This function takes a shapely LineString and creates an open curve with + specified parameters such as thickness, diameter, tolerance, and twist + options. It generates puzzle connectors at the ends of the curve and can + optionally add twist lock connectors along the curve. The function also + handles the creation of the joint based on the provided parameters, + ensuring that the resulting geometry meets the specified design + requirements. + + :param line: A shapely LineString representing the path of the curve. + :type line: LineString + :param thick: The thickness of the bar used in the joint. + :type thick: float + :param diameter: The diameter of the tool for joint creation. + :type diameter: float + :param tolerance: The tolerance in the joint. + :type tolerance: float + :param amount: The number of fingers in the joint; 0 means auto-generate. Defaults to + 0. + :type amount: int? + :param stem: The amount of radius the stem or neck of the joint will have. Defaults + to 1. + :type stem: float? + :param twist: Whether to add twist lock connectors. Defaults to False. + :type twist: bool? + :param t_neck: The percentage the twist neck will have compared to thickness. Defaults + to 0.5. + :type t_neck: float? + :param t_thick: The thickness of the twist material. Defaults to 0.01. + :type t_thick: float? + :param twist_amount: The amount of twist distributed on the curve, not counting joint twists. + Defaults to 1. + :type twist_amount: int? + :param which: Specifies the type of joint; options include 'M', 'F', 'MF', 'MM', 'FF'. + Defaults to 'MF'. + :type which: str? + :param twist_keep: Whether to keep the twist lock connectors. Defaults to False. + :type twist_keep: bool? + + :returns: + + This function does not return a value but modifies the geometry in the + Blender context. + :rtype: None + + +.. py:function:: tile(diameter, tolerance, tile_x_amount, tile_y_amount, stem=1) + + Create a tile shape based on specified dimensions and parameters. + + This function calculates the dimensions of a tile based on the provided + diameter and tolerance, as well as the number of tiles in the x and y + directions. It constructs the tile shape by creating a base and adding + features such as fingers for interlocking. The function also handles + transformations such as moving, rotating, and performing boolean + operations to achieve the desired tile geometry. + + :param diameter: The diameter of the tile. + :type diameter: float + :param tolerance: The tolerance to be applied to the tile dimensions. + :type tolerance: float + :param tile_x_amount: The number of tiles along the x-axis. + :type tile_x_amount: int + :param tile_y_amount: The number of tiles along the y-axis. + :type tile_y_amount: int + :param stem: A parameter affecting the tile's features. Defaults to 1. + :type stem: int? + + :returns: This function does not return a value but modifies global state. + :rtype: None + + diff --git a/_sources/autoapi/cam/simple/index.rst b/_sources/autoapi/cam/simple/index.rst new file mode 100644 index 000000000..a4482401e --- /dev/null +++ b/_sources/autoapi/cam/simple/index.rst @@ -0,0 +1,720 @@ +cam.simple +========== + +.. py:module:: cam.simple + +.. autoapi-nested-parse:: + + CNC CAM 'simple.py' © 2012 Vilem Novak + + Various helper functions, less complex than those found in the 'utils' files. + + + +Functions +--------- + +.. autoapisummary:: + + cam.simple.tuple_add + cam.simple.tuple_sub + cam.simple.tuple_mul + cam.simple.tuple_length + cam.simple.timinginit + cam.simple.timingstart + cam.simple.timingadd + cam.simple.timingprint + cam.simple.progress + cam.simple.activate + cam.simple.dist2d + cam.simple.delob + cam.simple.dupliob + cam.simple.addToGroup + cam.simple.compare + cam.simple.isVerticalLimit + cam.simple.getCachePath + cam.simple.getSimulationPath + cam.simple.safeFileName + cam.simple.strInUnits + cam.simple.select_multiple + cam.simple.join_multiple + cam.simple.remove_multiple + cam.simple.deselect + cam.simple.make_active + cam.simple.active_name + cam.simple.rename + cam.simple.union + cam.simple.intersect + cam.simple.difference + cam.simple.duplicate + cam.simple.mirrorx + cam.simple.mirrory + cam.simple.move + cam.simple.rotate + cam.simple.remove_doubles + cam.simple.add_overcut + cam.simple.add_bound_rectangle + cam.simple.add_rectangle + cam.simple.active_to_coords + cam.simple.active_to_shapely_poly + cam.simple.subdivide_short_lines + + +Module Contents +--------------- + +.. py:function:: tuple_add(t, t1) + + Add two tuples as vectors. + + This function takes two tuples, each representing a vector in three- + dimensional space, and returns a new tuple that is the element-wise sum + of the two input tuples. It assumes that both tuples contain exactly + three numeric elements. + + :param t: A tuple containing three numeric values representing the first vector. + :type t: tuple + :param t1: A tuple containing three numeric values representing the second vector. + :type t1: tuple + + :returns: + + A tuple containing three numeric values that represent the sum of the + input vectors. + :rtype: tuple + + +.. py:function:: tuple_sub(t, t1) + + Subtract two tuples element-wise. + + This function takes two tuples of three elements each and performs an + element-wise subtraction, treating the tuples as vectors. The result is + a new tuple containing the differences of the corresponding elements + from the input tuples. + + :param t: A tuple containing three numeric values. + :type t: tuple + :param t1: A tuple containing three numeric values. + :type t1: tuple + + :returns: A tuple containing the results of the element-wise subtraction. + :rtype: tuple + + +.. py:function:: tuple_mul(t, c) + + Multiply each element of a tuple by a given number. + + This function takes a tuple containing three elements and a numeric + value, then multiplies each element of the tuple by the provided number. + The result is returned as a new tuple containing the multiplied values. + + :param t: A tuple containing three numeric values. + :type t: tuple + :param c: A number by which to multiply each element of the tuple. + :type c: numeric + + :returns: A new tuple containing the results of the multiplication. + :rtype: tuple + + +.. py:function:: tuple_length(t) + + Get the length of a vector represented as a tuple. + + This function takes a tuple as input, which represents the coordinates + of a vector, and returns its length by creating a Vector object from the + tuple. The length is calculated using the appropriate mathematical + formula for vector length. + + :param t: A tuple representing the coordinates of the vector. + :type t: tuple + + :returns: The length of the vector. + :rtype: float + + +.. py:function:: timinginit() + + Initialize timing metrics. + + This function sets up the initial state for timing functions by + returning a list containing two zero values. These values can be used to + track elapsed time or other timing-related metrics in subsequent + operations. + + :returns: A list containing two zero values, representing the + initial timing metrics. + :rtype: list + + +.. py:function:: timingstart(tinf) + + Start timing by recording the current time. + + This function updates the second element of the provided list with the + current time in seconds since the epoch. It is useful for tracking the + start time of an operation or process. + + :param tinf: A list where the second element will be updated + with the current time. + :type tinf: list + + +.. py:function:: timingadd(tinf) + + Update the timing information. + + This function updates the first element of the `tinf` list by adding the + difference between the current time and the second element of the list. + It is typically used to track elapsed time in a timing context. + + :param tinf: A list where the first element is updated with the + :type tinf: list + + +.. py:function:: timingprint(tinf) + + Print the timing information. + + This function takes a tuple containing timing information and prints it + in a formatted string. It specifically extracts the first element of the + tuple, which is expected to represent time, and appends the string + 'seconds' to it before printing. + + :param tinf: A tuple where the first element is expected to be a numeric value + representing time. + :type tinf: tuple + + :returns: + + This function does not return any value; it only prints output to the + console. + :rtype: None + + +.. py:function:: progress(text, n=None) + + Report progress during script execution. + + This function outputs a progress message to the standard output. It is + designed to work for background operations and provides a formatted + string that includes the specified text and an optional numeric progress + value. If the numeric value is provided, it is formatted as a + percentage. + + :param text: The message to display as progress. + :type text: str + :param n: A float representing the progress as a + fraction (0.0 to 1.0). If not provided, no percentage will + be displayed. + :type n: float? + + :returns: + + This function does not return a value; it only prints + to the standard output. + :rtype: None + + +.. py:function:: activate(o) + + Makes an object active in Blender. + + This function sets the specified object as the active object in the + current Blender scene. It first deselects all objects, then selects the + given object and makes it the active object in the view layer. This is + useful for operations that require a specific object to be active, such + as transformations or modifications. + + :param o: The Blender object to be activated. + :type o: bpy.types.Object + + +.. py:function:: dist2d(v1, v2) + + Calculate the distance between two points in 2D space. + + This function computes the Euclidean distance between two points + represented by their coordinates in a 2D plane. It uses the Pythagorean + theorem to calculate the distance based on the differences in the x and + y coordinates of the points. + + :param v1: A tuple representing the coordinates of the first point (x1, y1). + :type v1: tuple + :param v2: A tuple representing the coordinates of the second point (x2, y2). + :type v2: tuple + + :returns: The Euclidean distance between the two points. + :rtype: float + + +.. py:function:: delob(ob) + + Delete an object in Blender for multiple uses. + + This function activates the specified object and then deletes it using + Blender's built-in operations. It is designed to facilitate the deletion + of objects within the Blender environment, ensuring that the object is + active before performing the deletion operation. + + :param ob: The Blender object to be deleted. + :type ob: Object + + +.. py:function:: dupliob(o, pos) + + Helper function for visualizing cutter positions in bullet simulation. + + This function duplicates the specified object and resizes it according + to a predefined scale factor. It also removes any existing rigidbody + properties from the duplicated object and sets its location to the + specified position. This is useful for managing multiple cutter + positions in a bullet simulation environment. + + :param o: The object to be duplicated. + :type o: Object + :param pos: The new position to place the duplicated object. + :type pos: Vector + + +.. py:function:: addToGroup(ob, groupname) + + Add an object to a specified group in Blender. + + This function activates the given object and checks if the specified + group exists in Blender's data. If the group does not exist, it creates + a new group with the provided name. If the group already exists, it + links the object to that group. + + :param ob: The object to be added to the group. + :type ob: Object + :param groupname: The name of the group to which the object will be added. + :type groupname: str + + +.. py:function:: compare(v1, v2, vmiddle, e) + + Comparison for optimization of paths. + + This function compares two vectors and checks if the distance between a + calculated vector and a reference vector is less than a specified + threshold. It normalizes the vector difference and scales it by the + length of another vector to determine if the resulting vector is within + the specified epsilon value. + + :param v1: The first vector for comparison. + :type v1: Vector + :param v2: The second vector for comparison. + :type v2: Vector + :param vmiddle: The middle vector used for calculating the + reference vector. + :type vmiddle: Vector + :param e: The threshold value for comparison. + :type e: float + + :returns: + + True if the distance is less than the threshold, + otherwise False. + :rtype: bool + + +.. py:function:: isVerticalLimit(v1, v2, limit) + + Test Path Segment on Verticality Threshold for protect_vertical option. + + This function evaluates the verticality of a path segment defined by two + points, v1 and v2, based on a specified limit. It calculates the angle + between the vertical vector and the vector formed by the two points. If + the angle is within the defined limit, it adjusts the vertical position + of either v1 or v2 to ensure that the segment adheres to the verticality + threshold. + + :param v1: A 3D point represented as a tuple (x, y, z). + :type v1: tuple + :param v2: A 3D point represented as a tuple (x, y, z). + :type v2: tuple + :param limit: The angle threshold for determining verticality. + :type limit: float + + :returns: The adjusted 3D points v1 and v2 after evaluating the verticality. + :rtype: tuple + + +.. py:function:: getCachePath(o) + + Get the cache path for a given object. + + This function constructs a cache path based on the current Blender + file's filepath and the name of the provided object. It retrieves the + base name of the file, removes the last six characters, and appends a + specified directory and the object's name to create a complete cache + path. + + :param o: The Blender object for which the cache path is being generated. + :type o: Object + + :returns: The constructed cache path as a string. + :rtype: str + + +.. py:function:: getSimulationPath() + + Get the simulation path for temporary camera files. + + This function retrieves the file path of the current Blender project and + constructs a new path for temporary camera files by appending 'temp_cam' + to the directory of the current file. The constructed path is returned + as a string. + + :returns: The path to the temporary camera directory. + :rtype: str + + +.. py:function:: safeFileName(name) + + Generate a safe file name from the given string. + + This function takes a string input and removes any characters that are + not considered valid for file names. The valid characters include + letters, digits, and a few special characters. The resulting string can + be used safely as a file name for exporting purposes. + + :param name: The input string to be sanitized into a safe file name. + :type name: str + + :returns: A sanitized version of the input string that contains only valid + characters for a file name. + :rtype: str + + +.. py:function:: strInUnits(x, precision=5) + + Convert a value to a string representation in the current unit system. + + This function takes a numeric value and converts it to a string + formatted according to the unit system set in the Blender context. If + the unit system is metric, the value is converted to millimeters. If the + unit system is imperial, the value is converted to inches. The precision + of the output can be specified. + + :param x: The numeric value to be converted. + :type x: float + :param precision: The number of decimal places to round to. + Defaults to 5. + :type precision: int? + + :returns: The string representation of the value in the appropriate units. + :rtype: str + + +.. py:function:: select_multiple(name) + + Select multiple objects in the scene based on their names. + + This function deselects all objects in the current Blender scene and + then selects all objects whose names start with the specified prefix. It + iterates through all objects in the scene and checks if their names + begin with the given string. If they do, those objects are selected; + otherwise, they are deselected. + + :param name: The prefix used to select objects in the scene. + :type name: str + + +.. py:function:: join_multiple(name) + + Join multiple objects and rename the final object. + + This function selects multiple objects in the Blender context, joins + them into a single object, and renames the resulting object to the + specified name. It is assumed that the objects to be joined are already + selected in the Blender interface. + + :param name: The new name for the joined object. + :type name: str + + +.. py:function:: remove_multiple(name) + + Remove multiple objects from the scene based on their name prefix. + + This function deselects all objects in the current Blender scene and + then iterates through all objects. If an object's name starts with the + specified prefix, it selects that object and deletes it from the scene. + This is useful for operations that require removing multiple objects + with a common naming convention. + + :param name: The prefix of the object names to be removed. + :type name: str + + +.. py:function:: deselect() + + Deselect all objects in the current Blender context. + + This function utilizes the Blender Python API to deselect all objects in + the current scene. It is useful for clearing selections before + performing other operations on objects. Raises: None + + +.. py:function:: make_active(name) + + Make an object active in the Blender scene. + + This function takes the name of an object and sets it as the active + object in the current Blender scene. It first deselects all objects, + then selects the specified object and makes it active, allowing for + further operations to be performed on it. + + :param name: The name of the object to be made active. + :type name: str + + +.. py:function:: active_name(name) + + Change the name of the active object in Blender. + + This function sets the name of the currently active object in the + Blender context to the specified name. It directly modifies the `name` + attribute of the active object, allowing users to rename objects + programmatically. + + :param name: The new name to assign to the active object. + :type name: str + + +.. py:function:: rename(name, name2) + + Rename an object and make it active. + + This function renames an object in the Blender context and sets it as + the active object. It first calls the `make_active` function to ensure + the object is active, then updates the name of the active object to the + new name provided. + + :param name: The current name of the object to be renamed. + :type name: str + :param name2: The new name to assign to the active object. + :type name2: str + + +.. py:function:: union(name) + + Perform a boolean union operation on objects. + + This function selects multiple objects that start with the given name, + performs a boolean union operation on them using Blender's operators, + and then renames the resulting object to the specified name. After the + operation, it removes the original objects that were used in the union + process. + + :param name: The base name of the objects to be unioned. + :type name: str + + +.. py:function:: intersect(name) + + Perform an intersection operation on a curve object. + + This function selects multiple objects based on the provided name and + then executes a boolean operation to create an intersection of the + selected objects. The resulting intersection is then named accordingly. + + :param name: The name of the object(s) to be selected for the intersection. + :type name: str + + +.. py:function:: difference(name, basename) + + Perform a boolean difference operation on objects. + + This function selects a series of objects specified by `name` and + performs a boolean difference operation with the object specified by + `basename`. After the operation, the resulting object is renamed to + 'booleandifference'. The original objects specified by `name` are + deleted after the operation. + + :param name: The name of the series of objects to select for the operation. + :type name: str + :param basename: The name of the base object to perform the boolean difference with. + :type basename: str + + +.. py:function:: duplicate(x=0.0, y=0.0) + + Duplicate an active object or move it based on the provided coordinates. + + This function duplicates the currently active object in Blender. If both + x and y are set to their default values (0), the object is duplicated in + place. If either x or y is non-zero, the object is duplicated and moved + by the specified x and y offsets. + + :param x: The x-coordinate offset for the duplication. + Defaults to 0. + :type x: float + :param y: The y-coordinate offset for the duplication. + Defaults to 0. + :type y: float + + +.. py:function:: mirrorx() + + Mirror the active object along the x-axis. + + This function utilizes Blender's operator to mirror the currently active + object in the 3D view along the x-axis. It sets the orientation to + global and applies the transformation based on the specified orientation + matrix and constraint axis. + + +.. py:function:: mirrory() + + Mirror the active object along the Y axis. + + This function uses Blender's operator to perform a mirror transformation + on the currently active object in the scene. The mirroring is done with + respect to the global coordinate system, specifically along the Y axis. + This can be useful for creating symmetrical objects or for correcting + the orientation of an object in a 3D environment. Raises: None + + +.. py:function:: move(x=0.0, y=0.0) + + Move the active object in the 3D space by applying a translation. + + This function translates the active object in Blender's 3D view by the + specified x and y values. It uses Blender's built-in operations to + perform the translation and then applies the transformation to the + object's location. + + :param x: The distance to move the object along the x-axis. Defaults to 0.0. + :type x: float + :param y: The distance to move the object along the y-axis. Defaults to 0.0. + :type y: float + + +.. py:function:: rotate(angle) + + Rotate the active object by a specified angle. + + This function modifies the rotation of the currently active object in + the Blender context by setting its Z-axis rotation to the given angle. + After updating the rotation, it applies the transformation to ensure + that the changes are saved to the object's data. + + :param angle: The angle in radians to rotate the active object + around the Z-axis. + :type angle: float + + +.. py:function:: remove_doubles() + + Remove duplicate vertices from the selected curve object. + + This function utilizes the Blender Python API to remove duplicate + vertices from the currently selected curve object in the Blender + environment. It is essential for cleaning up geometry and ensuring that + the curve behaves as expected without unnecessary complexity. + + +.. py:function:: add_overcut(diametre, overcut=True) + + Add overcut to the active object. + + This function adds an overcut to the currently active object in the + Blender context. If the `overcut` parameter is set to True, it performs + a series of operations including creating a curve overcut with the + specified diameter, deleting the original object, and renaming the new + object to match the original. The function also ensures that any + duplicate vertices are removed from the resulting object. + + :param diametre: The diameter to be used for the overcut. + :type diametre: float + :param overcut: A flag indicating whether to apply the overcut. Defaults to True. + :type overcut: bool + + +.. py:function:: add_bound_rectangle(xmin, ymin, xmax, ymax, name='bounds_rectangle') + + Add a bounding rectangle to a curve. + + This function creates a rectangle defined by the minimum and maximum x + and y coordinates provided as arguments. The rectangle is added to the + scene at the center of the defined bounds. The resulting rectangle is + named according to the 'name' parameter. + + :param xmin: The minimum x-coordinate of the rectangle. + :type xmin: float + :param ymin: The minimum y-coordinate of the rectangle. + :type ymin: float + :param xmax: The maximum x-coordinate of the rectangle. + :type xmax: float + :param ymax: The maximum y-coordinate of the rectangle. + :type ymax: float + :param name: The name of the resulting rectangle object. Defaults to + 'bounds_rectangle'. + :type name: str + + +.. py:function:: add_rectangle(width, height, center_x=True, center_y=True) + + Add a rectangle to the scene. + + This function creates a rectangle in the 3D space using the specified + width and height. The rectangle can be centered at the origin or offset + based on the provided parameters. If `center_x` or `center_y` is set to + True, the rectangle will be positioned at the center of the specified + dimensions; otherwise, it will be positioned based on the offsets. + + :param width: The width of the rectangle. + :type width: float + :param height: The height of the rectangle. + :type height: float + :param center_x: If True, centers the rectangle along the x-axis. Defaults to True. + :type center_x: bool? + :param center_y: If True, centers the rectangle along the y-axis. Defaults to True. + :type center_y: bool? + + +.. py:function:: active_to_coords() + + Convert the active object to a list of its vertex coordinates. + + This function duplicates the currently active object in the Blender + context, converts it to a mesh, and extracts the X and Y coordinates of + its vertices. After extracting the coordinates, it removes the temporary + mesh object created during the process. The resulting list contains + tuples of (x, y) coordinates for each vertex in the active object. + + :returns: A list of tuples, each containing the X and Y coordinates of the + vertices from the active object. + :rtype: list + + +.. py:function:: active_to_shapely_poly() + + Convert the active object to a Shapely polygon. + + This function retrieves the coordinates of the currently active object + and converts them into a Shapely Polygon data structure. It is useful + for geometric operations and spatial analysis using the Shapely library. + + :returns: A Shapely Polygon object created from the active object's coordinates. + :rtype: Polygon + + +.. py:function:: subdivide_short_lines(co) + + Subdivide all polylines to have at least three points. + + This function iterates through the splines of a curve, checks if they are not bezier + and if they have less or equal to two points. If so, each spline is subdivided to get + at least three points. + + :param co: A curve object to be analyzed and modified. + :type co: Object + + diff --git a/_sources/autoapi/cam/simulation/index.rst b/_sources/autoapi/cam/simulation/index.rst new file mode 100644 index 000000000..82e86f271 --- /dev/null +++ b/_sources/autoapi/cam/simulation/index.rst @@ -0,0 +1,123 @@ +cam.simulation +============== + +.. py:module:: cam.simulation + +.. autoapi-nested-parse:: + + CNC CAM 'simulation.py' © 2012 Vilem Novak + + Functions to generate a mesh simulation from CAM Chain / Operation data. + + + +Functions +--------- + +.. autoapisummary:: + + cam.simulation.createSimulationObject + cam.simulation.doSimulation + cam.simulation.generateSimulationImage + cam.simulation.simCutterSpot + + +Module Contents +--------------- + +.. py:function:: createSimulationObject(name, operations, i) + + Create a simulation object in Blender. + + This function creates a simulation object in Blender with the specified + name and operations. If an object with the given name already exists, it + retrieves that object; otherwise, it creates a new plane object and + applies several modifiers to it. The function also sets the object's + location and scale based on the provided operations and assigns a + texture to the object. + + :param name: The name of the simulation object to be created. + :type name: str + :param operations: A list of operation objects that contain bounding box information. + :type operations: list + :param i: The image to be used as a texture for the simulation object. + + +.. py:function:: doSimulation(name, operations) + :async: + + + Perform simulation of operations for a 3-axis system. + + This function iterates through a list of operations, retrieves the + necessary sources for each operation, and computes the bounds for the + operations. It then generates a simulation image based on the operations + and their limits, saves the image to a specified path, and finally + creates a simulation object in Blender using the generated image. + + :param name: The name to be used for the simulation object. + :type name: str + :param operations: A list of operations to be simulated. + :type operations: list + + +.. py:function:: generateSimulationImage(operations, limits) + :async: + + + Generate a simulation image based on provided operations and limits. + + This function creates a 2D simulation image by processing a series of + operations that define how the simulation should be conducted. It uses + the limits provided to determine the boundaries of the simulation area. + The function calculates the necessary resolution for the simulation + image based on the specified simulation detail and border width. It + iterates through each operation, simulating the effect of each operation + on the image, and updates the shape keys of the corresponding Blender + object to reflect the simulation results. The final output is a 2D array + representing the simulated image. + + :param operations: A list of operation objects that contain details + about the simulation, including feed rates and other parameters. + :type operations: list + :param limits: A tuple containing the minimum and maximum coordinates + (minx, miny, minz, maxx, maxy, maxz) that define the simulation + boundaries. + :type limits: tuple + + :returns: A 2D array representing the simulated image. + :rtype: np.ndarray + + +.. py:function:: simCutterSpot(xs, ys, z, cutterArray, si, getvolume=False) + + Simulates a cutter cutting into stock and optionally returns the volume + removed. + + This function takes the position of a cutter and modifies a stock image + by simulating the cutting process. It updates the stock image based on + the cutter's dimensions and position, ensuring that the stock does not + go below a certain level defined by the cutter's height. If requested, + it also calculates and returns the volume of material that has been + milled away. + + :param xs: The x-coordinate of the cutter's position. + :type xs: int + :param ys: The y-coordinate of the cutter's position. + :type ys: int + :param z: The height of the cutter. + :type z: float + :param cutterArray: A 2D array representing the cutter's shape. + :type cutterArray: numpy.ndarray + :param si: A 2D array representing the stock image to be modified. + :type si: numpy.ndarray + :param getvolume: If True, the function returns the volume removed. Defaults to False. + :type getvolume: bool? + + :returns: + + The volume of material removed if `getvolume` is True; otherwise, + returns 0. + :rtype: float + + diff --git a/_sources/autoapi/cam/slice/index.rst b/_sources/autoapi/cam/slice/index.rst new file mode 100644 index 000000000..2e0889927 --- /dev/null +++ b/_sources/autoapi/cam/slice/index.rst @@ -0,0 +1,117 @@ +cam.slice +========= + +.. py:module:: cam.slice + +.. autoapi-nested-parse:: + + CNC CAM 'slice.py' © 2021 Alain Pelletier + + Very simple slicing for 3D meshes, useful for plywood cutting. + Completely rewritten April 2021. + + + +Classes +------- + +.. autoapisummary:: + + cam.slice.SliceObjectsSettings + + +Functions +--------- + +.. autoapisummary:: + + cam.slice.slicing2d + cam.slice.slicing3d + cam.slice.sliceObject + + +Module Contents +--------------- + +.. py:function:: slicing2d(ob, height) + + Slice a 3D object at a specified height and convert it to a curve. + + This function applies transformations to the given object, switches to + edit mode, selects all vertices, and performs a bisect operation to + slice the object at the specified height. After slicing, it resets the + object's location and applies transformations again before converting + the object to a curve. If the conversion fails (for instance, if the + mesh was empty), the function deletes the mesh and returns False. + Otherwise, it returns True. + + :param ob: The Blender object to be sliced and converted. + :type ob: bpy.types.Object + :param height: The height at which to slice the object. + :type height: float + + :returns: True if the conversion to curve was successful, False otherwise. + :rtype: bool + + +.. py:function:: slicing3d(ob, start, end) + + Slice a 3D object along specified planes. + + This function applies transformations to a given object and slices it in + the Z-axis between two specified values, `start` and `end`. It first + ensures that the object is in edit mode and selects all vertices before + performing the slicing operations using the `bisect` method. After + slicing, it resets the object's location and applies the transformations + to maintain the changes. + + :param ob: The 3D object to be sliced. + :type ob: Object + :param start: The starting Z-coordinate for the slice. + :type start: float + :param end: The ending Z-coordinate for the slice. + :type end: float + + :returns: True if the slicing operation was successful. + :rtype: bool + + +.. py:function:: sliceObject(ob) + + Slice a 3D object into layers based on a specified thickness. + + This function takes a 3D object and slices it into multiple layers + according to the specified thickness. It creates a new collection for + the slices and optionally creates text labels for each slice if the + indexes parameter is set. The slicing can be done in either 2D or 3D + based on the user's selection. The function also handles the positioning + of the slices based on the object's bounding box. + + :param ob: The 3D object to be sliced. + :type ob: bpy.types.Object + + +.. py:class:: SliceObjectsSettings + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + Stores All Data for Machines + + + .. py:attribute:: slice_distance + :type: 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') + + + .. py:attribute:: slice_above0 + :type: BoolProperty(name='Slice Above 0', description='only slice model above 0', default=False) + + + .. py:attribute:: slice_3d + :type: BoolProperty(name='3D Slice', description='For 3D carving', default=False) + + + .. py:attribute:: indexes + :type: BoolProperty(name='Add Indexes', description='Adds index text of layer + index', default=True) + + diff --git a/_sources/autoapi/cam/strategy/index.rst b/_sources/autoapi/cam/strategy/index.rst new file mode 100644 index 000000000..0ca7c4929 --- /dev/null +++ b/_sources/autoapi/cam/strategy/index.rst @@ -0,0 +1,267 @@ +cam.strategy +============ + +.. py:module:: cam.strategy + +.. autoapi-nested-parse:: + + CNC CAM 'strategy.py' © 2012 Vilem Novak + + Strategy functionality of CNC CAM - e.g. Cutout, Parallel, Spiral, Waterline + The functions here are called with operators defined in 'ops.py' + + + +Attributes +---------- + +.. autoapisummary:: + + cam.strategy.SHAPELY + + +Functions +--------- + +.. autoapisummary:: + + cam.strategy.cutout + cam.strategy.curve + cam.strategy.proj_curve + cam.strategy.pocket + cam.strategy.drill + cam.strategy.medial_axis + cam.strategy.getLayers + cam.strategy.chunksToMesh + cam.strategy.checkminz + + +Module Contents +--------------- + +.. py:data:: SHAPELY + :value: True + + +.. py:function:: cutout(o) + :async: + + + Perform a cutout operation based on the provided parameters. + + This function calculates the necessary cutter offset based on the cutter + type and its parameters. It processes a list of objects to determine how + to cut them based on their geometry and the specified cutting type. The + function handles different cutter types such as 'VCARVE', 'CYLCONE', + 'BALLCONE', and 'BALLNOSE', applying specific calculations for each. It + also manages the layering and movement strategies for the cutting + operation, including options for lead-ins, ramps, and bridges. + + :param o: An object containing parameters for the cutout operation, + including cutter type, diameter, depth, and other settings. + :type o: object + + :returns: + + This function does not return a value but performs operations + on the provided object. + :rtype: None + + +.. py:function:: curve(o) + :async: + + + Process and convert curve objects into mesh chunks. + + This function takes an operation object and processes the curves + contained within it. It first checks if all objects are curves; if not, + it raises an exception. The function then converts the curves into + chunks, sorts them, and refines them. If layers are to be used, it + applies layer information to the chunks, adjusting their Z-offsets + accordingly. Finally, it converts the processed chunks into a mesh. + + :param o: An object containing operation parameters, including a list of + objects, flags for layer usage, and movement constraints. + :type o: Operation + + :returns: + + This function does not return a value; it performs operations on the + input. + :rtype: None + + :raises CamException: If not all objects in the operation are curves. + + +.. py:function:: proj_curve(s, o) + :async: + + + Project a curve onto another curve object. + + This function takes a source object and a target object, both of which + are expected to be curve objects. It projects the points of the source + curve onto the target curve, adjusting the start and end points based on + specified extensions. The resulting projected points are stored in the + source object's path samples. + + :param s: The source object containing the curve to be projected. + :type s: object + :param o: An object containing references to the curve objects + involved in the projection. + :type o: object + + :returns: + + This function does not return a value; it modifies the + source object's path samples in place. + :rtype: None + + :raises CamException: If the target curve is not of type 'CURVE'. + + +.. py:function:: pocket(o) + :async: + + + Perform pocketing operation based on the provided parameters. + + This function executes a pocketing operation using the specified + parameters from the object `o`. It calculates the cutter offset based on + the cutter type and depth, processes curves, and generates the necessary + chunks for the pocketing operation. The function also handles various + movement types and optimizations, including helix entry and retract + movements. + + :param o: An object containing parameters for the pocketing + :type o: object + + :returns: The function modifies the scene and generates geometry + based on the pocketing operation. + :rtype: None + + +.. py:function:: drill(o) + :async: + + + Perform a drilling operation on the specified objects. + + This function iterates through the objects in the provided context, + activating each object and applying transformations. It duplicates the + objects and processes them based on their type (CURVE or MESH). For + CURVE objects, it calculates the bounding box and center points of the + splines and bezier points, and generates chunks based on the specified + drill type. For MESH objects, it generates chunks from the vertices. The + function also manages layers and chunk depths for the drilling + operation. + + :param o: An object containing properties and methods required + for the drilling operation, including a list of + objects to drill, drill type, and depth parameters. + :type o: object + + :returns: + + This function does not return a value but performs operations + that modify the state of the Blender context. + :rtype: None + + +.. py:function:: medial_axis(o) + :async: + + + Generate the medial axis for a given operation. + + This function computes the medial axis of the specified operation, which + involves processing various cutter types and their parameters. It starts + by removing any existing medial mesh, then calculates the maximum depth + based on the cutter type and its properties. The function refines curves + and computes the Voronoi diagram for the points derived from the + operation's silhouette. It filters points and edges based on their + positions relative to the computed shapes, and generates a mesh + representation of the medial axis. Finally, it handles layers and + optionally adds a pocket operation if specified. + + :param o: An object containing parameters for the operation, including + cutter type, dimensions, and other relevant properties. + :type o: Operation + + :returns: A dictionary indicating the completion status of the operation. + :rtype: dict + + :raises CamException: If an unsupported cutter type is provided or if the input curve + is not closed. + + +.. py:function:: getLayers(operation, startdepth, enddepth) + + Returns a list of layers bounded by start depth and end depth. + + This function calculates the layers between the specified start and end + depths based on the step down value defined in the operation. If the + operation is set to use layers, it computes the number of layers by + dividing the difference between start and end depths by the step down + value. The function raises an exception if the start depth is lower than + the end depth. + + :param operation: An object that contains the properties `use_layers`, + `stepdown`, and `maxz` which are used to determine + how layers are generated. + :type operation: object + :param startdepth: The starting depth for layer calculation. + :type startdepth: float + :param enddepth: The ending depth for layer calculation. + :type enddepth: float + + :returns: + + A list of layers, where each layer is represented as a list + containing the start and end depths of that layer. + :rtype: list + + :raises CamException: If the start depth is lower than the end depth. + + +.. py:function:: chunksToMesh(chunks, o) + + Convert sampled chunks into a mesh path for a given optimization object. + + This function takes a list of sampled chunks and converts them into a + mesh path based on the specified optimization parameters. It handles + different machine axes configurations and applies optimizations as + needed. The resulting mesh is created in the Blender context, and the + function also manages the lifting and dropping of the cutter based on + the chunk positions. + + :param chunks: A list of chunk objects to be converted into a mesh. + :type chunks: list + :param o: An object containing optimization parameters and settings. + :type o: object + + :returns: + + The function creates a mesh in the Blender context but does not return a + value. + :rtype: None + + +.. py:function:: checkminz(o) + + Check the minimum value based on the specified condition. + + This function evaluates the 'minz_from' attribute of the input object + 'o'. If 'minz_from' is set to 'MATERIAL', it returns the value of + 'min.z'. Otherwise, it returns the value of 'minz'. + + :param o: An object that has attributes 'minz_from', 'min', and 'minz'. + :type o: object + + :returns: + + The minimum value, which can be either 'o.min.z' or 'o.minz' depending + on the condition. + + diff --git a/_sources/autoapi/cam/testing/index.rst b/_sources/autoapi/cam/testing/index.rst new file mode 100644 index 000000000..ff4dd1810 --- /dev/null +++ b/_sources/autoapi/cam/testing/index.rst @@ -0,0 +1,208 @@ +cam.testing +=========== + +.. py:module:: cam.testing + +.. autoapi-nested-parse:: + + CNC CAM 'testing.py' © 2012 Vilem Novak + + Functions for automated testing. + + + +Attributes +---------- + +.. autoapisummary:: + + cam.testing.tests + cam.testing.p + + +Functions +--------- + +.. autoapisummary:: + + cam.testing.addTestCurve + cam.testing.addTestMesh + cam.testing.deleteFirstVert + cam.testing.testCalc + cam.testing.testCutout + cam.testing.testPocket + cam.testing.testParallel + cam.testing.testWaterline + cam.testing.testSimulation + cam.testing.cleanUp + cam.testing.testOperation + cam.testing.testAll + + +Module Contents +--------------- + +.. py:function:: addTestCurve(loc) + + Add a test curve to the Blender scene. + + This function creates a Bezier circle at the specified location in the + Blender scene. It first adds a primitive Bezier circle, then enters edit + mode to duplicate the circle twice, resizing each duplicate to half its + original size. The function ensures that the transformations are applied + in the global orientation and does not use proportional editing. + + :param loc: A tuple representing the (x, y, z) coordinates where + the Bezier circle will be added in the 3D space. + :type loc: tuple + + +.. py:function:: addTestMesh(loc) + + Add a test mesh to the Blender scene. + + This function creates a monkey mesh and a plane mesh at the specified + location in the Blender scene. It first adds a monkey mesh with a small + radius, rotates it, and applies the transformation. Then, it toggles + into edit mode, adds a plane mesh, resizes it, and translates it + slightly before toggling back out of edit mode. + + :param loc: A tuple representing the (x, y, z) coordinates where + the meshes will be added in the Blender scene. + :type loc: tuple + + +.. py:function:: deleteFirstVert(ob) + + Delete the first vertex of a given object. + + This function activates the specified object, enters edit mode, + deselects all vertices, selects the first vertex, and then deletes it. + The function ensures that the object is properly updated after the + deletion. + + :param ob: The Blender object from which the first + :type ob: bpy.types.Object + + +.. py:function:: testCalc(o) + + Test the calculation of the camera path for a given object. + + This function invokes the Blender operator to calculate the camera path + for the specified object and then deletes the first vertex of that + object. It is intended to be used within a Blender environment where the + bpy module is available. + + :param o: The Blender object for which the camera path is to be calculated. + :type o: Object + + +.. py:function:: testCutout(pos) + + Test the cutout functionality in the scene. + + This function adds a test curve based on the provided position, performs + a camera operation, and sets the strategy to 'CUTOUT'. It then calls the + `testCalc` function to perform further calculations on the camera + operation. + + :param pos: A tuple containing the x and y coordinates for the + position of the test curve. + :type pos: tuple + + +.. py:function:: testPocket(pos) + + Test the pocket operation in a 3D scene. + + This function sets up a pocket operation by adding a test curve based on + the provided position. It configures the camera operation settings for + the pocket strategy, enabling helix entry and tangential retraction. + Finally, it performs a calculation based on the configured operation. + + :param pos: A tuple containing the x and y coordinates for + the position of the test curve. + :type pos: tuple + + +.. py:function:: testParallel(pos) + + Test the parallel functionality of the camera operations. + + This function adds a test mesh at a specified position and then performs + camera operations in the Blender environment. It sets the ambient + behavior of the camera operation to 'AROUND' and configures the material + radius around the model. Finally, it calculates the camera path based on + the current scene settings. + + :param pos: A tuple containing the x and y coordinates for + positioning the test mesh. + :type pos: tuple + + +.. py:function:: testWaterline(pos) + + Test the waterline functionality in the scene. + + This function adds a test mesh at a specified position and then performs + a camera operation with the strategy set to 'WATERLINE'. It also + configures the optimization pixel size for the operation. The function + is intended for use in a 3D environment where waterline calculations are + necessary for rendering or simulation. + + :param pos: A tuple containing the x and y coordinates for + the position of the test mesh. + :type pos: tuple + + +.. py:function:: testSimulation() + + Testsimulation function. + + +.. py:function:: cleanUp() + + Clean up the Blender scene by removing all objects and camera + operations. + + This function selects all objects in the current Blender scene and + deletes them. It also removes any camera operations that are present in + the scene. This is useful for resetting the scene to a clean state + before performing further operations. + + +.. py:function:: testOperation(i) + + Test the operation of a camera path in Blender. + + This function tests a specific camera operation by comparing the + generated camera path with an existing reference path. It retrieves the + camera operation from the scene and checks if the generated path matches + the expected path in terms of vertex count and positions. If there is no + existing reference path, it marks the new result as comparable. The + function generates a report detailing the results of the comparison, + including any discrepancies found. + + :param i: The index of the camera operation to test. + :type i: int + + :returns: A report summarizing the results of the operation test. + :rtype: str + + +.. py:function:: testAll() + + Run tests on all camera operations in the current scene. + + This function iterates through all camera operations defined in the + current Blender scene and executes a test for each operation. The + results of these tests are collected into a report string, which is then + printed to the console. This is useful for verifying the functionality + of camera operations within the Blender environment. + + +.. py:data:: tests + +.. py:data:: p + diff --git a/_sources/autoapi/cam/ui/index.rst b/_sources/autoapi/cam/ui/index.rst new file mode 100644 index 000000000..b0d1eb56e --- /dev/null +++ b/_sources/autoapi/cam/ui/index.rst @@ -0,0 +1,190 @@ +cam.ui +====== + +.. py:module:: cam.ui + +.. autoapi-nested-parse:: + + CNC CAM 'ui.py' © 2012 Vilem Novak + + Panels displayed in the 3D Viewport - Curve Tools, Creators and Import G-code + + + +Classes +------- + +.. autoapisummary:: + + cam.ui.CAM_UL_orientations + cam.ui.VIEW3D_PT_tools_curvetools + cam.ui.VIEW3D_PT_tools_create + cam.ui.CustomPanel + cam.ui.WM_OT_gcode_import + cam.ui.import_settings + + +Module Contents +--------------- + +.. py:class:: CAM_UL_orientations + + Bases: :py:obj:`bpy.types.UIList` + + + .. py:method:: draw_item(context, layout, data, item, icon, active_data, active_propname, index) + + +.. py:class:: VIEW3D_PT_tools_curvetools + + Bases: :py:obj:`bpy.types.Panel` + + + .. py:attribute:: bl_space_type + :value: 'VIEW_3D' + + + + .. py:attribute:: bl_region_type + :value: 'TOOLS' + + + + .. py:attribute:: bl_context + :value: 'objectmode' + + + + .. py:attribute:: bl_label + :value: 'Curve CAM Tools' + + + + .. py:method:: draw(context) + + +.. py:class:: VIEW3D_PT_tools_create + + Bases: :py:obj:`bpy.types.Panel` + + + .. py:attribute:: bl_space_type + :value: 'VIEW_3D' + + + + .. py:attribute:: bl_region_type + :value: 'TOOLS' + + + + .. py:attribute:: bl_context + :value: 'objectmode' + + + + .. py:attribute:: bl_label + :value: 'Curve CAM Creators' + + + + .. py:attribute:: bl_option + :value: 'DEFAULT_CLOSED' + + + + .. py:method:: draw(context) + + +.. py:class:: CustomPanel + + Bases: :py:obj:`bpy.types.Panel` + + + .. py:attribute:: bl_space_type + :value: 'VIEW_3D' + + + + .. py:attribute:: bl_region_type + :value: 'TOOLS' + + + + .. py:attribute:: bl_context + :value: 'objectmode' + + + + .. py:attribute:: bl_label + :value: 'Import G-code' + + + + .. py:attribute:: bl_idname + :value: 'OBJECT_PT_importgcode' + + + + .. py:attribute:: bl_options + + + .. py:method:: poll(context) + :classmethod: + + + + .. py:method:: draw(context) + + +.. py:class:: WM_OT_gcode_import + + Bases: :py:obj:`bpy.types.Operator`, :py:obj:`bpy_extras.io_utils.ImportHelper` + + + Import G-code, Travel Lines Don't Get Drawn + + + .. py:attribute:: bl_idname + :value: 'wm.gcode_import' + + + + .. py:attribute:: bl_label + :value: 'Import G-code' + + + + .. py:attribute:: filename_ext + :value: '.txt' + + + + .. py:attribute:: filter_glob + :type: StringProperty(default='*.*', options={'HIDDEN'}, maxlen=255) + + + .. py:method:: execute(context) + + +.. py:class:: import_settings + + Bases: :py:obj:`bpy.types.PropertyGroup` + + + .. py:attribute:: split_layers + :type: BoolProperty(name='Split Layers', description='Save every layer as single Objects in Collection', default=False) + + + .. py:attribute:: subdivide + :type: BoolProperty(name='Subdivide', description="Only Subdivide gcode segments that are bigger than 'Segment length' ", default=False) + + + .. py:attribute:: output + :type: EnumProperty(name='Output Type', items=(('mesh', 'Mesh', 'Make a mesh output'), ('curve', 'Curve', 'Make curve output')), default='curve') + + + .. py:attribute:: max_segment_size + :type: FloatProperty(name='', description='Only Segments bigger than this value get subdivided', default=0.001, min=0.0001, max=1.0, unit='LENGTH') + + diff --git a/_sources/autoapi/cam/utils/index.rst b/_sources/autoapi/cam/utils/index.rst new file mode 100644 index 000000000..36451ddc1 --- /dev/null +++ b/_sources/autoapi/cam/utils/index.rst @@ -0,0 +1,1334 @@ +cam.utils +========= + +.. py:module:: cam.utils + +.. autoapi-nested-parse:: + + CNC CAM 'utils.py' © 2012 Vilem Novak + + Main functionality of CNC CAM. + The functions here are called with operators defined in 'ops.py' + + + +Attributes +---------- + +.. autoapisummary:: + + cam.utils.SHAPELY + cam.utils.USE_PROFILER + cam.utils.was_hidden_dict + cam.utils._IS_LOADING_DEFAULTS + + +Classes +------- + +.. autoapisummary:: + + cam.utils.Point + + +Functions +--------- + +.. autoapisummary:: + + cam.utils.opencamlib_version + cam.utils.positionObject + cam.utils.getBoundsWorldspace + cam.utils.getSplineBounds + cam.utils.getOperationSources + cam.utils.getBounds + cam.utils.getBoundsMultiple + cam.utils.samplePathLow + cam.utils.sampleChunks + cam.utils.sampleChunksNAxis + cam.utils.extendChunks5axis + cam.utils.curveToShapely + cam.utils.silhoueteOffset + cam.utils.polygonBoolean + cam.utils.polygonConvexHull + cam.utils.Helix + cam.utils.comparezlevel + cam.utils.overlaps + cam.utils.connectChunksLow + cam.utils.getClosest + cam.utils.sortChunks + cam.utils.getVectorRight + cam.utils.cleanUpDict + cam.utils.dictRemove + cam.utils.addLoop + cam.utils.cutloops + cam.utils.getOperationSilhouete + cam.utils.getObjectSilhouete + cam.utils.getAmbient + cam.utils.getObjectOutline + cam.utils.addOrientationObject + cam.utils.removeOrientationObject + cam.utils.addTranspMat + cam.utils.addMachineAreaObject + cam.utils.addMaterialAreaObject + cam.utils.getContainer + cam.utils.unique + cam.utils.checkEqual + cam.utils.prepareIndexed + cam.utils.cleanupIndexed + cam.utils.rotTo2axes + cam.utils.reload_paths + cam.utils.updateMachine + cam.utils.updateMaterial + cam.utils.updateOperation + cam.utils.isValid + cam.utils.operationValid + cam.utils.isChainValid + cam.utils.updateOperationValid + cam.utils.updateChipload + cam.utils.updateOffsetImage + cam.utils.updateZbufferImage + cam.utils.updateStrategy + cam.utils.updateCutout + cam.utils.updateExact + cam.utils.updateOpencamlib + cam.utils.updateBridges + cam.utils.updateRotation + cam.utils.updateRest + cam.utils.getStrategyList + cam.utils.update_material + cam.utils.update_operation + cam.utils.update_exact_mode + cam.utils.update_opencamlib + cam.utils.update_zbuffer_image + cam.utils.check_operations_on_load + cam.utils.Add_Pocket + + +Module Contents +--------------- + +.. py:data:: SHAPELY + :value: True + + +.. py:function:: opencamlib_version() + + Return the version of the OpenCamLib library. + + This function attempts to import the OpenCamLib library and returns its + version. If the library is not available, it will return None. The + function first tries to import the library using the name 'ocl', and if + that fails, it attempts to import it using 'opencamlib' as an alias. If + both imports fail, it returns None. + + :returns: The version of OpenCamLib if available, None otherwise. + :rtype: str or None + + +.. py:function:: positionObject(operation) + + Position an object based on specified operation parameters. + + This function adjusts the location of a Blender object according to the + provided operation settings. It calculates the bounding box of the + object in world space and modifies its position based on the material's + center settings and specified z-positioning (BELOW, ABOVE, or CENTERED). + The function also applies transformations to the object if it is not of + type 'CURVE'. + + :param operation: An object containing parameters for positioning, + including object_name, use_modifiers, and material + settings. + :type operation: OperationType + + +.. py:function:: getBoundsWorldspace(obs, use_modifiers=False) + + Get the bounding box of a list of objects in world space. + + This function calculates the minimum and maximum coordinates that + encompass all the specified objects in the 3D world space. It iterates + through each object, taking into account their transformations and + modifiers if specified. The function supports different object types, + including meshes and fonts, and handles the conversion of font objects + to mesh format for accurate bounding box calculations. + + :param obs: A list of Blender objects to calculate bounds for. + :type obs: list + :param use_modifiers: If True, apply modifiers to the objects + before calculating bounds. Defaults to False. + :type use_modifiers: bool + + :returns: + + A tuple containing the minimum and maximum coordinates + in the format (minx, miny, minz, maxx, maxy, maxz). + :rtype: tuple + + :raises CamException: If an object type does not support CAM operations. + + +.. py:function:: getSplineBounds(ob, curve) + + Get the bounding box of a spline object. + + This function calculates the minimum and maximum coordinates (x, y, z) + of the given spline object by iterating through its bezier points and + regular points. It transforms the local coordinates to world coordinates + using the object's transformation matrix. The resulting bounds can be + used for various purposes, such as collision detection or rendering. + + :param ob: The object containing the spline whose bounds are to be calculated. + :type ob: Object + :param curve: The curve object that contains the bezier points and regular points. + :type curve: Curve + + :returns: A tuple containing the minimum and maximum coordinates in the + format (minx, miny, minz, maxx, maxy, maxz). + :rtype: tuple + + +.. py:function:: getOperationSources(o) + + Get operation sources based on the geometry source type. + + This function retrieves and sets the operation sources for a given + object based on its geometry source type. It handles three types of + geometry sources: 'OBJECT', 'COLLECTION', and 'IMAGE'. For 'OBJECT', it + selects the specified object and applies rotations if enabled. For + 'COLLECTION', it retrieves all objects within the specified collection. + For 'IMAGE', it sets a specific optimization flag. Additionally, it + determines whether the objects are curves or meshes based on the + geometry source. + + :param o: An object containing properties such as geometry_source, + object_name, collection_name, rotation_A, rotation_B, + enable_A, enable_B, old_rotation_A, old_rotation_B, + A_along_x, and optimisation. + :type o: Object + + :returns: + + This function does not return a value but modifies the + properties of the input object. + :rtype: None + + +.. py:function:: getBounds(o) + + Calculate the bounding box for a given object. + + This function determines the minimum and maximum coordinates of an + object's bounding box based on its geometry source. It handles different + geometry types such as OBJECT, COLLECTION, and CURVE. The function also + considers material properties and image cropping if applicable. The + bounding box is adjusted according to the object's material settings and + the optimization parameters defined in the object. + + :param o: An object containing geometry and material properties, as well as + optimization settings. + :type o: object + + :returns: + + This function modifies the input object in place and does not return a + value. + :rtype: None + + +.. py:function:: getBoundsMultiple(operations) + + Gets bounds of multiple operations for simulations or rest milling. + + This function iterates through a list of operations to determine the + minimum and maximum bounds in three-dimensional space (x, y, z). It + initializes the bounds to extreme values and updates them based on the + bounds of each operation. The function is primarily intended for use in + simulations or rest milling processes, although it is noted that the + implementation may not be optimal. + + :param operations: A list of operation objects, each containing + 'min' and 'max' attributes with 'x', 'y', + and 'z' coordinates. + :type operations: list + + :returns: + + A tuple containing the minimum and maximum bounds in the + order (minx, miny, minz, maxx, maxy, maxz). + :rtype: tuple + + +.. py:function:: samplePathLow(o, ch1, ch2, dosample) + + Generate a sample path between two channels. + + This function computes a series of points that form a path between two + given channels. It calculates the direction vector from the end of the + first channel to the start of the second channel and generates points + along this vector up to a specified distance. If sampling is enabled, it + modifies the z-coordinate of the generated points based on the cutter + shape or image sampling, ensuring that the path accounts for any + obstacles or features in the environment. + + :param o: An object containing optimization parameters and properties related to + the path generation. + :param ch1: The first channel object, which provides a point for the starting + location of the path. + :param ch2: The second channel object, which provides a point for the ending + location of the path. + :param dosample: A flag indicating whether to perform sampling along the generated path. + :type dosample: bool + + :returns: An object representing the generated path points. + :rtype: camPathChunk + + +.. py:function:: sampleChunks(o, pathSamples, layers) + :async: + + + Sample chunks of paths based on the provided parameters. + + This function processes the given path samples and layers to generate + chunks of points that represent the sampled paths. It takes into account + various optimization settings and strategies to determine how the points + are sampled and organized into layers. The function handles different + scenarios based on the object's properties and the specified layers, + ensuring that the resulting chunks are correctly structured for further + processing. + + :param o: An object containing various properties and settings + related to the sampling process. + :type o: object + :param pathSamples: A list of path samples to be processed. + :type pathSamples: list + :param layers: A list of layers defining the z-coordinate ranges + for sampling. + :type layers: list + + :returns: + + A list of sampled chunks, each containing points that represent + the sampled paths. + :rtype: list + + +.. py:function:: sampleChunksNAxis(o, pathSamples, layers) + :async: + + + Sample chunks along a specified axis based on provided paths and layers. + + This function processes a set of path samples and organizes them into + chunks according to specified layers. It prepares the collision world if + necessary, updates the cutter's rotation based on the path samples, and + handles the sampling of points along the paths. The function also + manages the relationships between the sampled points and their + respective layers, ensuring that the correct points are added to each + chunk. The resulting chunks can be used for further processing in a 3D + environment. + + :param o: An object containing properties such as min/max coordinates, + cutter shape, and other relevant parameters. + :type o: object + :param pathSamples: A list of path samples, each containing start points, + end points, and rotations. + :type pathSamples: list + :param layers: A list of layer definitions that specify the boundaries + for sampling. + :type layers: list + + :returns: A list of sampled chunks organized by layers. + :rtype: list + + +.. py:function:: extendChunks5axis(chunks, o) + + Extend chunks with 5-axis cutter start and end points. + + This function modifies the provided chunks by appending calculated start + and end points for a cutter based on the specified orientation and + movement parameters. It determines the starting position of the cutter + based on the machine's settings and the object's movement constraints. + The function iterates through each point in the chunks and updates their + start and end points accordingly. + + :param chunks: A list of chunk objects that will be modified. + :type chunks: list + :param o: An object containing movement and orientation data. + :type o: object + + +.. py:function:: curveToShapely(cob, use_modifiers=False) + + Convert a curve object to Shapely polygons. + + This function takes a curve object and converts it into a list of + Shapely polygons. It first breaks the curve into chunks and then + transforms those chunks into Shapely-compatible polygon representations. + The `use_modifiers` parameter allows for additional processing of the + curve before conversion, depending on the specific requirements of the + application. + + :param cob: The curve object to be converted. + :param use_modifiers: A flag indicating whether to apply modifiers + during the conversion process. Defaults to False. + :type use_modifiers: bool + + :returns: A list of Shapely polygons created from the curve object. + :rtype: list + + +.. py:function:: silhoueteOffset(context, offset, style, mitrelimit) + + Offset the silhouette of a curve or font object in Blender. + + This function takes an active curve or font object in Blender and + creates an offset silhouette based on the specified parameters. It first + retrieves the silhouette of the object and then applies a buffer + operation to create the offset shape. The resulting shape is then + converted back into a curve object in the Blender scene. + + :param context: The current Blender context. + :type context: bpy.context + :param offset: The distance to offset the silhouette. + :type offset: float + :param style: The join style for the offset. Defaults to 1. + :type style: int? + :param mitrelimit: The mitre limit for the offset. Defaults to 1.0. + :type mitrelimit: float? + + :returns: A dictionary indicating the operation is finished. + :rtype: dict + + +.. py:function:: polygonBoolean(context, boolean_type) + + Perform a boolean operation on selected polygons. + + This function takes the active object and applies a specified boolean + operation (UNION, DIFFERENCE, or INTERSECT) with respect to other + selected objects in the Blender context. It first converts the polygons + of the active object and the selected objects into a Shapely + MultiPolygon. Depending on the boolean type specified, it performs the + corresponding boolean operation and then converts the result back into a + Blender curve. + + :param context: The Blender context containing scene and object data. + :type context: bpy.context + :param boolean_type: The type of boolean operation to perform. + Must be one of 'UNION', 'DIFFERENCE', or 'INTERSECT'. + :type boolean_type: str + + :returns: A dictionary indicating the operation result, typically {'FINISHED'}. + :rtype: dict + + +.. py:function:: polygonConvexHull(context) + + Generate the convex hull of a polygon from the given context. + + This function duplicates the current object, joins it, and converts it + into a 3D mesh. It then extracts the X and Y coordinates of the vertices + to create a MultiPoint data structure using Shapely. Finally, it + computes the convex hull of these points and converts the result back + into a curve named 'ConvexHull'. Temporary objects created during this + process are deleted to maintain a clean workspace. + + :param context: The context in which the operation is performed, typically + related to Blender's current state. + + :returns: A dictionary indicating the operation's completion status. + :rtype: dict + + +.. py:function:: Helix(r, np, zstart, pend, rev) + + Generate a helix of points in 3D space. + + This function calculates a series of points that form a helix based on + the specified parameters. It starts from a given radius and + z-coordinate, and generates points by rotating around the z-axis while + moving linearly along the z-axis. The number of points generated is + determined by the number of turns (revolutions) and the number of points + per revolution. + + :param r: The radius of the helix. + :type r: float + :param np: The number of points per revolution. + :type np: int + :param zstart: The starting z-coordinate for the helix. + :type zstart: float + :param pend: A tuple containing the x, y, and z coordinates of the endpoint. + :type pend: tuple + :param rev: The number of revolutions to complete. + :type rev: int + + :returns: + + A list of tuples representing the coordinates of the points in the + helix. + :rtype: list + + +.. py:function:: comparezlevel(x) + +.. py:function:: overlaps(bb1, bb2) + + Determine if one bounding box is a child of another. + + This function checks if the first bounding box (bb1) is completely + contained within the second bounding box (bb2). It does this by + comparing the coordinates of both bounding boxes to see if all corners + of bb1 are within the bounds of bb2. + + :param bb1: A tuple representing the coordinates of the first bounding box + in the format (x_min, y_min, x_max, y_max). + :type bb1: tuple + :param bb2: A tuple representing the coordinates of the second bounding box + in the format (x_min, y_min, x_max, y_max). + :type bb2: tuple + + :returns: True if bb1 is a child of bb2, otherwise False. + :rtype: bool + + +.. py:function:: connectChunksLow(chunks, o) + :async: + + + Connects chunks that are close to each other without lifting, sampling + them 'low'. + + This function processes a list of chunks and connects those that are + within a specified distance based on the provided options. It takes into + account various strategies for connecting the chunks, including 'CARVE', + 'PENCIL', and 'MEDIAL_AXIS', and adjusts the merging distance + accordingly. The function also handles specific movement settings, such + as whether to stay low or to merge distances, and may resample chunks if + certain optimization conditions are met. + + :param chunks: A list of chunk objects to be connected. + :type chunks: list + :param o: An options object containing movement and strategy parameters. + :type o: object + + :returns: A list of connected chunk objects. + :rtype: list + + +.. py:function:: getClosest(o, pos, chunks) + + Find the closest chunk to a given position. + + This function iterates through a list of chunks and determines which + chunk is closest to the specified position. It checks if each chunk's + children are sorted before calculating the distance. The chunk with the + minimum distance to the given position is returned. + + :param o: An object representing the origin point. + :param pos: A position to which the closest chunk is calculated. + :param chunks: A list of chunk objects to evaluate. + :type chunks: list + + :returns: + + The closest chunk object to the specified position, or None if no valid + chunk is found. + :rtype: Chunk + + +.. py:function:: sortChunks(chunks, o, last_pos=None) + :async: + + + Sort a list of chunks based on a specified strategy. + + This function sorts a list of chunks according to the provided options + and the current position. It utilizes a recursive approach to find the + closest chunk to the current position and adapts its distance if it has + not been sorted before. The function also handles progress updates + asynchronously and adjusts the recursion limit to accommodate deep + recursion scenarios. + + :param chunks: A list of chunk objects to be sorted. + :type chunks: list + :param o: An options object that contains sorting strategy and other parameters. + :type o: object + :param last_pos: The last known position as a tuple of coordinates. + Defaults to None, which initializes the position to (0, 0, 0). + :type last_pos: tuple? + + :returns: A sorted list of chunk objects. + :rtype: list + + +.. py:function:: getVectorRight(lastv, verts) + + Get the index of the vector that is most to the right based on angle. + + This function calculates the angle between a reference vector (formed by + the last two vectors in `lastv`) and each vector in the `verts` list. It + identifies the vector that has the smallest angle with respect to the + reference vector, indicating that it is the most rightward vector in + relation to the specified direction. + + :param lastv: A list containing two vectors, where each vector is + represented as a tuple or list of coordinates. + :type lastv: list + :param verts: A list of vectors represented as tuples or lists of + coordinates. + :type verts: list + + :returns: + + The index of the vector in `verts` that is most to the right + based on the calculated angle. + :rtype: int + + +.. py:function:: cleanUpDict(ndict) + + Remove lonely points from a dictionary. + + This function iterates over the keys of the provided dictionary and + removes any entries that contain one or fewer associated values. It + continues to check for and remove "lonely" points until no more can be + found. The process is repeated until all such entries are eliminated + from the dictionary. + + :param ndict: A dictionary where keys are associated with lists of values. + :type ndict: dict + + :returns: + + This function modifies the input dictionary in place and does not return + a value. + :rtype: None + + +.. py:function:: dictRemove(dict, val) + + Remove a key and its associated values from a dictionary. + + This function takes a dictionary and a key (val) as input. It iterates + through the list of values associated with the given key and removes the + key from each of those values' lists. Finally, it removes the key itself + from the dictionary. + + :param dict: A dictionary where the key is associated with a list of values. + :type dict: dict + :param val: The key to be removed from the dictionary and from the lists of its + associated values. + + +.. py:function:: addLoop(parentloop, start, end) + + Add a loop to a parent loop structure. + + This function recursively checks if the specified start and end values + can be added as a new loop to the parent loop. If an existing loop + encompasses the new loop, it will call itself on that loop. If no such + loop exists, it appends the new loop defined by the start and end values + to the parent loop's list of loops. + + :param parentloop: A list representing the parent loop, where the + third element is a list of child loops. + :type parentloop: list + :param start: The starting value of the new loop to be added. + :type start: int + :param end: The ending value of the new loop to be added. + :type end: int + + :returns: + + This function modifies the parentloop in place and does not + return a value. + :rtype: None + + +.. py:function:: cutloops(csource, parentloop, loops) + + Cut loops from a source code segment. + + This function takes a source code segment and a parent loop defined by + its start and end indices, along with a list of nested loops. It creates + a copy of the source code segment and removes the specified nested loops + from it. The modified segment is then appended to the provided list of + loops. The function also recursively processes any nested loops found + within the parent loop. + + :param csource: The source code from which loops will be cut. + :type csource: str + :param parentloop: A tuple containing the start index, end index, and a list of nested + loops. + The list of nested loops should contain tuples with start and end + indices for each loop. + :type parentloop: tuple + :param loops: A list that will be populated with the modified source code segments + after + removing the specified loops. + :type loops: list + + :returns: + + This function modifies the `loops` list in place and does not return a + value. + :rtype: None + + +.. py:function:: getOperationSilhouete(operation) + + Gets the silhouette for the given operation. + + This function determines the silhouette of an operation using image + thresholding techniques. It handles different geometry sources, such as + objects or images, and applies specific methods based on the type of + geometry. If the geometry source is 'OBJECT' or 'COLLECTION', it checks + whether to process curves or not. The function also considers the number + of faces in mesh objects to decide on the appropriate method for + silhouette extraction. + + :param operation: An object containing the necessary data + :type operation: Operation + + :returns: The computed silhouette for the operation. + :rtype: Silhouette + + +.. py:function:: getObjectSilhouete(stype, objects=None, use_modifiers=False) + + Get the silhouette of objects based on the specified type. + + This function computes the silhouette of a given set of objects in + Blender based on the specified type. It can handle both curves and mesh + objects, converting curves to polygon format and calculating the + silhouette for mesh objects. The function also considers the use of + modifiers if specified. The silhouette is generated by processing the + geometry of the objects and returning a Shapely representation of the + silhouette. + + :param stype: The type of silhouette to generate ('CURVES' or 'OBJECTS'). + :type stype: str + :param objects: A list of Blender objects to process. Defaults to None. + :type objects: list? + :param use_modifiers: Whether to apply modifiers to the objects. Defaults to False. + :type use_modifiers: bool? + + :returns: The computed silhouette as a Shapely MultiPolygon. + :rtype: shapely.geometry.MultiPolygon + + +.. py:function:: getAmbient(o) + + Calculate and update the ambient geometry based on the provided object. + + This function computes the ambient shape for a given object based on its + properties, such as cutter restrictions and ambient behavior. It + determines the appropriate radius and creates the ambient geometry + either from the silhouette or as a polygon defined by the object's + minimum and maximum coordinates. If a limit curve is specified, it will + also intersect the ambient shape with the limit polygon. + + :param o: An object containing properties that define the ambient behavior, + cutter restrictions, and limit curve. + :type o: object + + :returns: The function updates the ambient property of the object in place. + :rtype: None + + +.. py:function:: getObjectOutline(radius, o, Offset) + + Get the outline of a geometric object based on specified parameters. + + This function generates an outline for a given geometric object by + applying a buffer operation to its polygons. The buffer radius can be + adjusted based on the `radius` parameter, and the operation can be + offset based on the `Offset` flag. The function also considers whether + the polygons should be merged or not, depending on the properties of the + object `o`. + + :param radius: The radius for the buffer operation. + :type radius: float + :param o: An object containing properties that influence the outline generation. + :type o: object + :param Offset: A flag indicating whether to apply a positive or negative offset. + :type Offset: bool + + :returns: The resulting outline of the geometric object as a MultiPolygon. + :rtype: MultiPolygon + + +.. py:function:: addOrientationObject(o) + + Set up orientation for a milling object. + + This function creates an orientation object in the Blender scene for + 4-axis and 5-axis milling operations. It checks if an orientation object + with the specified name already exists, and if not, it adds a new empty + object of type 'ARROWS'. The function then configures the rotation locks + and initial rotation angles based on the specified machine axes and + rotary axis. + + :param o: An object containing properties such as name, + :type o: object + + +.. py:function:: removeOrientationObject(o) + + Remove an orientation object from the current Blender scene. + + This function constructs the name of the orientation object based on the + name of the provided object and attempts to find and delete it from the + Blender scene. If the orientation object exists, it will be removed + using the `delob` function. + + :param o: The object whose orientation object is to be removed. + :type o: Object + + +.. py:function:: addTranspMat(ob, mname, color, alpha) + + Add a transparent material to a given object. + + This function checks if a material with the specified name already + exists in the Blender data. If it does, it retrieves that material; if + not, it creates a new material with the given name and enables the use + of nodes. The function then assigns the material to the specified + object, ensuring that it is applied correctly whether the object already + has materials or not. + + :param ob: The Blender object to which the material will be assigned. + :type ob: bpy.types.Object + :param mname: The name of the material to be added or retrieved. + :type mname: str + :param color: The RGBA color value for the material (not used in this function). + :type color: tuple + :param alpha: The transparency value for the material (not used in this function). + :type alpha: float + + +.. py:function:: addMachineAreaObject() + + Add a machine area object to the current Blender scene. + + This function checks if a machine object named 'CAM_machine' already + exists in the current scene. If it does not exist, it creates a new cube + mesh object, applies transformations, and modifies its geometry to + represent a machine area. The function ensures that the scene's unit + settings are set to metric before creating the object and restores the + original unit settings afterward. It also configures the display + properties of the object for better visibility in the scene. The + function operates within Blender's context and utilizes various Blender + operations to create and modify the mesh. It also handles the selection + state of the active object. + + +.. py:function:: addMaterialAreaObject() + + Add a material area object to the current Blender scene. + + This function checks if a material area object named 'CAM_material' + already exists in the current scene. If it does, it retrieves that + object; if not, it creates a new cube mesh object to serve as the + material area. The dimensions and location of the object are set based + on the current camera operation's bounds. The function also applies + transformations to ensure the object's location and dimensions are + correctly set. The created or retrieved object is configured to be non- + renderable and non-selectable in the viewport, while still being + selectable for operations. This is useful for visualizing the working + area of the camera without affecting the render output. Raises: + None + + +.. py:function:: getContainer() + + Get or create a container object for camera objects. + + This function checks if a container object named 'CAM_OBJECTS' exists in + the current Blender scene. If it does not exist, the function creates a + new empty object of type 'PLAIN_AXES', names it 'CAM_OBJECTS', and sets + its location to the origin (0, 0, 0). The newly created container is + also hidden. If the container already exists, it simply retrieves and + returns that object. + + :returns: + + The container object for camera objects, either newly created or + existing. + :rtype: bpy.types.Object + + +.. py:class:: Point(x, y, z) + +.. py:function:: unique(L) + + Return a list of unhashable elements in L, but without duplicates. + + This function processes a list of lists, specifically designed to handle + unhashable elements. It sorts the input list and removes duplicates by + comparing the elements based on their coordinates. The function counts + the number of duplicate vertices and the number of collinear points + along the Z-axis. + + :param L: A list of lists, where each inner list represents a point + :type L: list + + :returns: + + A tuple containing two integers: + - The first integer represents the count of duplicate vertices. + - The second integer represents the count of Z-collinear points. + :rtype: tuple + + +.. py:function:: checkEqual(lst) + +.. py:function:: prepareIndexed(o) + + Prepare and index objects in the given collection. + + This function stores the world matrices and parent relationships of the + objects in the provided collection. It then clears the parent + relationships while maintaining their transformations, sets the + orientation of the objects based on a specified orientation object, and + finally re-establishes the parent-child relationships with the + orientation object. The function also resets the location and rotation + of the orientation object to the origin. + + :param o: A collection of objects to be prepared and indexed. + :type o: ObjectCollection + + +.. py:function:: cleanupIndexed(operation) + + Clean up indexed operations by updating object orientations and paths. + + This function takes an operation object and updates the orientation of a + specified object in the scene based on the provided orientation matrix. + It also sets the location and rotation of a camera path object to match + the updated orientation. Additionally, it reassigns parent-child + relationships for the objects involved in the operation and updates + their world matrices. + + :param operation: An object containing the necessary data + :type operation: OperationType + + +.. py:function:: rotTo2axes(e, axescombination) + + Converts an Orientation Object Rotation to Rotation Defined by 2 + Rotational Axes on the Machine. + + This function takes an orientation object and a specified axes + combination, and computes the angles of rotation around two axes based + on the provided orientation. It supports different axes combinations for + indexed machining. The function utilizes vector mathematics to determine + the angles of rotation and returns them as a tuple. + + :param e: The orientation object representing the rotation. + :type e: OrientationObject + :param axescombination: A string indicating the axes combination ('CA' or 'CB'). + :type axescombination: str + + :returns: A tuple containing two angles (float) representing the rotation + around the specified axes. + :rtype: tuple + + +.. py:function:: reload_paths(o) + + Reload the camera path data from a pickle file. + + This function retrieves the camera path data associated with the given + object `o`. It constructs a new mesh from the path vertices and updates + the object's properties with the loaded data. If a previous path mesh + exists, it is removed to avoid memory leaks. The function also handles + the creation of a new mesh object if one does not already exist in the + current scene. + + :param o: The object for which the camera path is being + :type o: Object + + +.. py:data:: USE_PROFILER + :value: False + + +.. py:data:: was_hidden_dict + +.. py:data:: _IS_LOADING_DEFAULTS + :value: False + + +.. py:function:: updateMachine(self, context) + + Update the machine with the given context. + + This function is responsible for updating the machine state based on the + provided context. It prints a message indicating that the update process + has started. If the global variable _IS_LOADING_DEFAULTS is not set to + True, it proceeds to add a machine area object. + + :param context: The context in which the machine update is being performed. + + +.. py:function:: updateMaterial(self, context) + + Update the material in the given context. + + This method is responsible for updating the material based on the + provided context. It performs necessary operations to ensure that the + material is updated correctly. Currently, it prints a message indicating + the update process and calls the `addMaterialAreaObject` function to + handle additional material area object updates. + + :param context: The context in which the material update is performed. + + +.. py:function:: updateOperation(self, context) + + Update the visibility and selection state of camera operations in the + scene. + + This method manages the visibility of objects associated with camera + operations based on the current active operation. If the + 'hide_all_others' flag is set to true, it hides all other objects except + for the currently active one. If the flag is false, it restores the + visibility of previously hidden objects. The method also attempts to + highlight the currently active object in the 3D view and make it the + active object in the scene. + + :param context: The context containing the current scene and + :type context: bpy.types.Context + + +.. py:function:: isValid(o, context) + + Check the validity of a geometry source. + + This function verifies if the provided geometry source is valid based on + its type. It checks for three types of geometry sources: 'OBJECT', + 'COLLECTION', and 'IMAGE'. For 'OBJECT', it ensures that the object name + ends with '_cut_bridges' or exists in the Blender data objects. For + 'COLLECTION', it checks if the collection name exists and contains + objects. For 'IMAGE', it verifies if the source image name exists in the + Blender data images. + + :param o: An object containing geometry source information, including + attributes like `geometry_source`, `object_name`, `collection_name`, + and `source_image_name`. + :type o: object + :param context: The context in which the validation is performed (not used in this + function). + + :returns: True if the geometry source is valid, False otherwise. + :rtype: bool + + +.. py:function:: operationValid(self, context) + + Validate the current camera operation in the given context. + + This method checks if the active camera operation is valid based on the + current scene context. It updates the operation's validity status and + provides warnings if the source object is invalid. Additionally, it + configures specific settings related to image geometry sources. + + :param context: The context containing the scene and camera operations. + :type context: Context + + +.. py:function:: isChainValid(chain, context) + + Check the validity of a chain of operations within a given context. + + This function verifies if all operations in the provided chain are valid + according to the current scene context. It first checks if the chain + contains any operations. If it does, it iterates through each operation + in the chain and checks if it exists in the scene's camera operations. + If an operation is not found or is deemed invalid, the function returns + a tuple indicating the failure and provides an appropriate error + message. If all operations are valid, it returns a success indication. + + :param chain: The chain of operations to validate. + :type chain: Chain + :param context: The context containing the scene and camera operations. + :type context: Context + + :returns: + + A tuple containing a boolean indicating validity and an error message + (if any). The first element is True if valid, otherwise False. The + second element is an error message string. + :rtype: tuple + + +.. py:function:: updateOperationValid(self, context) + +.. py:function:: updateChipload(self, context) + + Update the chipload based on feedrate, spindle RPM, and cutter + parameters. + + This function calculates the chipload using the formula: chipload = + feedrate / (spindle_rpm * cutter_flutes). It also attempts to account + for chip thinning when cutting at less than 50% cutter engagement with + cylindrical end mills by combining two formulas. The first formula + provides the nominal chipload based on standard recommendations, while + the second formula adjusts for the cutter diameter and distance between + paths. The current implementation may not yield consistent results, and + there are concerns regarding the correctness of the units used in the + calculations. Further review and refinement of this function may be + necessary to improve accuracy and reliability. + + :param context: The context in which the update is performed (not used in this + implementation). + + :returns: This function does not return a value; it updates the chipload in place. + :rtype: None + + +.. py:function:: updateOffsetImage(self, context) + + Refresh the Offset Image Tag for re-rendering. + + This method updates the chip load and marks the offset image tag for re- + rendering. It sets the `changed` attribute to True and indicates that + the offset image tag needs to be updated. + + :param context: The context in which the update is performed. + + +.. py:function:: updateZbufferImage(self, context) + + Update the Z-buffer and offset image tags for recalculation. + + This method modifies the internal state to indicate that the Z-buffer + image and offset image tags need to be updated during the calculation + process. It sets the `changed` attribute to True and marks the relevant + tags for updating. Additionally, it calls the `getOperationSources` + function to ensure that the necessary operation sources are retrieved. + + :param context: The context in which the update is being performed. + + +.. py:function:: updateStrategy(o, context) + + Update the strategy of the given object. + + This function modifies the state of the object `o` by setting its + `changed` attribute to True and printing a message indicating that the + strategy is being updated. Depending on the value of `machine_axes` and + `strategy4axis`, it either adds or removes an orientation object + associated with `o`. Finally, it calls the `updateExact` function to + perform further updates based on the provided context. + + :param o: The object whose strategy is to be updated. + :type o: object + :param context: The context in which the update is performed. + :type context: object + + +.. py:function:: updateCutout(o, context) + +.. py:function:: updateExact(o, context) + + Update the state of an object for exact operations. + + This function modifies the properties of the given object `o` to + indicate that an update is required. It sets various flags related to + the object's state and checks the optimization settings. If the + optimization is set to use exact mode, it further checks the strategy + and inverse properties to determine if exact mode can be used. If not, + it disables the use of OpenCamLib. + + :param o: The object to be updated, which contains properties related + :type o: object + :param context: The context in which the update is being performed. + :type context: object + + :returns: This function does not return a value. + :rtype: None + + +.. py:function:: updateOpencamlib(o, context) + + Update the OpenCAMLib settings for a given operation. + + This function modifies the properties of the provided operation object + based on its current strategy and optimization settings. If the + operation's strategy is either 'POCKET' or 'MEDIAL_AXIS', and if + OpenCAMLib is being used for optimization, it disables the use of both + exact optimization and OpenCAMLib, indicating that the current operation + cannot utilize OpenCAMLib. + + :param o: The operation object containing optimization and strategy settings. + :type o: object + :param context: The context in which the operation is being updated. + :type context: object + + :returns: This function does not return any value. + :rtype: None + + +.. py:function:: updateBridges(o, context) + + Update the status of bridges. + + This function marks the bridge object as changed, indicating that an + update has occurred. It prints a message to the console for logging + purposes. The function takes in an object and a context, but the context + is not utilized within the function. + + :param o: The bridge object that needs to be updated. + :type o: object + :param context: Additional context for the update, not used in this function. + :type context: object + + +.. py:function:: updateRotation(o, context) + + Update the rotation of a specified object in Blender. + + This function modifies the rotation of a Blender object based on the + properties of the provided object 'o'. It checks which rotations are + enabled and applies the corresponding rotation values to the active + object in the scene. The rotation can be aligned either along the X or Y + axis, depending on the configuration of 'o'. + + :param o: An object containing rotation settings and flags. + :type o: object + :param context: The context in which the operation is performed. + :type context: object + + +.. py:function:: updateRest(o, context) + + Update the state of the object. + + This function modifies the given object by setting its 'changed' + attribute to True. It also prints a message indicating that the update + operation has been performed. + + :param o: The object to be updated. + :type o: object + :param context: The context in which the update is being performed. + :type context: object + + +.. py:function:: getStrategyList(scene, context) + + Get a list of available strategies for operations. + + This function retrieves a predefined list of operation strategies that + can be used in the context of a 3D scene. Each strategy is represented + as a tuple containing an identifier, a user-friendly name, and a + description of the operation. The list includes various operations such + as cutouts, pockets, drilling, and more. If experimental features are + enabled in the preferences, additional experimental strategies may be + included in the returned list. + + :param scene: The current scene context. + :param context: The current context in which the operation is being performed. + + :returns: + + A list of tuples, each containing the strategy identifier, + name, and description. + :rtype: list + + +.. py:function:: update_material(self, context) + +.. py:function:: update_operation(self, context) + + Update the camera operation based on the current context. + + This function retrieves the active camera operation from the Blender + context and updates it using the `updateRest` function. It accesses the + active operation from the scene's camera operations and passes the + current context to the updating function. + + :param context: The context in which the operation is being updated. + + +.. py:function:: update_exact_mode(self, context) + + Update the exact mode of the active camera operation. + + This function retrieves the currently active camera operation from the + Blender context and updates its exact mode using the `updateExact` + function. It accesses the active operation through the `cam_operations` + list in the current scene and passes the active operation along with the + current context to the `updateExact` function. + + :param context: The context in which the update is performed. + + +.. py:function:: update_opencamlib(self, context) + + Update the OpenCamLib with the current active operation. + + This function retrieves the currently active camera operation from the + Blender context and updates the OpenCamLib accordingly. It accesses the + active operation from the scene's camera operations and passes it along + with the current context to the update function. + + :param context: The context in which the operation is being performed, typically + provided by + Blender's internal API. + + +.. py:function:: update_zbuffer_image(self, context) + + Update the Z-buffer image based on the active camera operation. + + This function retrieves the currently active camera operation from the + Blender context and updates the Z-buffer image accordingly. It accesses + the scene's camera operations and invokes the `updateZbufferImage` + function with the active operation and context. + + :param context: The current Blender context. + :type context: bpy.context + + +.. py:function:: check_operations_on_load(context) + + Checks for any broken computations on load and resets them. + + This function verifies the presence of necessary Blender add-ons and + installs any that are missing. It also resets any ongoing computations + in camera operations and sets the interface level to the previously used + level when loading a new file. If the add-on has been updated, it copies + the necessary presets from the source to the target directory. + Additionally, it checks for updates to the camera plugin and updates + operation presets if required. + + :param context: The context in which the function is executed, typically containing + information about + the current Blender environment. + + +.. py:function:: Add_Pocket(maxdepth, sname, new_cutter_diameter) + + Add a pocket operation for the medial axis and profile cut. + + This function first deselects all objects in the scene and then checks + for any existing medial pocket objects, deleting them if found. It + verifies whether a medial pocket operation already exists in the camera + operations. If it does not exist, it creates a new pocket operation with + the specified parameters. The function also modifies the selected + object's silhouette offset based on the new cutter diameter. + + :param maxdepth: The maximum depth of the pocket to be created. + :type maxdepth: float + :param sname: The name of the object to which the pocket will be added. + :type sname: str + :param new_cutter_diameter: The diameter of the new cutter to be used. + :type new_cutter_diameter: float + + diff --git a/_sources/autoapi/cam/version/index.rst b/_sources/autoapi/cam/version/index.rst new file mode 100644 index 000000000..4271ec5d1 --- /dev/null +++ b/_sources/autoapi/cam/version/index.rst @@ -0,0 +1,21 @@ +cam.version +=========== + +.. py:module:: cam.version + + +Attributes +---------- + +.. autoapisummary:: + + cam.version.__version__ + + +Module Contents +--------------- + +.. py:data:: __version__ + :value: (1, 0, 50) + + diff --git a/_sources/autoapi/cam/voronoi/index.rst b/_sources/autoapi/cam/voronoi/index.rst new file mode 100644 index 000000000..d94d22c2b --- /dev/null +++ b/_sources/autoapi/cam/voronoi/index.rst @@ -0,0 +1,1402 @@ +cam.voronoi +=========== + +.. py:module:: cam.voronoi + +.. autoapi-nested-parse:: + + CNC CAM 'voronoi.py' + + Voronoi diagram calculator/ Delaunay triangulator + + - Voronoi Diagram Sweepline algorithm and C code by Steven Fortune, 1987, http://ect.bell-labs.com/who/sjf/ + - Python translation to file voronoi.py by Bill Simons, 2005, http://www.oxfish.com/ + - Additional changes for QGIS by Carson Farmer added November 2010 + - 2012 Ported to Python 3 and additional clip functions by domlysz at gmail.com + + Calculate Delaunay triangulation or the Voronoi polygons for a set of + 2D input points. + + Derived from code bearing the following notice: + + The author of this software is Steven Fortune. Copyright (c) 1994 by AT&T + Bell Laboratories. + Permission to use, copy, modify, and distribute this software for any + purpose without fee is hereby granted, provided that this entire notice + is included in all copies of any software which is or includes a copy + or modification of this software and in all copies of the supporting + documentation for such software. + THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED + WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY + REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY + OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. + + Comments were incorporated from Shane O'Sullivan's translation of the + original code into C++ (http://mapviewer.skynet.ie/voronoi.html) + + Steve Fortune's homepage: http://netlib.bell-labs.com/cm/cs/who/sjf/index.html + + + For programmatic use two functions are available: + + computeVoronoiDiagram(points, xBuff, yBuff, polygonsOutput=False, formatOutput=False) : + Takes : + - a list of point objects (which must have x and y fields). + - x and y buffer values which are the expansion percentages of the bounding box rectangle including all input points. + Returns : + - With default options : + A list of 2-tuples, representing the two points of each Voronoi diagram edge. + Each point contains 2-tuples which are the x,y coordinates of point. + if formatOutput is True, returns : + - a list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices. + - and a list of 2-tuples (v1, v2) representing edges of the Voronoi diagram. + v1 and v2 are the indices of the vertices at the end of the edge. + - If polygonsOutput option is True, returns : + A dictionary of polygons, keys are the indices of the input points, + values contains n-tuples representing the n points of each Voronoi diagram polygon. + Each point contains 2-tuples which are the x,y coordinates of point. + if formatOutput is True, returns : + - A list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices. + - and a dictionary of input points indices. Values contains n-tuples representing the n points of each Voronoi diagram polygon. + Each tuple contains the vertex indices of the polygon vertices. + + computeDelaunayTriangulation(points): + Takes a list of point objects (which must have x and y fields). + Returns a list of 3-tuples: the indices of the points that form a Delaunay triangle. + + + +Attributes +---------- + +.. autoapisummary:: + + cam.voronoi.TOLERANCE + cam.voronoi.BIG_FLOAT + cam.voronoi.PY3 + + +Classes +------- + +.. autoapisummary:: + + cam.voronoi.Context + cam.voronoi.Site + cam.voronoi.Edge + cam.voronoi.Halfedge + cam.voronoi.EdgeList + cam.voronoi.PriorityQueue + cam.voronoi.SiteList + + +Functions +--------- + +.. autoapisummary:: + + cam.voronoi.voronoi + cam.voronoi.isEqual + cam.voronoi.computeVoronoiDiagram + cam.voronoi.formatEdgesOutput + cam.voronoi.formatPolygonsOutput + cam.voronoi.computeDelaunayTriangulation + + +Module Contents +--------------- + +.. py:data:: TOLERANCE + :value: 1e-09 + + +.. py:data:: BIG_FLOAT + :value: 1e+38 + + +.. py:data:: PY3 + :value: True + + +.. py:class:: Context + + Bases: :py:obj:`object` + + + .. py:attribute:: doPrint + :value: 0 + + + + .. py:attribute:: debug + :value: 0 + + + + .. py:attribute:: extent + :value: () + + + + .. py:attribute:: triangulate + :value: False + + + + .. py:attribute:: vertices + :value: [] + + + + .. py:attribute:: lines + :value: [] + + + + .. py:attribute:: edges + :value: [] + + + + .. py:attribute:: triangles + :value: [] + + + + .. py:attribute:: polygons + + + .. py:method:: getClipEdges() + + Get the clipped edges based on the current extent. + + This function iterates through the edges of a geometric shape and + determines which edges are within the specified extent. It handles both + finite and infinite lines, clipping them as necessary to fit within the + defined boundaries. For finite lines, it checks if both endpoints are + within the extent, and if not, it calculates the intersection points + using the line equations. For infinite lines, it checks if at least one + endpoint is within the extent and clips accordingly. + + :returns: + + A list of tuples, where each tuple contains two points representing the + clipped edges. + :rtype: list + + + + .. py:method:: getClipPolygons(closePoly) + + Get clipped polygons based on the provided edges. + + This function processes a set of polygons defined by their edges and + vertices, clipping them according to the specified extent. It checks + whether each edge is finite or infinite and determines if the endpoints + of each edge are within the defined extent. If they are not, the + function calculates the intersection points with the extent boundaries. + The resulting clipped edges are then used to create polygons, which are + returned as a dictionary. The user can specify whether to close the + polygons or leave them open. + + :param closePoly: A flag indicating whether to close the polygons. + :type closePoly: bool + + :returns: + + A dictionary where keys are polygon indices and values are lists of + points defining the clipped polygons. + :rtype: dict + + + + .. py:method:: clipLine(x1, y1, equation, leftDir) + + Clip a line segment defined by its endpoints against a bounding box. + + This function calculates the intersection points of a line defined by + the given equation with the bounding box defined by the extent of the + object. Depending on the direction specified (left or right), it will + return the appropriate intersection point that lies within the bounds. + + :param x1: The x-coordinate of the first endpoint of the line. + :type x1: float + :param y1: The y-coordinate of the first endpoint of the line. + :type y1: float + :param equation: A tuple containing the coefficients (a, b, c) of + the line equation in the form ax + by + c = 0. + :type equation: tuple + :param leftDir: A boolean indicating the direction to clip the line. + If True, clip towards the left; otherwise, clip + towards the right. + :type leftDir: bool + + :returns: The coordinates of the clipped point as (x, y). + :rtype: tuple + + + + .. py:method:: inExtent(x, y) + + Check if a point is within the defined extent. + + This function determines whether the given coordinates (x, y) fall + within the boundaries defined by the extent of the object. The extent is + defined by its minimum and maximum x and y values (xmin, xmax, ymin, + ymax). The function returns True if the point is within these bounds, + and False otherwise. + + :param x: The x-coordinate of the point to check. + :type x: float + :param y: The y-coordinate of the point to check. + :type y: float + + :returns: True if the point (x, y) is within the extent, False otherwise. + :rtype: bool + + + + .. py:method:: orderPts(edges) + + Order points to form a polygon. + + This function takes a list of edges, where each edge is represented as a + pair of points, and orders the points to create a polygon. It identifies + the starting and ending points of the polygon and ensures that the + points are connected in the correct order. If all points are duplicates, + it recognizes that the polygon is complete and handles it accordingly. + + :param edges: A list of edges, where each edge is a tuple or list containing two + points. + :type edges: list + + :returns: + + A tuple containing: + - list: The ordered list of polygon points. + - bool: A flag indicating whether the polygon is complete. + :rtype: tuple + + + + .. py:method:: setClipBuffer(xpourcent, ypourcent) + + Set the clipping buffer based on percentage adjustments. + + This function modifies the clipping extent of an object by adjusting its + boundaries according to the specified percentage values for both the x + and y axes. It calculates the new minimum and maximum values for the x + and y coordinates by applying the given percentages to the current + extent. + + :param xpourcent: The percentage adjustment for the x-axis. + :type xpourcent: float + :param ypourcent: The percentage adjustment for the y-axis. + :type ypourcent: float + + :returns: This function does not return a value; it modifies the + object's extent in place. + :rtype: None + + + + .. py:method:: outSite(s) + + Handle output for a site object. + + This function processes the output based on the current settings of the + instance. If debugging is enabled, it prints the site number and its + coordinates. If triangulation is enabled, no action is taken. If + printing is enabled, it prints the coordinates of the site. + + :param s: An object representing a site, which should have + attributes 'sitenum', 'x', and 'y'. + :type s: object + + :returns: This function does not return a value. + :rtype: None + + + + .. py:method:: outVertex(s) + + Add a vertex to the list of vertices. + + This function appends the coordinates of a given vertex to the internal + list of vertices. Depending on the state of the debug, triangulate, and + doPrint flags, it may also print debug information or vertex coordinates + to the console. + + :param s: An object containing the attributes `x`, `y`, and + `sitenum` which represent the coordinates and + identifier of the vertex. + :type s: object + + :returns: This function does not return a value. + :rtype: None + + + + .. py:method:: outTriple(s1, s2, s3) + + Add a triangle defined by three site numbers to the list of triangles. + + This function takes three site objects, extracts their site numbers, and + appends a tuple of these site numbers to the `triangles` list. If + debugging is enabled, it prints the site numbers to the console. + Additionally, if triangulation is enabled and printing is allowed, it + prints the site numbers in a formatted manner. + + :param s1: The first site object. + :type s1: Site + :param s2: The second site object. + :type s2: Site + :param s3: The third site object. + :type s3: Site + + :returns: This function does not return a value. + :rtype: None + + + + .. py:method:: outBisector(edge) + + Process and log the outbisector of a given edge. + + This function appends the parameters of the edge (a, b, c) to the lines + list and optionally prints debugging information or the parameters based + on the state of the debug and doPrint flags. The function is designed to + handle geometric edges and their properties in a computational geometry + context. + + :param edge: An object representing an edge with attributes + a, b, c, edgenum, and reg. + :type edge: Edge + + :returns: This function does not return a value. + :rtype: None + + + + .. py:method:: outEdge(edge) + + Process an edge and update the associated polygons and edges. + + This function takes an edge as input and retrieves the site numbers + associated with its left and right endpoints. It then updates the + polygons dictionary to include the edge information for the regions + associated with the edge. If the regions are not already present in the + polygons dictionary, they are initialized. The function also appends the + edge information to the edges list. If triangulation is not enabled, it + prints the edge number and its associated site numbers. + + :param edge: An instance of the Edge class containing information + :type edge: Edge + + :returns: This function does not return a value. + :rtype: None + + + +.. py:function:: voronoi(siteList, context) + + Generate a Voronoi diagram from a list of sites. + + This function computes the Voronoi diagram for a given list of sites. It + utilizes a sweep line algorithm to process site events and circle + events, maintaining a priority queue and edge list to manage the + geometric relationships between the sites. The function outputs the + resulting edges, vertices, and bisectors to the provided context. + + :param siteList: A list of sites represented by their coordinates. + :type siteList: SiteList + :param context: An object that handles the output of the Voronoi diagram + elements, including sites, edges, and vertices. + :type context: Context + + :returns: + + This function does not return a value; it outputs results directly + to the context provided. + :rtype: None + + +.. py:function:: isEqual(a, b, relativeError=TOLERANCE) + + Check if two values are nearly equal within a specified relative error. + + This function determines if the absolute difference between two values + is within a specified relative error of the larger of the two values. It + is useful for comparing floating-point numbers where precision issues + may arise. + + :param a: The first value to compare. + :type a: float + :param b: The second value to compare. + :type b: float + :param relativeError: The allowed relative error for the comparison. + :type relativeError: float + + :returns: True if the values are considered nearly equal, False otherwise. + :rtype: bool + + +.. py:class:: Site(x=0.0, y=0.0, sitenum=0) + + Bases: :py:obj:`object` + + + .. py:attribute:: x + + + .. py:attribute:: y + + + .. py:attribute:: sitenum + + + .. py:method:: dump() + + Dump the site information. + + This function prints the site number along with its x and y coordinates + in a formatted string. It is primarily used for debugging or logging + purposes to provide a quick overview of the site's attributes. + + :returns: This function does not return any value. + :rtype: None + + + + .. py:method:: __lt__(other) + + Compare two objects based on their coordinates. + + This method implements the less-than comparison for objects that have x + and y attributes. It first compares the y coordinates; if they are + equal, it then compares the x coordinates. The method returns True if + the current object is considered less than the other object based on + these comparisons. + + :param other: The object to compare against, which must have + x and y attributes. + :type other: object + + :returns: + + True if the current object is less than the other object, + otherwise False. + :rtype: bool + + + + .. py:method:: __eq__(other) + + Determine equality between two objects. + + This method checks if the current object is equal to another object by + comparing their 'x' and 'y' attributes. If both attributes are equal for + the two objects, it returns True; otherwise, it returns False. + + :param other: The object to compare with the current object. + :type other: object + + :returns: True if both objects are equal, False otherwise. + :rtype: bool + + + + .. py:method:: distance(other) + + Calculate the distance between two points in a 2D space. + + This function computes the Euclidean distance between the current point + (represented by the instance's coordinates) and another point provided + as an argument. It uses the Pythagorean theorem to calculate the + distance based on the differences in the x and y coordinates of the two + points. + + :param other: Another point in 2D space to calculate the distance from. + :type other: Point + + :returns: The Euclidean distance between the two points. + :rtype: float + + + +.. py:class:: Edge + + Bases: :py:obj:`object` + + + .. py:attribute:: LE + :value: 0 + + + + .. py:attribute:: RE + :value: 1 + + + + .. py:attribute:: EDGE_NUM + :value: 0 + + + + .. py:attribute:: DELETED + + + .. py:attribute:: a + :value: 0.0 + + + + .. py:attribute:: b + :value: 0.0 + + + + .. py:attribute:: c + :value: 0.0 + + + + .. py:attribute:: ep + :value: [None, None] + + + + .. py:attribute:: reg + :value: [None, None] + + + + .. py:attribute:: edgenum + :value: 0 + + + + .. py:method:: dump() + + Dump the current state of the object. + + This function prints the values of the object's attributes, including + the edge number, and the values of a, b, c, as well as the ep and reg + attributes. It is useful for debugging purposes to understand the + current state of the object. + + .. attribute:: edgenum + + The edge number of the object. + + :type: int + + .. attribute:: a + + The value of attribute a. + + :type: float + + .. attribute:: b + + The value of attribute b. + + :type: float + + .. attribute:: c + + The value of attribute c. + + :type: float + + .. attribute:: ep + + The value of the ep attribute. + + .. attribute:: reg + + The value of the reg attribute. + + + + .. py:method:: setEndpoint(lrFlag, site) + + Set the endpoint for a given flag. + + This function assigns a site to the specified endpoint flag. It checks + if the corresponding endpoint for the opposite flag is not set to None. + If it is None, the function returns False; otherwise, it returns True. + + :param lrFlag: The flag indicating which endpoint to set. + :type lrFlag: int + :param site: The site to be assigned to the specified endpoint. + :type site: str + + :returns: True if the opposite endpoint is set, False otherwise. + :rtype: bool + + + + .. py:method:: bisect(s1, s2) + :staticmethod: + + + Bisect two sites to create a new edge. + + This function takes two site objects and computes the bisector edge + between them. It calculates the slope and intercept of the line that + bisects the two sites, storing the necessary parameters in a new edge + object. The edge is initialized with no endpoints, as it extends to + infinity. The function determines whether to fix x or y based on the + relative distances between the sites. + + :param s1: The first site to be bisected. + :type s1: Site + :param s2: The second site to be bisected. + :type s2: Site + + :returns: A new edge object representing the bisector between the two sites. + :rtype: Edge + + + +.. py:class:: Halfedge(edge=None, pm=Edge.LE) + + Bases: :py:obj:`object` + + + .. py:attribute:: left + :value: None + + + + .. py:attribute:: right + :value: None + + + + .. py:attribute:: qnext + :value: None + + + + .. py:attribute:: edge + + + .. py:attribute:: pm + + + .. py:attribute:: vertex + :value: None + + + + .. py:attribute:: ystar + + + .. py:method:: dump() + + Dump the internal state of the object. + + This function prints the current values of the object's attributes, + including left, right, edge, pm, vertex, and ystar. If the vertex + attribute is present and has a dump method, it will call that method to + print the vertex's internal state. Otherwise, it will print "None" for + the vertex. + + .. attribute:: left + + The left halfedge associated with this object. + + .. attribute:: right + + The right halfedge associated with this object. + + .. attribute:: edge + + The edge associated with this object. + + .. attribute:: pm + + The PM associated with this object. + + .. attribute:: vertex + + The vertex associated with this object, which may have its + own dump method. + + .. attribute:: ystar + + The ystar value associated with this object. + + + + .. py:method:: __lt__(other) + + Compare two objects based on their ystar and vertex attributes. + + This method implements the less-than comparison for objects. It first + compares the `ystar` attributes of the two objects. If they are equal, + it then compares the x-coordinate of their `vertex` attributes to + determine the order. + + :param other: The object to compare against. + :type other: YourClass + + :returns: + + True if the current object is less than the other object, False + otherwise. + :rtype: bool + + + + .. py:method:: __eq__(other) + + Check equality of two objects. + + This method compares the current object with another object to determine + if they are equal. It checks if the 'ystar' attribute and the 'x' + coordinate of the 'vertex' attribute are the same for both objects. + + :param other: The object to compare with the current instance. + :type other: object + + :returns: True if both objects are considered equal, False otherwise. + :rtype: bool + + + + .. py:method:: leftreg(default) + + Retrieve the left registration value based on the edge state. + + This function checks the state of the edge attribute. If the edge is not + set, it returns the provided default value. If the edge is set and its + property indicates a left edge (Edge.LE), it returns the left + registration value. Otherwise, it returns the right registration value. + + :param default: The value to return if the edge is not set. + + :returns: The left registration value if applicable, otherwise the default value. + + + + .. py:method:: rightreg(default) + + Retrieve the appropriate registration value based on the edge state. + + This function checks if the current edge is set. If it is not set, it + returns the provided default value. If the edge is set and the current + state is Edge.LE, it returns the registration value associated with + Edge.RE. Otherwise, it returns the registration value associated with + Edge.LE. + + :param default: The value to return if there is no edge set. + + :returns: + + The registration value corresponding to the current edge state or the + default value if no edge is set. + + + + .. py:method:: isPointRightOf(pt) + + Determine if a point is to the right of a half-edge. + + This function checks whether the given point `pt` is located to the + right of the half-edge represented by the current object. It takes into + account the position of the top site of the edge and various geometric + properties to make this determination. The function uses the edge's + parameters to evaluate the relationship between the point and the half- + edge. + + :param pt: A point object with x and y coordinates. + :type pt: Point + + :returns: True if the point is to the right of the half-edge, False otherwise. + :rtype: bool + + + + .. py:method:: intersect(other) + + Create a new site where two edges intersect. + + This function calculates the intersection point of two edges, + represented by the current instance and another instance passed as an + argument. It first checks if either edge is None, and if they belong to + the same parent region. If the edges are parallel or do not intersect, + it returns None. If an intersection point is found, it creates and + returns a new Site object at the intersection coordinates. + + :param other: Another edge to intersect with the current edge. + :type other: Edge + + :returns: A Site object representing the intersection point + if an intersection occurs; otherwise, None. + :rtype: Site or None + + + +.. py:class:: EdgeList(xmin, xmax, nsites) + + Bases: :py:obj:`object` + + + .. py:attribute:: hashsize + + + .. py:attribute:: xmin + + + .. py:attribute:: deltax + + + .. py:attribute:: hash + + + .. py:attribute:: leftend + + + .. py:attribute:: rightend + + + .. py:attribute:: right + + + .. py:attribute:: left + + + .. py:method:: insert(left, he) + + Insert a node into a doubly linked list. + + This function takes a node and inserts it into the list immediately + after the specified left node. It updates the pointers of the + surrounding nodes to maintain the integrity of the doubly linked list. + + :param left: The node after which the new node will be inserted. + :type left: Node + :param he: The new node to be inserted into the list. + :type he: Node + + + + .. py:method:: delete(he) + + Delete a node from a doubly linked list. + + This function updates the pointers of the neighboring nodes to remove + the specified node from the list. It also marks the node as deleted by + setting its edge attribute to Edge.DELETED. + + :param he: The node to be deleted from the list. + :type he: Node + + + + .. py:method:: gethash(b) + + Retrieve an entry from the hash table, ignoring deleted nodes. + + This function checks if the provided index is within the valid range of + the hash table. If the index is valid, it retrieves the corresponding + entry. If the entry is marked as deleted, it updates the hash table to + remove the reference to the deleted entry and returns None. + + :param b: The index in the hash table to retrieve the entry from. + :type b: int + + :returns: The entry at the specified index, or None if the index is out of bounds + or if the entry is marked as deleted. + :rtype: object + + + + .. py:method:: leftbnd(pt) + + Find the left boundary half-edge for a given point. + + This function computes the appropriate half-edge that is to the left of + the specified point. It utilizes a hash table to quickly locate the + half-edge that is closest to the desired position based on the + x-coordinate of the point. If the initial bucket derived from the + point's x-coordinate does not contain a valid half-edge, the function + will search adjacent buckets until it finds one. Once a half-edge is + located, it will traverse through the linked list of half-edges to find + the correct one that lies to the left of the point. + + :param pt: A point object containing x and y coordinates. + :type pt: Point + + :returns: The half-edge that is to the left of the given point. + :rtype: HalfEdge + + + +.. py:class:: PriorityQueue(ymin, ymax, nsites) + + Bases: :py:obj:`object` + + + .. py:attribute:: ymin + + + .. py:attribute:: deltay + + + .. py:attribute:: hashsize + + + .. py:attribute:: count + :value: 0 + + + + .. py:attribute:: minidx + :value: 0 + + + + .. py:attribute:: hash + :value: [] + + + + .. py:method:: __len__() + + Return the length of the object. + + This method returns the count of items in the object, which is useful + for determining how many elements are present. It is typically used to + support the built-in `len()` function. + + :returns: The number of items in the object. + :rtype: int + + + + .. py:method:: isEmpty() + + Check if the object is empty. + + This method determines whether the object contains any elements by + checking the value of the count attribute. If the count is zero, the + object is considered empty; otherwise, it is not. + + :returns: True if the object is empty, False otherwise. + :rtype: bool + + + + .. py:method:: insert(he, site, offset) + + Insert a new element into the data structure. + + This function inserts a new element represented by `he` into the + appropriate position in the data structure based on its value. It + updates the `ystar` attribute of the element and links it to the next + element in the list. The function also manages the count of elements in + the structure. + + :param he: The element to be inserted, which contains a vertex and + a y-coordinate. + :type he: Element + :param site: The site object that provides the y-coordinate for the + insertion. + :type site: Site + :param offset: The offset to be added to the y-coordinate of the site. + :type offset: float + + :returns: This function does not return a value. + :rtype: None + + + + .. py:method:: delete(he) + + Delete a specified element from the data structure. + + This function removes the specified element (he) from the linked list + associated with the corresponding bucket in the hash table. It traverses + the linked list until it finds the element to delete, updates the + pointers to bypass the deleted element, and decrements the count of + elements in the structure. If the element is found and deleted, its + vertex is set to None to indicate that it is no longer valid. + + :param he: The element to be deleted from the data structure. + :type he: Element + + + + .. py:method:: getBucket(he) + + Get the appropriate bucket index for a given value. + + This function calculates the bucket index based on the provided value + and the object's parameters. It ensures that the bucket index is within + the valid range, adjusting it if necessary. The calculation is based on + the difference between a specified value and a minimum value, scaled by + a delta value and the size of the hash table. The function also updates + the minimum index if the calculated bucket is lower than the current + minimum index. + + :param he: An object that contains the attribute `ystar`, which is used + in the bucket calculation. + + :returns: The calculated bucket index, constrained within the valid range. + :rtype: int + + + + .. py:method:: getMinPt() + + Retrieve the minimum point from a hash table. + + This function iterates through the hash table starting from the current + minimum index and finds the next non-null entry. It then extracts the + coordinates (x, y) of the vertex associated with that entry and returns + it as a Site object. + + :returns: An object representing the minimum point with x and y coordinates. + :rtype: Site + + + + .. py:method:: popMinHalfedge() + + Remove and return the minimum half-edge from the data structure. + + This function retrieves the minimum half-edge from a hash table, updates + the necessary pointers to maintain the integrity of the data structure, + and decrements the count of half-edges. It effectively removes the + minimum half-edge while ensuring that the next half-edge in the sequence + is correctly linked. + + :returns: The minimum half-edge that was removed from the data structure. + :rtype: HalfEdge + + + +.. py:class:: SiteList(pointList) + + Bases: :py:obj:`object` + + + .. py:attribute:: __sites + :value: [] + + + + .. py:attribute:: __sitenum + :value: 0 + + + + .. py:attribute:: __xmin + + + .. py:attribute:: __ymin + + + .. py:attribute:: __xmax + + + .. py:attribute:: __ymax + + + .. py:attribute:: __extent + + + .. py:method:: setSiteNumber(site) + + Set the site number for a given site. + + This function assigns a unique site number to the provided site object. + It updates the site object's 'sitenum' attribute with the current value + of the instance's private '__sitenum' attribute and then increments the + '__sitenum' for the next site. + + :param site: An object representing a site that has a 'sitenum' attribute. + :type site: object + + :returns: This function does not return a value. + :rtype: None + + + + .. py:class:: Iterator(lst) + + Bases: :py:obj:`object` + + + .. py:attribute:: generator + + + .. py:method:: __iter__() + + Return the iterator object itself. + + This method is part of the iterator protocol. It allows an object to be + iterable by returning the iterator object itself when the `__iter__` + method is called. This is typically used in conjunction with the + `__next__` method to iterate over the elements of the object. + + :returns: The iterator object itself. + :rtype: self + + + + .. py:method:: next() + + Retrieve the next item from a generator. + + This function attempts to get the next value from the provided + generator. It handles both Python 2 and Python 3 syntax for retrieving + the next item. If the generator is exhausted, it returns None instead of + raising an exception. + + :param this: An object that contains a generator attribute. + :type this: object + + :returns: The next item from the generator, or None if the generator is exhausted. + :rtype: object + + + + + .. py:method:: iterator() + + Create an iterator for the sites. + + This function returns an iterator object that allows iteration over the + collection of sites stored in the instance. It utilizes the + SiteList.Iterator class to facilitate the iteration process. + + :returns: An iterator for the sites in the SiteList. + :rtype: Iterator + + + + .. py:method:: __iter__() + + Iterate over the sites in the SiteList. + + This method returns an iterator for the SiteList, allowing for traversal + of the contained sites. It utilizes the internal Iterator class to + manage the iteration process. + + :returns: An iterator for the sites in the SiteList. + :rtype: Iterator + + + + .. py:method:: __len__() + + Return the number of sites. + + This method returns the length of the internal list of sites. It is used + to determine how many sites are currently stored in the object. The + length is calculated using the built-in `len()` function on the + `__sites` attribute. + + :returns: The number of sites in the object. + :rtype: int + + + + .. py:method:: _getxmin() + + Retrieve the minimum x-coordinate value. + + This function accesses and returns the private attribute __xmin, which + holds the minimum x-coordinate value for the object. It is typically + used in contexts where the minimum x value is needed for calculations or + comparisons. + + :returns: The minimum x-coordinate value. + :rtype: float + + + + .. py:method:: _getymin() + + Retrieve the minimum y-coordinate value. + + This function returns the minimum y-coordinate value stored in the + instance variable `__ymin`. It is typically used in contexts where the + minimum y-value is needed for calculations or comparisons. + + :returns: The minimum y-coordinate value. + :rtype: float + + + + .. py:method:: _getxmax() + + Retrieve the maximum x value. + + This function returns the maximum x value stored in the instance. It is + a private method intended for internal use within the class and provides + access to the __xmax attribute. + + :returns: The maximum x value. + :rtype: float + + + + .. py:method:: _getymax() + + Retrieve the maximum y-coordinate value. + + This function accesses and returns the private attribute __ymax, which + represents the maximum y-coordinate value stored in the instance. + + :returns: The maximum y-coordinate value. + :rtype: float + + + + .. py:method:: _getextent() + + Retrieve the extent of the object. + + This function returns the current extent of the object, which is + typically a representation of its boundaries or limits. The extent is + stored as a private attribute and can be used for various purposes such + as rendering, collision detection, or spatial analysis. + + :returns: The extent of the object, which may be in a specific format depending + on the implementation (e.g., a tuple, list, or custom object). + + + + .. py:attribute:: xmin + + + .. py:attribute:: ymin + + + .. py:attribute:: xmax + + + .. py:attribute:: ymax + + + .. py:attribute:: extent + + +.. py:function:: computeVoronoiDiagram(points, xBuff=0, yBuff=0, polygonsOutput=False, formatOutput=False, closePoly=True) + + Compute the Voronoi diagram for a set of points. + + This function takes a list of point objects and computes the Voronoi + diagram, which partitions the plane into regions based on the distance + to the input points. The function allows for optional buffering of the + bounding box and can return various formats of the output, including + edges or polygons of the Voronoi diagram. + + :param points: A list of point objects, each having 'x' and 'y' attributes. + :type points: list + :param xBuff: The expansion percentage of the bounding box in the x-direction. + Defaults to 0. + :type xBuff: float? + :param yBuff: The expansion percentage of the bounding box in the y-direction. + Defaults to 0. + :type yBuff: float? + :param polygonsOutput: If True, returns polygons instead of edges. Defaults to False. + :type polygonsOutput: bool? + :param formatOutput: If True, formats the output to include vertex coordinates. Defaults to + False. + :type formatOutput: bool? + :param closePoly: If True, closes the polygons by repeating the first point at the end. + Defaults to True. + :type closePoly: bool? + + :returns: - list: A list of 2-tuples representing the edges of the Voronoi + diagram, + where each tuple contains the x and y coordinates of the points. + If `formatOutput` is True: + - tuple: A list of 2-tuples for vertex coordinates and a list of edges + indices. + If `polygonsOutput` is True: + - dict: A dictionary where keys are indices of input points and values + are n-tuples + representing the vertices of each Voronoi polygon. + If `formatOutput` is True: + - tuple: A list of 2-tuples for vertex coordinates and a dictionary of + polygon vertex indices. + :rtype: If `polygonsOutput` is False + + +.. py:function:: formatEdgesOutput(edges) + + Format edges output for a list of edges. + + This function takes a list of edges, where each edge is represented as a + tuple of points. It extracts unique points from the edges and creates a + mapping of these points to their corresponding indices. The function + then returns a list of unique points and a list of edges represented by + their indices. + + :param edges: A list of edges, where each edge is a tuple containing points. + :type edges: list + + :returns: + + A tuple containing: + - list: A list of unique points extracted from the edges. + - list: A list of edges represented by their corresponding indices. + :rtype: tuple + + +.. py:function:: formatPolygonsOutput(polygons) + + Format the output of polygons into a standardized structure. + + This function takes a dictionary of polygons, where each polygon is + represented as a list of points. It extracts unique points from all + polygons and creates an index mapping for these points. The output + consists of a list of unique points and a dictionary that maps each + polygon's original indices to their corresponding indices in the unique + points list. + + :param polygons: A dictionary where keys are polygon identifiers and values + are lists of points (tuples) representing the vertices of + the polygons. + :type polygons: dict + + :returns: + + A tuple containing: + - list: A list of unique points (tuples) extracted from the input + polygons. + - dict: A dictionary mapping each polygon's identifier to a list of + indices + corresponding to the unique points. + :rtype: tuple + + +.. py:function:: computeDelaunayTriangulation(points) + + Compute the Delaunay triangulation for a set of points. + + This function takes a list of point objects, each of which must have 'x' + and 'y' fields. It computes the Delaunay triangulation and returns a + list of 3-tuples, where each tuple contains the indices of the points + that form a Delaunay triangle. The triangulation is performed using the + Voronoi diagram method. + + :param points: A list of point objects with 'x' and 'y' attributes. + :type points: list + + :returns: + + A list of 3-tuples representing the indices of points that + form Delaunay triangles. + :rtype: list + + diff --git a/_sources/autoapi/index.rst b/_sources/autoapi/index.rst new file mode 100644 index 000000000..e44f98efa --- /dev/null +++ b/_sources/autoapi/index.rst @@ -0,0 +1,11 @@ +API Reference +============= + +This page contains auto-generated API reference documentation [#f1]_. + +.. toctree:: + :titlesonly: + + /autoapi/cam/index + +.. [#f1] Created with `sphinx-autoapi `_ \ No newline at end of file diff --git a/_sources/cam.rst b/_sources/cam.rst new file mode 100644 index 000000000..e6ceb7686 --- /dev/null +++ b/_sources/cam.rst @@ -0,0 +1,327 @@ +cam package +=========== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + cam.nc + cam.opencamlib + cam.ui_panels + +Submodules +---------- + +cam.async\_op module +-------------------- + +.. automodule:: cam.async_op + :members: + :undoc-members: + :show-inheritance: + +cam.autoupdate module +--------------------- + +.. automodule:: cam.autoupdate + :members: + :undoc-members: + :show-inheritance: + +cam.basrelief module +-------------------- + +.. automodule:: cam.basrelief + :members: + :undoc-members: + :show-inheritance: + +cam.bridges module +------------------ + +.. automodule:: cam.bridges + :members: + :undoc-members: + :show-inheritance: + +cam.cam\_chunk module +--------------------- + +.. automodule:: cam.cam_chunk + :members: + :undoc-members: + :show-inheritance: + +cam.cam\_operation module +------------------------- + +.. automodule:: cam.cam_operation + :members: + :undoc-members: + :show-inheritance: + +cam.chain module +---------------- + +.. automodule:: cam.chain + :members: + :undoc-members: + :show-inheritance: + +cam.collision module +-------------------- + +.. automodule:: cam.collision + :members: + :undoc-members: + :show-inheritance: + +cam.constants module +-------------------- + +.. automodule:: cam.constants + :members: + :undoc-members: + :show-inheritance: + +cam.curvecamcreate module +------------------------- + +.. automodule:: cam.curvecamcreate + :members: + :undoc-members: + :show-inheritance: + +cam.curvecamequation module +--------------------------- + +.. automodule:: cam.curvecamequation + :members: + :undoc-members: + :show-inheritance: + +cam.curvecamtools module +------------------------ + +.. automodule:: cam.curvecamtools + :members: + :undoc-members: + :show-inheritance: + +cam.engine module +----------------- + +.. automodule:: cam.engine + :members: + :undoc-members: + :show-inheritance: + +cam.exception module +-------------------- + +.. automodule:: cam.exception + :members: + :undoc-members: + :show-inheritance: + +cam.gcodeimportparser module +---------------------------- + +.. automodule:: cam.gcodeimportparser + :members: + :undoc-members: + :show-inheritance: + +cam.gcodepath module +-------------------- + +.. automodule:: cam.gcodepath + :members: + :undoc-members: + :show-inheritance: + +cam.image\_utils module +----------------------- + +.. automodule:: cam.image_utils + :members: + :undoc-members: + :show-inheritance: + +cam.involute\_gear module +------------------------- + +.. automodule:: cam.involute_gear + :members: + :undoc-members: + :show-inheritance: + +cam.joinery module +------------------ + +.. automodule:: cam.joinery + :members: + :undoc-members: + :show-inheritance: + +cam.machine\_settings module +---------------------------- + +.. automodule:: cam.machine_settings + :members: + :undoc-members: + :show-inheritance: + +cam.numba\_wrapper module +------------------------- + +.. automodule:: cam.numba_wrapper + :members: + :undoc-members: + :show-inheritance: + +cam.ops module +-------------- + +.. automodule:: cam.ops + :members: + :undoc-members: + :show-inheritance: + +cam.pack module +--------------- + +.. automodule:: cam.pack + :members: + :undoc-members: + :show-inheritance: + +cam.parametric module +--------------------- + +.. automodule:: cam.parametric + :members: + :undoc-members: + :show-inheritance: + +cam.pattern module +------------------ + +.. automodule:: cam.pattern + :members: + :undoc-members: + :show-inheritance: + +cam.polygon\_utils\_cam module +------------------------------ + +.. automodule:: cam.polygon_utils_cam + :members: + :undoc-members: + :show-inheritance: + +cam.preferences module +---------------------- + +.. automodule:: cam.preferences + :members: + :undoc-members: + :show-inheritance: + +cam.preset\_managers module +--------------------------- + +.. automodule:: cam.preset_managers + :members: + :undoc-members: + :show-inheritance: + +cam.puzzle\_joinery module +-------------------------- + +.. automodule:: cam.puzzle_joinery + :members: + :undoc-members: + :show-inheritance: + +cam.simple module +----------------- + +.. automodule:: cam.simple + :members: + :undoc-members: + :show-inheritance: + +cam.simulation module +--------------------- + +.. automodule:: cam.simulation + :members: + :undoc-members: + :show-inheritance: + +cam.slice module +---------------- + +.. automodule:: cam.slice + :members: + :undoc-members: + :show-inheritance: + +cam.strategy module +------------------- + +.. automodule:: cam.strategy + :members: + :undoc-members: + :show-inheritance: + +cam.testing module +------------------ + +.. automodule:: cam.testing + :members: + :undoc-members: + :show-inheritance: + +cam.ui module +------------- + +.. automodule:: cam.ui + :members: + :undoc-members: + :show-inheritance: + +cam.utils module +---------------- + +.. automodule:: cam.utils + :members: + :undoc-members: + :show-inheritance: + +cam.version module +------------------ + +.. automodule:: cam.version + :members: + :undoc-members: + :show-inheritance: + +cam.voronoi module +------------------ + +.. automodule:: cam.voronoi + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: cam + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/index.rst b/_sources/index.rst new file mode 100644 index 000000000..530b84f2d --- /dev/null +++ b/_sources/index.rst @@ -0,0 +1,30 @@ +.. BlenderCAM documentation master file, created by + sphinx-quickstart on Sun Sep 8 08:23:06 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to BlenderCAM's API Documentation! +========================================== + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + overview + styleguide + testing + workflows + +This site serves as an introduction to the code behind BlenderCAM. + +If you just want to know how to use the addon to mill projects, check out the `wiki `_ + +This resource is for people who want to contribute code to BlenderCAM, people who want to modify the addon for their needs, or anyone who simply want to better understand what is happening 'under the hood'. + +:doc:`overview` offers a guide to the addon files and how they relate to one another. + +:doc:`styleguide` gives tips on editors, linting, formatting etc. + +:doc:`testing` has information on how to run and contribute to the Test Suite. + +:doc:`workflows` contains an explanation of how the addon, testing and documentation are automated via Github Actions. diff --git a/_sources/modules.rst b/_sources/modules.rst new file mode 100644 index 000000000..21fe0e84e --- /dev/null +++ b/_sources/modules.rst @@ -0,0 +1,7 @@ +cam +=== + +.. toctree:: + :maxdepth: 4 + + cam diff --git a/_sources/overview.rst b/_sources/overview.rst new file mode 100644 index 000000000..6a4136114 --- /dev/null +++ b/_sources/overview.rst @@ -0,0 +1,270 @@ +=========== +Overview +=========== +BlenderCAM's code can be broken down into categories: + +1. Core Functions +2. Extra Functions +3. Reference Files +4. User Interface +5. Dependencies + +Core Functions +============== +The core function of the BlenderCAM addon is to take whatever object is in the viewport and generate toolpaths along that object according to a milling strategy set by the user. +These operations can be exported alone, or combined into chains to be exported and run together. + +Extra Functions +=============== +Beyond simply creating toolpaths for existing objects, BlenderCAM can also create the objects (curves) and edit them through a number of operations. + +There are modules dedicated to creating reliefs, joinery, puzzle joinery and gears. + +There is also a simulation module to allow a preview of what the final product will look like, as well as an asynchronous module that allows progress reports on calculations. + +Reference Files +=============== +Presets for machines, tools, operations and preprocessors comprise the majority of the files in the addon. + +User Interface +============== +Files related to Blender's UI - all the panels, buttons etc that you can click on in the addon, as well as menus, pie menus, popup dialogs etc. + +Dependencies +============ +Python wheels - executable binaries packed in for all supported systems. + +Complete Addon Layout +--------------------- + +:: + + cam/ + ├── nc/ + │ ├── LICENSE + │ ├── __init__.py + │ ├── anilam_crusader_m.py + │ ├── anilam_crusader_m_read.py + │ ├── attach.py + │ ├── cad_iso_read.py + │ ├── cad_nc_read.py + │ ├── cad_read.py + │ ├── centroid1.py + │ ├── centroid1_read.py + │ ├── drag_knife.py + │ ├── emc2.py + │ ├── emc2_read.py + │ ├── emc2b.py + │ ├── emc2b_crc.py + │ ├── emc2b_crc_read.py + │ ├── emc2b_read.py + │ ├── emc2tap.py + │ ├── emc2tap_read.py + │ ├── fadal.py + │ ├── format.py + │ ├── gantry_router.py + │ ├── gantry_router_read.py + │ ├── gravos.py + │ ├── grbl.py + │ ├── heiden.py + │ ├── heiden530.py + │ ├── heiden_read.py + │ ├── hm50.py + │ ├── hm50_read.py + │ ├── hpgl2d.py + │ ├── hpgl2d_read.py + │ ├── hpgl2dv.py + │ ├── hpgl2dv_read.py + │ ├── hpgl3d.py + │ ├── hpgl3d_read.py + │ ├── hxml_writer.py + │ ├── iso.py + │ ├── iso_codes.py + │ ├── iso_crc.py + │ ├── iso_crc_read.py + │ ├── iso_modal.py + │ ├── iso_modal_read.py + │ ├── iso_read.py + │ ├── lathe1.py + │ ├── lathe1_read.py + │ ├── lynx_otter_o.py + │ ├── mach3.py + │ ├── mach3_read.py + │ ├── machines.txt + │ ├── makerbotHBP.py + │ ├── makerbotHBP_read.py + │ ├── makerbot_codes.py + │ ├── nc.py + │ ├── nc_read.py + │ ├── nclathe_read.py + │ ├── num_reader.py + │ ├── printbot3d.py + │ ├── printbot3d_read.py + │ ├── recreator.py + │ ├── rez2.py + │ ├── rez2_read.py + │ ├── series1.py + │ ├── series1_read.py + │ ├── shopbot_mtc.py + │ ├── siegkx1.py + │ ├── siegkx1_read.py + │ ├── tnc151.py + │ ├── tnc151_read.py + │ └── winpc.py + ├── opencamlib/ + │ ├── __init__.py + │ ├── oclSample.py + │ ├── opencamlib.py + │ └── opencamlib_readme.txt + ├── pie_menu/ + │ ├── active_op/ + │ │ ├── pie_area.py + │ │ ├── pie_cutter.py + │ │ ├── pie_feedrate.py + │ │ ├── pie_gcode.py + │ │ ├── pie_movement.py + │ │ ├── pie_operation.py + │ │ └── pie_setup.py + │ ├── pie_cam.py + │ ├── pie_chains.py + │ ├── pie_curvecreators.py + │ ├── pie_curvetools.py + │ ├── pie_info.py + │ ├── pie_machine.py + │ ├── pie_material.py + │ └── pie_pack_slice_relief.py + ├── presets/ + │ ├── cam_cutters/ + │ │ ├── BALLCONE_1.00mm.py + │ │ ├── ball_1.00mm.py + │ │ ├── ball_1.50mm.py + │ │ ├── ball_10.00mm.py + │ │ ├── ball_12.00mm.py + │ │ ├── ball_16.00mm.py + │ │ ├── ball_2.00mm.py + │ │ ├── ball_2.50mm.py + │ │ ├── ball_20.00mm.py + │ │ ├── ball_3.00mm.py + │ │ ├── ball_3.50mm.py + │ │ ├── ball_4.00mm.py + │ │ ├── ball_5.00mm.py + │ │ ├── ball_6.00mm.py + │ │ ├── ball_7.00mm.py + │ │ ├── ball_8.00mm.py + │ │ ├── end_cyl_1.00mm.py + │ │ ├── end_cyl_1.50mm.py + │ │ ├── end_cyl_10.00mm.py + │ │ ├── end_cyl_12.00mm.py + │ │ ├── end_cyl_16.00mm.py + │ │ ├── end_cyl_2.00mm.py + │ │ ├── end_cyl_2.50mm.py + │ │ ├── end_cyl_20.00mm.py + │ │ ├── end_cyl_3.00mm.py + │ │ ├── end_cyl_3.50mm.py + │ │ ├── end_cyl_4.00mm.py + │ │ ├── end_cyl_5.00mm.py + │ │ ├── end_cyl_6.00mm.py + │ │ ├── end_cyl_7.00mm.py + │ │ ├── end_cyl_8.00mm.py + │ │ ├── v-carve_3mm_45deg.py + │ │ ├── v-carve_3mm_60deg.py + │ │ ├── v-carve_6mm_45deg.py + │ │ └── v-carve_6mm_60deg.py + │ ├── cam_machines/ + │ │ ├── emc_test_2.py + │ │ └── kk1000s.py + │ └── cam_operations/ + │ ├── Fin_Ball_3,0_Block_All.py + │ ├── Fin_Ball_3,0_Block_Around.py + │ ├── Fin_Ball_3,0_Circles_All_EXPERIMENTAL.py + │ ├── Fin_Ball_3,0_Circles_Around_EXPERIMENTAL.py + │ ├── Fin_Ball_3,0_Cross_All.py + │ ├── Fin_Ball_3,0_Cross_Around.py + │ ├── Fin_Ball_3,0_Cutout.py + │ ├── Fin_Ball_3,0_Outline_Fill_EXPERIMENTAL.py + │ ├── Fin_Ball_3,0_Parallel_All.py + │ ├── Fin_Ball_3,0_Parallel_Around.py + │ ├── Fin_Ball_3,0_Pencil_EXPERIMENTAL.py + │ ├── Fin_Ball_3,0_Pocket_EXPERIMENTAL.py + │ ├── Fin_Ball_3,0_Spiral_All.py + │ ├── Fin_Ball_3,0_Spiral_Around.py + │ ├── Finishing_3mm_ballnose.py + │ ├── Rou_Ball_3,0_Block_All.py + │ ├── Rou_Ball_3,0_Block_Around.py + │ ├── Rou_Ball_3,0_Circles_All_EXPERIMENTAL.py + │ ├── Rou_Ball_3,0_Circles_Around_EXPERIMENTAL.py + │ ├── Rou_Ball_3,0_Cross_All.py + │ ├── Rou_Ball_3,0_Cross_Around.py + │ ├── Rou_Ball_3,0_Cutout.py + │ ├── Rou_Ball_3,0_Outline_Fill_EXPERIMENTAL.py + │ ├── Rou_Ball_3,0_Parallel_All.py + │ ├── Rou_Ball_3,0_Parallel_Around.py + │ ├── Rou_Ball_3,0_Pencil_EXPERIMENTAL.py + │ ├── Rou_Ball_3,0_Pocket_EXPERIMENTAL.py + │ ├── Rou_Ball_3,0_Spiral_All.py + │ └── Rou_Ball_3,0_Spiral_Around.py + ├── tests/ + │ ├── test_data + │ ├── TESTING_PROCEDURE + │ ├── gcode_generator.py + │ ├── install_addon.py + │ └── test_suite.py + └── ui_panels/ + ├── __init__.py + ├── area.py + ├── buttons_panel.py + ├── chains.py + ├── cutter.py + ├── feedrate.py + ├── gcode.py + ├── info.py + ├── interface.py + ├── machine.py + ├── material.py + ├── movement.py + ├── op_properties.py + ├── operations.py + ├── optimisation.py + ├── pack.py + └── slice.py + LICENSE + __init__.py + async_op.py + autoupdate.py + backgroundop_.py + basrelief.py + bridges.py + cam_chunk.py + cam_operation.py + chain.py + collision.py + constants.py + curvecamcreate.py + curvecamequation.py + curvecamtools.py + engine.py + exception.py + gcodeimportparser.py + gcodepath.py + image_utils.py + involute_gear.py + joinery.py + machine_settings.py + numba_wrapper.py + ops.py + pack.py + parametric.py + pattern.py + polygon_utils_cam.py + preferences.py + preset_managers.py + puzzle_joinery.py + simple.py + simulation.py + slice.py + strategy.py + testing.py + ui.py + utils.py + version.py + voronoi.py diff --git a/_sources/styleguide.rst b/_sources/styleguide.rst new file mode 100644 index 000000000..f009c4a46 --- /dev/null +++ b/_sources/styleguide.rst @@ -0,0 +1,4 @@ +Style Guide +=========== + +(coming soon) diff --git a/_sources/testing.rst b/_sources/testing.rst new file mode 100644 index 000000000..29ce82743 --- /dev/null +++ b/_sources/testing.rst @@ -0,0 +1,3 @@ +Test Suite +=========== +(coming soon) diff --git a/_sources/workflows.rst b/_sources/workflows.rst new file mode 100644 index 000000000..0338dfa00 --- /dev/null +++ b/_sources/workflows.rst @@ -0,0 +1,3 @@ +Workflows & Actions +=================== +(coming soon) diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 000000000..2af6139e6 --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,925 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 270px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 000000000..4d67807d1 --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 000000000..8b5f9d8dc --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '1.0.38', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 000000000..a858a410e Binary files /dev/null and b/_static/file.png differ diff --git a/_static/graphviz.css b/_static/graphviz.css new file mode 100644 index 000000000..027576e34 --- /dev/null +++ b/_static/graphviz.css @@ -0,0 +1,19 @@ +/* + * graphviz.css + * ~~~~~~~~~~~~ + * + * Sphinx stylesheet -- graphviz extension. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +img.graphviz { + border: 0; + max-width: 100%; +} + +object.graphviz { + max-width: 100%; +} diff --git a/_static/images/logo_binder.svg b/_static/images/logo_binder.svg new file mode 100644 index 000000000..45fecf751 --- /dev/null +++ b/_static/images/logo_binder.svg @@ -0,0 +1,19 @@ + + + + +logo + + + + + + + + diff --git a/_static/images/logo_colab.png b/_static/images/logo_colab.png new file mode 100644 index 000000000..b7560ec21 Binary files /dev/null and b/_static/images/logo_colab.png differ diff --git a/_static/images/logo_deepnote.svg b/_static/images/logo_deepnote.svg new file mode 100644 index 000000000..fa77ebfc2 --- /dev/null +++ b/_static/images/logo_deepnote.svg @@ -0,0 +1 @@ + diff --git a/_static/images/logo_jupyterhub.svg b/_static/images/logo_jupyterhub.svg new file mode 100644 index 000000000..60cfe9f22 --- /dev/null +++ b/_static/images/logo_jupyterhub.svg @@ -0,0 +1 @@ +logo_jupyterhubHub diff --git a/_static/language_data.js b/_static/language_data.js new file mode 100644 index 000000000..367b8ed81 --- /dev/null +++ b/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/_static/locales/ar/LC_MESSAGES/booktheme.mo b/_static/locales/ar/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..15541a6a3 Binary files /dev/null and b/_static/locales/ar/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ar/LC_MESSAGES/booktheme.po b/_static/locales/ar/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..34d404c6d --- /dev/null +++ b/_static/locales/ar/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ar\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "طباعة إلى PDF" + +msgid "Theme by the" +msgstr "موضوع بواسطة" + +msgid "Download source file" +msgstr "تنزيل ملف المصدر" + +msgid "open issue" +msgstr "قضية مفتوحة" + +msgid "Contents" +msgstr "محتويات" + +msgid "previous page" +msgstr "الصفحة السابقة" + +msgid "Download notebook file" +msgstr "تنزيل ملف دفتر الملاحظات" + +msgid "Copyright" +msgstr "حقوق النشر" + +msgid "Download this page" +msgstr "قم بتنزيل هذه الصفحة" + +msgid "Source repository" +msgstr "مستودع المصدر" + +msgid "By" +msgstr "بواسطة" + +msgid "repository" +msgstr "مخزن" + +msgid "Last updated on" +msgstr "آخر تحديث في" + +msgid "Toggle navigation" +msgstr "تبديل التنقل" + +msgid "Sphinx Book Theme" +msgstr "موضوع كتاب أبو الهول" + +msgid "suggest edit" +msgstr "أقترح تحرير" + +msgid "Open an issue" +msgstr "افتح قضية" + +msgid "Launch" +msgstr "إطلاق" + +msgid "Fullscreen mode" +msgstr "وضع ملء الشاشة" + +msgid "Edit this page" +msgstr "قم بتحرير هذه الصفحة" + +msgid "By the" +msgstr "بواسطة" + +msgid "next page" +msgstr "الصفحة التالية" diff --git a/_static/locales/bg/LC_MESSAGES/booktheme.mo b/_static/locales/bg/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..da9512003 Binary files /dev/null and b/_static/locales/bg/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/bg/LC_MESSAGES/booktheme.po b/_static/locales/bg/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..7420c19eb --- /dev/null +++ b/_static/locales/bg/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: bg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Печат в PDF" + +msgid "Theme by the" +msgstr "Тема от" + +msgid "Download source file" +msgstr "Изтеглете изходния файл" + +msgid "open issue" +msgstr "отворен брой" + +msgid "Contents" +msgstr "Съдържание" + +msgid "previous page" +msgstr "предишна страница" + +msgid "Download notebook file" +msgstr "Изтеглете файла на бележника" + +msgid "Copyright" +msgstr "Авторско право" + +msgid "Download this page" +msgstr "Изтеглете тази страница" + +msgid "Source repository" +msgstr "Хранилище на източника" + +msgid "By" +msgstr "От" + +msgid "repository" +msgstr "хранилище" + +msgid "Last updated on" +msgstr "Последна актуализация на" + +msgid "Toggle navigation" +msgstr "Превключване на навигацията" + +msgid "Sphinx Book Theme" +msgstr "Тема на книгата Sphinx" + +msgid "suggest edit" +msgstr "предложи редактиране" + +msgid "Open an issue" +msgstr "Отворете проблем" + +msgid "Launch" +msgstr "Стартиране" + +msgid "Fullscreen mode" +msgstr "Режим на цял екран" + +msgid "Edit this page" +msgstr "Редактирайте тази страница" + +msgid "By the" +msgstr "По" + +msgid "next page" +msgstr "Следваща страница" diff --git a/_static/locales/bn/LC_MESSAGES/booktheme.mo b/_static/locales/bn/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..6b96639b7 Binary files /dev/null and b/_static/locales/bn/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/bn/LC_MESSAGES/booktheme.po b/_static/locales/bn/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..63a07c362 --- /dev/null +++ b/_static/locales/bn/LC_MESSAGES/booktheme.po @@ -0,0 +1,63 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: bn\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "পিডিএফ প্রিন্ট করুন" + +msgid "Theme by the" +msgstr "থিম দ্বারা" + +msgid "Download source file" +msgstr "উত্স ফাইল ডাউনলোড করুন" + +msgid "open issue" +msgstr "খোলা সমস্যা" + +msgid "previous page" +msgstr "আগের পৃষ্ঠা" + +msgid "Download notebook file" +msgstr "নোটবুক ফাইল ডাউনলোড করুন" + +msgid "Copyright" +msgstr "কপিরাইট" + +msgid "Download this page" +msgstr "এই পৃষ্ঠাটি ডাউনলোড করুন" + +msgid "Source repository" +msgstr "উত্স সংগ্রহস্থল" + +msgid "By" +msgstr "দ্বারা" + +msgid "Last updated on" +msgstr "সর্বশেষ আপডেট" + +msgid "Toggle navigation" +msgstr "নেভিগেশন টগল করুন" + +msgid "Sphinx Book Theme" +msgstr "স্পিনিক্স বুক থিম" + +msgid "Open an issue" +msgstr "একটি সমস্যা খুলুন" + +msgid "Launch" +msgstr "শুরু করা" + +msgid "Edit this page" +msgstr "এই পৃষ্ঠাটি সম্পাদনা করুন" + +msgid "By the" +msgstr "দ্বারা" + +msgid "next page" +msgstr "পরবর্তী পৃষ্ঠা" diff --git a/_static/locales/ca/LC_MESSAGES/booktheme.mo b/_static/locales/ca/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..a4dd30e9b Binary files /dev/null and b/_static/locales/ca/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ca/LC_MESSAGES/booktheme.po b/_static/locales/ca/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..8fb358bf1 --- /dev/null +++ b/_static/locales/ca/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ca\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Imprimeix a PDF" + +msgid "Theme by the" +msgstr "Tema del" + +msgid "Download source file" +msgstr "Baixeu el fitxer font" + +msgid "open issue" +msgstr "número obert" + +msgid "previous page" +msgstr "Pàgina anterior" + +msgid "Download notebook file" +msgstr "Descarregar fitxer de quadern" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Download this page" +msgstr "Descarregueu aquesta pàgina" + +msgid "Source repository" +msgstr "Dipòsit de fonts" + +msgid "By" +msgstr "Per" + +msgid "Last updated on" +msgstr "Darrera actualització el" + +msgid "Toggle navigation" +msgstr "Commuta la navegació" + +msgid "Sphinx Book Theme" +msgstr "Tema del llibre Esfinx" + +msgid "suggest edit" +msgstr "suggerir edició" + +msgid "Open an issue" +msgstr "Obriu un número" + +msgid "Launch" +msgstr "Llançament" + +msgid "Edit this page" +msgstr "Editeu aquesta pàgina" + +msgid "By the" +msgstr "Per la" + +msgid "next page" +msgstr "pàgina següent" diff --git a/_static/locales/cs/LC_MESSAGES/booktheme.mo b/_static/locales/cs/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..c39e01a6a Binary files /dev/null and b/_static/locales/cs/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/cs/LC_MESSAGES/booktheme.po b/_static/locales/cs/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..c6ef46908 --- /dev/null +++ b/_static/locales/cs/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: cs\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Tisk do PDF" + +msgid "Theme by the" +msgstr "Téma od" + +msgid "Download source file" +msgstr "Stáhněte si zdrojový soubor" + +msgid "open issue" +msgstr "otevřené číslo" + +msgid "Contents" +msgstr "Obsah" + +msgid "previous page" +msgstr "předchozí stránka" + +msgid "Download notebook file" +msgstr "Stáhnout soubor poznámkového bloku" + +msgid "Copyright" +msgstr "autorská práva" + +msgid "Download this page" +msgstr "Stáhněte si tuto stránku" + +msgid "Source repository" +msgstr "Zdrojové úložiště" + +msgid "By" +msgstr "Podle" + +msgid "repository" +msgstr "úložiště" + +msgid "Last updated on" +msgstr "Naposledy aktualizováno" + +msgid "Toggle navigation" +msgstr "Přepnout navigaci" + +msgid "Sphinx Book Theme" +msgstr "Téma knihy Sfinga" + +msgid "suggest edit" +msgstr "navrhnout úpravy" + +msgid "Open an issue" +msgstr "Otevřete problém" + +msgid "Launch" +msgstr "Zahájení" + +msgid "Fullscreen mode" +msgstr "Režim celé obrazovky" + +msgid "Edit this page" +msgstr "Upravit tuto stránku" + +msgid "By the" +msgstr "Podle" + +msgid "next page" +msgstr "další strana" diff --git a/_static/locales/da/LC_MESSAGES/booktheme.mo b/_static/locales/da/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..f43157d70 Binary files /dev/null and b/_static/locales/da/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/da/LC_MESSAGES/booktheme.po b/_static/locales/da/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..306a38e52 --- /dev/null +++ b/_static/locales/da/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: da\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Udskriv til PDF" + +msgid "Theme by the" +msgstr "Tema af" + +msgid "Download source file" +msgstr "Download kildefil" + +msgid "open issue" +msgstr "åbent nummer" + +msgid "Contents" +msgstr "Indhold" + +msgid "previous page" +msgstr "forrige side" + +msgid "Download notebook file" +msgstr "Download notesbog-fil" + +msgid "Copyright" +msgstr "ophavsret" + +msgid "Download this page" +msgstr "Download denne side" + +msgid "Source repository" +msgstr "Kildelager" + +msgid "By" +msgstr "Ved" + +msgid "repository" +msgstr "lager" + +msgid "Last updated on" +msgstr "Sidst opdateret den" + +msgid "Toggle navigation" +msgstr "Skift navigation" + +msgid "Sphinx Book Theme" +msgstr "Sphinx bogtema" + +msgid "suggest edit" +msgstr "foreslå redigering" + +msgid "Open an issue" +msgstr "Åbn et problem" + +msgid "Launch" +msgstr "Start" + +msgid "Fullscreen mode" +msgstr "Fuldskærmstilstand" + +msgid "Edit this page" +msgstr "Rediger denne side" + +msgid "By the" +msgstr "Ved" + +msgid "next page" +msgstr "Næste side" diff --git a/_static/locales/de/LC_MESSAGES/booktheme.mo b/_static/locales/de/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..648b565c7 Binary files /dev/null and b/_static/locales/de/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/de/LC_MESSAGES/booktheme.po b/_static/locales/de/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..4925360d4 --- /dev/null +++ b/_static/locales/de/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "In PDF drucken" + +msgid "Theme by the" +msgstr "Thema von der" + +msgid "Download source file" +msgstr "Quelldatei herunterladen" + +msgid "open issue" +msgstr "offenes Thema" + +msgid "Contents" +msgstr "Inhalt" + +msgid "previous page" +msgstr "vorherige Seite" + +msgid "Download notebook file" +msgstr "Notebook-Datei herunterladen" + +msgid "Copyright" +msgstr "Urheberrechte ©" + +msgid "Download this page" +msgstr "Laden Sie diese Seite herunter" + +msgid "Source repository" +msgstr "Quell-Repository" + +msgid "By" +msgstr "Durch" + +msgid "repository" +msgstr "Repository" + +msgid "Last updated on" +msgstr "Zuletzt aktualisiert am" + +msgid "Toggle navigation" +msgstr "Navigation umschalten" + +msgid "Sphinx Book Theme" +msgstr "Sphinx-Buch-Thema" + +msgid "suggest edit" +msgstr "vorschlagen zu bearbeiten" + +msgid "Open an issue" +msgstr "Öffnen Sie ein Problem" + +msgid "Launch" +msgstr "Starten" + +msgid "Fullscreen mode" +msgstr "Vollbildmodus" + +msgid "Edit this page" +msgstr "Bearbeite diese Seite" + +msgid "By the" +msgstr "Bis zum" + +msgid "next page" +msgstr "Nächste Seite" diff --git a/_static/locales/el/LC_MESSAGES/booktheme.mo b/_static/locales/el/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..fca6e9355 Binary files /dev/null and b/_static/locales/el/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/el/LC_MESSAGES/booktheme.po b/_static/locales/el/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..3e01acbd9 --- /dev/null +++ b/_static/locales/el/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: el\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Εκτύπωση σε PDF" + +msgid "Theme by the" +msgstr "Θέμα από το" + +msgid "Download source file" +msgstr "Λήψη αρχείου προέλευσης" + +msgid "open issue" +msgstr "ανοιχτό ζήτημα" + +msgid "Contents" +msgstr "Περιεχόμενα" + +msgid "previous page" +msgstr "προηγούμενη σελίδα" + +msgid "Download notebook file" +msgstr "Λήψη αρχείου σημειωματάριου" + +msgid "Copyright" +msgstr "Πνευματική ιδιοκτησία" + +msgid "Download this page" +msgstr "Λήψη αυτής της σελίδας" + +msgid "Source repository" +msgstr "Αποθήκη πηγής" + +msgid "By" +msgstr "Με" + +msgid "repository" +msgstr "αποθήκη" + +msgid "Last updated on" +msgstr "Τελευταία ενημέρωση στις" + +msgid "Toggle navigation" +msgstr "Εναλλαγή πλοήγησης" + +msgid "Sphinx Book Theme" +msgstr "Θέμα βιβλίου Sphinx" + +msgid "suggest edit" +msgstr "προτείνω επεξεργασία" + +msgid "Open an issue" +msgstr "Ανοίξτε ένα ζήτημα" + +msgid "Launch" +msgstr "Εκτόξευση" + +msgid "Fullscreen mode" +msgstr "ΛΕΙΤΟΥΡΓΙΑ ΠΛΗΡΟΥΣ ΟΘΟΝΗΣ" + +msgid "Edit this page" +msgstr "Επεξεργαστείτε αυτήν τη σελίδα" + +msgid "By the" +msgstr "Από το" + +msgid "next page" +msgstr "επόμενη σελίδα" diff --git a/_static/locales/eo/LC_MESSAGES/booktheme.mo b/_static/locales/eo/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..d1072bbec Binary files /dev/null and b/_static/locales/eo/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/eo/LC_MESSAGES/booktheme.po b/_static/locales/eo/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..f7ed2262d --- /dev/null +++ b/_static/locales/eo/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: eo\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Presi al PDF" + +msgid "Theme by the" +msgstr "Temo de la" + +msgid "Download source file" +msgstr "Elŝutu fontodosieron" + +msgid "open issue" +msgstr "malferma numero" + +msgid "Contents" +msgstr "Enhavo" + +msgid "previous page" +msgstr "antaŭa paĝo" + +msgid "Download notebook file" +msgstr "Elŝutu kajeran dosieron" + +msgid "Copyright" +msgstr "Kopirajto" + +msgid "Download this page" +msgstr "Elŝutu ĉi tiun paĝon" + +msgid "Source repository" +msgstr "Fonto-deponejo" + +msgid "By" +msgstr "De" + +msgid "repository" +msgstr "deponejo" + +msgid "Last updated on" +msgstr "Laste ĝisdatigita la" + +msgid "Toggle navigation" +msgstr "Ŝalti navigadon" + +msgid "Sphinx Book Theme" +msgstr "Sfinksa Libro-Temo" + +msgid "suggest edit" +msgstr "sugesti redaktadon" + +msgid "Open an issue" +msgstr "Malfermu numeron" + +msgid "Launch" +msgstr "Lanĉo" + +msgid "Fullscreen mode" +msgstr "Plenekrana reĝimo" + +msgid "Edit this page" +msgstr "Redaktu ĉi tiun paĝon" + +msgid "By the" +msgstr "Per la" + +msgid "next page" +msgstr "sekva paĝo" diff --git a/_static/locales/es/LC_MESSAGES/booktheme.mo b/_static/locales/es/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..ba2ee4dc2 Binary files /dev/null and b/_static/locales/es/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/es/LC_MESSAGES/booktheme.po b/_static/locales/es/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..5e0029e5f --- /dev/null +++ b/_static/locales/es/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Imprimir en PDF" + +msgid "Theme by the" +msgstr "Tema por el" + +msgid "Download source file" +msgstr "Descargar archivo fuente" + +msgid "open issue" +msgstr "Tema abierto" + +msgid "Contents" +msgstr "Contenido" + +msgid "previous page" +msgstr "pagina anterior" + +msgid "Download notebook file" +msgstr "Descargar archivo de cuaderno" + +msgid "Copyright" +msgstr "Derechos de autor" + +msgid "Download this page" +msgstr "Descarga esta pagina" + +msgid "Source repository" +msgstr "Repositorio de origen" + +msgid "By" +msgstr "Por" + +msgid "repository" +msgstr "repositorio" + +msgid "Last updated on" +msgstr "Ultima actualización en" + +msgid "Toggle navigation" +msgstr "Navegación de palanca" + +msgid "Sphinx Book Theme" +msgstr "Tema del libro de la esfinge" + +msgid "suggest edit" +msgstr "sugerir editar" + +msgid "Open an issue" +msgstr "Abrir un problema" + +msgid "Launch" +msgstr "Lanzamiento" + +msgid "Fullscreen mode" +msgstr "Modo de pantalla completa" + +msgid "Edit this page" +msgstr "Edita esta página" + +msgid "By the" +msgstr "Por el" + +msgid "next page" +msgstr "siguiente página" diff --git a/_static/locales/et/LC_MESSAGES/booktheme.mo b/_static/locales/et/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..983b82391 Binary files /dev/null and b/_static/locales/et/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/et/LC_MESSAGES/booktheme.po b/_static/locales/et/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..8680982a9 --- /dev/null +++ b/_static/locales/et/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: et\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Prindi PDF-i" + +msgid "Theme by the" +msgstr "Teema" + +msgid "Download source file" +msgstr "Laadige alla lähtefail" + +msgid "open issue" +msgstr "avatud küsimus" + +msgid "Contents" +msgstr "Sisu" + +msgid "previous page" +msgstr "eelmine leht" + +msgid "Download notebook file" +msgstr "Laadige sülearvuti fail alla" + +msgid "Copyright" +msgstr "Autoriõigus" + +msgid "Download this page" +msgstr "Laadige see leht alla" + +msgid "Source repository" +msgstr "Allikahoidla" + +msgid "By" +msgstr "Kõrval" + +msgid "repository" +msgstr "hoidla" + +msgid "Last updated on" +msgstr "Viimati uuendatud" + +msgid "Toggle navigation" +msgstr "Lülita navigeerimine sisse" + +msgid "Sphinx Book Theme" +msgstr "Sfinksiraamatu teema" + +msgid "suggest edit" +msgstr "soovita muuta" + +msgid "Open an issue" +msgstr "Avage probleem" + +msgid "Launch" +msgstr "Käivitage" + +msgid "Fullscreen mode" +msgstr "Täisekraanirežiim" + +msgid "Edit this page" +msgstr "Muutke seda lehte" + +msgid "By the" +msgstr "Autor" + +msgid "next page" +msgstr "järgmine leht" diff --git a/_static/locales/fi/LC_MESSAGES/booktheme.mo b/_static/locales/fi/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..d8ac05459 Binary files /dev/null and b/_static/locales/fi/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/fi/LC_MESSAGES/booktheme.po b/_static/locales/fi/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..34dac2183 --- /dev/null +++ b/_static/locales/fi/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fi\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Tulosta PDF-tiedostoon" + +msgid "Theme by the" +msgstr "Teeman tekijä" + +msgid "Download source file" +msgstr "Lataa lähdetiedosto" + +msgid "open issue" +msgstr "avoin ongelma" + +msgid "Contents" +msgstr "Sisällys" + +msgid "previous page" +msgstr "Edellinen sivu" + +msgid "Download notebook file" +msgstr "Lataa muistikirjatiedosto" + +msgid "Copyright" +msgstr "Tekijänoikeus" + +msgid "Download this page" +msgstr "Lataa tämä sivu" + +msgid "Source repository" +msgstr "Lähteen arkisto" + +msgid "By" +msgstr "Tekijä" + +msgid "repository" +msgstr "arkisto" + +msgid "Last updated on" +msgstr "Viimeksi päivitetty" + +msgid "Toggle navigation" +msgstr "Vaihda navigointia" + +msgid "Sphinx Book Theme" +msgstr "Sphinx-kirjan teema" + +msgid "suggest edit" +msgstr "ehdottaa muokkausta" + +msgid "Open an issue" +msgstr "Avaa ongelma" + +msgid "Launch" +msgstr "Tuoda markkinoille" + +msgid "Fullscreen mode" +msgstr "Koko näytön tila" + +msgid "Edit this page" +msgstr "Muokkaa tätä sivua" + +msgid "By the" +msgstr "Mukaan" + +msgid "next page" +msgstr "seuraava sivu" diff --git a/_static/locales/fr/LC_MESSAGES/booktheme.mo b/_static/locales/fr/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..f663d39f0 Binary files /dev/null and b/_static/locales/fr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/fr/LC_MESSAGES/booktheme.po b/_static/locales/fr/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..8991a1b87 --- /dev/null +++ b/_static/locales/fr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Imprimer au format PDF" + +msgid "Theme by the" +msgstr "Thème par le" + +msgid "Download source file" +msgstr "Télécharger le fichier source" + +msgid "open issue" +msgstr "signaler un problème" + +msgid "Contents" +msgstr "Contenu" + +msgid "previous page" +msgstr "page précédente" + +msgid "Download notebook file" +msgstr "Télécharger le fichier notebook" + +msgid "Copyright" +msgstr "droits d'auteur" + +msgid "Download this page" +msgstr "Téléchargez cette page" + +msgid "Source repository" +msgstr "Dépôt source" + +msgid "By" +msgstr "Par" + +msgid "repository" +msgstr "dépôt" + +msgid "Last updated on" +msgstr "Dernière mise à jour le" + +msgid "Toggle navigation" +msgstr "Basculer la navigation" + +msgid "Sphinx Book Theme" +msgstr "Thème du livre Sphinx" + +msgid "suggest edit" +msgstr "suggestion de modification" + +msgid "Open an issue" +msgstr "Ouvrez un problème" + +msgid "Launch" +msgstr "lancement" + +msgid "Fullscreen mode" +msgstr "Mode plein écran" + +msgid "Edit this page" +msgstr "Modifier cette page" + +msgid "By the" +msgstr "Par le" + +msgid "next page" +msgstr "page suivante" diff --git a/_static/locales/hr/LC_MESSAGES/booktheme.mo b/_static/locales/hr/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..eca4a1a28 Binary files /dev/null and b/_static/locales/hr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/hr/LC_MESSAGES/booktheme.po b/_static/locales/hr/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..42c4233d0 --- /dev/null +++ b/_static/locales/hr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: hr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Ispis u PDF" + +msgid "Theme by the" +msgstr "Tema autora" + +msgid "Download source file" +msgstr "Preuzmi izvornu datoteku" + +msgid "open issue" +msgstr "otvoreno izdanje" + +msgid "Contents" +msgstr "Sadržaj" + +msgid "previous page" +msgstr "Prethodna stranica" + +msgid "Download notebook file" +msgstr "Preuzmi datoteku bilježnice" + +msgid "Copyright" +msgstr "Autorska prava" + +msgid "Download this page" +msgstr "Preuzmite ovu stranicu" + +msgid "Source repository" +msgstr "Izvorno spremište" + +msgid "By" +msgstr "Po" + +msgid "repository" +msgstr "spremište" + +msgid "Last updated on" +msgstr "Posljednje ažuriranje:" + +msgid "Toggle navigation" +msgstr "Uključi / isključi navigaciju" + +msgid "Sphinx Book Theme" +msgstr "Tema knjige Sphinx" + +msgid "suggest edit" +msgstr "predloži uređivanje" + +msgid "Open an issue" +msgstr "Otvorite izdanje" + +msgid "Launch" +msgstr "Pokrenite" + +msgid "Fullscreen mode" +msgstr "Način preko cijelog zaslona" + +msgid "Edit this page" +msgstr "Uredite ovu stranicu" + +msgid "By the" +msgstr "Od strane" + +msgid "next page" +msgstr "sljedeća stranica" diff --git a/_static/locales/id/LC_MESSAGES/booktheme.mo b/_static/locales/id/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..d07a06a9d Binary files /dev/null and b/_static/locales/id/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/id/LC_MESSAGES/booktheme.po b/_static/locales/id/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..b8d8d898e --- /dev/null +++ b/_static/locales/id/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: id\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Cetak ke PDF" + +msgid "Theme by the" +msgstr "Tema oleh" + +msgid "Download source file" +msgstr "Unduh file sumber" + +msgid "open issue" +msgstr "masalah terbuka" + +msgid "Contents" +msgstr "Isi" + +msgid "previous page" +msgstr "halaman sebelumnya" + +msgid "Download notebook file" +msgstr "Unduh file notebook" + +msgid "Copyright" +msgstr "hak cipta" + +msgid "Download this page" +msgstr "Unduh halaman ini" + +msgid "Source repository" +msgstr "Repositori sumber" + +msgid "By" +msgstr "Oleh" + +msgid "repository" +msgstr "gudang" + +msgid "Last updated on" +msgstr "Terakhir diperbarui saat" + +msgid "Toggle navigation" +msgstr "Alihkan navigasi" + +msgid "Sphinx Book Theme" +msgstr "Tema Buku Sphinx" + +msgid "suggest edit" +msgstr "menyarankan edit" + +msgid "Open an issue" +msgstr "Buka masalah" + +msgid "Launch" +msgstr "Meluncurkan" + +msgid "Fullscreen mode" +msgstr "Mode layar penuh" + +msgid "Edit this page" +msgstr "Edit halaman ini" + +msgid "By the" +msgstr "Oleh" + +msgid "next page" +msgstr "halaman selanjutnya" diff --git a/_static/locales/it/LC_MESSAGES/booktheme.mo b/_static/locales/it/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..53ba476ed Binary files /dev/null and b/_static/locales/it/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/it/LC_MESSAGES/booktheme.po b/_static/locales/it/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..36fca59f8 --- /dev/null +++ b/_static/locales/it/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Stampa in PDF" + +msgid "Theme by the" +msgstr "Tema di" + +msgid "Download source file" +msgstr "Scarica il file sorgente" + +msgid "open issue" +msgstr "questione aperta" + +msgid "Contents" +msgstr "Contenuti" + +msgid "previous page" +msgstr "pagina precedente" + +msgid "Download notebook file" +msgstr "Scarica il file del taccuino" + +msgid "Copyright" +msgstr "Diritto d'autore" + +msgid "Download this page" +msgstr "Scarica questa pagina" + +msgid "Source repository" +msgstr "Repository di origine" + +msgid "By" +msgstr "Di" + +msgid "repository" +msgstr "repository" + +msgid "Last updated on" +msgstr "Ultimo aggiornamento il" + +msgid "Toggle navigation" +msgstr "Attiva / disattiva la navigazione" + +msgid "Sphinx Book Theme" +msgstr "Tema del libro della Sfinge" + +msgid "suggest edit" +msgstr "suggerisci modifica" + +msgid "Open an issue" +msgstr "Apri un problema" + +msgid "Launch" +msgstr "Lanciare" + +msgid "Fullscreen mode" +msgstr "Modalità schermo intero" + +msgid "Edit this page" +msgstr "Modifica questa pagina" + +msgid "By the" +msgstr "Dal" + +msgid "next page" +msgstr "pagina successiva" diff --git a/_static/locales/iw/LC_MESSAGES/booktheme.mo b/_static/locales/iw/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..a45c6575e Binary files /dev/null and b/_static/locales/iw/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/iw/LC_MESSAGES/booktheme.po b/_static/locales/iw/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..dede9cb08 --- /dev/null +++ b/_static/locales/iw/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: iw\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "הדפס לקובץ PDF" + +msgid "Theme by the" +msgstr "נושא מאת" + +msgid "Download source file" +msgstr "הורד את קובץ המקור" + +msgid "open issue" +msgstr "בעיה פתוחה" + +msgid "Contents" +msgstr "תוכן" + +msgid "previous page" +msgstr "עמוד קודם" + +msgid "Download notebook file" +msgstr "הורד קובץ מחברת" + +msgid "Copyright" +msgstr "זכויות יוצרים" + +msgid "Download this page" +msgstr "הורד דף זה" + +msgid "Source repository" +msgstr "מאגר המקורות" + +msgid "By" +msgstr "על ידי" + +msgid "repository" +msgstr "מאגר" + +msgid "Last updated on" +msgstr "עודכן לאחרונה ב" + +msgid "Toggle navigation" +msgstr "החלף ניווט" + +msgid "Sphinx Book Theme" +msgstr "נושא ספר ספינקס" + +msgid "suggest edit" +msgstr "מציע לערוך" + +msgid "Open an issue" +msgstr "פתח גיליון" + +msgid "Launch" +msgstr "לְהַשִׁיק" + +msgid "Fullscreen mode" +msgstr "מצב מסך מלא" + +msgid "Edit this page" +msgstr "ערוך דף זה" + +msgid "By the" +msgstr "דרך" + +msgid "next page" +msgstr "עמוד הבא" diff --git a/_static/locales/ja/LC_MESSAGES/booktheme.mo b/_static/locales/ja/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..1cefd29ce Binary files /dev/null and b/_static/locales/ja/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ja/LC_MESSAGES/booktheme.po b/_static/locales/ja/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..2615f0d87 --- /dev/null +++ b/_static/locales/ja/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ja\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "PDFに印刷" + +msgid "Theme by the" +msgstr "のテーマ" + +msgid "Download source file" +msgstr "ソースファイルをダウンロード" + +msgid "open issue" +msgstr "未解決の問題" + +msgid "Contents" +msgstr "目次" + +msgid "previous page" +msgstr "前のページ" + +msgid "Download notebook file" +msgstr "ノートブックファイルをダウンロード" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Download this page" +msgstr "このページをダウンロード" + +msgid "Source repository" +msgstr "ソースリポジトリ" + +msgid "By" +msgstr "著者" + +msgid "repository" +msgstr "リポジトリ" + +msgid "Last updated on" +msgstr "最終更新日" + +msgid "Toggle navigation" +msgstr "ナビゲーションを切り替え" + +msgid "Sphinx Book Theme" +msgstr "スフィンクスの本のテーマ" + +msgid "suggest edit" +msgstr "編集を提案する" + +msgid "Open an issue" +msgstr "問題を報告" + +msgid "Launch" +msgstr "起動" + +msgid "Fullscreen mode" +msgstr "全画面モード" + +msgid "Edit this page" +msgstr "このページを編集" + +msgid "By the" +msgstr "によって" + +msgid "next page" +msgstr "次のページ" diff --git a/_static/locales/ko/LC_MESSAGES/booktheme.mo b/_static/locales/ko/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..06c7ec938 Binary files /dev/null and b/_static/locales/ko/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ko/LC_MESSAGES/booktheme.po b/_static/locales/ko/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..c9e13a427 --- /dev/null +++ b/_static/locales/ko/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ko\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "PDF로 인쇄" + +msgid "Theme by the" +msgstr "테마별" + +msgid "Download source file" +msgstr "소스 파일 다운로드" + +msgid "open issue" +msgstr "열린 문제" + +msgid "Contents" +msgstr "내용" + +msgid "previous page" +msgstr "이전 페이지" + +msgid "Download notebook file" +msgstr "노트북 파일 다운로드" + +msgid "Copyright" +msgstr "저작권" + +msgid "Download this page" +msgstr "이 페이지 다운로드" + +msgid "Source repository" +msgstr "소스 저장소" + +msgid "By" +msgstr "으로" + +msgid "repository" +msgstr "저장소" + +msgid "Last updated on" +msgstr "마지막 업데이트" + +msgid "Toggle navigation" +msgstr "탐색 전환" + +msgid "Sphinx Book Theme" +msgstr "스핑크스 도서 테마" + +msgid "suggest edit" +msgstr "편집 제안" + +msgid "Open an issue" +msgstr "이슈 열기" + +msgid "Launch" +msgstr "시작하다" + +msgid "Fullscreen mode" +msgstr "전체 화면으로보기" + +msgid "Edit this page" +msgstr "이 페이지 편집" + +msgid "By the" +msgstr "에 의해" + +msgid "next page" +msgstr "다음 페이지" diff --git a/_static/locales/lt/LC_MESSAGES/booktheme.mo b/_static/locales/lt/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..4468ba04b Binary files /dev/null and b/_static/locales/lt/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/lt/LC_MESSAGES/booktheme.po b/_static/locales/lt/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..35eabd955 --- /dev/null +++ b/_static/locales/lt/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: lt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Spausdinti į PDF" + +msgid "Theme by the" +msgstr "Tema" + +msgid "Download source file" +msgstr "Atsisiųsti šaltinio failą" + +msgid "open issue" +msgstr "atviras klausimas" + +msgid "Contents" +msgstr "Turinys" + +msgid "previous page" +msgstr "Ankstesnis puslapis" + +msgid "Download notebook file" +msgstr "Atsisiųsti nešiojamojo kompiuterio failą" + +msgid "Copyright" +msgstr "Autorių teisės" + +msgid "Download this page" +msgstr "Atsisiųskite šį puslapį" + +msgid "Source repository" +msgstr "Šaltinio saugykla" + +msgid "By" +msgstr "Iki" + +msgid "repository" +msgstr "saugykla" + +msgid "Last updated on" +msgstr "Paskutinį kartą atnaujinta" + +msgid "Toggle navigation" +msgstr "Perjungti naršymą" + +msgid "Sphinx Book Theme" +msgstr "Sfinkso knygos tema" + +msgid "suggest edit" +msgstr "pasiūlyti redaguoti" + +msgid "Open an issue" +msgstr "Atidarykite problemą" + +msgid "Launch" +msgstr "Paleiskite" + +msgid "Fullscreen mode" +msgstr "Pilno ekrano režimas" + +msgid "Edit this page" +msgstr "Redaguoti šį puslapį" + +msgid "By the" +msgstr "Prie" + +msgid "next page" +msgstr "Kitas puslapis" diff --git a/_static/locales/lv/LC_MESSAGES/booktheme.mo b/_static/locales/lv/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..74aa4d898 Binary files /dev/null and b/_static/locales/lv/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/lv/LC_MESSAGES/booktheme.po b/_static/locales/lv/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..ee1bd08df --- /dev/null +++ b/_static/locales/lv/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: lv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Drukāt PDF formātā" + +msgid "Theme by the" +msgstr "Autora tēma" + +msgid "Download source file" +msgstr "Lejupielādēt avota failu" + +msgid "open issue" +msgstr "atklāts jautājums" + +msgid "Contents" +msgstr "Saturs" + +msgid "previous page" +msgstr "iepriekšējā lapa" + +msgid "Download notebook file" +msgstr "Lejupielādēt piezīmju grāmatiņu" + +msgid "Copyright" +msgstr "Autortiesības" + +msgid "Download this page" +msgstr "Lejupielādējiet šo lapu" + +msgid "Source repository" +msgstr "Avota krātuve" + +msgid "By" +msgstr "Autors" + +msgid "repository" +msgstr "krātuve" + +msgid "Last updated on" +msgstr "Pēdējoreiz atjaunināts" + +msgid "Toggle navigation" +msgstr "Pārslēgt navigāciju" + +msgid "Sphinx Book Theme" +msgstr "Sfinksa grāmatas tēma" + +msgid "suggest edit" +msgstr "ieteikt rediģēt" + +msgid "Open an issue" +msgstr "Atveriet problēmu" + +msgid "Launch" +msgstr "Uzsākt" + +msgid "Fullscreen mode" +msgstr "Pilnekrāna režīms" + +msgid "Edit this page" +msgstr "Rediģēt šo lapu" + +msgid "By the" +msgstr "Ar" + +msgid "next page" +msgstr "nākamā lapaspuse" diff --git a/_static/locales/ml/LC_MESSAGES/booktheme.mo b/_static/locales/ml/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..2736e8fcf Binary files /dev/null and b/_static/locales/ml/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ml/LC_MESSAGES/booktheme.po b/_static/locales/ml/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..d471277d6 --- /dev/null +++ b/_static/locales/ml/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ml\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "PDF- ലേക്ക് പ്രിന്റുചെയ്യുക" + +msgid "Theme by the" +msgstr "പ്രമേയം" + +msgid "Download source file" +msgstr "ഉറവിട ഫയൽ ഡൗൺലോഡുചെയ്യുക" + +msgid "open issue" +msgstr "തുറന്ന പ്രശ്നം" + +msgid "previous page" +msgstr "മുൻപത്തെ താൾ" + +msgid "Download notebook file" +msgstr "നോട്ട്ബുക്ക് ഫയൽ ഡൺലോഡ് ചെയ്യുക" + +msgid "Copyright" +msgstr "പകർപ്പവകാശം" + +msgid "Download this page" +msgstr "ഈ പേജ് ഡൗൺലോഡുചെയ്യുക" + +msgid "Source repository" +msgstr "ഉറവിട ശേഖരം" + +msgid "By" +msgstr "എഴുതിയത്" + +msgid "Last updated on" +msgstr "അവസാനം അപ്‌ഡേറ്റുചെയ്‌തത്" + +msgid "Toggle navigation" +msgstr "നാവിഗേഷൻ ടോഗിൾ ചെയ്യുക" + +msgid "Sphinx Book Theme" +msgstr "സ്ഫിങ്ക്സ് പുസ്തക തീം" + +msgid "suggest edit" +msgstr "എഡിറ്റുചെയ്യാൻ നിർദ്ദേശിക്കുക" + +msgid "Open an issue" +msgstr "ഒരു പ്രശ്നം തുറക്കുക" + +msgid "Launch" +msgstr "സമാരംഭിക്കുക" + +msgid "Edit this page" +msgstr "ഈ പേജ് എഡിറ്റുചെയ്യുക" + +msgid "By the" +msgstr "എഴുതിയത്" + +msgid "next page" +msgstr "അടുത്ത പേജ്" diff --git a/_static/locales/mr/LC_MESSAGES/booktheme.mo b/_static/locales/mr/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..fe530100d Binary files /dev/null and b/_static/locales/mr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/mr/LC_MESSAGES/booktheme.po b/_static/locales/mr/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..f3694acfa --- /dev/null +++ b/_static/locales/mr/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: mr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "पीडीएफवर मुद्रित करा" + +msgid "Theme by the" +msgstr "द्वारा थीम" + +msgid "Download source file" +msgstr "स्त्रोत फाइल डाउनलोड करा" + +msgid "open issue" +msgstr "खुला मुद्दा" + +msgid "previous page" +msgstr "मागील पान" + +msgid "Download notebook file" +msgstr "नोटबुक फाईल डाउनलोड करा" + +msgid "Copyright" +msgstr "कॉपीराइट" + +msgid "Download this page" +msgstr "हे पृष्ठ डाउनलोड करा" + +msgid "Source repository" +msgstr "स्त्रोत भांडार" + +msgid "By" +msgstr "द्वारा" + +msgid "Last updated on" +msgstr "अखेरचे अद्यतनित" + +msgid "Toggle navigation" +msgstr "नेव्हिगेशन टॉगल करा" + +msgid "Sphinx Book Theme" +msgstr "स्फिंक्स बुक थीम" + +msgid "suggest edit" +msgstr "संपादन सुचवा" + +msgid "Open an issue" +msgstr "एक मुद्दा उघडा" + +msgid "Launch" +msgstr "लाँच करा" + +msgid "Edit this page" +msgstr "हे पृष्ठ संपादित करा" + +msgid "By the" +msgstr "द्वारा" + +msgid "next page" +msgstr "पुढील पृष्ठ" diff --git a/_static/locales/ms/LC_MESSAGES/booktheme.mo b/_static/locales/ms/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..f02603fa2 Binary files /dev/null and b/_static/locales/ms/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ms/LC_MESSAGES/booktheme.po b/_static/locales/ms/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..65b7c6026 --- /dev/null +++ b/_static/locales/ms/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ms\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Cetak ke PDF" + +msgid "Theme by the" +msgstr "Tema oleh" + +msgid "Download source file" +msgstr "Muat turun fail sumber" + +msgid "open issue" +msgstr "isu terbuka" + +msgid "previous page" +msgstr "halaman sebelumnya" + +msgid "Download notebook file" +msgstr "Muat turun fail buku nota" + +msgid "Copyright" +msgstr "hak cipta" + +msgid "Download this page" +msgstr "Muat turun halaman ini" + +msgid "Source repository" +msgstr "Repositori sumber" + +msgid "By" +msgstr "Oleh" + +msgid "Last updated on" +msgstr "Terakhir dikemas kini pada" + +msgid "Toggle navigation" +msgstr "Togol navigasi" + +msgid "Sphinx Book Theme" +msgstr "Tema Buku Sphinx" + +msgid "suggest edit" +msgstr "cadangkan edit" + +msgid "Open an issue" +msgstr "Buka masalah" + +msgid "Launch" +msgstr "Lancarkan" + +msgid "Edit this page" +msgstr "Edit halaman ini" + +msgid "By the" +msgstr "Oleh" + +msgid "next page" +msgstr "muka surat seterusnya" diff --git a/_static/locales/nl/LC_MESSAGES/booktheme.mo b/_static/locales/nl/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..e59e7ecb3 Binary files /dev/null and b/_static/locales/nl/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/nl/LC_MESSAGES/booktheme.po b/_static/locales/nl/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..71bd1cda7 --- /dev/null +++ b/_static/locales/nl/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Afdrukken naar pdf" + +msgid "Theme by the" +msgstr "Thema door de" + +msgid "Download source file" +msgstr "Download het bronbestand" + +msgid "open issue" +msgstr "open probleem" + +msgid "Contents" +msgstr "Inhoud" + +msgid "previous page" +msgstr "vorige pagina" + +msgid "Download notebook file" +msgstr "Download notebookbestand" + +msgid "Copyright" +msgstr "auteursrechten" + +msgid "Download this page" +msgstr "Download deze pagina" + +msgid "Source repository" +msgstr "Bronopslagplaats" + +msgid "By" +msgstr "Door" + +msgid "repository" +msgstr "repository" + +msgid "Last updated on" +msgstr "Laatst geupdate op" + +msgid "Toggle navigation" +msgstr "Schakel navigatie" + +msgid "Sphinx Book Theme" +msgstr "Sphinx-boekthema" + +msgid "suggest edit" +msgstr "suggereren bewerken" + +msgid "Open an issue" +msgstr "Open een probleem" + +msgid "Launch" +msgstr "Lancering" + +msgid "Fullscreen mode" +msgstr "Volledig scherm" + +msgid "Edit this page" +msgstr "bewerk deze pagina" + +msgid "By the" +msgstr "Door de" + +msgid "next page" +msgstr "volgende bladzijde" diff --git a/_static/locales/no/LC_MESSAGES/booktheme.mo b/_static/locales/no/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..6cd15c88d Binary files /dev/null and b/_static/locales/no/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/no/LC_MESSAGES/booktheme.po b/_static/locales/no/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..b21346a51 --- /dev/null +++ b/_static/locales/no/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: no\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Skriv ut til PDF" + +msgid "Theme by the" +msgstr "Tema av" + +msgid "Download source file" +msgstr "Last ned kildefilen" + +msgid "open issue" +msgstr "åpent nummer" + +msgid "Contents" +msgstr "Innhold" + +msgid "previous page" +msgstr "forrige side" + +msgid "Download notebook file" +msgstr "Last ned notatbokfilen" + +msgid "Copyright" +msgstr "opphavsrett" + +msgid "Download this page" +msgstr "Last ned denne siden" + +msgid "Source repository" +msgstr "Kildedepot" + +msgid "By" +msgstr "Av" + +msgid "repository" +msgstr "oppbevaringssted" + +msgid "Last updated on" +msgstr "Sist oppdatert den" + +msgid "Toggle navigation" +msgstr "Bytt navigasjon" + +msgid "Sphinx Book Theme" +msgstr "Sphinx boktema" + +msgid "suggest edit" +msgstr "foreslå redigering" + +msgid "Open an issue" +msgstr "Åpne et problem" + +msgid "Launch" +msgstr "Start" + +msgid "Fullscreen mode" +msgstr "Fullskjerm-modus" + +msgid "Edit this page" +msgstr "Rediger denne siden" + +msgid "By the" +msgstr "Ved" + +msgid "next page" +msgstr "neste side" diff --git a/_static/locales/pl/LC_MESSAGES/booktheme.mo b/_static/locales/pl/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..9ebb584f7 Binary files /dev/null and b/_static/locales/pl/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/pl/LC_MESSAGES/booktheme.po b/_static/locales/pl/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..1b7233f4f --- /dev/null +++ b/_static/locales/pl/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Drukuj do PDF" + +msgid "Theme by the" +msgstr "Motyw autorstwa" + +msgid "Download source file" +msgstr "Pobierz plik źródłowy" + +msgid "open issue" +msgstr "otwarty problem" + +msgid "Contents" +msgstr "Zawartość" + +msgid "previous page" +msgstr "Poprzednia strona" + +msgid "Download notebook file" +msgstr "Pobierz plik notatnika" + +msgid "Copyright" +msgstr "prawa autorskie" + +msgid "Download this page" +msgstr "Pobierz tę stronę" + +msgid "Source repository" +msgstr "Repozytorium źródłowe" + +msgid "By" +msgstr "Przez" + +msgid "repository" +msgstr "magazyn" + +msgid "Last updated on" +msgstr "Ostatnia aktualizacja" + +msgid "Toggle navigation" +msgstr "Przełącz nawigację" + +msgid "Sphinx Book Theme" +msgstr "Motyw książki Sphinx" + +msgid "suggest edit" +msgstr "zaproponuj edycję" + +msgid "Open an issue" +msgstr "Otwórz problem" + +msgid "Launch" +msgstr "Uruchomić" + +msgid "Fullscreen mode" +msgstr "Pełny ekran" + +msgid "Edit this page" +msgstr "Edytuj tę strone" + +msgid "By the" +msgstr "Przez" + +msgid "next page" +msgstr "Następna strona" diff --git a/_static/locales/pt/LC_MESSAGES/booktheme.mo b/_static/locales/pt/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..d0ddb8728 Binary files /dev/null and b/_static/locales/pt/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/pt/LC_MESSAGES/booktheme.po b/_static/locales/pt/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..1b27314d6 --- /dev/null +++ b/_static/locales/pt/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Imprimir em PDF" + +msgid "Theme by the" +msgstr "Tema por" + +msgid "Download source file" +msgstr "Baixar arquivo fonte" + +msgid "open issue" +msgstr "questão aberta" + +msgid "Contents" +msgstr "Conteúdo" + +msgid "previous page" +msgstr "página anterior" + +msgid "Download notebook file" +msgstr "Baixar arquivo de notebook" + +msgid "Copyright" +msgstr "direito autoral" + +msgid "Download this page" +msgstr "Baixe esta página" + +msgid "Source repository" +msgstr "Repositório fonte" + +msgid "By" +msgstr "De" + +msgid "repository" +msgstr "repositório" + +msgid "Last updated on" +msgstr "Última atualização em" + +msgid "Toggle navigation" +msgstr "Alternar de navegação" + +msgid "Sphinx Book Theme" +msgstr "Tema do livro Sphinx" + +msgid "suggest edit" +msgstr "sugerir edição" + +msgid "Open an issue" +msgstr "Abra um problema" + +msgid "Launch" +msgstr "Lançamento" + +msgid "Fullscreen mode" +msgstr "Modo tela cheia" + +msgid "Edit this page" +msgstr "Edite essa página" + +msgid "By the" +msgstr "Pelo" + +msgid "next page" +msgstr "próxima página" diff --git a/_static/locales/ro/LC_MESSAGES/booktheme.mo b/_static/locales/ro/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..3c36ab1df Binary files /dev/null and b/_static/locales/ro/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ro/LC_MESSAGES/booktheme.po b/_static/locales/ro/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..1783ad2c4 --- /dev/null +++ b/_static/locales/ro/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ro\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Imprimați în PDF" + +msgid "Theme by the" +msgstr "Tema de" + +msgid "Download source file" +msgstr "Descărcați fișierul sursă" + +msgid "open issue" +msgstr "problema deschisă" + +msgid "Contents" +msgstr "Cuprins" + +msgid "previous page" +msgstr "pagina anterioară" + +msgid "Download notebook file" +msgstr "Descărcați fișierul notebook" + +msgid "Copyright" +msgstr "Drepturi de autor" + +msgid "Download this page" +msgstr "Descarcă această pagină" + +msgid "Source repository" +msgstr "Depozit sursă" + +msgid "By" +msgstr "De" + +msgid "repository" +msgstr "repertoriu" + +msgid "Last updated on" +msgstr "Ultima actualizare la" + +msgid "Toggle navigation" +msgstr "Comutare navigare" + +msgid "Sphinx Book Theme" +msgstr "Tema Sphinx Book" + +msgid "suggest edit" +msgstr "sugerează editare" + +msgid "Open an issue" +msgstr "Deschideți o problemă" + +msgid "Launch" +msgstr "Lansa" + +msgid "Fullscreen mode" +msgstr "Modul ecran întreg" + +msgid "Edit this page" +msgstr "Editați această pagină" + +msgid "By the" +msgstr "Langa" + +msgid "next page" +msgstr "pagina următoare" diff --git a/_static/locales/ru/LC_MESSAGES/booktheme.mo b/_static/locales/ru/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..6b8ca41f3 Binary files /dev/null and b/_static/locales/ru/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ru/LC_MESSAGES/booktheme.po b/_static/locales/ru/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..b1176b7ae --- /dev/null +++ b/_static/locales/ru/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Распечатать в PDF" + +msgid "Theme by the" +msgstr "Тема от" + +msgid "Download source file" +msgstr "Скачать исходный файл" + +msgid "open issue" +msgstr "открытый вопрос" + +msgid "Contents" +msgstr "Содержание" + +msgid "previous page" +msgstr "Предыдущая страница" + +msgid "Download notebook file" +msgstr "Скачать файл записной книжки" + +msgid "Copyright" +msgstr "авторское право" + +msgid "Download this page" +msgstr "Загрузите эту страницу" + +msgid "Source repository" +msgstr "Исходный репозиторий" + +msgid "By" +msgstr "По" + +msgid "repository" +msgstr "хранилище" + +msgid "Last updated on" +msgstr "Последнее обновление" + +msgid "Toggle navigation" +msgstr "Переключить навигацию" + +msgid "Sphinx Book Theme" +msgstr "Тема книги Сфинкс" + +msgid "suggest edit" +msgstr "предложить редактировать" + +msgid "Open an issue" +msgstr "Открыть вопрос" + +msgid "Launch" +msgstr "Запуск" + +msgid "Fullscreen mode" +msgstr "Полноэкранный режим" + +msgid "Edit this page" +msgstr "Редактировать эту страницу" + +msgid "By the" +msgstr "Посредством" + +msgid "next page" +msgstr "Следующая страница" diff --git a/_static/locales/sk/LC_MESSAGES/booktheme.mo b/_static/locales/sk/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..59bd0ddfa Binary files /dev/null and b/_static/locales/sk/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/sk/LC_MESSAGES/booktheme.po b/_static/locales/sk/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..650128817 --- /dev/null +++ b/_static/locales/sk/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sk\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Tlač do PDF" + +msgid "Theme by the" +msgstr "Téma od" + +msgid "Download source file" +msgstr "Stiahnite si zdrojový súbor" + +msgid "open issue" +msgstr "otvorené vydanie" + +msgid "Contents" +msgstr "Obsah" + +msgid "previous page" +msgstr "predchádzajúca strana" + +msgid "Download notebook file" +msgstr "Stiahnite si zošit" + +msgid "Copyright" +msgstr "Autorské práva" + +msgid "Download this page" +msgstr "Stiahnite si túto stránku" + +msgid "Source repository" +msgstr "Zdrojové úložisko" + +msgid "By" +msgstr "Autor:" + +msgid "repository" +msgstr "Úložisko" + +msgid "Last updated on" +msgstr "Posledná aktualizácia dňa" + +msgid "Toggle navigation" +msgstr "Prepnúť navigáciu" + +msgid "Sphinx Book Theme" +msgstr "Téma knihy Sfinga" + +msgid "suggest edit" +msgstr "navrhnúť úpravu" + +msgid "Open an issue" +msgstr "Otvorte problém" + +msgid "Launch" +msgstr "Spustiť" + +msgid "Fullscreen mode" +msgstr "Režim celej obrazovky" + +msgid "Edit this page" +msgstr "Upraviť túto stránku" + +msgid "By the" +msgstr "Podľa" + +msgid "next page" +msgstr "ďalšia strana" diff --git a/_static/locales/sl/LC_MESSAGES/booktheme.mo b/_static/locales/sl/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..87bf26de6 Binary files /dev/null and b/_static/locales/sl/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/sl/LC_MESSAGES/booktheme.po b/_static/locales/sl/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..3c7e3a866 --- /dev/null +++ b/_static/locales/sl/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Natisni v PDF" + +msgid "Theme by the" +msgstr "Tema avtorja" + +msgid "Download source file" +msgstr "Prenesite izvorno datoteko" + +msgid "open issue" +msgstr "odprto vprašanje" + +msgid "Contents" +msgstr "Vsebina" + +msgid "previous page" +msgstr "Prejšnja stran" + +msgid "Download notebook file" +msgstr "Prenesite datoteko zvezka" + +msgid "Copyright" +msgstr "avtorske pravice" + +msgid "Download this page" +msgstr "Prenesite to stran" + +msgid "Source repository" +msgstr "Izvorno skladišče" + +msgid "By" +msgstr "Avtor" + +msgid "repository" +msgstr "odlagališče" + +msgid "Last updated on" +msgstr "Nazadnje posodobljeno dne" + +msgid "Toggle navigation" +msgstr "Preklopi navigacijo" + +msgid "Sphinx Book Theme" +msgstr "Tema knjige Sphinx" + +msgid "suggest edit" +msgstr "predlagajte urejanje" + +msgid "Open an issue" +msgstr "Odprite številko" + +msgid "Launch" +msgstr "Kosilo" + +msgid "Fullscreen mode" +msgstr "Celozaslonski način" + +msgid "Edit this page" +msgstr "Uredite to stran" + +msgid "By the" +msgstr "Avtor" + +msgid "next page" +msgstr "Naslednja stran" diff --git a/_static/locales/sr/LC_MESSAGES/booktheme.mo b/_static/locales/sr/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..ec740f485 Binary files /dev/null and b/_static/locales/sr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/sr/LC_MESSAGES/booktheme.po b/_static/locales/sr/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..773b8adae --- /dev/null +++ b/_static/locales/sr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Испис у ПДФ" + +msgid "Theme by the" +msgstr "Тхеме би" + +msgid "Download source file" +msgstr "Преузми изворну датотеку" + +msgid "open issue" +msgstr "отворено издање" + +msgid "Contents" +msgstr "Садржај" + +msgid "previous page" +msgstr "Претходна страница" + +msgid "Download notebook file" +msgstr "Преузмите датотеку бележнице" + +msgid "Copyright" +msgstr "Ауторско право" + +msgid "Download this page" +msgstr "Преузмите ову страницу" + +msgid "Source repository" +msgstr "Изворно спремиште" + +msgid "By" +msgstr "Од стране" + +msgid "repository" +msgstr "спремиште" + +msgid "Last updated on" +msgstr "Последње ажурирање" + +msgid "Toggle navigation" +msgstr "Укључи / искључи навигацију" + +msgid "Sphinx Book Theme" +msgstr "Тема књиге Спхинк" + +msgid "suggest edit" +msgstr "предложи уређивање" + +msgid "Open an issue" +msgstr "Отворите издање" + +msgid "Launch" +msgstr "Лансирање" + +msgid "Fullscreen mode" +msgstr "Режим целог екрана" + +msgid "Edit this page" +msgstr "Уредите ову страницу" + +msgid "By the" +msgstr "Од" + +msgid "next page" +msgstr "Следећа страна" diff --git a/_static/locales/sv/LC_MESSAGES/booktheme.mo b/_static/locales/sv/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..b07dc76ff Binary files /dev/null and b/_static/locales/sv/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/sv/LC_MESSAGES/booktheme.po b/_static/locales/sv/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..bcac54c07 --- /dev/null +++ b/_static/locales/sv/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Skriv ut till PDF" + +msgid "Theme by the" +msgstr "Tema av" + +msgid "Download source file" +msgstr "Ladda ner källfil" + +msgid "open issue" +msgstr "öppna problemrapport" + +msgid "Contents" +msgstr "Innehåll" + +msgid "previous page" +msgstr "föregående sida" + +msgid "Download notebook file" +msgstr "Ladda ner notebook-fil" + +msgid "Copyright" +msgstr "Upphovsrätt" + +msgid "Download this page" +msgstr "Ladda ner den här sidan" + +msgid "Source repository" +msgstr "Källkodsrepositorium" + +msgid "By" +msgstr "Av" + +msgid "repository" +msgstr "repositorium" + +msgid "Last updated on" +msgstr "Senast uppdaterad den" + +msgid "Toggle navigation" +msgstr "Växla navigering" + +msgid "Sphinx Book Theme" +msgstr "Sphinx Boktema" + +msgid "suggest edit" +msgstr "föreslå ändring" + +msgid "Open an issue" +msgstr "Öppna en problemrapport" + +msgid "Launch" +msgstr "Öppna" + +msgid "Fullscreen mode" +msgstr "Fullskärmsläge" + +msgid "Edit this page" +msgstr "Redigera den här sidan" + +msgid "By the" +msgstr "Av den" + +msgid "next page" +msgstr "nästa sida" diff --git a/_static/locales/ta/LC_MESSAGES/booktheme.mo b/_static/locales/ta/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..29f52e1f6 Binary files /dev/null and b/_static/locales/ta/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ta/LC_MESSAGES/booktheme.po b/_static/locales/ta/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..b48bdfaf1 --- /dev/null +++ b/_static/locales/ta/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ta\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "PDF இல் அச்சிடுக" + +msgid "Theme by the" +msgstr "வழங்கிய தீம்" + +msgid "Download source file" +msgstr "மூல கோப்பைப் பதிவிறக்குக" + +msgid "open issue" +msgstr "திறந்த பிரச்சினை" + +msgid "previous page" +msgstr "முந்தைய பக்கம்" + +msgid "Download notebook file" +msgstr "நோட்புக் கோப்பைப் பதிவிறக்கவும்" + +msgid "Copyright" +msgstr "பதிப்புரிமை" + +msgid "Download this page" +msgstr "இந்தப் பக்கத்தைப் பதிவிறக்கவும்" + +msgid "Source repository" +msgstr "மூல களஞ்சியம்" + +msgid "By" +msgstr "வழங்கியவர்" + +msgid "Last updated on" +msgstr "கடைசியாக புதுப்பிக்கப்பட்டது" + +msgid "Toggle navigation" +msgstr "வழிசெலுத்தலை நிலைமாற்று" + +msgid "Sphinx Book Theme" +msgstr "ஸ்பிங்க்ஸ் புத்தக தீம்" + +msgid "suggest edit" +msgstr "திருத்த பரிந்துரைக்கவும்" + +msgid "Open an issue" +msgstr "சிக்கலைத் திறக்கவும்" + +msgid "Launch" +msgstr "தொடங்க" + +msgid "Edit this page" +msgstr "இந்தப் பக்கத்தைத் திருத்தவும்" + +msgid "By the" +msgstr "மூலம்" + +msgid "next page" +msgstr "அடுத்த பக்கம்" diff --git a/_static/locales/te/LC_MESSAGES/booktheme.mo b/_static/locales/te/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..0a5f4b46a Binary files /dev/null and b/_static/locales/te/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/te/LC_MESSAGES/booktheme.po b/_static/locales/te/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..952278f5f --- /dev/null +++ b/_static/locales/te/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: te\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "PDF కి ముద్రించండి" + +msgid "Theme by the" +msgstr "ద్వారా థీమ్" + +msgid "Download source file" +msgstr "మూల ఫైల్‌ను డౌన్‌లోడ్ చేయండి" + +msgid "open issue" +msgstr "ఓపెన్ ఇష్యూ" + +msgid "previous page" +msgstr "ముందు పేజి" + +msgid "Download notebook file" +msgstr "నోట్బుక్ ఫైల్ను డౌన్లోడ్ చేయండి" + +msgid "Copyright" +msgstr "కాపీరైట్" + +msgid "Download this page" +msgstr "ఈ పేజీని డౌన్‌లోడ్ చేయండి" + +msgid "Source repository" +msgstr "మూల రిపోజిటరీ" + +msgid "By" +msgstr "ద్వారా" + +msgid "Last updated on" +msgstr "చివరిగా నవీకరించబడింది" + +msgid "Toggle navigation" +msgstr "నావిగేషన్‌ను టోగుల్ చేయండి" + +msgid "Sphinx Book Theme" +msgstr "సింహిక పుస్తక థీమ్" + +msgid "suggest edit" +msgstr "సవరించమని సూచించండి" + +msgid "Open an issue" +msgstr "సమస్యను తెరవండి" + +msgid "Launch" +msgstr "ప్రారంభించండి" + +msgid "Edit this page" +msgstr "ఈ పేజీని సవరించండి" + +msgid "By the" +msgstr "ద్వారా" + +msgid "next page" +msgstr "తరువాతి పేజీ" diff --git a/_static/locales/tg/LC_MESSAGES/booktheme.mo b/_static/locales/tg/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..b21c6c634 Binary files /dev/null and b/_static/locales/tg/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/tg/LC_MESSAGES/booktheme.po b/_static/locales/tg/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..c33dc4217 --- /dev/null +++ b/_static/locales/tg/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Чоп ба PDF" + +msgid "Theme by the" +msgstr "Мавзӯъи аз" + +msgid "Download source file" +msgstr "Файли манбаъро зеркашӣ кунед" + +msgid "open issue" +msgstr "барориши кушод" + +msgid "Contents" +msgstr "Мундариҷа" + +msgid "previous page" +msgstr "саҳифаи қаблӣ" + +msgid "Download notebook file" +msgstr "Файли дафтарро зеркашӣ кунед" + +msgid "Copyright" +msgstr "Ҳуқуқи муаллиф" + +msgid "Download this page" +msgstr "Ин саҳифаро зеркашӣ кунед" + +msgid "Source repository" +msgstr "Анбори манбаъ" + +msgid "By" +msgstr "Бо" + +msgid "repository" +msgstr "анбор" + +msgid "Last updated on" +msgstr "Last навсозӣ дар" + +msgid "Toggle navigation" +msgstr "Гузаришро иваз кунед" + +msgid "Sphinx Book Theme" +msgstr "Сфинкс Мавзӯи китоб" + +msgid "suggest edit" +msgstr "пешниҳод вироиш" + +msgid "Open an issue" +msgstr "Масъаларо кушоед" + +msgid "Launch" +msgstr "Оғоз" + +msgid "Fullscreen mode" +msgstr "Ҳолати экрани пурра" + +msgid "Edit this page" +msgstr "Ин саҳифаро таҳрир кунед" + +msgid "By the" +msgstr "Бо" + +msgid "next page" +msgstr "саҳифаи оянда" diff --git a/_static/locales/th/LC_MESSAGES/booktheme.mo b/_static/locales/th/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..abede98aa Binary files /dev/null and b/_static/locales/th/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/th/LC_MESSAGES/booktheme.po b/_static/locales/th/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..9d24294a7 --- /dev/null +++ b/_static/locales/th/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: th\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "พิมพ์เป็น PDF" + +msgid "Theme by the" +msgstr "ธีมโดย" + +msgid "Download source file" +msgstr "ดาวน์โหลดไฟล์ต้นฉบับ" + +msgid "open issue" +msgstr "เปิดปัญหา" + +msgid "Contents" +msgstr "สารบัญ" + +msgid "previous page" +msgstr "หน้าที่แล้ว" + +msgid "Download notebook file" +msgstr "ดาวน์โหลดไฟล์สมุดบันทึก" + +msgid "Copyright" +msgstr "ลิขสิทธิ์" + +msgid "Download this page" +msgstr "ดาวน์โหลดหน้านี้" + +msgid "Source repository" +msgstr "ที่เก็บซอร์ส" + +msgid "By" +msgstr "โดย" + +msgid "repository" +msgstr "ที่เก็บ" + +msgid "Last updated on" +msgstr "ปรับปรุงล่าสุดเมื่อ" + +msgid "Toggle navigation" +msgstr "ไม่ต้องสลับช่องทาง" + +msgid "Sphinx Book Theme" +msgstr "ธีมหนังสือสฟิงซ์" + +msgid "suggest edit" +msgstr "แนะนำแก้ไข" + +msgid "Open an issue" +msgstr "เปิดปัญหา" + +msgid "Launch" +msgstr "เปิด" + +msgid "Fullscreen mode" +msgstr "โหมดเต็มหน้าจอ" + +msgid "Edit this page" +msgstr "แก้ไขหน้านี้" + +msgid "By the" +msgstr "โดย" + +msgid "next page" +msgstr "หน้าต่อไป" diff --git a/_static/locales/tl/LC_MESSAGES/booktheme.mo b/_static/locales/tl/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..8df1b7331 Binary files /dev/null and b/_static/locales/tl/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/tl/LC_MESSAGES/booktheme.po b/_static/locales/tl/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..20e0d07ce --- /dev/null +++ b/_static/locales/tl/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "I-print sa PDF" + +msgid "Theme by the" +msgstr "Tema ng" + +msgid "Download source file" +msgstr "Mag-download ng file ng pinagmulan" + +msgid "open issue" +msgstr "bukas na isyu" + +msgid "previous page" +msgstr "Nakaraang pahina" + +msgid "Download notebook file" +msgstr "Mag-download ng file ng notebook" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Download this page" +msgstr "I-download ang pahinang ito" + +msgid "Source repository" +msgstr "Pinagmulan ng imbakan" + +msgid "By" +msgstr "Ni" + +msgid "Last updated on" +msgstr "Huling na-update noong" + +msgid "Toggle navigation" +msgstr "I-toggle ang pag-navigate" + +msgid "Sphinx Book Theme" +msgstr "Tema ng Sphinx Book" + +msgid "suggest edit" +msgstr "iminumungkahi i-edit" + +msgid "Open an issue" +msgstr "Magbukas ng isyu" + +msgid "Launch" +msgstr "Ilunsad" + +msgid "Edit this page" +msgstr "I-edit ang pahinang ito" + +msgid "By the" +msgstr "Sa pamamagitan ng" + +msgid "next page" +msgstr "Susunod na pahina" diff --git a/_static/locales/tr/LC_MESSAGES/booktheme.mo b/_static/locales/tr/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..029ae18af Binary files /dev/null and b/_static/locales/tr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/tr/LC_MESSAGES/booktheme.po b/_static/locales/tr/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..a77eb0273 --- /dev/null +++ b/_static/locales/tr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "PDF olarak yazdır" + +msgid "Theme by the" +msgstr "Tarafından tema" + +msgid "Download source file" +msgstr "Kaynak dosyayı indirin" + +msgid "open issue" +msgstr "Açık konu" + +msgid "Contents" +msgstr "İçindekiler" + +msgid "previous page" +msgstr "önceki sayfa" + +msgid "Download notebook file" +msgstr "Defter dosyasını indirin" + +msgid "Copyright" +msgstr "Telif hakkı" + +msgid "Download this page" +msgstr "Bu sayfayı indirin" + +msgid "Source repository" +msgstr "Kaynak kod deposu" + +msgid "By" +msgstr "Tarafından" + +msgid "repository" +msgstr "depo" + +msgid "Last updated on" +msgstr "Son güncelleme tarihi" + +msgid "Toggle navigation" +msgstr "Gezinmeyi değiştir" + +msgid "Sphinx Book Theme" +msgstr "Sfenks Kitap Teması" + +msgid "suggest edit" +msgstr "düzenleme öner" + +msgid "Open an issue" +msgstr "Bir sorunu açın" + +msgid "Launch" +msgstr "Başlatmak" + +msgid "Fullscreen mode" +msgstr "Tam ekran modu" + +msgid "Edit this page" +msgstr "Bu sayfayı düzenle" + +msgid "By the" +msgstr "Tarafından" + +msgid "next page" +msgstr "sonraki Sayfa" diff --git a/_static/locales/uk/LC_MESSAGES/booktheme.mo b/_static/locales/uk/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..16ab78909 Binary files /dev/null and b/_static/locales/uk/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/uk/LC_MESSAGES/booktheme.po b/_static/locales/uk/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..993dd0781 --- /dev/null +++ b/_static/locales/uk/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: uk\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "Друк у форматі PDF" + +msgid "Theme by the" +msgstr "Тема від" + +msgid "Download source file" +msgstr "Завантажити вихідний файл" + +msgid "open issue" +msgstr "відкритий випуск" + +msgid "Contents" +msgstr "Зміст" + +msgid "previous page" +msgstr "Попередня сторінка" + +msgid "Download notebook file" +msgstr "Завантажте файл блокнота" + +msgid "Copyright" +msgstr "Авторське право" + +msgid "Download this page" +msgstr "Завантажте цю сторінку" + +msgid "Source repository" +msgstr "Джерело сховища" + +msgid "By" +msgstr "Автор" + +msgid "repository" +msgstr "сховище" + +msgid "Last updated on" +msgstr "Останнє оновлення:" + +msgid "Toggle navigation" +msgstr "Переключити навігацію" + +msgid "Sphinx Book Theme" +msgstr "Тема книги \"Сфінкс\"" + +msgid "suggest edit" +msgstr "запропонувати редагувати" + +msgid "Open an issue" +msgstr "Відкрийте випуск" + +msgid "Launch" +msgstr "Запуск" + +msgid "Fullscreen mode" +msgstr "Повноекранний режим" + +msgid "Edit this page" +msgstr "Редагувати цю сторінку" + +msgid "By the" +msgstr "По" + +msgid "next page" +msgstr "Наступна сторінка" diff --git a/_static/locales/ur/LC_MESSAGES/booktheme.mo b/_static/locales/ur/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..de8c84b93 Binary files /dev/null and b/_static/locales/ur/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ur/LC_MESSAGES/booktheme.po b/_static/locales/ur/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..2f774267f --- /dev/null +++ b/_static/locales/ur/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ur\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "پی ڈی ایف پرنٹ کریں" + +msgid "Theme by the" +msgstr "کے ذریعہ تھیم" + +msgid "Download source file" +msgstr "سورس فائل ڈاؤن لوڈ کریں" + +msgid "open issue" +msgstr "کھلا مسئلہ" + +msgid "previous page" +msgstr "سابقہ ​​صفحہ" + +msgid "Download notebook file" +msgstr "نوٹ بک فائل ڈاؤن لوڈ کریں" + +msgid "Copyright" +msgstr "کاپی رائٹ" + +msgid "Download this page" +msgstr "اس صفحے کو ڈاؤن لوڈ کریں" + +msgid "Source repository" +msgstr "ماخذ ذخیرہ" + +msgid "By" +msgstr "بذریعہ" + +msgid "Last updated on" +msgstr "آخری بار تازہ کاری ہوئی" + +msgid "Toggle navigation" +msgstr "نیویگیشن ٹوگل کریں" + +msgid "Sphinx Book Theme" +msgstr "سپنکس بک تھیم" + +msgid "suggest edit" +msgstr "ترمیم کی تجویز کریں" + +msgid "Open an issue" +msgstr "ایک مسئلہ کھولیں" + +msgid "Launch" +msgstr "لانچ کریں" + +msgid "Edit this page" +msgstr "اس صفحے میں ترمیم کریں" + +msgid "By the" +msgstr "کی طرف" + +msgid "next page" +msgstr "اگلا صفحہ" diff --git a/_static/locales/vi/LC_MESSAGES/booktheme.mo b/_static/locales/vi/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..2bb32555c Binary files /dev/null and b/_static/locales/vi/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/vi/LC_MESSAGES/booktheme.po b/_static/locales/vi/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..33159f3ef --- /dev/null +++ b/_static/locales/vi/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: vi\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "In sang PDF" + +msgid "Theme by the" +msgstr "Chủ đề của" + +msgid "Download source file" +msgstr "Tải xuống tệp nguồn" + +msgid "open issue" +msgstr "vấn đề mở" + +msgid "Contents" +msgstr "Nội dung" + +msgid "previous page" +msgstr "trang trước" + +msgid "Download notebook file" +msgstr "Tải xuống tệp sổ tay" + +msgid "Copyright" +msgstr "Bản quyền" + +msgid "Download this page" +msgstr "Tải xuống trang này" + +msgid "Source repository" +msgstr "Kho nguồn" + +msgid "By" +msgstr "Bởi" + +msgid "repository" +msgstr "kho" + +msgid "Last updated on" +msgstr "Cập nhật lần cuối vào" + +msgid "Toggle navigation" +msgstr "Chuyển đổi điều hướng thành" + +msgid "Sphinx Book Theme" +msgstr "Chủ đề sách nhân sư" + +msgid "suggest edit" +msgstr "đề nghị chỉnh sửa" + +msgid "Open an issue" +msgstr "Mở một vấn đề" + +msgid "Launch" +msgstr "Phóng" + +msgid "Fullscreen mode" +msgstr "Chế độ toàn màn hình" + +msgid "Edit this page" +msgstr "chỉnh sửa trang này" + +msgid "By the" +msgstr "Bằng" + +msgid "next page" +msgstr "Trang tiếp theo" diff --git a/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo b/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..0e3235d09 Binary files /dev/null and b/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/zh_CN/LC_MESSAGES/booktheme.po b/_static/locales/zh_CN/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..2e519ef45 --- /dev/null +++ b/_static/locales/zh_CN/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_CN\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "列印成 PDF" + +msgid "Theme by the" +msgstr "主题作者:" + +msgid "Download source file" +msgstr "下载源文件" + +msgid "open issue" +msgstr "创建议题" + +msgid "Contents" +msgstr "目录" + +msgid "previous page" +msgstr "上一页" + +msgid "Download notebook file" +msgstr "下载笔记本文件" + +msgid "Copyright" +msgstr "版权" + +msgid "Download this page" +msgstr "下载此页面" + +msgid "Source repository" +msgstr "源码库" + +msgid "By" +msgstr "作者:" + +msgid "repository" +msgstr "仓库" + +msgid "Last updated on" +msgstr "上次更新时间:" + +msgid "Toggle navigation" +msgstr "显示或隐藏导航栏" + +msgid "Sphinx Book Theme" +msgstr "Sphinx Book 主题" + +msgid "suggest edit" +msgstr "提出修改建议" + +msgid "Open an issue" +msgstr "创建议题" + +msgid "Launch" +msgstr "启动" + +msgid "Fullscreen mode" +msgstr "全屏模式" + +msgid "Edit this page" +msgstr "编辑此页面" + +msgid "By the" +msgstr "作者:" + +msgid "next page" +msgstr "下一页" diff --git a/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo b/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo new file mode 100644 index 000000000..9116fa95d Binary files /dev/null and b/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/zh_TW/LC_MESSAGES/booktheme.po b/_static/locales/zh_TW/LC_MESSAGES/booktheme.po new file mode 100644 index 000000000..beecb076b --- /dev/null +++ b/_static/locales/zh_TW/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Print to PDF" +msgstr "列印成 PDF" + +msgid "Theme by the" +msgstr "佈景主題作者:" + +msgid "Download source file" +msgstr "下載原始檔" + +msgid "open issue" +msgstr "公開的問題" + +msgid "Contents" +msgstr "目錄" + +msgid "previous page" +msgstr "上一頁" + +msgid "Download notebook file" +msgstr "下載 Notebook 檔案" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Download this page" +msgstr "下載此頁面" + +msgid "Source repository" +msgstr "來源儲存庫" + +msgid "By" +msgstr "作者:" + +msgid "repository" +msgstr "儲存庫" + +msgid "Last updated on" +msgstr "最後更新時間:" + +msgid "Toggle navigation" +msgstr "顯示或隱藏導覽列" + +msgid "Sphinx Book Theme" +msgstr "Sphinx Book 佈景主題" + +msgid "suggest edit" +msgstr "提出修改建議" + +msgid "Open an issue" +msgstr "開啟議題" + +msgid "Launch" +msgstr "啟動" + +msgid "Fullscreen mode" +msgstr "全螢幕模式" + +msgid "Edit this page" +msgstr "編輯此頁面" + +msgid "By the" +msgstr "作者:" + +msgid "next page" +msgstr "下一頁" diff --git a/_static/logo_blendercam.png b/_static/logo_blendercam.png new file mode 100644 index 000000000..ccdcc7d06 Binary files /dev/null and b/_static/logo_blendercam.png differ diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 000000000..d96755fda Binary files /dev/null and b/_static/minus.png differ diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 000000000..7107cec93 Binary files /dev/null and b/_static/plus.png differ diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 000000000..012e6a00a --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,152 @@ +html[data-theme="light"] .highlight pre { line-height: 125%; } +html[data-theme="light"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight .hll { background-color: #fae4c2 } +html[data-theme="light"] .highlight { background: #fefefe; color: #080808 } +html[data-theme="light"] .highlight .c { color: #515151 } /* Comment */ +html[data-theme="light"] .highlight .err { color: #a12236 } /* Error */ +html[data-theme="light"] .highlight .k { color: #6730c5 } /* Keyword */ +html[data-theme="light"] .highlight .l { color: #7f4707 } /* Literal */ +html[data-theme="light"] .highlight .n { color: #080808 } /* Name */ +html[data-theme="light"] .highlight .o { color: #00622f } /* Operator */ +html[data-theme="light"] .highlight .p { color: #080808 } /* Punctuation */ +html[data-theme="light"] .highlight .ch { color: #515151 } /* Comment.Hashbang */ +html[data-theme="light"] .highlight .cm { color: #515151 } /* Comment.Multiline */ +html[data-theme="light"] .highlight .cp { color: #515151 } /* Comment.Preproc */ +html[data-theme="light"] .highlight .cpf { color: #515151 } /* Comment.PreprocFile */ +html[data-theme="light"] .highlight .c1 { color: #515151 } /* Comment.Single */ +html[data-theme="light"] .highlight .cs { color: #515151 } /* Comment.Special */ +html[data-theme="light"] .highlight .gd { color: #005b82 } /* Generic.Deleted */ +html[data-theme="light"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="light"] .highlight .gh { color: #005b82 } /* Generic.Heading */ +html[data-theme="light"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="light"] .highlight .gu { color: #005b82 } /* Generic.Subheading */ +html[data-theme="light"] .highlight .kc { color: #6730c5 } /* Keyword.Constant */ +html[data-theme="light"] .highlight .kd { color: #6730c5 } /* Keyword.Declaration */ +html[data-theme="light"] .highlight .kn { color: #6730c5 } /* Keyword.Namespace */ +html[data-theme="light"] .highlight .kp { color: #6730c5 } /* Keyword.Pseudo */ +html[data-theme="light"] .highlight .kr { color: #6730c5 } /* Keyword.Reserved */ +html[data-theme="light"] .highlight .kt { color: #7f4707 } /* Keyword.Type */ +html[data-theme="light"] .highlight .ld { color: #7f4707 } /* Literal.Date */ +html[data-theme="light"] .highlight .m { color: #7f4707 } /* Literal.Number */ +html[data-theme="light"] .highlight .s { color: #00622f } /* Literal.String */ +html[data-theme="light"] .highlight .na { color: #912583 } /* Name.Attribute */ +html[data-theme="light"] .highlight .nb { color: #7f4707 } /* Name.Builtin */ +html[data-theme="light"] .highlight .nc { color: #005b82 } /* Name.Class */ +html[data-theme="light"] .highlight .no { color: #005b82 } /* Name.Constant */ +html[data-theme="light"] .highlight .nd { color: #7f4707 } /* Name.Decorator */ +html[data-theme="light"] .highlight .ni { color: #00622f } /* Name.Entity */ +html[data-theme="light"] .highlight .ne { color: #6730c5 } /* Name.Exception */ +html[data-theme="light"] .highlight .nf { color: #005b82 } /* Name.Function */ +html[data-theme="light"] .highlight .nl { color: #7f4707 } /* Name.Label */ +html[data-theme="light"] .highlight .nn { color: #080808 } /* Name.Namespace */ +html[data-theme="light"] .highlight .nx { color: #080808 } /* Name.Other */ +html[data-theme="light"] .highlight .py { color: #005b82 } /* Name.Property */ +html[data-theme="light"] .highlight .nt { color: #005b82 } /* Name.Tag */ +html[data-theme="light"] .highlight .nv { color: #a12236 } /* Name.Variable */ +html[data-theme="light"] .highlight .ow { color: #6730c5 } /* Operator.Word */ +html[data-theme="light"] .highlight .pm { color: #080808 } /* Punctuation.Marker */ +html[data-theme="light"] .highlight .w { color: #080808 } /* Text.Whitespace */ +html[data-theme="light"] .highlight .mb { color: #7f4707 } /* Literal.Number.Bin */ +html[data-theme="light"] .highlight .mf { color: #7f4707 } /* Literal.Number.Float */ +html[data-theme="light"] .highlight .mh { color: #7f4707 } /* Literal.Number.Hex */ +html[data-theme="light"] .highlight .mi { color: #7f4707 } /* Literal.Number.Integer */ +html[data-theme="light"] .highlight .mo { color: #7f4707 } /* Literal.Number.Oct */ +html[data-theme="light"] .highlight .sa { color: #00622f } /* Literal.String.Affix */ +html[data-theme="light"] .highlight .sb { color: #00622f } /* Literal.String.Backtick */ +html[data-theme="light"] .highlight .sc { color: #00622f } /* Literal.String.Char */ +html[data-theme="light"] .highlight .dl { color: #00622f } /* Literal.String.Delimiter */ +html[data-theme="light"] .highlight .sd { color: #00622f } /* Literal.String.Doc */ +html[data-theme="light"] .highlight .s2 { color: #00622f } /* Literal.String.Double */ +html[data-theme="light"] .highlight .se { color: #00622f } /* Literal.String.Escape */ +html[data-theme="light"] .highlight .sh { color: #00622f } /* Literal.String.Heredoc */ +html[data-theme="light"] .highlight .si { color: #00622f } /* Literal.String.Interpol */ +html[data-theme="light"] .highlight .sx { color: #00622f } /* Literal.String.Other */ +html[data-theme="light"] .highlight .sr { color: #a12236 } /* Literal.String.Regex */ +html[data-theme="light"] .highlight .s1 { color: #00622f } /* Literal.String.Single */ +html[data-theme="light"] .highlight .ss { color: #005b82 } /* Literal.String.Symbol */ +html[data-theme="light"] .highlight .bp { color: #7f4707 } /* Name.Builtin.Pseudo */ +html[data-theme="light"] .highlight .fm { color: #005b82 } /* Name.Function.Magic */ +html[data-theme="light"] .highlight .vc { color: #a12236 } /* Name.Variable.Class */ +html[data-theme="light"] .highlight .vg { color: #a12236 } /* Name.Variable.Global */ +html[data-theme="light"] .highlight .vi { color: #a12236 } /* Name.Variable.Instance */ +html[data-theme="light"] .highlight .vm { color: #7f4707 } /* Name.Variable.Magic */ +html[data-theme="light"] .highlight .il { color: #7f4707 } /* Literal.Number.Integer.Long */ +html[data-theme="dark"] .highlight pre { line-height: 125%; } +html[data-theme="dark"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight .hll { background-color: #ffd9002e } +html[data-theme="dark"] .highlight { background: #2b2b2b; color: #f8f8f2 } +html[data-theme="dark"] .highlight .c { color: #ffd900 } /* Comment */ +html[data-theme="dark"] .highlight .err { color: #ffa07a } /* Error */ +html[data-theme="dark"] .highlight .k { color: #dcc6e0 } /* Keyword */ +html[data-theme="dark"] .highlight .l { color: #ffd900 } /* Literal */ +html[data-theme="dark"] .highlight .n { color: #f8f8f2 } /* Name */ +html[data-theme="dark"] .highlight .o { color: #abe338 } /* Operator */ +html[data-theme="dark"] .highlight .p { color: #f8f8f2 } /* Punctuation */ +html[data-theme="dark"] .highlight .ch { color: #ffd900 } /* Comment.Hashbang */ +html[data-theme="dark"] .highlight .cm { color: #ffd900 } /* Comment.Multiline */ +html[data-theme="dark"] .highlight .cp { color: #ffd900 } /* Comment.Preproc */ +html[data-theme="dark"] .highlight .cpf { color: #ffd900 } /* Comment.PreprocFile */ +html[data-theme="dark"] .highlight .c1 { color: #ffd900 } /* Comment.Single */ +html[data-theme="dark"] .highlight .cs { color: #ffd900 } /* Comment.Special */ +html[data-theme="dark"] .highlight .gd { color: #00e0e0 } /* Generic.Deleted */ +html[data-theme="dark"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="dark"] .highlight .gh { color: #00e0e0 } /* Generic.Heading */ +html[data-theme="dark"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="dark"] .highlight .gu { color: #00e0e0 } /* Generic.Subheading */ +html[data-theme="dark"] .highlight .kc { color: #dcc6e0 } /* Keyword.Constant */ +html[data-theme="dark"] .highlight .kd { color: #dcc6e0 } /* Keyword.Declaration */ +html[data-theme="dark"] .highlight .kn { color: #dcc6e0 } /* Keyword.Namespace */ +html[data-theme="dark"] .highlight .kp { color: #dcc6e0 } /* Keyword.Pseudo */ +html[data-theme="dark"] .highlight .kr { color: #dcc6e0 } /* Keyword.Reserved */ +html[data-theme="dark"] .highlight .kt { color: #ffd900 } /* Keyword.Type */ +html[data-theme="dark"] .highlight .ld { color: #ffd900 } /* Literal.Date */ +html[data-theme="dark"] .highlight .m { color: #ffd900 } /* Literal.Number */ +html[data-theme="dark"] .highlight .s { color: #abe338 } /* Literal.String */ +html[data-theme="dark"] .highlight .na { color: #ffd900 } /* Name.Attribute */ +html[data-theme="dark"] .highlight .nb { color: #ffd900 } /* Name.Builtin */ +html[data-theme="dark"] .highlight .nc { color: #00e0e0 } /* Name.Class */ +html[data-theme="dark"] .highlight .no { color: #00e0e0 } /* Name.Constant */ +html[data-theme="dark"] .highlight .nd { color: #ffd900 } /* Name.Decorator */ +html[data-theme="dark"] .highlight .ni { color: #abe338 } /* Name.Entity */ +html[data-theme="dark"] .highlight .ne { color: #dcc6e0 } /* Name.Exception */ +html[data-theme="dark"] .highlight .nf { color: #00e0e0 } /* Name.Function */ +html[data-theme="dark"] .highlight .nl { color: #ffd900 } /* Name.Label */ +html[data-theme="dark"] .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ +html[data-theme="dark"] .highlight .nx { color: #f8f8f2 } /* Name.Other */ +html[data-theme="dark"] .highlight .py { color: #00e0e0 } /* Name.Property */ +html[data-theme="dark"] .highlight .nt { color: #00e0e0 } /* Name.Tag */ +html[data-theme="dark"] .highlight .nv { color: #ffa07a } /* Name.Variable */ +html[data-theme="dark"] .highlight .ow { color: #dcc6e0 } /* Operator.Word */ +html[data-theme="dark"] .highlight .pm { color: #f8f8f2 } /* Punctuation.Marker */ +html[data-theme="dark"] .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ +html[data-theme="dark"] .highlight .mb { color: #ffd900 } /* Literal.Number.Bin */ +html[data-theme="dark"] .highlight .mf { color: #ffd900 } /* Literal.Number.Float */ +html[data-theme="dark"] .highlight .mh { color: #ffd900 } /* Literal.Number.Hex */ +html[data-theme="dark"] .highlight .mi { color: #ffd900 } /* Literal.Number.Integer */ +html[data-theme="dark"] .highlight .mo { color: #ffd900 } /* Literal.Number.Oct */ +html[data-theme="dark"] .highlight .sa { color: #abe338 } /* Literal.String.Affix */ +html[data-theme="dark"] .highlight .sb { color: #abe338 } /* Literal.String.Backtick */ +html[data-theme="dark"] .highlight .sc { color: #abe338 } /* Literal.String.Char */ +html[data-theme="dark"] .highlight .dl { color: #abe338 } /* Literal.String.Delimiter */ +html[data-theme="dark"] .highlight .sd { color: #abe338 } /* Literal.String.Doc */ +html[data-theme="dark"] .highlight .s2 { color: #abe338 } /* Literal.String.Double */ +html[data-theme="dark"] .highlight .se { color: #abe338 } /* Literal.String.Escape */ +html[data-theme="dark"] .highlight .sh { color: #abe338 } /* Literal.String.Heredoc */ +html[data-theme="dark"] .highlight .si { color: #abe338 } /* Literal.String.Interpol */ +html[data-theme="dark"] .highlight .sx { color: #abe338 } /* Literal.String.Other */ +html[data-theme="dark"] .highlight .sr { color: #ffa07a } /* Literal.String.Regex */ +html[data-theme="dark"] .highlight .s1 { color: #abe338 } /* Literal.String.Single */ +html[data-theme="dark"] .highlight .ss { color: #00e0e0 } /* Literal.String.Symbol */ +html[data-theme="dark"] .highlight .bp { color: #ffd900 } /* Name.Builtin.Pseudo */ +html[data-theme="dark"] .highlight .fm { color: #00e0e0 } /* Name.Function.Magic */ +html[data-theme="dark"] .highlight .vc { color: #ffa07a } /* Name.Variable.Class */ +html[data-theme="dark"] .highlight .vg { color: #ffa07a } /* Name.Variable.Global */ +html[data-theme="dark"] .highlight .vi { color: #ffa07a } /* Name.Variable.Instance */ +html[data-theme="dark"] .highlight .vm { color: #ffd900 } /* Name.Variable.Magic */ +html[data-theme="dark"] .highlight .il { color: #ffd900 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_static/sbt-webpack-macros.html b/_static/sbt-webpack-macros.html new file mode 100644 index 000000000..6cbf559fa --- /dev/null +++ b/_static/sbt-webpack-macros.html @@ -0,0 +1,11 @@ + +{% macro head_pre_bootstrap() %} + +{% endmacro %} + +{% macro body_post() %} + +{% endmacro %} diff --git a/_static/scripts/bootstrap.js b/_static/scripts/bootstrap.js new file mode 100644 index 000000000..c8178debb --- /dev/null +++ b/_static/scripts/bootstrap.js @@ -0,0 +1,3 @@ +/*! For license information please see bootstrap.js.LICENSE.txt */ +(()=>{"use strict";var t={d:(e,i)=>{for(var n in i)t.o(i,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:i[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.r(e),t.d(e,{afterMain:()=>E,afterRead:()=>v,afterWrite:()=>C,applyStyles:()=>$,arrow:()=>J,auto:()=>a,basePlacements:()=>l,beforeMain:()=>y,beforeRead:()=>_,beforeWrite:()=>A,bottom:()=>s,clippingParents:()=>d,computeStyles:()=>it,createPopper:()=>Dt,createPopperBase:()=>St,createPopperLite:()=>$t,detectOverflow:()=>_t,end:()=>h,eventListeners:()=>st,flip:()=>bt,hide:()=>wt,left:()=>r,main:()=>w,modifierPhases:()=>O,offset:()=>Et,placements:()=>g,popper:()=>f,popperGenerator:()=>Lt,popperOffsets:()=>At,preventOverflow:()=>Tt,read:()=>b,reference:()=>p,right:()=>o,start:()=>c,top:()=>n,variationPlacements:()=>m,viewport:()=>u,write:()=>T});var i={};t.r(i),t.d(i,{Alert:()=>Oe,Button:()=>ke,Carousel:()=>li,Collapse:()=>Ei,Dropdown:()=>Ki,Modal:()=>Ln,Offcanvas:()=>Kn,Popover:()=>bs,ScrollSpy:()=>Ls,Tab:()=>Js,Toast:()=>po,Tooltip:()=>fs});var n="top",s="bottom",o="right",r="left",a="auto",l=[n,s,o,r],c="start",h="end",d="clippingParents",u="viewport",f="popper",p="reference",m=l.reduce((function(t,e){return t.concat([e+"-"+c,e+"-"+h])}),[]),g=[].concat(l,[a]).reduce((function(t,e){return t.concat([e,e+"-"+c,e+"-"+h])}),[]),_="beforeRead",b="read",v="afterRead",y="beforeMain",w="main",E="afterMain",A="beforeWrite",T="write",C="afterWrite",O=[_,b,v,y,w,E,A,T,C];function x(t){return t?(t.nodeName||"").toLowerCase():null}function k(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function L(t){return t instanceof k(t).Element||t instanceof Element}function S(t){return t instanceof k(t).HTMLElement||t instanceof HTMLElement}function D(t){return"undefined"!=typeof ShadowRoot&&(t instanceof k(t).ShadowRoot||t instanceof ShadowRoot)}const $={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];S(s)&&x(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});S(n)&&x(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function I(t){return t.split("-")[0]}var N=Math.max,P=Math.min,M=Math.round;function j(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function F(){return!/^((?!chrome|android).)*safari/i.test(j())}function H(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&S(t)&&(s=t.offsetWidth>0&&M(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&M(n.height)/t.offsetHeight||1);var r=(L(t)?k(t):window).visualViewport,a=!F()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function B(t){var e=H(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function W(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&D(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function z(t){return k(t).getComputedStyle(t)}function R(t){return["table","td","th"].indexOf(x(t))>=0}function q(t){return((L(t)?t.ownerDocument:t.document)||window.document).documentElement}function V(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||(D(t)?t.host:null)||q(t)}function Y(t){return S(t)&&"fixed"!==z(t).position?t.offsetParent:null}function K(t){for(var e=k(t),i=Y(t);i&&R(i)&&"static"===z(i).position;)i=Y(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===z(i).position)?e:i||function(t){var e=/firefox/i.test(j());if(/Trident/i.test(j())&&S(t)&&"fixed"===z(t).position)return null;var i=V(t);for(D(i)&&(i=i.host);S(i)&&["html","body"].indexOf(x(i))<0;){var n=z(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Q(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function X(t,e,i){return N(t,P(e,i))}function U(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function G(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const J={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,c=t.options,h=i.elements.arrow,d=i.modifiersData.popperOffsets,u=I(i.placement),f=Q(u),p=[r,o].indexOf(u)>=0?"height":"width";if(h&&d){var m=function(t,e){return U("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:G(t,l))}(c.padding,i),g=B(h),_="y"===f?n:r,b="y"===f?s:o,v=i.rects.reference[p]+i.rects.reference[f]-d[f]-i.rects.popper[p],y=d[f]-i.rects.reference[f],w=K(h),E=w?"y"===f?w.clientHeight||0:w.clientWidth||0:0,A=v/2-y/2,T=m[_],C=E-g[p]-m[b],O=E/2-g[p]/2+A,x=X(T,O,C),k=f;i.modifiersData[a]=((e={})[k]=x,e.centerOffset=x-O,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&W(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,l=t.placement,c=t.variation,d=t.offsets,u=t.position,f=t.gpuAcceleration,p=t.adaptive,m=t.roundOffsets,g=t.isFixed,_=d.x,b=void 0===_?0:_,v=d.y,y=void 0===v?0:v,w="function"==typeof m?m({x:b,y}):{x:b,y};b=w.x,y=w.y;var E=d.hasOwnProperty("x"),A=d.hasOwnProperty("y"),T=r,C=n,O=window;if(p){var x=K(i),L="clientHeight",S="clientWidth";x===k(i)&&"static"!==z(x=q(i)).position&&"absolute"===u&&(L="scrollHeight",S="scrollWidth"),(l===n||(l===r||l===o)&&c===h)&&(C=s,y-=(g&&x===O&&O.visualViewport?O.visualViewport.height:x[L])-a.height,y*=f?1:-1),l!==r&&(l!==n&&l!==s||c!==h)||(T=o,b-=(g&&x===O&&O.visualViewport?O.visualViewport.width:x[S])-a.width,b*=f?1:-1)}var D,$=Object.assign({position:u},p&&tt),I=!0===m?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:M(i*s)/s||0,y:M(n*s)/s||0}}({x:b,y},k(i)):{x:b,y};return b=I.x,y=I.y,f?Object.assign({},$,((D={})[C]=A?"0":"",D[T]=E?"0":"",D.transform=(O.devicePixelRatio||1)<=1?"translate("+b+"px, "+y+"px)":"translate3d("+b+"px, "+y+"px, 0)",D)):Object.assign({},$,((e={})[C]=A?y+"px":"",e[T]=E?b+"px":"",e.transform="",e))}const it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:I(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var nt={passive:!0};const st={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=k(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,nt)})),a&&l.addEventListener("resize",i.update,nt),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,nt)})),a&&l.removeEventListener("resize",i.update,nt)}},data:{}};var ot={left:"right",right:"left",bottom:"top",top:"bottom"};function rt(t){return t.replace(/left|right|bottom|top/g,(function(t){return ot[t]}))}var at={start:"end",end:"start"};function lt(t){return t.replace(/start|end/g,(function(t){return at[t]}))}function ct(t){var e=k(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ht(t){return H(q(t)).left+ct(t).scrollLeft}function dt(t){var e=z(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function ut(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:S(t)&&dt(t)?t:ut(V(t))}function ft(t,e){var i;void 0===e&&(e=[]);var n=ut(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=k(n),r=s?[o].concat(o.visualViewport||[],dt(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ft(V(r)))}function pt(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function mt(t,e,i){return e===u?pt(function(t,e){var i=k(t),n=q(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=F();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+ht(t),y:l}}(t,i)):L(e)?function(t,e){var i=H(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):pt(function(t){var e,i=q(t),n=ct(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=N(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=N(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ht(t),l=-n.scrollTop;return"rtl"===z(s||i).direction&&(a+=N(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(q(t)))}function gt(t){var e,i=t.reference,a=t.element,l=t.placement,d=l?I(l):null,u=l?Z(l):null,f=i.x+i.width/2-a.width/2,p=i.y+i.height/2-a.height/2;switch(d){case n:e={x:f,y:i.y-a.height};break;case s:e={x:f,y:i.y+i.height};break;case o:e={x:i.x+i.width,y:p};break;case r:e={x:i.x-a.width,y:p};break;default:e={x:i.x,y:i.y}}var m=d?Q(d):null;if(null!=m){var g="y"===m?"height":"width";switch(u){case c:e[m]=e[m]-(i[g]/2-a[g]/2);break;case h:e[m]=e[m]+(i[g]/2-a[g]/2)}}return e}function _t(t,e){void 0===e&&(e={});var i=e,r=i.placement,a=void 0===r?t.placement:r,c=i.strategy,h=void 0===c?t.strategy:c,m=i.boundary,g=void 0===m?d:m,_=i.rootBoundary,b=void 0===_?u:_,v=i.elementContext,y=void 0===v?f:v,w=i.altBoundary,E=void 0!==w&&w,A=i.padding,T=void 0===A?0:A,C=U("number"!=typeof T?T:G(T,l)),O=y===f?p:f,k=t.rects.popper,D=t.elements[E?O:y],$=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ft(V(t)),i=["absolute","fixed"].indexOf(z(t).position)>=0&&S(t)?K(t):t;return L(i)?e.filter((function(t){return L(t)&&W(t,i)&&"body"!==x(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=mt(t,i,n);return e.top=N(s.top,e.top),e.right=P(s.right,e.right),e.bottom=P(s.bottom,e.bottom),e.left=N(s.left,e.left),e}),mt(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(L(D)?D:D.contextElement||q(t.elements.popper),g,b,h),I=H(t.elements.reference),M=gt({reference:I,element:k,strategy:"absolute",placement:a}),j=pt(Object.assign({},k,M)),F=y===f?j:I,B={top:$.top-F.top+C.top,bottom:F.bottom-$.bottom+C.bottom,left:$.left-F.left+C.left,right:F.right-$.right+C.right},R=t.modifiersData.offset;if(y===f&&R){var Y=R[a];Object.keys(B).forEach((function(t){var e=[o,s].indexOf(t)>=0?1:-1,i=[n,s].indexOf(t)>=0?"y":"x";B[t]+=Y[i]*e}))}return B}const bt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,h=t.name;if(!e.modifiersData[h]._skip){for(var d=i.mainAxis,u=void 0===d||d,f=i.altAxis,p=void 0===f||f,_=i.fallbackPlacements,b=i.padding,v=i.boundary,y=i.rootBoundary,w=i.altBoundary,E=i.flipVariations,A=void 0===E||E,T=i.allowedAutoPlacements,C=e.options.placement,O=I(C),x=_||(O!==C&&A?function(t){if(I(t)===a)return[];var e=rt(t);return[lt(t),e,lt(e)]}(C):[rt(C)]),k=[C].concat(x).reduce((function(t,i){return t.concat(I(i)===a?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,c=i.allowedAutoPlacements,h=void 0===c?g:c,d=Z(n),u=d?a?m:m.filter((function(t){return Z(t)===d})):l,f=u.filter((function(t){return h.indexOf(t)>=0}));0===f.length&&(f=u);var p=f.reduce((function(e,i){return e[i]=_t(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[I(i)],e}),{});return Object.keys(p).sort((function(t,e){return p[t]-p[e]}))}(e,{placement:i,boundary:v,rootBoundary:y,padding:b,flipVariations:A,allowedAutoPlacements:T}):i)}),[]),L=e.rects.reference,S=e.rects.popper,D=new Map,$=!0,N=k[0],P=0;P=0,B=H?"width":"height",W=_t(e,{placement:M,boundary:v,rootBoundary:y,altBoundary:w,padding:b}),z=H?F?o:r:F?s:n;L[B]>S[B]&&(z=rt(z));var R=rt(z),q=[];if(u&&q.push(W[j]<=0),p&&q.push(W[z]<=0,W[R]<=0),q.every((function(t){return t}))){N=M,$=!1;break}D.set(M,q)}if($)for(var V=function(t){var e=k.find((function(e){var i=D.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return N=e,"break"},Y=A?3:1;Y>0&&"break"!==V(Y);Y--);e.placement!==N&&(e.modifiersData[h]._skip=!0,e.placement=N,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function vt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function yt(t){return[n,o,s,r].some((function(e){return t[e]>=0}))}const wt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=_t(e,{elementContext:"reference"}),a=_t(e,{altBoundary:!0}),l=vt(r,n),c=vt(a,s,o),h=yt(l),d=yt(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Et={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,s=t.name,a=i.offset,l=void 0===a?[0,0]:a,c=g.reduce((function(t,i){return t[i]=function(t,e,i){var s=I(t),a=[r,n].indexOf(s)>=0?-1:1,l="function"==typeof i?i(Object.assign({},e,{placement:t})):i,c=l[0],h=l[1];return c=c||0,h=(h||0)*a,[r,o].indexOf(s)>=0?{x:h,y:c}:{x:c,y:h}}(i,e.rects,l),t}),{}),h=c[e.placement],d=h.x,u=h.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=d,e.modifiersData.popperOffsets.y+=u),e.modifiersData[s]=c}},At={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=gt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Tt={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,a=t.name,l=i.mainAxis,h=void 0===l||l,d=i.altAxis,u=void 0!==d&&d,f=i.boundary,p=i.rootBoundary,m=i.altBoundary,g=i.padding,_=i.tether,b=void 0===_||_,v=i.tetherOffset,y=void 0===v?0:v,w=_t(e,{boundary:f,rootBoundary:p,padding:g,altBoundary:m}),E=I(e.placement),A=Z(e.placement),T=!A,C=Q(E),O="x"===C?"y":"x",x=e.modifiersData.popperOffsets,k=e.rects.reference,L=e.rects.popper,S="function"==typeof y?y(Object.assign({},e.rects,{placement:e.placement})):y,D="number"==typeof S?{mainAxis:S,altAxis:S}:Object.assign({mainAxis:0,altAxis:0},S),$=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,M={x:0,y:0};if(x){if(h){var j,F="y"===C?n:r,H="y"===C?s:o,W="y"===C?"height":"width",z=x[C],R=z+w[F],q=z-w[H],V=b?-L[W]/2:0,Y=A===c?k[W]:L[W],U=A===c?-L[W]:-k[W],G=e.elements.arrow,J=b&&G?B(G):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[F],it=tt[H],nt=X(0,k[W],J[W]),st=T?k[W]/2-V-nt-et-D.mainAxis:Y-nt-et-D.mainAxis,ot=T?-k[W]/2+V+nt+it+D.mainAxis:U+nt+it+D.mainAxis,rt=e.elements.arrow&&K(e.elements.arrow),at=rt?"y"===C?rt.clientTop||0:rt.clientLeft||0:0,lt=null!=(j=null==$?void 0:$[C])?j:0,ct=z+ot-lt,ht=X(b?P(R,z+st-lt-at):R,z,b?N(q,ct):q);x[C]=ht,M[C]=ht-z}if(u){var dt,ut="x"===C?n:r,ft="x"===C?s:o,pt=x[O],mt="y"===O?"height":"width",gt=pt+w[ut],bt=pt-w[ft],vt=-1!==[n,r].indexOf(E),yt=null!=(dt=null==$?void 0:$[O])?dt:0,wt=vt?gt:pt-k[mt]-L[mt]-yt+D.altAxis,Et=vt?pt+k[mt]+L[mt]-yt-D.altAxis:bt,At=b&&vt?function(t,e,i){var n=X(t,e,i);return n>i?i:n}(wt,pt,Et):X(b?wt:gt,pt,b?Et:bt);x[O]=At,M[O]=At-pt}e.modifiersData[a]=M}},requiresIfExists:["offset"]};function Ct(t,e,i){void 0===i&&(i=!1);var n,s,o=S(e),r=S(e)&&function(t){var e=t.getBoundingClientRect(),i=M(e.width)/t.offsetWidth||1,n=M(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=q(e),l=H(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==x(e)||dt(a))&&(c=(n=e)!==k(n)&&S(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:ct(n)),S(e)?((h=H(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=ht(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function Ot(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var xt={placement:"bottom",modifiers:[],strategy:"absolute"};function kt(){for(var t=arguments.length,e=new Array(t),i=0;iIt.has(t)&&It.get(t).get(e)||null,remove(t,e){if(!It.has(t))return;const i=It.get(t);i.delete(e),0===i.size&&It.delete(t)}},Pt="transitionend",Mt=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),jt=t=>{t.dispatchEvent(new Event(Pt))},Ft=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),Ht=t=>Ft(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(Mt(t)):null,Bt=t=>{if(!Ft(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},Wt=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),zt=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?zt(t.parentNode):null},Rt=()=>{},qt=t=>{t.offsetHeight},Vt=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,Yt=[],Kt=()=>"rtl"===document.documentElement.dir,Qt=t=>{var e;e=()=>{const e=Vt();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(Yt.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of Yt)t()})),Yt.push(e)):e()},Xt=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,Ut=(t,e,i=!0)=>{if(!i)return void Xt(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let s=!1;const o=({target:i})=>{i===e&&(s=!0,e.removeEventListener(Pt,o),Xt(t))};e.addEventListener(Pt,o),setTimeout((()=>{s||jt(e)}),n)},Gt=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},Jt=/[^.]*(?=\..*)\.|.*/,Zt=/\..*/,te=/::\d+$/,ee={};let ie=1;const ne={mouseenter:"mouseover",mouseleave:"mouseout"},se=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function oe(t,e){return e&&`${e}::${ie++}`||t.uidEvent||ie++}function re(t){const e=oe(t);return t.uidEvent=e,ee[e]=ee[e]||{},ee[e]}function ae(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function le(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=ue(t);return se.has(o)||(o=t),[n,s,o]}function ce(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=le(e,i,n);if(e in ne){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=re(t),c=l[a]||(l[a]={}),h=ae(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=oe(r,e.replace(Jt,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return pe(s,{delegateTarget:r}),n.oneOff&&fe.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return pe(n,{delegateTarget:t}),i.oneOff&&fe.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function he(t,e,i,n,s){const o=ae(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function de(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&he(t,e,i,r.callable,r.delegationSelector)}function ue(t){return t=t.replace(Zt,""),ne[t]||t}const fe={on(t,e,i,n){ce(t,e,i,n,!1)},one(t,e,i,n){ce(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=le(e,i,n),a=r!==e,l=re(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))de(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(te,"");a&&!e.includes(s)||he(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;he(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=Vt();let s=null,o=!0,r=!0,a=!1;e!==ue(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=pe(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function pe(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function me(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function ge(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const _e={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${ge(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${ge(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=me(t.dataset[n])}return e},getDataAttribute:(t,e)=>me(t.getAttribute(`data-bs-${ge(e)}`))};class be{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=Ft(e)?_e.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...Ft(e)?_e.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],o=Ft(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${o}" but expected type "${s}".`)}var i}}class ve extends be{constructor(t,e){super(),(t=Ht(t))&&(this._element=t,this._config=this._getConfig(e),Nt.set(this._element,this.constructor.DATA_KEY,this))}dispose(){Nt.remove(this._element,this.constructor.DATA_KEY),fe.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){Ut(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return Nt.get(Ht(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const ye=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>Mt(t))).join(","):null},we={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!Wt(t)&&Bt(t)))},getSelectorFromElement(t){const e=ye(t);return e&&we.findOne(e)?e:null},getElementFromSelector(t){const e=ye(t);return e?we.findOne(e):null},getMultipleElementsFromSelector(t){const e=ye(t);return e?we.find(e):[]}},Ee=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;fe.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),Wt(this))return;const s=we.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},Ae=".bs.alert",Te=`close${Ae}`,Ce=`closed${Ae}`;class Oe extends ve{static get NAME(){return"alert"}close(){if(fe.trigger(this._element,Te).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),fe.trigger(this._element,Ce),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Oe.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}Ee(Oe,"close"),Qt(Oe);const xe='[data-bs-toggle="button"]';class ke extends ve{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=ke.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}fe.on(document,"click.bs.button.data-api",xe,(t=>{t.preventDefault();const e=t.target.closest(xe);ke.getOrCreateInstance(e).toggle()})),Qt(ke);const Le=".bs.swipe",Se=`touchstart${Le}`,De=`touchmove${Le}`,$e=`touchend${Le}`,Ie=`pointerdown${Le}`,Ne=`pointerup${Le}`,Pe={endCallback:null,leftCallback:null,rightCallback:null},Me={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class je extends be{constructor(t,e){super(),this._element=t,t&&je.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Pe}static get DefaultType(){return Me}static get NAME(){return"swipe"}dispose(){fe.off(this._element,Le)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),Xt(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&Xt(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(fe.on(this._element,Ie,(t=>this._start(t))),fe.on(this._element,Ne,(t=>this._end(t))),this._element.classList.add("pointer-event")):(fe.on(this._element,Se,(t=>this._start(t))),fe.on(this._element,De,(t=>this._move(t))),fe.on(this._element,$e,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const Fe=".bs.carousel",He=".data-api",Be="ArrowLeft",We="ArrowRight",ze="next",Re="prev",qe="left",Ve="right",Ye=`slide${Fe}`,Ke=`slid${Fe}`,Qe=`keydown${Fe}`,Xe=`mouseenter${Fe}`,Ue=`mouseleave${Fe}`,Ge=`dragstart${Fe}`,Je=`load${Fe}${He}`,Ze=`click${Fe}${He}`,ti="carousel",ei="active",ii=".active",ni=".carousel-item",si=ii+ni,oi={[Be]:Ve,[We]:qe},ri={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},ai={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class li extends ve{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=we.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===ti&&this.cycle()}static get Default(){return ri}static get DefaultType(){return ai}static get NAME(){return"carousel"}next(){this._slide(ze)}nextWhenVisible(){!document.hidden&&Bt(this._element)&&this.next()}prev(){this._slide(Re)}pause(){this._isSliding&&jt(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?fe.one(this._element,Ke,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void fe.one(this._element,Ke,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?ze:Re;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&fe.on(this._element,Qe,(t=>this._keydown(t))),"hover"===this._config.pause&&(fe.on(this._element,Xe,(()=>this.pause())),fe.on(this._element,Ue,(()=>this._maybeEnableCycle()))),this._config.touch&&je.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of we.find(".carousel-item img",this._element))fe.on(t,Ge,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(qe)),rightCallback:()=>this._slide(this._directionToOrder(Ve)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new je(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=oi[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=we.findOne(ii,this._indicatorsElement);e.classList.remove(ei),e.removeAttribute("aria-current");const i=we.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(ei),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===ze,s=e||Gt(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>fe.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(Ye).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),qt(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(ei),i.classList.remove(ei,c,l),this._isSliding=!1,r(Ke)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return we.findOne(si,this._element)}_getItems(){return we.find(ni,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return Kt()?t===qe?Re:ze:t===qe?ze:Re}_orderToDirection(t){return Kt()?t===Re?qe:Ve:t===Re?Ve:qe}static jQueryInterface(t){return this.each((function(){const e=li.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}fe.on(document,Ze,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=we.getElementFromSelector(this);if(!e||!e.classList.contains(ti))return;t.preventDefault();const i=li.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===_e.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),fe.on(window,Je,(()=>{const t=we.find('[data-bs-ride="carousel"]');for(const e of t)li.getOrCreateInstance(e)})),Qt(li);const ci=".bs.collapse",hi=`show${ci}`,di=`shown${ci}`,ui=`hide${ci}`,fi=`hidden${ci}`,pi=`click${ci}.data-api`,mi="show",gi="collapse",_i="collapsing",bi=`:scope .${gi} .${gi}`,vi='[data-bs-toggle="collapse"]',yi={parent:null,toggle:!0},wi={parent:"(null|element)",toggle:"boolean"};class Ei extends ve{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=we.find(vi);for(const t of i){const e=we.getSelectorFromElement(t),i=we.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return yi}static get DefaultType(){return wi}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Ei.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(fe.trigger(this._element,hi).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(gi),this._element.classList.add(_i),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(_i),this._element.classList.add(gi,mi),this._element.style[e]="",fe.trigger(this._element,di)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(fe.trigger(this._element,ui).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,qt(this._element),this._element.classList.add(_i),this._element.classList.remove(gi,mi);for(const t of this._triggerArray){const e=we.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(_i),this._element.classList.add(gi),fe.trigger(this._element,fi)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(mi)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=Ht(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(vi);for(const e of t){const t=we.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=we.find(bi,this._config.parent);return we.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Ei.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}fe.on(document,pi,vi,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of we.getMultipleElementsFromSelector(this))Ei.getOrCreateInstance(t,{toggle:!1}).toggle()})),Qt(Ei);const Ai="dropdown",Ti=".bs.dropdown",Ci=".data-api",Oi="ArrowUp",xi="ArrowDown",ki=`hide${Ti}`,Li=`hidden${Ti}`,Si=`show${Ti}`,Di=`shown${Ti}`,$i=`click${Ti}${Ci}`,Ii=`keydown${Ti}${Ci}`,Ni=`keyup${Ti}${Ci}`,Pi="show",Mi='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',ji=`${Mi}.${Pi}`,Fi=".dropdown-menu",Hi=Kt()?"top-end":"top-start",Bi=Kt()?"top-start":"top-end",Wi=Kt()?"bottom-end":"bottom-start",zi=Kt()?"bottom-start":"bottom-end",Ri=Kt()?"left-start":"right-start",qi=Kt()?"right-start":"left-start",Vi={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},Yi={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class Ki extends ve{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=we.next(this._element,Fi)[0]||we.prev(this._element,Fi)[0]||we.findOne(Fi,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return Vi}static get DefaultType(){return Yi}static get NAME(){return Ai}toggle(){return this._isShown()?this.hide():this.show()}show(){if(Wt(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!fe.trigger(this._element,Si,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))fe.on(t,"mouseover",Rt);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Pi),this._element.classList.add(Pi),fe.trigger(this._element,Di,t)}}hide(){if(Wt(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!fe.trigger(this._element,ki,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.off(t,"mouseover",Rt);this._popper&&this._popper.destroy(),this._menu.classList.remove(Pi),this._element.classList.remove(Pi),this._element.setAttribute("aria-expanded","false"),_e.removeDataAttribute(this._menu,"popper"),fe.trigger(this._element,Li,t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!Ft(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ai.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===e)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let t=this._element;"parent"===this._config.reference?t=this._parent:Ft(this._config.reference)?t=Ht(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const i=this._getPopperConfig();this._popper=Dt(t,this._menu,i)}_isShown(){return this._menu.classList.contains(Pi)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return Ri;if(t.classList.contains("dropstart"))return qi;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?Bi:Hi:e?zi:Wi}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(_e.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...Xt(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=we.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>Bt(t)));i.length&&Gt(i,e,t===xi,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=Ki.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=we.find(ji);for(const i of e){const e=Ki.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Oi,xi].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Mi)?this:we.prev(this,Mi)[0]||we.next(this,Mi)[0]||we.findOne(Mi,t.delegateTarget.parentNode),o=Ki.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}fe.on(document,Ii,Mi,Ki.dataApiKeydownHandler),fe.on(document,Ii,Fi,Ki.dataApiKeydownHandler),fe.on(document,$i,Ki.clearMenus),fe.on(document,Ni,Ki.clearMenus),fe.on(document,$i,Mi,(function(t){t.preventDefault(),Ki.getOrCreateInstance(this).toggle()})),Qt(Ki);const Qi="backdrop",Xi="show",Ui=`mousedown.bs.${Qi}`,Gi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ji={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Zi extends be{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Gi}static get DefaultType(){return Ji}static get NAME(){return Qi}show(t){if(!this._config.isVisible)return void Xt(t);this._append();const e=this._getElement();this._config.isAnimated&&qt(e),e.classList.add(Xi),this._emulateAnimation((()=>{Xt(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Xi),this._emulateAnimation((()=>{this.dispose(),Xt(t)}))):Xt(t)}dispose(){this._isAppended&&(fe.off(this._element,Ui),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=Ht(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),fe.on(t,Ui,(()=>{Xt(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){Ut(t,this._getElement(),this._config.isAnimated)}}const tn=".bs.focustrap",en=`focusin${tn}`,nn=`keydown.tab${tn}`,sn="backward",on={autofocus:!0,trapElement:null},rn={autofocus:"boolean",trapElement:"element"};class an extends be{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return on}static get DefaultType(){return rn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),fe.off(document,tn),fe.on(document,en,(t=>this._handleFocusin(t))),fe.on(document,nn,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,fe.off(document,tn))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=we.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===sn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?sn:"forward")}}const ln=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",cn=".sticky-top",hn="padding-right",dn="margin-right";class un{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,hn,(e=>e+t)),this._setElementAttributes(ln,hn,(e=>e+t)),this._setElementAttributes(cn,dn,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,hn),this._resetElementAttributes(ln,hn),this._resetElementAttributes(cn,dn)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&_e.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=_e.getDataAttribute(t,e);null!==i?(_e.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(Ft(t))e(t);else for(const i of we.find(t,this._element))e(i)}}const fn=".bs.modal",pn=`hide${fn}`,mn=`hidePrevented${fn}`,gn=`hidden${fn}`,_n=`show${fn}`,bn=`shown${fn}`,vn=`resize${fn}`,yn=`click.dismiss${fn}`,wn=`mousedown.dismiss${fn}`,En=`keydown.dismiss${fn}`,An=`click${fn}.data-api`,Tn="modal-open",Cn="show",On="modal-static",xn={backdrop:!0,focus:!0,keyboard:!0},kn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ln extends ve{constructor(t,e){super(t,e),this._dialog=we.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new un,this._addEventListeners()}static get Default(){return xn}static get DefaultType(){return kn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||fe.trigger(this._element,_n,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Tn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(fe.trigger(this._element,pn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Cn),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){fe.off(window,fn),fe.off(this._dialog,fn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Zi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new an({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=we.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),qt(this._element),this._element.classList.add(Cn),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,fe.trigger(this._element,bn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){fe.on(this._element,En,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),fe.on(window,vn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),fe.on(this._element,wn,(t=>{fe.one(this._element,yn,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Tn),this._resetAdjustments(),this._scrollBar.reset(),fe.trigger(this._element,gn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(fe.trigger(this._element,mn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(On)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(On),this._queueCallback((()=>{this._element.classList.remove(On),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=Kt()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=Kt()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Ln.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}fe.on(document,An,'[data-bs-toggle="modal"]',(function(t){const e=we.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),fe.one(e,_n,(t=>{t.defaultPrevented||fe.one(e,gn,(()=>{Bt(this)&&this.focus()}))}));const i=we.findOne(".modal.show");i&&Ln.getInstance(i).hide(),Ln.getOrCreateInstance(e).toggle(this)})),Ee(Ln),Qt(Ln);const Sn=".bs.offcanvas",Dn=".data-api",$n=`load${Sn}${Dn}`,In="show",Nn="showing",Pn="hiding",Mn=".offcanvas.show",jn=`show${Sn}`,Fn=`shown${Sn}`,Hn=`hide${Sn}`,Bn=`hidePrevented${Sn}`,Wn=`hidden${Sn}`,zn=`resize${Sn}`,Rn=`click${Sn}${Dn}`,qn=`keydown.dismiss${Sn}`,Vn={backdrop:!0,keyboard:!0,scroll:!1},Yn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Kn extends ve{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Vn}static get DefaultType(){return Yn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||fe.trigger(this._element,jn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new un).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Nn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(In),this._element.classList.remove(Nn),fe.trigger(this._element,Fn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(fe.trigger(this._element,Hn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Pn),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(In,Pn),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new un).reset(),fe.trigger(this._element,Wn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Zi({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():fe.trigger(this._element,Bn)}:null})}_initializeFocusTrap(){return new an({trapElement:this._element})}_addEventListeners(){fe.on(this._element,qn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():fe.trigger(this._element,Bn))}))}static jQueryInterface(t){return this.each((function(){const e=Kn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}fe.on(document,Rn,'[data-bs-toggle="offcanvas"]',(function(t){const e=we.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this))return;fe.one(e,Wn,(()=>{Bt(this)&&this.focus()}));const i=we.findOne(Mn);i&&i!==e&&Kn.getInstance(i).hide(),Kn.getOrCreateInstance(e).toggle(this)})),fe.on(window,$n,(()=>{for(const t of we.find(Mn))Kn.getOrCreateInstance(t).show()})),fe.on(window,zn,(()=>{for(const t of we.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Kn.getOrCreateInstance(t).hide()})),Ee(Kn),Qt(Kn);const Qn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Xn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Un=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Gn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Xn.has(i)||Boolean(Un.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Jn={allowList:Qn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Zn={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},ts={entry:"(string|element|function|null)",selector:"(string|element)"};class es extends be{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Jn}static get DefaultType(){return Zn}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},ts)}_setContent(t,e,i){const n=we.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?Ft(e)?this._putElementInTemplate(Ht(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Gn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return Xt(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const is=new Set(["sanitize","allowList","sanitizeFn"]),ns="fade",ss="show",os=".tooltip-inner",rs=".modal",as="hide.bs.modal",ls="hover",cs="focus",hs={AUTO:"auto",TOP:"top",RIGHT:Kt()?"left":"right",BOTTOM:"bottom",LEFT:Kt()?"right":"left"},ds={allowList:Qn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},us={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class fs extends ve{constructor(t,i){if(void 0===e)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,i),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return ds}static get DefaultType(){return us}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),fe.off(this._element.closest(rs),as,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=fe.trigger(this._element,this.constructor.eventName("show")),e=(zt(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),fe.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(ss),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.on(t,"mouseover",Rt);this._queueCallback((()=>{fe.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!fe.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(ss),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.off(t,"mouseover",Rt);this._activeTrigger.click=!1,this._activeTrigger[cs]=!1,this._activeTrigger[ls]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),fe.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ns,ss),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ns),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new es({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[os]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ns)}_isShown(){return this.tip&&this.tip.classList.contains(ss)}_createPopper(t){const e=Xt(this._config.placement,[this,t,this._element]),i=hs[e.toUpperCase()];return Dt(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return Xt(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...Xt(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)fe.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ls?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ls?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");fe.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?cs:ls]=!0,e._enter()})),fe.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?cs:ls]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},fe.on(this._element.closest(rs),as,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=_e.getDataAttributes(this._element);for(const t of Object.keys(e))is.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:Ht(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=fs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Qt(fs);const ps=".popover-header",ms=".popover-body",gs={...fs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},_s={...fs.DefaultType,content:"(null|string|element|function)"};class bs extends fs{static get Default(){return gs}static get DefaultType(){return _s}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[ps]:this._getTitle(),[ms]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=bs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Qt(bs);const vs=".bs.scrollspy",ys=`activate${vs}`,ws=`click${vs}`,Es=`load${vs}.data-api`,As="active",Ts="[href]",Cs=".nav-link",Os=`${Cs}, .nav-item > ${Cs}, .list-group-item`,xs={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},ks={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ls extends ve{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return xs}static get DefaultType(){return ks}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=Ht(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(fe.off(this._config.target,ws),fe.on(this._config.target,ws,Ts,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=we.find(Ts,this._config.target);for(const e of t){if(!e.hash||Wt(e))continue;const t=we.findOne(decodeURI(e.hash),this._element);Bt(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(As),this._activateParents(t),fe.trigger(this._element,ys,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))we.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(As);else for(const e of we.parents(t,".nav, .list-group"))for(const t of we.prev(e,Os))t.classList.add(As)}_clearActiveClass(t){t.classList.remove(As);const e=we.find(`${Ts}.${As}`,t);for(const t of e)t.classList.remove(As)}static jQueryInterface(t){return this.each((function(){const e=Ls.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}fe.on(window,Es,(()=>{for(const t of we.find('[data-bs-spy="scroll"]'))Ls.getOrCreateInstance(t)})),Qt(Ls);const Ss=".bs.tab",Ds=`hide${Ss}`,$s=`hidden${Ss}`,Is=`show${Ss}`,Ns=`shown${Ss}`,Ps=`click${Ss}`,Ms=`keydown${Ss}`,js=`load${Ss}`,Fs="ArrowLeft",Hs="ArrowRight",Bs="ArrowUp",Ws="ArrowDown",zs="Home",Rs="End",qs="active",Vs="fade",Ys="show",Ks=".dropdown-toggle",Qs=`:not(${Ks})`,Xs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Us=`.nav-link${Qs}, .list-group-item${Qs}, [role="tab"]${Qs}, ${Xs}`,Gs=`.${qs}[data-bs-toggle="tab"], .${qs}[data-bs-toggle="pill"], .${qs}[data-bs-toggle="list"]`;class Js extends ve{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),fe.on(this._element,Ms,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?fe.trigger(e,Ds,{relatedTarget:t}):null;fe.trigger(t,Is,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(qs),this._activate(we.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),fe.trigger(t,Ns,{relatedTarget:e})):t.classList.add(Ys)}),t,t.classList.contains(Vs)))}_deactivate(t,e){t&&(t.classList.remove(qs),t.blur(),this._deactivate(we.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),fe.trigger(t,$s,{relatedTarget:e})):t.classList.remove(Ys)}),t,t.classList.contains(Vs)))}_keydown(t){if(![Fs,Hs,Bs,Ws,zs,Rs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!Wt(t)));let i;if([zs,Rs].includes(t.key))i=e[t.key===zs?0:e.length-1];else{const n=[Hs,Ws].includes(t.key);i=Gt(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Js.getOrCreateInstance(i).show())}_getChildren(){return we.find(Us,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=we.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=we.findOne(t,i);s&&s.classList.toggle(n,e)};n(Ks,qs),n(".dropdown-menu",Ys),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(qs)}_getInnerElement(t){return t.matches(Us)?t:we.findOne(Us,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Js.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}fe.on(document,Ps,Xs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this)||Js.getOrCreateInstance(this).show()})),fe.on(window,js,(()=>{for(const t of we.find(Gs))Js.getOrCreateInstance(t)})),Qt(Js);const Zs=".bs.toast",to=`mouseover${Zs}`,eo=`mouseout${Zs}`,io=`focusin${Zs}`,no=`focusout${Zs}`,so=`hide${Zs}`,oo=`hidden${Zs}`,ro=`show${Zs}`,ao=`shown${Zs}`,lo="hide",co="show",ho="showing",uo={animation:"boolean",autohide:"boolean",delay:"number"},fo={animation:!0,autohide:!0,delay:5e3};class po extends ve{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return fo}static get DefaultType(){return uo}static get NAME(){return"toast"}show(){fe.trigger(this._element,ro).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(lo),qt(this._element),this._element.classList.add(co,ho),this._queueCallback((()=>{this._element.classList.remove(ho),fe.trigger(this._element,ao),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(fe.trigger(this._element,so).defaultPrevented||(this._element.classList.add(ho),this._queueCallback((()=>{this._element.classList.add(lo),this._element.classList.remove(ho,co),fe.trigger(this._element,oo)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(co),super.dispose()}isShown(){return this._element.classList.contains(co)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){fe.on(this._element,to,(t=>this._onInteraction(t,!0))),fe.on(this._element,eo,(t=>this._onInteraction(t,!1))),fe.on(this._element,io,(t=>this._onInteraction(t,!0))),fe.on(this._element,no,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=po.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}function mo(t){"loading"!=document.readyState?t():document.addEventListener("DOMContentLoaded",t)}Ee(po),Qt(po),mo((function(){[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')).map((function(t){return new fs(t,{delay:{show:500,hide:100}})}))})),mo((function(){document.getElementById("pst-back-to-top").addEventListener("click",(function(){document.body.scrollTop=0,document.documentElement.scrollTop=0}))})),mo((function(){var t=document.getElementById("pst-back-to-top"),e=document.getElementsByClassName("bd-header")[0].getBoundingClientRect();window.addEventListener("scroll",(function(){this.oldScroll>this.scrollY&&this.scrollY>e.bottom?t.style.display="block":t.style.display="none",this.oldScroll=this.scrollY}))})),window.bootstrap=i})(); +//# sourceMappingURL=bootstrap.js.map \ No newline at end of file diff --git a/_static/scripts/bootstrap.js.LICENSE.txt b/_static/scripts/bootstrap.js.LICENSE.txt new file mode 100644 index 000000000..28755c2c5 --- /dev/null +++ b/_static/scripts/bootstrap.js.LICENSE.txt @@ -0,0 +1,5 @@ +/*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ diff --git a/_static/scripts/bootstrap.js.map b/_static/scripts/bootstrap.js.map new file mode 100644 index 000000000..e9e815891 --- /dev/null +++ b/_static/scripts/bootstrap.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts/bootstrap.js","mappings":"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","sources":["webpack://pydata_sphinx_theme/webpack/bootstrap","webpack://pydata_sphinx_theme/webpack/runtime/define property getters","webpack://pydata_sphinx_theme/webpack/runtime/hasOwnProperty shorthand","webpack://pydata_sphinx_theme/webpack/runtime/make namespace object","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/enums.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/applyStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getBasePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/math.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/userAgent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/contains.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/within.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/expandToHashMap.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/arrow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getVariation.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/computeStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/eventListeners.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/rectToClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/detectOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/flip.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/hide.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/offset.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getAltAxis.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/orderModifiers.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/createPopper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/debounce.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergeByName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper-lite.js","webpack://pydata_sphinx_theme/./node_modules/bootstrap/dist/js/bootstrap.esm.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/mixin.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/bootstrap.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n });\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref) {\n var name = _ref.name,\n _ref$options = _ref.options,\n options = _ref$options === void 0 ? {} : _ref$options,\n effect = _ref.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","/*!\n * Bootstrap v5.3.3 (https://getbootstrap.com/)\n * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nimport * as Popper from '@popperjs/core';\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\n\nconst elementMap = new Map();\nconst Data = {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map());\n }\n const instanceMap = elementMap.get(element);\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`);\n return;\n }\n instanceMap.set(key, instance);\n },\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null;\n }\n return null;\n },\n remove(element, key) {\n if (!elementMap.has(element)) {\n return;\n }\n const instanceMap = elementMap.get(element);\n instanceMap.delete(key);\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element);\n }\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1000000;\nconst MILLISECONDS_MULTIPLIER = 1000;\nconst TRANSITION_END = 'transitionend';\n\n/**\n * Properly escape IDs selectors to handle weird IDs\n * @param {string} selector\n * @returns {string}\n */\nconst parseSelector = selector => {\n if (selector && window.CSS && window.CSS.escape) {\n // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`);\n }\n return selector;\n};\n\n// Shout-out Angus Croll (https://goo.gl/pxwQGp)\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`;\n }\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase();\n};\n\n/**\n * Public Util API\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID);\n } while (document.getElementById(prefix));\n return prefix;\n};\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0;\n }\n\n // Get transition-duration of the element\n let {\n transitionDuration,\n transitionDelay\n } = window.getComputedStyle(element);\n const floatTransitionDuration = Number.parseFloat(transitionDuration);\n const floatTransitionDelay = Number.parseFloat(transitionDelay);\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0;\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0];\n transitionDelay = transitionDelay.split(',')[0];\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n};\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END));\n};\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false;\n }\n if (typeof object.jquery !== 'undefined') {\n object = object[0];\n }\n return typeof object.nodeType !== 'undefined';\n};\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object;\n }\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(parseSelector(object));\n }\n return null;\n};\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false;\n }\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n // Handle `details` element as its content may falsie appear visible when it is closed\n const closedDetails = element.closest('details:not([open])');\n if (!closedDetails) {\n return elementIsVisible;\n }\n if (closedDetails !== element) {\n const summary = element.closest('summary');\n if (summary && summary.parentNode !== closedDetails) {\n return false;\n }\n if (summary === null) {\n return false;\n }\n }\n return elementIsVisible;\n};\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true;\n }\n if (element.classList.contains('disabled')) {\n return true;\n }\n if (typeof element.disabled !== 'undefined') {\n return element.disabled;\n }\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n};\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null;\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode();\n return root instanceof ShadowRoot ? root : null;\n }\n if (element instanceof ShadowRoot) {\n return element;\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null;\n }\n return findShadowRoot(element.parentNode);\n};\nconst noop = () => {};\n\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\nconst reflow = element => {\n element.offsetHeight; // eslint-disable-line no-unused-expressions\n};\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery;\n }\n return null;\n};\nconst DOMContentLoadedCallbacks = [];\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback();\n }\n });\n }\n DOMContentLoadedCallbacks.push(callback);\n } else {\n callback();\n }\n};\nconst isRTL = () => document.documentElement.dir === 'rtl';\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery();\n /* istanbul ignore if */\n if ($) {\n const name = plugin.NAME;\n const JQUERY_NO_CONFLICT = $.fn[name];\n $.fn[name] = plugin.jQueryInterface;\n $.fn[name].Constructor = plugin;\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT;\n return plugin.jQueryInterface;\n };\n }\n });\n};\nconst execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;\n};\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback);\n return;\n }\n const durationPadding = 5;\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;\n let called = false;\n const handler = ({\n target\n }) => {\n if (target !== transitionElement) {\n return;\n }\n called = true;\n transitionElement.removeEventListener(TRANSITION_END, handler);\n execute(callback);\n };\n transitionElement.addEventListener(TRANSITION_END, handler);\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement);\n }\n }, emulatedDuration);\n};\n\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length;\n let index = list.indexOf(activeElement);\n\n // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];\n }\n index += shouldGetNext ? 1 : -1;\n if (isCycleAllowed) {\n index = (index + listLength) % listLength;\n }\n return list[Math.max(0, Math.min(index, listLength - 1))];\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/;\nconst stripNameRegex = /\\..*/;\nconst stripUidRegex = /::\\d+$/;\nconst eventRegistry = {}; // Events storage\nlet uidEvent = 1;\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n};\nconst nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']);\n\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++;\n}\nfunction getElementEvents(element) {\n const uid = makeEventUid(element);\n element.uidEvent = uid;\n eventRegistry[uid] = eventRegistry[uid] || {};\n return eventRegistry[uid];\n}\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, {\n delegateTarget: element\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn);\n }\n return fn.apply(element, [event]);\n };\n}\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector);\n for (let {\n target\n } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue;\n }\n hydrateObj(event, {\n delegateTarget: target\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn);\n }\n return fn.apply(target, [event]);\n }\n }\n };\n}\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector);\n}\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string';\n // TODO: tooltip passes `false` instead of selector, so we need to check\n const callable = isDelegated ? delegationFunction : handler || delegationFunction;\n let typeEvent = getTypeEvent(originalTypeEvent);\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent;\n }\n return [isDelegated, callable, typeEvent];\n}\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n\n // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {\n return fn.call(this, event);\n }\n };\n };\n callable = wrapFunction(callable);\n }\n const events = getElementEvents(element);\n const handlers = events[typeEvent] || (events[typeEvent] = {});\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null);\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff;\n return;\n }\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''));\n const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);\n fn.delegationSelector = isDelegated ? handler : null;\n fn.callable = callable;\n fn.oneOff = oneOff;\n fn.uidEvent = uid;\n handlers[uid] = fn;\n element.addEventListener(typeEvent, fn, isDelegated);\n}\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector);\n if (!fn) {\n return;\n }\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));\n delete events[typeEvent][fn.uidEvent];\n}\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {};\n for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n}\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '');\n return customEvents[event] || event;\n}\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false);\n },\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true);\n },\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n const inNamespace = typeEvent !== originalTypeEvent;\n const events = getElementEvents(element);\n const storeElementEvent = events[typeEvent] || {};\n const isNamespace = originalTypeEvent.startsWith('.');\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return;\n }\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null);\n return;\n }\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1));\n }\n }\n for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '');\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n },\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null;\n }\n const $ = getjQuery();\n const typeEvent = getTypeEvent(event);\n const inNamespace = event !== typeEvent;\n let jQueryEvent = null;\n let bubbles = true;\n let nativeDispatch = true;\n let defaultPrevented = false;\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args);\n $(element).trigger(jQueryEvent);\n bubbles = !jQueryEvent.isPropagationStopped();\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped();\n defaultPrevented = jQueryEvent.isDefaultPrevented();\n }\n const evt = hydrateObj(new Event(event, {\n bubbles,\n cancelable: true\n }), args);\n if (defaultPrevented) {\n evt.preventDefault();\n }\n if (nativeDispatch) {\n element.dispatchEvent(evt);\n }\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault();\n }\n return evt;\n }\n};\nfunction hydrateObj(obj, meta = {}) {\n for (const [key, value] of Object.entries(meta)) {\n try {\n obj[key] = value;\n } catch (_unused) {\n Object.defineProperty(obj, key, {\n configurable: true,\n get() {\n return value;\n }\n });\n }\n }\n return obj;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(value) {\n if (value === 'true') {\n return true;\n }\n if (value === 'false') {\n return false;\n }\n if (value === Number(value).toString()) {\n return Number(value);\n }\n if (value === '' || value === 'null') {\n return null;\n }\n if (typeof value !== 'string') {\n return value;\n }\n try {\n return JSON.parse(decodeURIComponent(value));\n } catch (_unused) {\n return value;\n }\n}\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`);\n}\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value);\n },\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`);\n },\n getDataAttributes(element) {\n if (!element) {\n return {};\n }\n const attributes = {};\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '');\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);\n attributes[pureKey] = normalizeData(element.dataset[key]);\n }\n return attributes;\n },\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`));\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {};\n }\n static get DefaultType() {\n return {};\n }\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!');\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n return config;\n }\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse\n\n return {\n ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n };\n }\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const [property, expectedTypes] of Object.entries(configTypes)) {\n const value = config[property];\n const valueType = isElement(value) ? 'element' : toType(value);\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`);\n }\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst VERSION = '5.3.3';\n\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super();\n element = getElement(element);\n if (!element) {\n return;\n }\n this._element = element;\n this._config = this._getConfig(config);\n Data.set(this._element, this.constructor.DATA_KEY, this);\n }\n\n // Public\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY);\n EventHandler.off(this._element, this.constructor.EVENT_KEY);\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null;\n }\n }\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated);\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n\n // Static\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY);\n }\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);\n }\n static get VERSION() {\n return VERSION;\n }\n static get DATA_KEY() {\n return `bs.${this.NAME}`;\n }\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`;\n }\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target');\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href');\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) {\n return null;\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`;\n }\n selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null;\n }\n return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null;\n};\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector));\n },\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector);\n },\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector));\n },\n parents(element, selector) {\n const parents = [];\n let ancestor = element.parentNode.closest(selector);\n while (ancestor) {\n parents.push(ancestor);\n ancestor = ancestor.parentNode.closest(selector);\n }\n return parents;\n },\n prev(element, selector) {\n let previous = element.previousElementSibling;\n while (previous) {\n if (previous.matches(selector)) {\n return [previous];\n }\n previous = previous.previousElementSibling;\n }\n return [];\n },\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling;\n while (next) {\n if (next.matches(selector)) {\n return [next];\n }\n next = next.nextElementSibling;\n }\n return [];\n },\n focusableChildren(element) {\n const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable=\"true\"]'].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',');\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el));\n },\n getSelectorFromElement(element) {\n const selector = getSelector(element);\n if (selector) {\n return SelectorEngine.findOne(selector) ? selector : null;\n }\n return null;\n },\n getElementFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.findOne(selector) : null;\n },\n getMultipleElementsFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.find(selector) : [];\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`;\n const name = component.NAME;\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`);\n const instance = component.getOrCreateInstance(target);\n\n // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n instance[method]();\n });\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$f = 'alert';\nconst DATA_KEY$a = 'bs.alert';\nconst EVENT_KEY$b = `.${DATA_KEY$a}`;\nconst EVENT_CLOSE = `close${EVENT_KEY$b}`;\nconst EVENT_CLOSED = `closed${EVENT_KEY$b}`;\nconst CLASS_NAME_FADE$5 = 'fade';\nconst CLASS_NAME_SHOW$8 = 'show';\n\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$f;\n }\n\n // Public\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE);\n if (closeEvent.defaultPrevented) {\n return;\n }\n this._element.classList.remove(CLASS_NAME_SHOW$8);\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5);\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated);\n }\n\n // Private\n _destroyElement() {\n this._element.remove();\n EventHandler.trigger(this._element, EVENT_CLOSED);\n this.dispose();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nenableDismissTrigger(Alert, 'close');\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$e = 'button';\nconst DATA_KEY$9 = 'bs.button';\nconst EVENT_KEY$a = `.${DATA_KEY$9}`;\nconst DATA_API_KEY$6 = '.data-api';\nconst CLASS_NAME_ACTIVE$3 = 'active';\nconst SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle=\"button\"]';\nconst EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`;\n\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$e;\n }\n\n // Public\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3));\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this);\n if (config === 'toggle') {\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => {\n event.preventDefault();\n const button = event.target.closest(SELECTOR_DATA_TOGGLE$5);\n const data = Button.getOrCreateInstance(button);\n data.toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$d = 'swipe';\nconst EVENT_KEY$9 = '.bs.swipe';\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`;\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`;\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`;\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`;\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`;\nconst POINTER_TYPE_TOUCH = 'touch';\nconst POINTER_TYPE_PEN = 'pen';\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event';\nconst SWIPE_THRESHOLD = 40;\nconst Default$c = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n};\nconst DefaultType$c = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n};\n\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super();\n this._element = element;\n if (!element || !Swipe.isSupported()) {\n return;\n }\n this._config = this._getConfig(config);\n this._deltaX = 0;\n this._supportPointerEvents = Boolean(window.PointerEvent);\n this._initEvents();\n }\n\n // Getters\n static get Default() {\n return Default$c;\n }\n static get DefaultType() {\n return DefaultType$c;\n }\n static get NAME() {\n return NAME$d;\n }\n\n // Public\n dispose() {\n EventHandler.off(this._element, EVENT_KEY$9);\n }\n\n // Private\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX;\n return;\n }\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX;\n }\n }\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX;\n }\n this._handleSwipe();\n execute(this._config.endCallback);\n }\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX;\n }\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX);\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return;\n }\n const direction = absDeltaX / this._deltaX;\n this._deltaX = 0;\n if (!direction) {\n return;\n }\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback);\n }\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event));\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event));\n this._element.classList.add(CLASS_NAME_POINTER_EVENT);\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event));\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event));\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event));\n }\n }\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH);\n }\n\n // Static\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$c = 'carousel';\nconst DATA_KEY$8 = 'bs.carousel';\nconst EVENT_KEY$8 = `.${DATA_KEY$8}`;\nconst DATA_API_KEY$5 = '.data-api';\nconst ARROW_LEFT_KEY$1 = 'ArrowLeft';\nconst ARROW_RIGHT_KEY$1 = 'ArrowRight';\nconst TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next';\nconst ORDER_PREV = 'prev';\nconst DIRECTION_LEFT = 'left';\nconst DIRECTION_RIGHT = 'right';\nconst EVENT_SLIDE = `slide${EVENT_KEY$8}`;\nconst EVENT_SLID = `slid${EVENT_KEY$8}`;\nconst EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`;\nconst EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`;\nconst EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`;\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`;\nconst EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst CLASS_NAME_CAROUSEL = 'carousel';\nconst CLASS_NAME_ACTIVE$2 = 'active';\nconst CLASS_NAME_SLIDE = 'slide';\nconst CLASS_NAME_END = 'carousel-item-end';\nconst CLASS_NAME_START = 'carousel-item-start';\nconst CLASS_NAME_NEXT = 'carousel-item-next';\nconst CLASS_NAME_PREV = 'carousel-item-prev';\nconst SELECTOR_ACTIVE = '.active';\nconst SELECTOR_ITEM = '.carousel-item';\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;\nconst SELECTOR_ITEM_IMG = '.carousel-item img';\nconst SELECTOR_INDICATORS = '.carousel-indicators';\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]';\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT\n};\nconst Default$b = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n};\nconst DefaultType$b = {\n interval: '(number|boolean)',\n // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._interval = null;\n this._activeElement = null;\n this._isSliding = false;\n this.touchTimeout = null;\n this._swipeHelper = null;\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);\n this._addEventListeners();\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$b;\n }\n static get DefaultType() {\n return DefaultType$b;\n }\n static get NAME() {\n return NAME$c;\n }\n\n // Public\n next() {\n this._slide(ORDER_NEXT);\n }\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next();\n }\n }\n prev() {\n this._slide(ORDER_PREV);\n }\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element);\n }\n this._clearInterval();\n }\n cycle() {\n this._clearInterval();\n this._updateInterval();\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval);\n }\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle());\n return;\n }\n this.cycle();\n }\n to(index) {\n const items = this._getItems();\n if (index > items.length - 1 || index < 0) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index));\n return;\n }\n const activeIndex = this._getItemIndex(this._getActive());\n if (activeIndex === index) {\n return;\n }\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;\n this._slide(order, items[index]);\n }\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose();\n }\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n config.defaultInterval = config.interval;\n return config;\n }\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event));\n }\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause());\n EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle());\n }\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners();\n }\n }\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault());\n }\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return;\n }\n\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause();\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout);\n }\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval);\n };\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n };\n this._swipeHelper = new Swipe(this._element, swipeConfig);\n }\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return;\n }\n const direction = KEY_TO_DIRECTION[event.key];\n if (direction) {\n event.preventDefault();\n this._slide(this._directionToOrder(direction));\n }\n }\n _getItemIndex(element) {\n return this._getItems().indexOf(element);\n }\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return;\n }\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2);\n activeIndicator.removeAttribute('aria-current');\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement);\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2);\n newActiveIndicator.setAttribute('aria-current', 'true');\n }\n }\n _updateInterval() {\n const element = this._activeElement || this._getActive();\n if (!element) {\n return;\n }\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);\n this._config.interval = elementInterval || this._config.defaultInterval;\n }\n _slide(order, element = null) {\n if (this._isSliding) {\n return;\n }\n const activeElement = this._getActive();\n const isNext = order === ORDER_NEXT;\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap);\n if (nextElement === activeElement) {\n return;\n }\n const nextElementIndex = this._getItemIndex(nextElement);\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n });\n };\n const slideEvent = triggerEvent(EVENT_SLIDE);\n if (slideEvent.defaultPrevented) {\n return;\n }\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // TODO: change tests that use empty divs to avoid this check\n return;\n }\n const isCycling = Boolean(this._interval);\n this.pause();\n this._isSliding = true;\n this._setActiveIndicatorElement(nextElementIndex);\n this._activeElement = nextElement;\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;\n nextElement.classList.add(orderClassName);\n reflow(nextElement);\n activeElement.classList.add(directionalClassName);\n nextElement.classList.add(directionalClassName);\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName);\n nextElement.classList.add(CLASS_NAME_ACTIVE$2);\n activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName);\n this._isSliding = false;\n triggerEvent(EVENT_SLID);\n };\n this._queueCallback(completeCallBack, activeElement, this._isAnimated());\n if (isCycling) {\n this.cycle();\n }\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE);\n }\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);\n }\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element);\n }\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval);\n this._interval = null;\n }\n }\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;\n }\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;\n }\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;\n }\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config);\n if (typeof config === 'number') {\n data.to(config);\n return;\n }\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return;\n }\n event.preventDefault();\n const carousel = Carousel.getOrCreateInstance(target);\n const slideIndex = this.getAttribute('data-bs-slide-to');\n if (slideIndex) {\n carousel.to(slideIndex);\n carousel._maybeEnableCycle();\n return;\n }\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next();\n carousel._maybeEnableCycle();\n return;\n }\n carousel.prev();\n carousel._maybeEnableCycle();\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$3, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE);\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel);\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$b = 'collapse';\nconst DATA_KEY$7 = 'bs.collapse';\nconst EVENT_KEY$7 = `.${DATA_KEY$7}`;\nconst DATA_API_KEY$4 = '.data-api';\nconst EVENT_SHOW$6 = `show${EVENT_KEY$7}`;\nconst EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`;\nconst EVENT_HIDE$6 = `hide${EVENT_KEY$7}`;\nconst EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`;\nconst EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`;\nconst CLASS_NAME_SHOW$7 = 'show';\nconst CLASS_NAME_COLLAPSE = 'collapse';\nconst CLASS_NAME_COLLAPSING = 'collapsing';\nconst CLASS_NAME_COLLAPSED = 'collapsed';\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`;\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal';\nconst WIDTH = 'width';\nconst HEIGHT = 'height';\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';\nconst SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle=\"collapse\"]';\nconst Default$a = {\n parent: null,\n toggle: true\n};\nconst DefaultType$a = {\n parent: '(null|element)',\n toggle: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isTransitioning = false;\n this._triggerArray = [];\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4);\n for (const elem of toggleList) {\n const selector = SelectorEngine.getSelectorFromElement(elem);\n const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element);\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem);\n }\n }\n this._initializeChildren();\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown());\n }\n if (this._config.toggle) {\n this.toggle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$a;\n }\n static get DefaultType() {\n return DefaultType$a;\n }\n static get NAME() {\n return NAME$b;\n }\n\n // Public\n toggle() {\n if (this._isShown()) {\n this.hide();\n } else {\n this.show();\n }\n }\n show() {\n if (this._isTransitioning || this._isShown()) {\n return;\n }\n let activeChildren = [];\n\n // find active children\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, {\n toggle: false\n }));\n }\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n for (const activeInstance of activeChildren) {\n activeInstance.hide();\n }\n const dimension = this._getDimension();\n this._element.classList.remove(CLASS_NAME_COLLAPSE);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.style[dimension] = 0;\n this._addAriaAndCollapsedClass(this._triggerArray, true);\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n this._element.style[dimension] = '';\n EventHandler.trigger(this._element, EVENT_SHOWN$6);\n };\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n const scrollSize = `scroll${capitalizedDimension}`;\n this._queueCallback(complete, this._element, true);\n this._element.style[dimension] = `${this._element[scrollSize]}px`;\n }\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n const dimension = this._getDimension();\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`;\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n for (const trigger of this._triggerArray) {\n const element = SelectorEngine.getElementFromSelector(trigger);\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false);\n }\n }\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE);\n EventHandler.trigger(this._element, EVENT_HIDDEN$6);\n };\n this._element.style[dimension] = '';\n this._queueCallback(complete, this._element, true);\n }\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW$7);\n }\n\n // Private\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle); // Coerce string values\n config.parent = getElement(config.parent);\n return config;\n }\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT;\n }\n _initializeChildren() {\n if (!this._config.parent) {\n return;\n }\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4);\n for (const element of children) {\n const selected = SelectorEngine.getElementFromSelector(element);\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected));\n }\n }\n }\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent);\n // remove children if greater depth\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element));\n }\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return;\n }\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen);\n element.setAttribute('aria-expanded', isOpen);\n }\n }\n\n // Static\n static jQueryInterface(config) {\n const _config = {};\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false;\n }\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config);\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') {\n event.preventDefault();\n }\n for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n Collapse.getOrCreateInstance(element, {\n toggle: false\n }).toggle();\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$a = 'dropdown';\nconst DATA_KEY$6 = 'bs.dropdown';\nconst EVENT_KEY$6 = `.${DATA_KEY$6}`;\nconst DATA_API_KEY$3 = '.data-api';\nconst ESCAPE_KEY$2 = 'Escape';\nconst TAB_KEY$1 = 'Tab';\nconst ARROW_UP_KEY$1 = 'ArrowUp';\nconst ARROW_DOWN_KEY$1 = 'ArrowDown';\nconst RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE$5 = `hide${EVENT_KEY$6}`;\nconst EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`;\nconst EVENT_SHOW$5 = `show${EVENT_KEY$6}`;\nconst EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`;\nconst EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst CLASS_NAME_SHOW$6 = 'show';\nconst CLASS_NAME_DROPUP = 'dropup';\nconst CLASS_NAME_DROPEND = 'dropend';\nconst CLASS_NAME_DROPSTART = 'dropstart';\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center';\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';\nconst SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)';\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`;\nconst SELECTOR_MENU = '.dropdown-menu';\nconst SELECTOR_NAVBAR = '.navbar';\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav';\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)';\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start';\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end';\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start';\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end';\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start';\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start';\nconst PLACEMENT_TOPCENTER = 'top';\nconst PLACEMENT_BOTTOMCENTER = 'bottom';\nconst Default$9 = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n};\nconst DefaultType$9 = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n};\n\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._popper = null;\n this._parent = this._element.parentNode; // dropdown wrapper\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent);\n this._inNavbar = this._detectNavbar();\n }\n\n // Getters\n static get Default() {\n return Default$9;\n }\n static get DefaultType() {\n return DefaultType$9;\n }\n static get NAME() {\n return NAME$a;\n }\n\n // Public\n toggle() {\n return this._isShown() ? this.hide() : this.show();\n }\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget);\n if (showEvent.defaultPrevented) {\n return;\n }\n this._createPopper();\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n this._element.focus();\n this._element.setAttribute('aria-expanded', true);\n this._menu.classList.add(CLASS_NAME_SHOW$6);\n this._element.classList.add(CLASS_NAME_SHOW$6);\n EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget);\n }\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n this._completeHide(relatedTarget);\n }\n dispose() {\n if (this._popper) {\n this._popper.destroy();\n }\n super.dispose();\n }\n update() {\n this._inNavbar = this._detectNavbar();\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Private\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget);\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n if (this._popper) {\n this._popper.destroy();\n }\n this._menu.classList.remove(CLASS_NAME_SHOW$6);\n this._element.classList.remove(CLASS_NAME_SHOW$6);\n this._element.setAttribute('aria-expanded', 'false');\n Manipulator.removeDataAttribute(this._menu, 'popper');\n EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);\n }\n _getConfig(config) {\n config = super._getConfig(config);\n if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME$a.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);\n }\n return config;\n }\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)');\n }\n let referenceElement = this._element;\n if (this._config.reference === 'parent') {\n referenceElement = this._parent;\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference);\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference;\n }\n const popperConfig = this._getPopperConfig();\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig);\n }\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW$6);\n }\n _getPlacement() {\n const parentDropdown = this._parent;\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER;\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end';\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP;\n }\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM;\n }\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null;\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n };\n\n // Disable Popper if we have a static display or Dropdown is in Navbar\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }];\n }\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _selectMenuItem({\n key,\n target\n }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element));\n if (!items.length) {\n return;\n }\n\n // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) {\n return;\n }\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN);\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle);\n if (!context || context._config.autoClose === false) {\n continue;\n }\n const composedPath = event.composedPath();\n const isMenuTarget = composedPath.includes(context._menu);\n if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) {\n continue;\n }\n\n // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue;\n }\n const relatedTarget = {\n relatedTarget: context._element\n };\n if (event.type === 'click') {\n relatedTarget.clickEvent = event;\n }\n context._completeHide(relatedTarget);\n }\n }\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n const isInput = /input|textarea/i.test(event.target.tagName);\n const isEscapeEvent = event.key === ESCAPE_KEY$2;\n const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key);\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return;\n }\n if (isInput && !isEscapeEvent) {\n return;\n }\n event.preventDefault();\n\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode);\n const instance = Dropdown.getOrCreateInstance(getToggleButton);\n if (isUpOrDownEvent) {\n event.stopPropagation();\n instance.show();\n instance._selectMenuItem(event);\n return;\n }\n if (instance._isShown()) {\n // else is escape and we check if it is shown\n event.stopPropagation();\n instance.hide();\n getToggleButton.focus();\n }\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) {\n event.preventDefault();\n Dropdown.getOrCreateInstance(this).toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$9 = 'backdrop';\nconst CLASS_NAME_FADE$4 = 'fade';\nconst CLASS_NAME_SHOW$5 = 'show';\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`;\nconst Default$8 = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true,\n // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n};\nconst DefaultType$8 = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n};\n\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isAppended = false;\n this._element = null;\n }\n\n // Getters\n static get Default() {\n return Default$8;\n }\n static get DefaultType() {\n return DefaultType$8;\n }\n static get NAME() {\n return NAME$9;\n }\n\n // Public\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._append();\n const element = this._getElement();\n if (this._config.isAnimated) {\n reflow(element);\n }\n element.classList.add(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n execute(callback);\n });\n }\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._getElement().classList.remove(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n this.dispose();\n execute(callback);\n });\n }\n dispose() {\n if (!this._isAppended) {\n return;\n }\n EventHandler.off(this._element, EVENT_MOUSEDOWN);\n this._element.remove();\n this._isAppended = false;\n }\n\n // Private\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div');\n backdrop.className = this._config.className;\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE$4);\n }\n this._element = backdrop;\n }\n return this._element;\n }\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement);\n return config;\n }\n _append() {\n if (this._isAppended) {\n return;\n }\n const element = this._getElement();\n this._config.rootElement.append(element);\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback);\n });\n this._isAppended = true;\n }\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated);\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$8 = 'focustrap';\nconst DATA_KEY$5 = 'bs.focustrap';\nconst EVENT_KEY$5 = `.${DATA_KEY$5}`;\nconst EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`;\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`;\nconst TAB_KEY = 'Tab';\nconst TAB_NAV_FORWARD = 'forward';\nconst TAB_NAV_BACKWARD = 'backward';\nconst Default$7 = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n};\nconst DefaultType$7 = {\n autofocus: 'boolean',\n trapElement: 'element'\n};\n\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isActive = false;\n this._lastTabNavDirection = null;\n }\n\n // Getters\n static get Default() {\n return Default$7;\n }\n static get DefaultType() {\n return DefaultType$7;\n }\n static get NAME() {\n return NAME$8;\n }\n\n // Public\n activate() {\n if (this._isActive) {\n return;\n }\n if (this._config.autofocus) {\n this._config.trapElement.focus();\n }\n EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event));\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event));\n this._isActive = true;\n }\n deactivate() {\n if (!this._isActive) {\n return;\n }\n this._isActive = false;\n EventHandler.off(document, EVENT_KEY$5);\n }\n\n // Private\n _handleFocusin(event) {\n const {\n trapElement\n } = this._config;\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return;\n }\n const elements = SelectorEngine.focusableChildren(trapElement);\n if (elements.length === 0) {\n trapElement.focus();\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus();\n } else {\n elements[0].focus();\n }\n }\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return;\n }\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top';\nconst SELECTOR_STICKY_CONTENT = '.sticky-top';\nconst PROPERTY_PADDING = 'padding-right';\nconst PROPERTY_MARGIN = 'margin-right';\n\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body;\n }\n\n // Public\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth;\n return Math.abs(window.innerWidth - documentWidth);\n }\n hide() {\n const width = this.getWidth();\n this._disableOverFlow();\n // give padding to element to balance the hidden scrollbar width\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width);\n }\n reset() {\n this._resetElementAttributes(this._element, 'overflow');\n this._resetElementAttributes(this._element, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN);\n }\n isOverflowing() {\n return this.getWidth() > 0;\n }\n\n // Private\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow');\n this._element.style.overflow = 'hidden';\n }\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth();\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return;\n }\n this._saveInitialAttribute(element, styleProperty);\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty);\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty);\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue);\n }\n }\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty);\n // We only want to remove the property if the value is `null`; the value can also be zero\n if (value === null) {\n element.style.removeProperty(styleProperty);\n return;\n }\n Manipulator.removeDataAttribute(element, styleProperty);\n element.style.setProperty(styleProperty, value);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector);\n return;\n }\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel);\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$7 = 'modal';\nconst DATA_KEY$4 = 'bs.modal';\nconst EVENT_KEY$4 = `.${DATA_KEY$4}`;\nconst DATA_API_KEY$2 = '.data-api';\nconst ESCAPE_KEY$1 = 'Escape';\nconst EVENT_HIDE$4 = `hide${EVENT_KEY$4}`;\nconst EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`;\nconst EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`;\nconst EVENT_SHOW$4 = `show${EVENT_KEY$4}`;\nconst EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`;\nconst EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`;\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`;\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`;\nconst EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`;\nconst EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`;\nconst CLASS_NAME_OPEN = 'modal-open';\nconst CLASS_NAME_FADE$3 = 'fade';\nconst CLASS_NAME_SHOW$4 = 'show';\nconst CLASS_NAME_STATIC = 'modal-static';\nconst OPEN_SELECTOR$1 = '.modal.show';\nconst SELECTOR_DIALOG = '.modal-dialog';\nconst SELECTOR_MODAL_BODY = '.modal-body';\nconst SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle=\"modal\"]';\nconst Default$6 = {\n backdrop: true,\n focus: true,\n keyboard: true\n};\nconst DefaultType$6 = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._isShown = false;\n this._isTransitioning = false;\n this._scrollBar = new ScrollBarHelper();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$6;\n }\n static get DefaultType() {\n return DefaultType$6;\n }\n static get NAME() {\n return NAME$7;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._isTransitioning = true;\n this._scrollBar.hide();\n document.body.classList.add(CLASS_NAME_OPEN);\n this._adjustDialog();\n this._backdrop.show(() => this._showElement(relatedTarget));\n }\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._isShown = false;\n this._isTransitioning = true;\n this._focustrap.deactivate();\n this._element.classList.remove(CLASS_NAME_SHOW$4);\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());\n }\n dispose() {\n EventHandler.off(window, EVENT_KEY$4);\n EventHandler.off(this._dialog, EVENT_KEY$4);\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n handleUpdate() {\n this._adjustDialog();\n }\n\n // Private\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop),\n // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element);\n }\n this._element.style.display = 'block';\n this._element.removeAttribute('aria-hidden');\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.scrollTop = 0;\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);\n if (modalBody) {\n modalBody.scrollTop = 0;\n }\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_SHOW$4);\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate();\n }\n this._isTransitioning = false;\n EventHandler.trigger(this._element, EVENT_SHOWN$4, {\n relatedTarget\n });\n };\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated());\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => {\n if (event.key !== ESCAPE_KEY$1) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n this._triggerBackdropTransition();\n });\n EventHandler.on(window, EVENT_RESIZE$1, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog();\n }\n });\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return;\n }\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition();\n return;\n }\n if (this._config.backdrop) {\n this.hide();\n }\n });\n });\n }\n _hideModal() {\n this._element.style.display = 'none';\n this._element.setAttribute('aria-hidden', true);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n this._isTransitioning = false;\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN);\n this._resetAdjustments();\n this._scrollBar.reset();\n EventHandler.trigger(this._element, EVENT_HIDDEN$4);\n });\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE$3);\n }\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1);\n if (hideEvent.defaultPrevented) {\n return;\n }\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const initialOverflowY = this._element.style.overflowY;\n // return if the following background transition hasn't yet completed\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return;\n }\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden';\n }\n this._element.classList.add(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY;\n }, this._dialog);\n }, this._dialog);\n this._element.focus();\n }\n\n /**\n * The following methods are used to handle overflowing modals\n */\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const scrollbarWidth = this._scrollBar.getWidth();\n const isBodyOverflowing = scrollbarWidth > 0;\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n }\n _resetAdjustments() {\n this._element.style.paddingLeft = '';\n this._element.style.paddingRight = '';\n }\n\n // Static\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](relatedTarget);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n EventHandler.one(target, EVENT_SHOW$4, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$4, () => {\n if (isVisible(this)) {\n this.focus();\n }\n });\n });\n\n // avoid conflict when clicking modal toggler while another one is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1);\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide();\n }\n const data = Modal.getOrCreateInstance(target);\n data.toggle(this);\n});\nenableDismissTrigger(Modal);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$6 = 'offcanvas';\nconst DATA_KEY$3 = 'bs.offcanvas';\nconst EVENT_KEY$3 = `.${DATA_KEY$3}`;\nconst DATA_API_KEY$1 = '.data-api';\nconst EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst ESCAPE_KEY = 'Escape';\nconst CLASS_NAME_SHOW$3 = 'show';\nconst CLASS_NAME_SHOWING$1 = 'showing';\nconst CLASS_NAME_HIDING = 'hiding';\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop';\nconst OPEN_SELECTOR = '.offcanvas.show';\nconst EVENT_SHOW$3 = `show${EVENT_KEY$3}`;\nconst EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`;\nconst EVENT_HIDE$3 = `hide${EVENT_KEY$3}`;\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`;\nconst EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`;\nconst EVENT_RESIZE = `resize${EVENT_KEY$3}`;\nconst EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`;\nconst SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle=\"offcanvas\"]';\nconst Default$5 = {\n backdrop: true,\n keyboard: true,\n scroll: false\n};\nconst DefaultType$5 = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isShown = false;\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$5;\n }\n static get DefaultType() {\n return DefaultType$5;\n }\n static get NAME() {\n return NAME$6;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._backdrop.show();\n if (!this._config.scroll) {\n new ScrollBarHelper().hide();\n }\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.classList.add(CLASS_NAME_SHOWING$1);\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate();\n }\n this._element.classList.add(CLASS_NAME_SHOW$3);\n this._element.classList.remove(CLASS_NAME_SHOWING$1);\n EventHandler.trigger(this._element, EVENT_SHOWN$3, {\n relatedTarget\n });\n };\n this._queueCallback(completeCallBack, this._element, true);\n }\n hide() {\n if (!this._isShown) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._focustrap.deactivate();\n this._element.blur();\n this._isShown = false;\n this._element.classList.add(CLASS_NAME_HIDING);\n this._backdrop.hide();\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n if (!this._config.scroll) {\n new ScrollBarHelper().reset();\n }\n EventHandler.trigger(this._element, EVENT_HIDDEN$3);\n };\n this._queueCallback(completeCallback, this._element, true);\n }\n dispose() {\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n\n // Private\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n return;\n }\n this.hide();\n };\n\n // 'static' option will be translated to true, and booleans will keep their value\n const isVisible = Boolean(this._config.backdrop);\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n });\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$3, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus();\n }\n });\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide();\n }\n const data = Offcanvas.getOrCreateInstance(target);\n data.toggle(this);\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$2, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show();\n }\n});\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide();\n }\n }\n});\nenableDismissTrigger(Offcanvas);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n// js-docs-start allow-list\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\nconst DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n dd: [],\n div: [],\n dl: [],\n dt: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n};\n// js-docs-end allow-list\n\nconst uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']);\n\n/**\n * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n * contexts.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n */\n// eslint-disable-next-line unicorn/better-regex\nconst SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase();\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue));\n }\n return true;\n }\n\n // Check if a regular expression validates the attribute.\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName));\n};\nfunction sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml;\n }\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml);\n }\n const domParser = new window.DOMParser();\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'));\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase();\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove();\n continue;\n }\n const attributeList = [].concat(...element.attributes);\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []);\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName);\n }\n }\n }\n return createdDocument.body.innerHTML;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$5 = 'TemplateFactory';\nconst Default$4 = {\n allowList: DefaultAllowlist,\n content: {},\n // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
'\n};\nconst DefaultType$4 = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n};\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n};\n\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n }\n\n // Getters\n static get Default() {\n return Default$4;\n }\n static get DefaultType() {\n return DefaultType$4;\n }\n static get NAME() {\n return NAME$5;\n }\n\n // Public\n getContent() {\n return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean);\n }\n hasContent() {\n return this.getContent().length > 0;\n }\n changeContent(content) {\n this._checkContent(content);\n this._config.content = {\n ...this._config.content,\n ...content\n };\n return this;\n }\n toHtml() {\n const templateWrapper = document.createElement('div');\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template);\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector);\n }\n const template = templateWrapper.children[0];\n const extraClass = this._resolvePossibleFunction(this._config.extraClass);\n if (extraClass) {\n template.classList.add(...extraClass.split(' '));\n }\n return template;\n }\n\n // Private\n _typeCheckConfig(config) {\n super._typeCheckConfig(config);\n this._checkContent(config.content);\n }\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({\n selector,\n entry: content\n }, DefaultContentType);\n }\n }\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template);\n if (!templateElement) {\n return;\n }\n content = this._resolvePossibleFunction(content);\n if (!content) {\n templateElement.remove();\n return;\n }\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement);\n return;\n }\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content);\n return;\n }\n templateElement.textContent = content;\n }\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this]);\n }\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = '';\n templateElement.append(element);\n return;\n }\n templateElement.textContent = element.textContent;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$4 = 'tooltip';\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);\nconst CLASS_NAME_FADE$2 = 'fade';\nconst CLASS_NAME_MODAL = 'modal';\nconst CLASS_NAME_SHOW$2 = 'show';\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner';\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;\nconst EVENT_MODAL_HIDE = 'hide.bs.modal';\nconst TRIGGER_HOVER = 'hover';\nconst TRIGGER_FOCUS = 'focus';\nconst TRIGGER_CLICK = 'click';\nconst TRIGGER_MANUAL = 'manual';\nconst EVENT_HIDE$2 = 'hide';\nconst EVENT_HIDDEN$2 = 'hidden';\nconst EVENT_SHOW$2 = 'show';\nconst EVENT_SHOWN$2 = 'shown';\nconst EVENT_INSERTED = 'inserted';\nconst EVENT_CLICK$1 = 'click';\nconst EVENT_FOCUSIN$1 = 'focusin';\nconst EVENT_FOCUSOUT$1 = 'focusout';\nconst EVENT_MOUSEENTER = 'mouseenter';\nconst EVENT_MOUSELEAVE = 'mouseleave';\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n};\nconst Default$3 = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 6],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
' + '
' + '
' + '
',\n title: '',\n trigger: 'hover focus'\n};\nconst DefaultType$3 = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n};\n\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)');\n }\n super(element, config);\n\n // Private\n this._isEnabled = true;\n this._timeout = 0;\n this._isHovered = null;\n this._activeTrigger = {};\n this._popper = null;\n this._templateFactory = null;\n this._newContent = null;\n\n // Protected\n this.tip = null;\n this._setListeners();\n if (!this._config.selector) {\n this._fixTitle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$3;\n }\n static get DefaultType() {\n return DefaultType$3;\n }\n static get NAME() {\n return NAME$4;\n }\n\n // Public\n enable() {\n this._isEnabled = true;\n }\n disable() {\n this._isEnabled = false;\n }\n toggleEnabled() {\n this._isEnabled = !this._isEnabled;\n }\n toggle() {\n if (!this._isEnabled) {\n return;\n }\n this._activeTrigger.click = !this._activeTrigger.click;\n if (this._isShown()) {\n this._leave();\n return;\n }\n this._enter();\n }\n dispose() {\n clearTimeout(this._timeout);\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));\n }\n this._disposePopper();\n super.dispose();\n }\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements');\n }\n if (!(this._isWithContent() && this._isEnabled)) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2));\n const shadowRoot = findShadowRoot(this._element);\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);\n if (showEvent.defaultPrevented || !isInTheDom) {\n return;\n }\n\n // TODO: v6 remove this or make it optional\n this._disposePopper();\n const tip = this._getTipElement();\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'));\n const {\n container\n } = this._config;\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip);\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));\n }\n this._popper = this._createPopper(tip);\n tip.classList.add(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2));\n if (this._isHovered === false) {\n this._leave();\n }\n this._isHovered = false;\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n hide() {\n if (!this._isShown()) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2));\n if (hideEvent.defaultPrevented) {\n return;\n }\n const tip = this._getTipElement();\n tip.classList.remove(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n this._activeTrigger[TRIGGER_CLICK] = false;\n this._activeTrigger[TRIGGER_FOCUS] = false;\n this._activeTrigger[TRIGGER_HOVER] = false;\n this._isHovered = null; // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return;\n }\n if (!this._isHovered) {\n this._disposePopper();\n }\n this._element.removeAttribute('aria-describedby');\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2));\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n update() {\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Protected\n _isWithContent() {\n return Boolean(this._getTitle());\n }\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());\n }\n return this.tip;\n }\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml();\n\n // TODO: remove this check in v6\n if (!tip) {\n return null;\n }\n tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2);\n // TODO: v6 the following can be achieved with CSS only\n tip.classList.add(`bs-${this.constructor.NAME}-auto`);\n const tipId = getUID(this.constructor.NAME).toString();\n tip.setAttribute('id', tipId);\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE$2);\n }\n return tip;\n }\n setContent(content) {\n this._newContent = content;\n if (this._isShown()) {\n this._disposePopper();\n this.show();\n }\n }\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content);\n } else {\n this._templateFactory = new TemplateFactory({\n ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n });\n }\n return this._templateFactory;\n }\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n };\n }\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');\n }\n\n // Private\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());\n }\n _isAnimated() {\n return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2);\n }\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2);\n }\n _createPopper(tip) {\n const placement = execute(this._config.placement, [this, tip, this._element]);\n const attachment = AttachmentMap[placement.toUpperCase()];\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment));\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this._element]);\n }\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [{\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }, {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n }, {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement);\n }\n }]\n };\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _setListeners() {\n const triggers = this._config.trigger.split(' ');\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context.toggle();\n });\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1);\n const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1);\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;\n context._enter();\n });\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);\n context._leave();\n });\n }\n }\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide();\n }\n };\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n }\n _fixTitle() {\n const title = this._element.getAttribute('title');\n if (!title) {\n return;\n }\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title);\n }\n this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility\n this._element.removeAttribute('title');\n }\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true;\n return;\n }\n this._isHovered = true;\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show();\n }\n }, this._config.delay.show);\n }\n _leave() {\n if (this._isWithActiveTrigger()) {\n return;\n }\n this._isHovered = false;\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide();\n }\n }, this._config.delay.hide);\n }\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout);\n this._timeout = setTimeout(handler, timeout);\n }\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true);\n }\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element);\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute];\n }\n }\n config = {\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n };\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container);\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n };\n }\n if (typeof config.title === 'number') {\n config.title = config.title.toString();\n }\n if (typeof config.content === 'number') {\n config.content = config.content.toString();\n }\n return config;\n }\n _getDelegateConfig() {\n const config = {};\n for (const [key, value] of Object.entries(this._config)) {\n if (this.constructor.Default[key] !== value) {\n config[key] = value;\n }\n }\n config.selector = false;\n config.trigger = 'manual';\n\n // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n return config;\n }\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy();\n this._popper = null;\n }\n if (this.tip) {\n this.tip.remove();\n this.tip = null;\n }\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Tooltip);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$3 = 'popover';\nconst SELECTOR_TITLE = '.popover-header';\nconst SELECTOR_CONTENT = '.popover-body';\nconst Default$2 = {\n ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
' + '
' + '

' + '
' + '
',\n trigger: 'click'\n};\nconst DefaultType$2 = {\n ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n};\n\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default$2;\n }\n static get DefaultType() {\n return DefaultType$2;\n }\n static get NAME() {\n return NAME$3;\n }\n\n // Overrides\n _isWithContent() {\n return this._getTitle() || this._getContent();\n }\n\n // Private\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n };\n }\n _getContent() {\n return this._resolvePossibleFunction(this._config.content);\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Popover);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$2 = 'scrollspy';\nconst DATA_KEY$2 = 'bs.scrollspy';\nconst EVENT_KEY$2 = `.${DATA_KEY$2}`;\nconst DATA_API_KEY = '.data-api';\nconst EVENT_ACTIVATE = `activate${EVENT_KEY$2}`;\nconst EVENT_CLICK = `click${EVENT_KEY$2}`;\nconst EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`;\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\nconst CLASS_NAME_ACTIVE$1 = 'active';\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]';\nconst SELECTOR_TARGET_LINKS = '[href]';\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\nconst SELECTOR_NAV_LINKS = '.nav-link';\nconst SELECTOR_NAV_ITEMS = '.nav-item';\nconst SELECTOR_LIST_ITEMS = '.list-group-item';\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;\nconst SELECTOR_DROPDOWN = '.dropdown';\nconst SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle';\nconst Default$1 = {\n offset: null,\n // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n};\nconst DefaultType$1 = {\n offset: '(number|null)',\n // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n};\n\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n\n // this._element is the observablesContainer and config.target the menu links wrapper\n this._targetLinks = new Map();\n this._observableSections = new Map();\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;\n this._activeTarget = null;\n this._observer = null;\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n };\n this.refresh(); // initialize\n }\n\n // Getters\n static get Default() {\n return Default$1;\n }\n static get DefaultType() {\n return DefaultType$1;\n }\n static get NAME() {\n return NAME$2;\n }\n\n // Public\n refresh() {\n this._initializeTargetsAndObservables();\n this._maybeEnableSmoothScroll();\n if (this._observer) {\n this._observer.disconnect();\n } else {\n this._observer = this._getNewObserver();\n }\n for (const section of this._observableSections.values()) {\n this._observer.observe(section);\n }\n }\n dispose() {\n this._observer.disconnect();\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body;\n\n // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));\n }\n return config;\n }\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return;\n }\n\n // unregister any previous listeners\n EventHandler.off(this._config.target, EVENT_CLICK);\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash);\n if (observableSection) {\n event.preventDefault();\n const root = this._rootElement || window;\n const height = observableSection.offsetTop - this._element.offsetTop;\n if (root.scrollTo) {\n root.scrollTo({\n top: height,\n behavior: 'smooth'\n });\n return;\n }\n\n // Chrome 60 doesn't support `scrollTo`\n root.scrollTop = height;\n }\n });\n }\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n };\n return new IntersectionObserver(entries => this._observerCallback(entries), options);\n }\n\n // The logic of selection\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop;\n this._process(targetElement(entry));\n };\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;\n this._previousScrollData.parentScrollTop = parentScrollTop;\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null;\n this._clearActiveClass(targetElement(entry));\n continue;\n }\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;\n // if we are scrolling down, pick the bigger offsetTop\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry);\n // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n if (!parentScrollTop) {\n return;\n }\n continue;\n }\n\n // if we are scrolling up, pick the smallest offsetTop\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry);\n }\n }\n }\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map();\n this._observableSections = new Map();\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue;\n }\n const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element);\n\n // ensure that the observableSection exists & is visible\n if (isVisible(observableSection)) {\n this._targetLinks.set(decodeURI(anchor.hash), anchor);\n this._observableSections.set(anchor.hash, observableSection);\n }\n }\n }\n _process(target) {\n if (this._activeTarget === target) {\n return;\n }\n this._clearActiveClass(this._config.target);\n this._activeTarget = target;\n target.classList.add(CLASS_NAME_ACTIVE$1);\n this._activateParents(target);\n EventHandler.trigger(this._element, EVENT_ACTIVATE, {\n relatedTarget: target\n });\n }\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1);\n return;\n }\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both