| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | import argparse |
| | import base64 |
| | import copy |
| | import datetime |
| | import os |
| | import pathlib |
| | import re |
| | import tempfile |
| | from typing import Any, List, Tuple |
| |
|
| | import FreeCAD |
| | import Path |
| | import Path.Base.Util as PathUtil |
| | import Path.Post.Processor |
| | import Path.Post.UtilsArguments |
| | import Path.Post.UtilsExport |
| | import Path.Post.Utils as PostUtils |
| | import Path.Post.UtilsParse |
| | import Path.Main.Job |
| |
|
| | translate = FreeCAD.Qt.translate |
| |
|
| | if DEBUG := False: |
| | Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) |
| | Path.Log.trackModule(Path.Log.thisModule()) |
| | else: |
| | Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) |
| |
|
| |
|
| | def convert_option_to_attr(option_name): |
| | |
| | if option_name.startswith("--"): |
| | option_name = option_name[2:] |
| | elif option_name.startswith("-"): |
| | option_name = option_name[1:] |
| |
|
| | return option_name.replace("-", "_") |
| |
|
| |
|
| | SNAPMAKER_MACHINES = dict( |
| | Original=dict( |
| | key="Original", |
| | name="Snapmaker Original", |
| | boundaries_table=[ |
| | |
| | dict(boundaries=dict(X=125, Y=125, Z=50), toolhead="Original_CNC", mods=set()), |
| | ], |
| | lead=dict(X=8, Y=8, Z=8), |
| | ), |
| | Original_Z_Extension=dict( |
| | key="Original_Z_Extension", |
| | name="Snapmaker Original with Z extension", |
| | boundaries_table=[ |
| | |
| | dict(boundaries=dict(X=125, Y=125, Z=146), toolhead="Original_CNC", mods=set()), |
| | ], |
| | lead=dict(X=8, Y=8, Z=8), |
| | ), |
| | A150=dict( |
| | key="A150", |
| | name="Snapmaker 2 A150", |
| | boundaries_table=[ |
| | |
| | dict(boundaries=dict(X=145, Y=160, Z=90), toolhead="50W_CNC", mods=set()), |
| | dict(boundaries=dict(X=145, Y=148, Z=90), toolhead="50W_CNC", mods={"BK"}), |
| | ], |
| | lead=dict(X=8, Y=8, Z=8), |
| | ), |
| | A250=dict( |
| | key="A250", |
| | name="Snapmaker 2 A250", |
| | boundaries_table=[ |
| | |
| | dict(boundaries=dict(X=230, Y=250, Z=180), toolhead="50W_CNC", mods=set()), |
| | dict(boundaries=dict(X=230, Y=238, Z=180), toolhead="50W_CNC", mods={"BK"}), |
| | dict(boundaries=dict(X=230, Y=235, Z=180), toolhead="50W_CNC", mods={"QS"}), |
| | dict(boundaries=dict(X=230, Y=223, Z=180), toolhead="50W_CNC", mods={"BK", "QS"}), |
| | dict(boundaries=dict(X=230, Y=225, Z=180), toolhead="200W_CNC", mods={"BK"}), |
| | dict(boundaries=dict(X=230, Y=210, Z=180), toolhead="200W_CNC", mods={"BK", "QS"}), |
| | ], |
| | lead=dict(X=8, Y=8, Z=8), |
| | ), |
| | A250T=dict( |
| | key="A250T", |
| | name="Snapmaker 2 A250T", |
| | boundaries_table=[ |
| | |
| | dict(boundaries=dict(X=230, Y=250, Z=180), toolhead="50W_CNC", mods=set()), |
| | dict(boundaries=dict(X=230, Y=238, Z=180), toolhead="50W_CNC", mods={"BK"}), |
| | dict(boundaries=dict(X=230, Y=235, Z=180), toolhead="50W_CNC", mods={"QS"}), |
| | dict(boundaries=dict(X=230, Y=223, Z=180), toolhead="50W_CNC", mods={"BK", "QS"}), |
| | dict(boundaries=dict(X=230, Y=225, Z=180), toolhead="200W_CNC", mods={"BK"}), |
| | dict(boundaries=dict(X=230, Y=210, Z=180), toolhead="200W_CNC", mods={"BK", "QS"}), |
| | ], |
| | lead=dict(X=20, Y=20, Z=8), |
| | ), |
| | A350=dict( |
| | key="A350", |
| | name="Snapmaker 2 A350", |
| | boundaries_table=[ |
| | |
| | dict(boundaries=dict(X=320, Y=350, Z=275), toolhead="50W_CNC", mods=set()), |
| | dict(boundaries=dict(X=320, Y=338, Z=275), toolhead="50W_CNC", mods={"BK"}), |
| | dict(boundaries=dict(X=320, Y=335, Z=275), toolhead="50W_CNC", mods={"QS"}), |
| | dict(boundaries=dict(X=320, Y=323, Z=275), toolhead="50W_CNC", mods={"BK", "QS"}), |
| | dict(boundaries=dict(X=320, Y=325, Z=275), toolhead="200W_CNC", mods={"BK"}), |
| | dict(boundaries=dict(X=320, Y=310, Z=275), toolhead="200W_CNC", mods={"BK", "QS"}), |
| | ], |
| | lead=dict(X=8, Y=8, Z=8), |
| | ), |
| | A350T=dict( |
| | key="A350T", |
| | name="Snapmaker 2 A350T", |
| | boundaries_table=[ |
| | |
| | dict(boundaries=dict(X=320, Y=350, Z=275), toolhead="50W_CNC", mods=set()), |
| | dict(boundaries=dict(X=320, Y=338, Z=275), toolhead="50W_CNC", mods={"BK"}), |
| | dict(boundaries=dict(X=320, Y=335, Z=275), toolhead="50W_CNC", mods={"QS"}), |
| | dict(boundaries=dict(X=320, Y=323, Z=275), toolhead="50W_CNC", mods={"BK", "QS"}), |
| | dict(boundaries=dict(X=320, Y=325, Z=275), toolhead="200W_CNC", mods={"BK"}), |
| | dict(boundaries=dict(X=320, Y=310, Z=275), toolhead="200W_CNC", mods={"BK", "QS"}), |
| | ], |
| | lead=dict(X=20, Y=20, Z=8), |
| | ), |
| | Artisan=dict( |
| | key="Artisan", |
| | name="Snapmaker Artisan", |
| | boundaries_table=[ |
| | dict(boundaries=dict(X=400, Y=400, Z=400), toolhead="200W_CNC", mods=set()), |
| | ], |
| | lead=dict(X=40, Y=40, Z=8), |
| | ), |
| | ) |
| |
|
| | |
| | |
| | SNAPMAKER_MOD_KITS = { |
| | "QS": dict( |
| | key="QS", |
| | name="Quick Swap Kit", |
| | option_name="--quick-swap", |
| | option_help_text="Indicates that the quick swap kit is installed. Only compatible with Snapmaker 2 machines.", |
| | ), |
| | "BK": dict( |
| | key="BK", |
| | name="Bracing Kit", |
| | option_name="--bracing-kit", |
| | option_help_text="Indicates that the bracing kit is installed. Only compatible with Snapmaker 2 machines.", |
| | ), |
| | } |
| |
|
| | |
| | |
| | SNAPMAKER_TOOLHEADS = { |
| | "Original_CNC": dict( |
| | key="Original_CNC", |
| | name="Original CNC module", |
| | speed_rpm=dict(min=0, max=7000), |
| | boundaries_delta=dict(X=0, Y=0, Z=0), |
| | has_percent=True, |
| | has_speed_s=False, |
| | ), |
| | "50W_CNC": dict( |
| | key="50W_CNC", |
| | name="50W CNC module", |
| | speed_rpm=dict(min=0, max=12000), |
| | boundaries_delta=dict(X=0, Y=0, Z=0), |
| | has_percent=True, |
| | has_speed_s=False, |
| | ), |
| | "200W_CNC": dict( |
| | key="200W_CNC", |
| | name="200W CNC module", |
| | speed_rpm=dict(min=0, max=18000), |
| | boundaries_delta=dict(X=0, Y=-13, Z=0), |
| | has_percent=True, |
| | has_speed_s=True, |
| | ), |
| | } |
| |
|
| |
|
| | class CoordinatesAction(argparse.Action): |
| | """argparse Action to handle coordinates (x,y,z)""" |
| |
|
| | def __call__(self, parser, namespace, values, option_string=None): |
| | match = re.match( |
| | r"^\s*(?P<X>-?\d+\.?\d*),?\s*(?P<Y>-?\d+\.?\d*),?\s*(?P<Z>-?\d+\.?\d*)\s*$", values |
| | ) |
| | if match: |
| | |
| | params = {key: float(value) for key, value in match.groupdict().items()} |
| | setattr(namespace, self.dest, params) |
| | else: |
| | raise argparse.ArgumentError(None, message="invalid coordinates provided") |
| |
|
| |
|
| | class ExtremaAction(argparse.Action): |
| | """argparse Action to handle integer extrema (min,max)""" |
| |
|
| | def __call__(self, parser, namespace, values, option_string=None): |
| | if match := re.match(r"^ *(\d+),? *(\d+) *$", values): |
| | |
| | params = { |
| | key: int(value) |
| | for key, value in zip( |
| | ( |
| | "min", |
| | "max", |
| | ), |
| | match.groups(), |
| | ) |
| | } |
| | setattr(namespace, self.dest, params) |
| | else: |
| | raise argparse.ArgumentError(None, message="invalid values provided, should be int,int") |
| |
|
| |
|
| | class Snapmaker(Path.Post.Processor.PostProcessor): |
| | """FreeCAD postprocessor targeting Snapmaker machines with CNC capabilities""" |
| |
|
| | def __init__(self, job) -> None: |
| | super().__init__( |
| | job=job, |
| | tooltip=translate("CAM", "Snapmaker post processor"), |
| | tooltipargs=[""], |
| | units="Metric", |
| | ) |
| |
|
| | self.initialize() |
| |
|
| | def initialize(self): |
| | """initialize values and arguments""" |
| | self.values: dict[str, Any] = dict() |
| | self.argument_defaults: dict[str, bool] = dict() |
| | self.arguments_visible: dict[str, bool] = dict() |
| | self.parser = argparse.ArgumentParser() |
| |
|
| | self.snapmaker_init_values() |
| | self.snapmaker_init_argument_defaults() |
| | self.snapmaker_init_arguments_visible() |
| | self.parser = self.snapmaker_init_parser( |
| | self.values, self.argument_defaults, self.arguments_visible |
| | ) |
| |
|
| | |
| | all_arguments_visible = dict() |
| | for key in iter(self.arguments_visible): |
| | all_arguments_visible[key] = True |
| | self.visible_parser = self.snapmaker_init_parser( |
| | self.values, self.argument_defaults, all_arguments_visible |
| | ) |
| |
|
| | FreeCAD.Console.PrintLog(f'{self.values["POSTPROCESSOR_FILE_NAME"]}: initialized.\n') |
| |
|
| | def snapmaker_init_values(self): |
| | """Initialize values that are used throughout the postprocessor.""" |
| | Path.Post.UtilsArguments.init_shared_values(self.values) |
| |
|
| | |
| | self.values["POSTPROCESSOR_FILE_NAME"] = __name__ |
| | self.values["COMMENT_SYMBOL"] = ";" |
| | self.values["ENABLE_MACHINE_SPECIFIC_COMMANDS"] = True |
| | self.values["END_OF_LINE_CHARACTERS"] = "\n" |
| | self.values["FINISH_LABEL"] = "End" |
| | self.values["LINE_INCREMENT"] = 1 |
| | self.values["MACHINE_NAME"] = None |
| | self.values["MODAL"] = False |
| | self.values["OUTPUT_PATH_LABELS"] = True |
| | self.values["OUTPUT_HEADER"] = ( |
| | True |
| | ) |
| | self.values["OUTPUT_TOOL_CHANGE"] = True |
| | self.values["PARAMETER_ORDER"] = [ |
| | "X", |
| | "Y", |
| | "Z", |
| | "A", |
| | "B", |
| | "C", |
| | "I", |
| | "J", |
| | "F", |
| | "S", |
| | "T", |
| | "Q", |
| | "R", |
| | "L", |
| | "H", |
| | "D", |
| | "P", |
| | "O", |
| | ] |
| | self.values["PREAMBLE"] = """G90\nG17""" |
| | self.values["PRE_OPERATION"] = """""" |
| | self.values["POST_OPERATION"] = """""" |
| | self.values["POSTAMBLE"] = """M400\nM5""" |
| | self.values["SHOW_MACHINE_UNITS"] = False |
| | self.values["SPINDLE_DECIMALS"] = 0 |
| | self.values["SPINDLE_WAIT"] = 4.0 |
| | self.values["TOOL_CHANGE"] = "M76" |
| | self.values["TRANSLATE_DRILL_CYCLES"] = True |
| | self.values["USE_TLO"] = False |
| |
|
| | |
| | self.values["THUMBNAIL"] = True |
| | self.values["BOUNDARIES"] = None |
| | self.values["BOUNDARIES_CHECK"] = False |
| | self.values["MACHINES"] = SNAPMAKER_MACHINES |
| | self.values["MOD_KITS_ALL"] = SNAPMAKER_MOD_KITS |
| | self.values["TOOLHEADS"] = SNAPMAKER_TOOLHEADS |
| | self.values["TOOLHEAD_NAME"] = None |
| | self.values["SPINDLE_SPEEDS"] = dict() |
| | self.values["SPINDLE_PERCENT"] = None |
| |
|
| | def snapmaker_init_argument_defaults(self) -> None: |
| | """Initialize which arguments (in a pair) are shown as the default argument.""" |
| | Path.Post.UtilsArguments.init_argument_defaults(self.argument_defaults) |
| |
|
| | self.argument_defaults["tlo"] = False |
| | self.argument_defaults["translate-drill"] = True |
| |
|
| | |
| | self.argument_defaults["thumbnail"] = True |
| | self.argument_defaults["gui"] = True |
| | self.argument_defaults["boundaries-check"] = True |
| |
|
| | def snapmaker_init_arguments_visible(self) -> None: |
| | """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" |
| | Path.Post.UtilsArguments.init_arguments_visible(self.arguments_visible) |
| |
|
| | self.arguments_visible["axis-modal"] = False |
| | self.arguments_visible["header"] = False |
| | self.arguments_visible["return-to"] = True |
| | self.arguments_visible["tlo"] = False |
| | self.arguments_visible["tool_change"] = True |
| | self.arguments_visible["translate-drill"] = False |
| | self.arguments_visible["wait-for-spindle"] = True |
| |
|
| | |
| | self.arguments_visible["thumbnail"] = True |
| | self.arguments_visible["gui"] = True |
| | self.arguments_visible["boundaries"] = True |
| | self.arguments_visible["boundaries-check"] = True |
| | self.arguments_visible["machine"] = True |
| | self.arguments_visible["toolhead"] = True |
| | self.arguments_visible["line-increment"] = True |
| | self.arguments_visible["spindle-speeds"] = True |
| |
|
| | def snapmaker_init_parser( |
| | self, values, argument_defaults, arguments_visible |
| | ) -> argparse.ArgumentParser: |
| | """Initialize the postprocessor arguments parser""" |
| | parser = Path.Post.UtilsArguments.init_shared_arguments( |
| | values, argument_defaults, arguments_visible |
| | ) |
| |
|
| | |
| | group = parser.add_argument_group("Snapmaker only arguments") |
| | |
| | |
| | group.add_argument( |
| | "--thumbnail", |
| | action="store_true", |
| | default=argument_defaults["thumbnail"], |
| | help="Include a thumbnail (require --gui)", |
| | ) |
| | group.add_argument( |
| | "--no-thumbnail", action="store_false", dest="thumbnail", help="Remove thumbnail" |
| | ) |
| |
|
| | group.add_argument( |
| | "--gui", |
| | action="store_true", |
| | default=argument_defaults["gui"], |
| | help="allow the postprocessor to execute GUI methods", |
| | ) |
| | group.add_argument( |
| | "--no-gui", |
| | action="store_false", |
| | dest="gui", |
| | help="Execute postprocessor without requiring GUI", |
| | ) |
| |
|
| | group.add_argument( |
| | "--boundaries-check", |
| | action="store_true", |
| | default=argument_defaults["boundaries-check"], |
| | help="check boundaries according to the machine build area", |
| | ) |
| | group.add_argument( |
| | "--no-boundaries-check", |
| | action="store_false", |
| | dest="boundaries_check", |
| | help="Disable boundaries check", |
| | ) |
| |
|
| | group.add_argument( |
| | "--boundaries", |
| | action=CoordinatesAction, |
| | default=None, |
| | help='Custom boundaries (e.g. "100, 200, 300"). Overrides boundaries from --machine', |
| | ) |
| |
|
| | group.add_argument( |
| | "--machine", |
| | default=None, |
| | required=True, |
| | choices=self.values["MACHINES"].keys(), |
| | help=f"Snapmaker machine. Choose from [{self.values['MACHINES'].keys()}].", |
| | ) |
| |
|
| | for key, value in SNAPMAKER_MOD_KITS.items(): |
| | group.add_argument( |
| | value["option_name"], |
| | default=False, |
| | action="store_true", |
| | help=value["option_help_text"], |
| | ) |
| |
|
| | group.add_argument( |
| | "--toolhead", |
| | default=None, |
| | choices=self.values["TOOLHEADS"].keys(), |
| | help=f"Snapmaker toolhead. Choose from [{self.values['TOOLHEADS'].keys()}].", |
| | ) |
| |
|
| | group.add_argument( |
| | "--spindle-speeds", |
| | action=ExtremaAction, |
| | default=None, |
| | help="Set minimum/maximum spindle speeds as --spindle-speeds='min,max'", |
| | ) |
| |
|
| | group.add_argument( |
| | "--spindle-percent", |
| | action="store_true", |
| | default=None, |
| | help="use percent as toolhead spindle speed unit (default: use RPM if supported by toolhead, otherwise percent)", |
| | ) |
| |
|
| | group.add_argument( |
| | "--line-number", |
| | type=int, |
| | default=self.values["line_number"], |
| | help="Set the line starting value", |
| | ) |
| |
|
| | group.add_argument( |
| | "--line-increment", |
| | type=int, |
| | default=self.values["LINE_INCREMENT"], |
| | help="Set the line increment value", |
| | ) |
| |
|
| | return parser |
| |
|
| | def snapmaker_process_arguments( |
| | self, filename: str = "-" |
| | ) -> Tuple[bool, str | argparse.Namespace]: |
| | """Process any arguments to the postprocessor.""" |
| | (flag, args) = Path.Post.UtilsArguments.process_shared_arguments( |
| | self.values, self.parser, self._job.PostProcessorArgs, self.visible_parser, filename |
| | ) |
| | if flag: |
| | self._units = self.values["UNITS"] |
| |
|
| | |
| | machine = self.values["MACHINES"][args.machine] |
| | self.values["MACHINE_KEY"] = machine["key"] |
| | self.values["MACHINE_NAME"] = machine["name"] |
| |
|
| | compatible_toolheads = {bt["toolhead"] for bt in machine["boundaries_table"]} |
| |
|
| | if args.toolhead: |
| | if args.toolhead not in compatible_toolheads: |
| | FreeCAD.Console.PrintError( |
| | f"Selected --toolhead={args.toolhead} is not compatible with machine {machine['name']}." |
| | + f" Choose from [{compatible_toolheads}]\n" |
| | ) |
| | flag = False |
| | return (flag, args) |
| | toolhead = self.values["TOOLHEADS"][args.toolhead] |
| | elif len(compatible_toolheads) == 1: |
| | toolhead_key = next(iter(compatible_toolheads)) |
| | toolhead = self.values["TOOLHEADS"][toolhead_key] |
| | else: |
| | FreeCAD.Console.PrintError( |
| | f"Machine {machine['name']} has multiple compatible toolheads:\n" |
| | f"{compatible_toolheads}\n" |
| | "Please add --toolhead argument.\n" |
| | ) |
| | flag = False |
| | return (flag, args) |
| | self.values["TOOLHEAD_KEY"] = toolhead["key"] |
| | self.values["TOOLHEAD_NAME"] = toolhead["name"] |
| |
|
| | self.values["SPINDLE_SPEEDS"] = toolhead["speed_rpm"] |
| |
|
| | if args.spindle_speeds: |
| | self.values["SPINDLE_SPEEDS"] = args.spindle_speeds |
| |
|
| | if args.spindle_percent is not None: |
| | if toolhead["has_percent"]: |
| | self.values["SPINDLE_PERCENT"] = True |
| | else: |
| | FreeCAD.Console.PrintError( |
| | f"Requested spindle speed in percent, but toolhead {toolhead['name']}" |
| | + " does not support speed as percent.\n" |
| | ) |
| | flag = False |
| | return (flag, args) |
| | else: |
| | |
| | self.values["SPINDLE_PERCENT"] = ( |
| | toolhead["has_percent"] and not toolhead["has_speed_s"] |
| | ) |
| | if self.values["SPINDLE_PERCENT"]: |
| | FreeCAD.Console.PrintWarning( |
| | "Spindle speed will be controlled using using percentages.\n" |
| | ) |
| | else: |
| | FreeCAD.Console.PrintWarning("Spindle speed will be controlled using using RPM.\n") |
| |
|
| | self.values["MOD_KITS_INSTALLED"] = [] |
| | if args.boundaries: |
| | self.values["BOUNDARIES"] = args.boundaries |
| | self.values["MACHINE_NAME"] += " Boundaries override=" + str(args.boundaries) |
| | else: |
| | compatible_modkit_combos = [ |
| | bt["mods"] |
| | for bt in machine["boundaries_table"] |
| | if toolhead["key"] == bt["toolhead"] |
| | ] |
| | configured_modkits = set() |
| |
|
| | |
| | for mod_kit in self.values["MOD_KITS_ALL"].values(): |
| | if getattr(args, convert_option_to_attr(mod_kit["option_name"])): |
| | configured_modkits.add(mod_kit["key"]) |
| | self.values["MACHINE_NAME"] += " " + mod_kit["name"] |
| | self.values["MOD_KITS_INSTALLED"].append(mod_kit["key"]) |
| |
|
| | if configured_modkits not in compatible_modkit_combos: |
| | FreeCAD.Console.PrintError( |
| | f"Machine {machine['name']} with toolhead {toolhead['name']}" |
| | + f" is not compatible with modkit {configured_modkits if configured_modkits else None}.\n" |
| | + f" Choose from {compatible_modkit_combos}." |
| | ) |
| | flag = False |
| | return (flag, args) |
| |
|
| | |
| | boundaries_table_entry_l = [ |
| | bt |
| | for bt in machine["boundaries_table"] |
| | if bt["toolhead"] == toolhead["key"] and bt["mods"] == configured_modkits |
| | ] |
| | assert len(boundaries_table_entry_l) == 1 |
| | boundaries_table_entry = boundaries_table_entry_l[0] |
| |
|
| | |
| | self.values["BOUNDARIES"] = copy.deepcopy(boundaries_table_entry["boundaries"]) |
| | self.values["MACHINE_NAME"] += " " + toolhead["name"] |
| |
|
| | self.values["THUMBNAIL"] = args.thumbnail |
| | self.values["ALLOW_GUI"] = args.gui |
| | self.values["line_number"] = args.line_number |
| | self.values["LINE_INCREMENT"] = args.line_increment |
| |
|
| | if args.boundaries_check and not self.values["BOUNDARIES"]: |
| | FreeCAD.Console.PrintError("Boundary check skipped: no valid boundaries supplied\n") |
| | self.values["BOUNDARIES_CHECK"] = False |
| | else: |
| | self.values["BOUNDARIES_CHECK"] = args.boundaries_check |
| |
|
| | return flag, args |
| |
|
| | def snapmaker_process_postables(self, filename: str = "-") -> List[Tuple[str, str]]: |
| | """process job sections to gcode""" |
| | sections: List[Tuple[str, str]] = list() |
| |
|
| | postables = self._buildPostList() |
| |
|
| | |
| | if len(postables) > 1 and filename != "-": |
| | filename = pathlib.Path(filename) |
| | filename = str(filename.with_stem(filename.stem + "_{name}")) |
| |
|
| | for name, objects in postables: |
| | gcode = self.export_common(objects, filename.format(name=name)) |
| | sections.append((name, gcode)) |
| |
|
| | return sections |
| |
|
| | def get_thumbnail(self) -> str: |
| | """generate a thumbnail of the job from the given objects""" |
| | if self.values["THUMBNAIL"] is False: |
| | return "thumbnail: deactivated." |
| |
|
| | if not (self.values["ALLOW_GUI"] and FreeCAD.GuiUp): |
| | FreeCAD.Console.PrintError( |
| | "GUI access required: thumbnail generation skipped. Consider adding --gui\n" |
| | ) |
| | return "thumbnail: GUI required." |
| |
|
| | |
| | import FreeCADGui |
| |
|
| | view = FreeCADGui.activeDocument().activeView() |
| | selection = FreeCADGui.Selection |
| |
|
| | |
| | selected = [ |
| | obj.Object for obj in selection.getCompleteSelection() if hasattr(obj, "Object") |
| | ] |
| | selection.clearSelection() |
| |
|
| | |
| | FreeCADGui.runCommand("Std_SelectAll", 0) |
| | all = [] |
| | for obj in selection.getCompleteSelection(): |
| | if hasattr(obj, "Object"): |
| | all.append((obj.Object, obj.Object.Visibility)) |
| | obj.Object.ViewObject.hide() |
| |
|
| | |
| | for model in self._job.Model.Group: |
| | model.ViewObject.show() |
| | selection.addSelection(model.Document.Name, model.Name) |
| | view.fitAll() |
| | view.viewIsometric() |
| | selection.clearSelection() |
| |
|
| | |
| | with tempfile.TemporaryDirectory() as temp: |
| | path = os.path.join(temp, "thumbnail.png") |
| | view.saveImage(path, 720, 480, "Transparent") |
| | with open(path, "rb") as file: |
| | data = file.read() |
| |
|
| | |
| | for obj, visibility in all: |
| | if visibility: |
| | obj.ViewObject.show() |
| |
|
| | |
| | for obj in selected: |
| | selection.clearSelection() |
| | selection.addSelection(obj.Document.Name, obj.Name) |
| |
|
| | return f"thumbnail: data:image/png;base64,{base64.b64encode(data).decode()}" |
| |
|
| | def output_header(self, gcode: List[str]): |
| | """custom method derived from Path.Post.UtilsExport.output_header""" |
| | cam_file: str |
| | comment: str |
| |
|
| | if not self.values["OUTPUT_HEADER"]: |
| | return |
| |
|
| | def add_comment(text): |
| | com = Path.Post.UtilsParse.create_comment(self.values, text) |
| | gcode.append(f"{Path.Post.UtilsParse.linenumber(self.values)}{com}") |
| |
|
| | add_comment("Header Start") |
| | add_comment("header_type: cnc") |
| | add_comment(f'machine: {self.values["MACHINE_NAME"]}') |
| | comment = Path.Post.UtilsParse.create_comment( |
| | self.values, f'Post Processor: {self.values["POSTPROCESSOR_FILE_NAME"]}' |
| | ) |
| | gcode.append(f"{Path.Post.UtilsParse.linenumber(self.values)}{comment}") |
| | if FreeCAD.ActiveDocument: |
| | cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName) |
| | else: |
| | cam_file = "<None>" |
| | add_comment(f"CAM File: {cam_file}") |
| | add_comment(f"Output Time: {datetime.datetime.now()}") |
| | add_comment(self.get_thumbnail()) |
| |
|
| | def convert_spindle(self, gcode: List[str]) -> List[str]: |
| | """convert spindle speed values from RPM to percent (%) (M3/M4 commands)""" |
| | if self.values["SPINDLE_PERCENT"] is False: |
| | return gcode |
| |
|
| | |
| | |
| | for index, commandline in enumerate( |
| | gcode |
| | ): |
| | if match := re.match(r"(?P<command>M0?[34])\D.*(?P<spindle>S\d+.?\d*)", commandline): |
| | percent = ( |
| | float(match.group("spindle")[1:]) * 100 / self.values["SPINDLE_SPEEDS"]["max"] |
| | ) |
| | gcode[index] = ( |
| | gcode[index][: match.span("spindle")[0]] |
| | + f'P{percent:.{self.values["SPINDLE_DECIMALS"]}f}' |
| | + gcode[index][match.span("spindle")[1] :] |
| | ) |
| | return gcode |
| |
|
| | def check_boundaries(self, gcode: List[str]) -> bool: |
| | """Check boundaries and return whether it succeeded""" |
| | status = True |
| | FreeCAD.Console.PrintLog("Boundaries check\n") |
| |
|
| | extrema = dict(X=[0, 0], Y=[0, 0], Z=[0, 0]) |
| | position = dict(X=0, Y=0, Z=0) |
| | relative = False |
| |
|
| | for index, commandline in enumerate(gcode): |
| | if re.match(r"G90(?:\D|$)", commandline): |
| | relative = False |
| | elif re.match(r"G91(?:\D|$)", commandline): |
| | relative = True |
| | elif re.match(r"G0?[12](?:\D|$)", commandline): |
| | for axis, value in re.findall( |
| | r"(?P<axis>[XYZ])(?P<value>-?\d+\.?\d*)(?:\D|$)", commandline |
| | ): |
| | if relative: |
| | position[axis] += float(value) |
| | else: |
| | position[axis] = float(value) |
| | extrema[axis][0] = min(extrema[axis][0], position[axis]) |
| | extrema[axis][1] = max(extrema[axis][1], position[axis]) |
| |
|
| | for axis in extrema.keys(): |
| | if abs(extrema[axis][1] - extrema[axis][0]) > self.values["BOUNDARIES"][axis]: |
| | |
| | FreeCAD.Console.PrintWarning( |
| | f"Boundary check: job exceeds machine limit on {axis} axis\n" |
| | ) |
| | status = False |
| |
|
| | return status |
| |
|
| | def export_common(self, objects: List, filename: str | pathlib.Path) -> str: |
| | """custom method derived from Path.Post.UtilsExport.export_common""" |
| | final: str |
| | gcode: List = [] |
| | result: bool |
| |
|
| | for obj in objects: |
| | if not hasattr(obj, "Path"): |
| | print(f"The object {obj.Name} is not a path.") |
| | print("Please select only path and Compounds.") |
| | return "" |
| |
|
| | Path.Post.UtilsExport.check_canned_cycles(self.values) |
| | self.output_header(gcode) |
| | Path.Post.UtilsExport.output_safetyblock(self.values, gcode) |
| | Path.Post.UtilsExport.output_tool_list(self.values, gcode, objects) |
| | Path.Post.UtilsExport.output_preamble(self.values, gcode) |
| | Path.Post.UtilsExport.output_motion_mode(self.values, gcode) |
| | Path.Post.UtilsExport.output_units(self.values, gcode) |
| |
|
| | for obj in objects: |
| | |
| | if not PathUtil.activeForOp(obj): |
| | continue |
| | coolant_mode = PathUtil.coolantModeForOp(obj) |
| | Path.Post.UtilsExport.output_start_bcnc(self.values, gcode, obj) |
| | Path.Post.UtilsExport.output_preop(self.values, gcode, obj) |
| | Path.Post.UtilsExport.output_coolant_on(self.values, gcode, coolant_mode) |
| | |
| | Path.Post.UtilsParse.parse_a_group(self.values, gcode, obj) |
| |
|
| | Path.Post.UtilsExport.output_postop(self.values, gcode, obj) |
| | Path.Post.UtilsExport.output_coolant_off(self.values, gcode, coolant_mode) |
| |
|
| | Path.Post.UtilsExport.output_return_to(self.values, gcode) |
| | |
| | |
| | |
| | |
| | |
| | |
| | Path.Post.UtilsExport.output_end_bcnc(self.values, gcode) |
| | Path.Post.UtilsExport.output_postamble_header(self.values, gcode) |
| | Path.Post.UtilsExport.output_tool_return(self.values, gcode) |
| | Path.Post.UtilsExport.output_safetyblock(self.values, gcode) |
| | Path.Post.UtilsExport.output_postamble(self.values, gcode) |
| | gcode = self.convert_spindle(gcode) |
| |
|
| | if self.values["BOUNDARIES_CHECK"]: |
| | self.check_boundaries(gcode) |
| |
|
| | |
| | gcode.append("") |
| | final = self.values["END_OF_LINE_CHARACTERS"].join(gcode) |
| |
|
| | if FreeCAD.GuiUp and self.values["SHOW_EDITOR"]: |
| | |
| | dia = PostUtils.GCodeEditorDialog() |
| | |
| | if self.values["END_OF_LINE_CHARACTERS"] == "\n": |
| | dia.editor.setPlainText(final) |
| | if dia.exec_(): |
| | final = dia.editor.toPlainText() |
| | else: |
| | final_for_editor = "\n".join(gcode) |
| | dia.editor.setPlainText(final_for_editor) |
| | if dia.exec_(): |
| | final_for_editor = dia.editor.toPlainText() |
| | |
| | final = final_for_editor.replace("\n", self.values["END_OF_LINE_CHARACTERS"]) |
| |
|
| | if not filename == "-": |
| | with open(filename, "w", encoding="utf-8", newline="") as gfile: |
| | gfile.write(final) |
| |
|
| | return final |
| |
|
| | def export(self, filename: str | pathlib.Path = "-"): |
| | """process gcode and export""" |
| | (flag, args) = self.snapmaker_process_arguments() |
| | if flag: |
| | return self.snapmaker_process_postables(filename) |
| | if args is None: |
| | return None |
| | return [("allitems", args)] |
| |
|
| | @property |
| | def tooltip(self) -> str: |
| | tooltip = "Postprocessor of the FreeCAD CAM workbench for the Snapmaker machines" |
| | return tooltip |
| |
|
| | @property |
| | def tooltipArgs(self) -> str: |
| | return self.parser.format_help() |
| |
|
| |
|
| | if __name__ == "__main__": |
| | Snapmaker(None).visible_parser.format_help() |
| |
|