diff --git a/.gitignore b/.gitignore index 5e7e526..0a887ad 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,3 @@ release/ *.blend *.blend1 *.bgcode -tests/Link to Configs -profiles/prusaslicer/Link to Configs diff --git a/__init__.py b/__init__.py index 1bfe9ac..321034b 100644 --- a/__init__.py +++ b/__init__.py @@ -2,8 +2,6 @@ from __future__ import annotations import bpy from . import registry from .core.nodeslicer import sockets, nodes, tree # sockets and nodes need also to be imported in order for them to be autoregistered -from .ui import panels -from .property_groups.bpy_property_groups import SlicerObjectPropertyGroup bl_info = { "name": "NodeSlicer", @@ -15,19 +13,13 @@ bl_info = { "category": "Node", } -_registered_nodeslicer = False _keymaps = [] def register(): - global _registered_nodeslicer registry.blender_register_classes() bpy.types.NODE_MT_add.append(tree.draw_add_menu) bpy.app.handlers.depsgraph_update_post.append(tree.depsgraph_update_handler) - if not hasattr(bpy.types.Object, "nodeslicer"): - setattr(bpy.types.Object, "nodeslicer", bpy.props.PointerProperty(type=SlicerObjectPropertyGroup, name="nodeslicer")) - _registered_nodeslicer = True - # Register Keymaps wm = bpy.context.window_manager assert wm @@ -44,7 +36,6 @@ def register(): print('Nodeslicer Loaded') def unregister(): - global _registered_nodeslicer try: if tree.depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(tree.depsgraph_update_handler) @@ -67,15 +58,6 @@ def unregister(): registry.blender_unregister_classes() - if _registered_nodeslicer: - try: - if hasattr(bpy.types.Object, "nodeslicer"): - delattr(bpy.types.Object, "nodeslicer") - except Exception as e: - print(f"Error removing property: {e}") - finally: - _registered_nodeslicer = False - print('Nodeslicer Unloaded') if __name__ == "__main__": diff --git a/core/nodeslicer/nodes/inputs.py b/core/nodeslicer/nodes/inputs.py index fc3aa18..e6ad854 100644 --- a/core/nodeslicer/nodes/inputs.py +++ b/core/nodeslicer/nodes/inputs.py @@ -91,157 +91,11 @@ class SLICENODE_CollectionObjects(SliceNode): return bpy.data.objects[socket_name] return None -@register_class -class SLICENODE_WipeTower(SliceNode): - bl_label = "Wipe Tower" - - def init(self, context): - self.add_input("NodeSocketFloat", "X Position").value = 170.0 - self.add_input("NodeSocketFloat", "Y Position").value = 140.0 - self.add_output("SLICESOCK_SettingsRo", "Settings") - - def evaluate_output(self, socket_name: str, **_kwargs) -> Any: - if socket_name != "Settings": - return {} - - raw_x: Any = self.resolve("X Position") - raw_y: Any = self.resolve("Y Position") - - if isinstance(raw_x, (list, tuple, np.ndarray)): - pos_x = float(raw_x[0]) if len(raw_x) >= 1 else 0.0 - pos_y = float(raw_x[1]) if len(raw_x) >= 2 else (float(raw_y) if raw_y is not None else 0.0) - else: - pos_x = float(raw_x) if raw_x is not None else 0.0 - pos_y = float(raw_y) if raw_y is not None else 0.0 - - return { - "wipe_tower_position": (pos_x, pos_y) - } - -@register_class -class SLICENODE_OffsetObject(SliceNode): - bl_label = "Offset Object" - - def init(self, context): - self.add_input("NodeSocketFloat", "X Offset").value = 0.0 - self.add_input("NodeSocketFloat", "Y Offset").value = 0.0 - self.add_input("NodeSocketFloat", "Z Offset").value = 0.0 - self.add_output("SLICESOCK_SettingsRo", "Settings") - - def evaluate_output(self, socket_name: str, **_kwargs) -> Any: - if socket_name != "Settings": - return {} - - raw_x: Any = self.resolve("X Offset") - raw_y: Any = self.resolve("Y Offset") - raw_z: Any = self.resolve("Z Offset") - - if isinstance(raw_x, (list, tuple, np.ndarray)): - ox = float(raw_x[0]) if len(raw_x) >= 1 else 0.0 - oy = float(raw_x[1]) if len(raw_x) >= 2 else (float(raw_y) if raw_y is not None else 0.0) - oz = float(raw_x[2]) if len(raw_x) >= 3 else (float(raw_z) if raw_z is not None else 0.0) - else: - ox = float(raw_x) if raw_x is not None else 0.0 - oy = float(raw_y) if raw_y is not None else 0.0 - oz = float(raw_z) if raw_z is not None else 0.0 - - return { - "offset_object": (ox, oy, oz) - } - @register_class class SLICENODE_InputSettings(InputNode): input_type = "Settings" bl_icon = "OBJECT_DATA" -@register_class -class SLICENODE_PrusaslicerConf(SliceNode): - bl_label = "PrusaSlicer Config" - bl_icon = "PROPERTIES" - - _cache = None - - @classmethod - def get_cache(cls): - if cls._cache is None: - from ....infra.prusaslicer_profiles import LocalCache - cls._cache = LocalCache() - return cls._cache - - def ensure_profiles_loaded(self): - cache = self.get_cache() - if not cache.profiles: - # Use a path relative to the addon directory - # Note: adjust paths to point to the correct addon root - addon_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) - profiles_path = os.path.join(addon_dir, "profiles", "prusaslicer") - if os.path.isdir(profiles_path): - cache.load([profiles_path]) - - if self.printer_id != '': - cache.evaluate_compatibility({self.printer_id}, set(cache.vendors)) - - def update_profiles(self, context): - cache = self.get_cache() - if self.printer_id != '': - cache.evaluate_compatibility({self.printer_id}, set(cache.vendors)) - - notify_tree_changed(self, context) - - def search_printers(self, _context, edit_text): - self.ensure_profiles_loaded() - cache = self.get_cache() - items: list[str] = [p.id for k, p in cache.printer_profiles.items() if edit_text.lower() in p.id.lower()] - items.sort() - return items - - def search_conf(self, edit_text: str, conf_type: str) -> list[str]: - if not (printer_id := self.printer_id): return [] - - self.ensure_profiles_loaded() - cache = self.get_cache() - if not (printer := cache.printer_profiles.get("printer:" + printer_id)): return [] - - profiles = printer.compatible_filament_profiles if conf_type == "filaments" else printer.compatible_print_profiles - - items: list[str] = [p.id for p in profiles if edit_text.lower() in p.id.lower()] - items.sort() - return items - - def search_filaments(self, _context, edit_text) -> list[str]: - return self.search_conf(edit_text, conf_type="filaments") - - def search_prints(self, _context, edit_text) -> list[str]: - return self.search_conf(edit_text, conf_type="prints") - - printer_id: str = NfStringProperty(name="Printer", default='', update=update_profiles, search=search_printers) - filament_id: str = NfStringProperty(name="Filament", default='', search=search_filaments) - print_id: str = NfStringProperty(name="Print", default='', search=search_prints) - - def init(self, context): - self.add_output("SLICESOCK_PrusaslicerConf", "Config") - - def draw_buttons(self, context, layout): - assert layout - self.ensure_profiles_loaded() - layout.prop(self, "printer_id") - if self.printer_id != '': - layout.prop(self, "filament_id") - layout.prop(self, "print_id") - - def evaluate_output(self, socket_name: str, **_kwargs) -> Any: - self.ensure_profiles_loaded() - if self.printer_id == '' or self.filament_id == '' or self.print_id == '': - return None - - cache = self.get_cache() - return { - 'printer': "printer:" + self.printer_id, - 'filament': ["filament:" + self.filament_id], - 'print': "print:" + self.print_id, - 'cache': cache - } - @register_class class SLICENODE_InputPrinter(SliceNode): bl_label = "Printer" diff --git a/core/nodeslicer/nodes/mesh.py b/core/nodeslicer/nodes/mesh.py index 86a5934..8772102 100644 --- a/core/nodeslicer/nodes/mesh.py +++ b/core/nodeslicer/nodes/mesh.py @@ -135,9 +135,10 @@ class SLICENODE_SeparateMesh(SliceNode): so_sel = SlicingObject() so_sel.name = so.name + "_sel" so_sel.parent = so.parent - so_sel.object_type = so.object_type - so_sel.extruder = so.extruder so_sel.mesh = so.mesh[mask] + so_sel.transforms = [dict(t) for t in getattr(so, "transforms", [])] + so_sel.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties() + so_sel.pdf_object_properties = list(getattr(so, "pdf_object_properties", [])) for k, v in so.attributes.items(): so_sel.attributes[k] = { "domain": v["domain"], @@ -149,9 +150,10 @@ class SLICENODE_SeparateMesh(SliceNode): so_inv = SlicingObject() so_inv.name = so.name + "_inv" so_inv.parent = so.parent - so_inv.object_type = so.object_type - so_inv.extruder = so.extruder so_inv.mesh = so.mesh[~mask] + so_inv.transforms = [dict(t) for t in getattr(so, "transforms", [])] + so_inv.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties() + so_inv.pdf_object_properties = list(getattr(so, "pdf_object_properties", [])) for k, v in so.attributes.items(): so_inv.attributes[k] = { "domain": v["domain"], @@ -246,13 +248,10 @@ class SLICENODE_SeparateMeshByRange(SliceNode): base_mesh_name = so.name so_new.name = f"{base_mesh_name}{i:0{num_digits}d}" so_new.parent = so.parent - so_new.object_type = so.object_type - so_new.extruder = so.extruder so_new.mesh = so.mesh[mask] so_new.transforms = [dict(t) for t in getattr(so, "transforms", [])] so_new.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties() so_new.pdf_object_properties = list(getattr(so, "pdf_object_properties", [])) - so_new.modifiers = so.modifiers.copy() if so.modifiers else [] for k, v in so.attributes.items(): so_new.attributes[k] = { "domain": v["domain"], @@ -431,9 +430,6 @@ def _copy_slicing_object(so: SlicingObject) -> SlicingObject: new_so.attributes[k] = list(v) else: new_so.attributes[k] = copy.deepcopy(v) - new_so.object_type = so.object_type - new_so.modifiers = so.modifiers.copy() if so.modifiers else [] - new_so.extruder = so.extruder new_so.transforms = [dict(t) for t in getattr(so, "transforms", [])] new_so.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties() new_so.pdf_object_properties = list(getattr(so, "pdf_object_properties", [])) @@ -448,9 +444,6 @@ def _copy_slicing_collection(sc: SlicingCollection) -> SlicingCollection: def _copy_slicing_group(sg: SlicingGroup) -> SlicingGroup: new_sg = SlicingGroup([]) new_sg.collections = {k: _copy_slicing_collection(sc) for k, sc in sg.collections.items()} - new_sg.wipe_tower_xy = sg.wipe_tower_xy.copy() - new_sg.wipe_tower_rotation_deg = sg.wipe_tower_rotation_deg - new_sg.object_offset = sg.object_offset.copy() new_sg.transforms = [dict(t) for t in getattr(sg, "transforms", [])] return new_sg @@ -477,17 +470,6 @@ def _transform_geometry_item(geom: Any, t_arr: np.ndarray, r_arr: np.ndarray, s_ col.rotate(r_arr) if any(t != 0.0 for t in t_arr): col.offset(t_arr) - if any(t != 0.0 for t in t_arr): - copied_data.wipe_tower_xy = copied_data.wipe_tower_xy + t_arr[0:2] - if any(r != 0.0 for r in r_arr): - rz = r_arr[2] - cz, sz = np.cos(rz), np.sin(rz) - rot_mat_2d = np.array([[cz, -sz], [sz, cz]]) - copied_data.wipe_tower_xy = copied_data.wipe_tower_xy @ rot_mat_2d.T - import math - copied_data.wipe_tower_rotation_deg = round(copied_data.wipe_tower_rotation_deg + rz * 180 / math.pi, 5) - if any(s != 1.0 for s in s_arr): - copied_data.wipe_tower_xy *= s_arr[0:2] if not hasattr(copied_data, "transforms") or copied_data.transforms is None: copied_data.transforms = [] @@ -836,124 +818,3 @@ class SLICENODE_AssignPropertyToObject(SliceNode): return _assign_properties_to_item(geom, props) - -OBJECT_TYPE_ITEMS: list[tuple[str, str, str]] = [ - ("NO_CHANGE", "Keep Original", "Do not modify the object/part type"), - ("ModelPart", "Model Part", "Standard model part"), - ("ParameterModifier", "Modifier", "Parameter modifier volume"), - ("NegativeVolume", "Negative Volume", "Negative volume for subtractions"), - ("SupportBlocker", "Support Blocker", "Support blocker volume"), - ("SupportEnforcer", "Support Enforcer", "Support enforcer volume"), - ("WipeTower", "Wipe Tower", "Wipe tower marker"), -] - -EXTRUDER_ITEMS: list[tuple[str, str, str]] = [ - ("NO_CHANGE", "Keep Original", "Do not modify extruder assignment"), - ("0", "Default Extruder", "Default Extruder"), - ("1", "Extruder 1", "Extruder 1"), - ("2", "Extruder 2", "Extruder 2"), - ("3", "Extruder 3", "Extruder 3"), - ("4", "Extruder 4", "Extruder 4"), - ("5", "Extruder 5", "Extruder 5"), -] - - -def _apply_modifiers_to_so( - so: SlicingObject, - part_type: str, - extruder: str, - modifiers_dict: dict[str, str] | None -) -> None: - if part_type != "NO_CHANGE": - so.object_type = part_type - if extruder != "NO_CHANGE": - so.extruder = int(extruder) - if modifiers_dict: - existing = {m['param_id']: str(m['param_value']) for m in getattr(so, 'modifiers', []) if m and 'param_id' in m} - for k, v in modifiers_dict.items(): - existing[str(k)] = str(v) - so.modifiers = [{'param_id': k, 'param_value': v} for k, v in existing.items()] - - -def _assign_modifiers_to_item( - geom: Any, - part_type: str = "NO_CHANGE", - extruder: str = "NO_CHANGE", - modifiers_dict: dict[str, str] | None = None -) -> Any: - if geom is None: - return None - elif isinstance(geom, (list, tuple)): - return [_assign_modifiers_to_item(item, part_type, extruder, modifiers_dict) for item in geom] - elif isinstance(geom, SlicingGroup): - copied = _copy_slicing_group(geom) - for coll in copied.collections.values(): - for obj in coll.objects: - _apply_modifiers_to_so(obj, part_type, extruder, modifiers_dict) - return copied - elif isinstance(geom, SlicingCollection): - copied = _copy_slicing_collection(geom) - for obj in copied.objects: - _apply_modifiers_to_so(obj, part_type, extruder, modifiers_dict) - return copied - elif isinstance(geom, SlicingObject): - copied = _copy_slicing_object(geom) - _apply_modifiers_to_so(copied, part_type, extruder, modifiers_dict) - return copied - return geom - - -@register_class -class SLICENODE_AssignModifiers(SliceNode): - bl_label = "Assign Modifiers" - - part_type: str = NfEnumProperty( - name="Part Type", - description="PrusaSlicer part/volume type", - items=OBJECT_TYPE_ITEMS, - default="NO_CHANGE" - ) - extruder: str = NfEnumProperty( - name="Extruder", - description="Extruder assignment", - items=EXTRUDER_ITEMS, - default="NO_CHANGE" - ) - - def init(self, context): - self.add_input("SLICESOCK_Mesh", "Geometry") - self.add_input("SLICESOCK_Settings", "Modifiers") - self.add_output("SLICESOCK_Mesh", "Geometry") - - def draw_buttons(self, context, layout): - assert layout - layout.prop(self, "part_type", text="Type") - layout.prop(self, "extruder", text="Extruder") - - def evaluate_output(self, socket_name: str, **_kwargs) -> Any: - if socket_name != "Geometry": - return None - - geom: Any = self.resolve("Geometry") - if not geom: - return None - - raw_modifiers = self.resolve("Modifiers") - if raw_modifiers is None: - raw_modifiers = self.resolve("Settings") - - mod_dict: dict[str, str] = {} - if raw_modifiers: - if isinstance(raw_modifiers, dict): - for k, v in raw_modifiers.items(): - mod_dict[str(k)] = str(v) - elif isinstance(raw_modifiers, (list, tuple)): - for item in raw_modifiers: - if isinstance(item, tuple) and len(item) == 2: - mod_dict[str(item[0])] = str(item[1]) - elif isinstance(item, dict): - for k, v in item.items(): - mod_dict[str(k)] = str(v) - - return _assign_modifiers_to_item(geom, self.part_type, self.extruder, mod_dict if mod_dict else None) - diff --git a/core/nodeslicer/nodes/system.py b/core/nodeslicer/nodes/system.py index 3b12926..69c9329 100644 --- a/core/nodeslicer/nodes/system.py +++ b/core/nodeslicer/nodes/system.py @@ -4,14 +4,10 @@ from pathlib import Path import os import bpy import tempfile -import shlex -import shutil -import subprocess -import numpy as np from ..bpy_ns_types import NfBoolProperty, NfStringProperty from ....registry import register_class -from .base import ExecSliceNode, SliceNode, SliceNodeJoiner, SlicingCollection, SlicingGroup +from .base import ExecSliceNode, SliceNodeJoiner @register_class class SLICENODE_PrinterList(ExecSliceNode): @@ -153,227 +149,6 @@ class SLICENODE_PrinterList(ExecSliceNode): return 'ok' -@register_class -class SLICENODE_PrusaslicerSlice(SliceNode): - bl_label = "PrusaSlicer Slice" - - prusaslicer_executable: str = NfStringProperty(name="PrusaSlicer Path", subtype='FILE_PATH', default='flatpak run com.prusa3d.PrusaSlicer') - - def init(self, context): - self.add_input("SLICESOCK_PrusaslicerConf", "Config") - self.add_input("SLICESOCK_Settings", "Settings") - self.add_multi_socket_group("SLICESOCK_Mesh", "NodeSocketString", name="Slicing Group", in_name="Mesh", out_name="Gcode", in_display_shape='LIST') - - def draw_buttons(self, context, layout): - assert layout - layout.prop(self, "prusaslicer_executable") - - def evaluate_output(self, socket_name: str, identifier: str = "", **kwargs) -> Any: - if identifier.startswith("Slicing Group_ms_out_"): - try: - idx = int(identifier.split("_")[-1]) - return self.evaluate_for_index(idx) - except (ValueError, IndexError): - return "" - return super().evaluate_output(socket_name, **kwargs) - - def evaluate_for_index(self, index: int, operator=None) -> str: - # Optimization: Only resolve the specific mesh input for this index - target_id = f"Slicing Group_ms_in_{index}" - mesh_data: SlicingCollection | SlicingGroup = self.resolve(target_id) - - conf_data = self.resolve("Config") - overrides = self.resolve("Settings") - - if not conf_data: - return "" - - # Extract data from config dict - printer_id = conf_data['printer'] - filament_ids = conf_data['filament'] - print_id = conf_data['print'] - cache = conf_data['cache'] - - # Generate the SlicingGroup - if isinstance(mesh_data, SlicingGroup): - slicing_group = mesh_data - else: - slicing_group = SlicingGroup([]) - slicing_group.collections = {mesh_data.objects[0].name: mesh_data} - - # Offset by the bed center - def parse_bed_center(bed_shape_str: str) -> np.ndarray: - try: - points = [] - for p in bed_shape_str.split(','): - x, y = map(float, p.split('x')) - points.append((x, y)) - points_np = np.array(points) - return (points_np.min(axis=0) + points_np.max(axis=0)) / 2.0 - except: - return np.array([125.0, 105.0]) - - bed_shape = cache.profiles[printer_id].all_conf_dict.get('bed_shape', '0x0,250x0,250x210,0x210') - bed_center = parse_bed_center(bed_shape) - - a_z = -slicing_group.min_z - offset = np.array([bed_center[0], bed_center[1], a_z]) - np.array([slicing_group.center_xy[0], slicing_group.center_xy[1], 0]) - slicing_group.offset(offset) - - # Check for wipe_tower_position override - if overrides and "wipe_tower_position" in overrides: - wipe_tower_pos = overrides["wipe_tower_position"] - try: - wx = float(wipe_tower_pos[0]) - wy = float(wipe_tower_pos[1]) - slicing_group.wipe_tower_xy = np.array([wx, wy]) - except Exception: - pass - - # Check for offset_object override - if overrides and "offset_object" in overrides: - obj_offset = overrides["offset_object"] - try: - ox = float(obj_offset[0]) - oy = float(obj_offset[1]) - oz = float(obj_offset[2]) if len(obj_offset) > 2 else 0.0 - slicing_group.object_offset = np.array([ox, oy, oz]) - except Exception: - pass - - # Prepare overrides for generate_conf_writer (excluding wipe_tower_position & offset_object) - writer_overrides = { - k: {'value': v} for k, v in (overrides or {}).items() if k not in ("wipe_tower_position", "offset_object") - } - - # Generate the ConfigWriter - from ....infra.prusaslicer_profiles import ConfigWriter - conf_writer: ConfigWriter = cache.generate_conf_writer( - printer_id, - filament_ids, - print_id, - overrides=writer_overrides - ) - - binary_gcode_value = conf_writer.config_dict.get('binary_gcode', '0') - conf_writer.config_dict['binary_gcode'] = '0' - - # Prepare 3mf path - temp_dir = Path(tempfile.gettempdir()) - output_path = temp_dir / f"tmp_{index}.3mf" - - try: - from ....infra._3mf_writer._3mf import prepare_3mf - prepare_3mf(output_path, slicing_group, conf_writer, z_gcodes=[]) - - exe_str = self.prusaslicer_executable - if not exe_str: return "" - - cmd_parts = shlex.split(exe_str) - if not cmd_parts: return "" - - if not shutil.which(cmd_parts[0]): - abs_path = bpy.path.abspath(cmd_parts[0]) - if os.path.exists(abs_path): - cmd_parts[0] = abs_path - - if not shutil.which(cmd_parts[0]): - return "" - - gcode_path = output_path.with_suffix(".gcode") - cmd = cmd_parts + ["--export-gcode", str(output_path), "--output", str(gcode_path)] - - process = subprocess.run(cmd, capture_output=True, text=True) - if process.returncode != 0: - return "" - - if gcode_path.exists(): - with open(gcode_path, 'r', encoding='utf-8') as f: - gcode_result = f.read() - gcode_result += f"\n; binary_gcode = {binary_gcode_value}\n" - - output_path.unlink() - if gcode_path.exists(): gcode_path.unlink() - - return gcode_result - else: - return "" - - except Exception as e: - import traceback - traceback.print_exc() - return "" - -@register_class -class SLICENODE_PrusaslicerPreview(ExecSliceNode): - bl_label = "PrusaSlicer Preview" - button_label = "Open Preview" - - prusaslicer_executable: str = NfStringProperty( - name="PrusaSlicer Path", - subtype='FILE_PATH', - default='flatpak run com.prusa3d.PrusaSlicer' - ) - - def init(self, context): - self._init_exec() - self.add_multi_socket_group("NodeSocketString", name="Gcode") - - def draw_buttons(self, context, layout): - super().draw_buttons(context, layout) - layout.prop(self, "prusaslicer_executable") - - def evaluate(self, operator=None): - from ....infra.blender.blender_slice_nodes_generic import resolve_input - gcodes: list[str] = [g for g in self.resolve_multi_input("Gcode") if g] - if not gcodes: - if operator: operator.report({'ERROR'}, "No Gcode to preview") - return 'no gcode' - - exe_str = self.prusaslicer_executable - if not exe_str: - if operator: operator.report({'ERROR'}, "PrusaSlicer executable path is empty") - return 'error' - - # Parse command (handles multi-word commands like flatpak) - cmd_parts = shlex.split(exe_str) - if not cmd_parts: - if operator: operator.report({'ERROR'}, "Invalid PrusaSlicer command") - return 'error' - - # Try to resolve the first part as a path if it's not a command in PATH - if not shutil.which(cmd_parts[0]): - abs_path = bpy.path.abspath(cmd_parts[0]) - if os.path.exists(abs_path): - cmd_parts[0] = abs_path - - # Final check if command exists - if not shutil.which(cmd_parts[0]): - if operator: operator.report({'ERROR'}, f"Command or executable not found: {cmd_parts[0]}") - return 'error' - - # Write to temp files - # We use named temporary files that stay around for a bit. - # PrusaSlicer needs the files to exist when it starts. - try: - for gcode in gcodes: - fd, temp_path = tempfile.mkstemp(suffix='.gcode') - with os.fdopen(fd, 'w', encoding='utf-8') as f: - f.write(gcode) - - cmd = cmd_parts + ["--gcodeviewer", temp_path] - - # Use Popen to not block Blender - subprocess.Popen(cmd) - - if operator: - operator.report({'INFO'}, f"PrusaSlicer Previews launched") - except Exception as e: - if operator: operator.report({'ERROR'}, f"Failed to launch preview: {str(e)}") - return 'error' - - return 'ok' - @register_class class SLICENODE_JoinExecution(SliceNodeJoiner): bl_label = "Join Execution" diff --git a/core/nodeslicer/sockets.py b/core/nodeslicer/sockets.py index 5fb4166..f596c07 100644 --- a/core/nodeslicer/sockets.py +++ b/core/nodeslicer/sockets.py @@ -86,12 +86,6 @@ class SLICESOCK_Intrinsics(LabelNodeSocket): default = None bl_color = (0.2, 0.2, 0.2, 1.0) -@register_class -class SLICESOCK_PrusaslicerConf(LabelNodeSocket): - bl_label = "PrusaSlicer Config" - default = None - bl_color = (0.95, 0., 0., 1.0) # #F26C21 - @register_class class SettingItem(bpy.types.PropertyGroup): field: str = NfStringProperty(name="Field", default="") diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index af5c275..20c59fa 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -3,7 +3,6 @@ import bpy from bpy.types import NodeTree, Operator, Menu from bpy.app.handlers import persistent -from ...infra.blender.mixins import update_collection from ...infra.blender.blender_slice_nodes_generic import node_execution_feedback, resolve_input from ...registry import register_class @@ -114,26 +113,12 @@ def tree_update_handler(scene, depsgraph): finally: _is_updating = False -def object_init_handler(scene, depsgraph): - obj = bpy.context.active_object - if not obj or not hasattr(obj, "nodeslicer"): - return - - pg = getattr(obj, 'nodeslicer', None) - if not pg: - return - - update_collection(pg.modifiers) - - - @persistent def depsgraph_update_handler(scene, depsgraph): global _is_updating, _tree_changed if _is_updating: return tree_update_handler(scene, depsgraph) - object_init_handler(scene, depsgraph) # Sync SLICENODE_Group sockets if interface changed and migrate legacy sockets for tree in bpy.data.node_groups: @@ -535,22 +520,6 @@ class SLICENODE_MT_Geometry(Menu): layout.operator("node.add_node", text="Math").type = "SLICENODE_FloatMath" layout.operator("node.add_node", text="Integer Math").type = "SLICENODE_IntMath" -@register_class -class SLICENODE_MT_PrusaSlicer(Menu): - bl_idname = "SLICENODE_MT_PrusaSlicer" - bl_label = "PrusaSlicer" - - def draw(self, context): - layout = self.layout - assert layout - layout.operator("node.add_node", text="PrusaSlicer Config").type = "SLICENODE_PrusaslicerConf" - layout.operator("node.add_node", text="Assign Modifiers").type = "SLICENODE_AssignModifiers" - layout.operator("node.add_node", text="Wipe Tower").type = "SLICENODE_WipeTower" - layout.operator("node.add_node", text="Offset Object").type = "SLICENODE_OffsetObject" - layout.separator() - layout.operator("node.add_node", text="PrusaSlicer Slice").type = "SLICENODE_PrusaslicerSlice" - layout.operator("node.add_node", text="PrusaSlicer Preview").type = "SLICENODE_PrusaslicerPreview" - @register_class class SLICENODE_MT_PdfExporter(Menu): bl_idname = "SLICENODE_MT_PdfExporter" @@ -640,7 +609,6 @@ class SLICENODE_MT_Menu(Menu): assert layout layout.menu("SLICENODE_MT_DataInput") layout.menu("SLICENODE_MT_Geometry") - layout.menu("SLICENODE_MT_PrusaSlicer") layout.menu("SLICENODE_MT_PdfExporter") layout.menu("SLICENODE_MT_Gcode") layout.menu("SLICENODE_MT_Joiners") diff --git a/doc/NODES.md b/doc/NODES.md index 5c2e39b..7dc72b2 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -110,29 +110,6 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Math**: Float math operations. - **Integer Math**: Simple integer operations (Add, Multiply, etc.). -### PrusaSlicer -- **PrusaSlicer Config**: Configures print, filament, and printer profiles from PrusaSlicer INI bundles. -- **Assign Modifiers**: Assigns PrusaSlicer part/volume types (`ModelPart`, `ParameterModifier`, `NegativeVolume`, `SupportBlocker`, `SupportEnforcer`, `WipeTower`), extruder assignments (`0` to `5`), and config parameter modifiers directly to geometry (`SlicingObject`, `SlicingCollection`, or `SlicingGroup`) for 3MF export. - - **Geometry**: Generic geometry input socket (`SLICESOCK_Mesh`). - - **Modifiers**: Optional input socket accepting configuration settings dictionary (`SLICESOCK_Settings`). - - **Part Type**: Dropdown property specifying target part/volume type (`Keep Original`, `Model Part`, `Modifier`, `Negative Volume`, `Support Blocker`, `Support Enforcer`, `Wipe Tower`). - - **Extruder**: Dropdown property specifying target extruder assignment (`Keep Original`, `Default Extruder`, `Extruder 1-5`). -- **Wipe Tower**: Configures the Wipe Tower printbed position (`wipe_tower_position`) passed to 3MF metadata (`Prusa_Slicer_wipe_tower_information.xml`) without injecting into profile INI settings. - - **Position X**: X coordinate of the wipe tower on the printbed. - - **Position Y**: Y coordinate of the wipe tower on the printbed. -- **Offset Object**: Configures the object placement transform offset (`offset_object`) applied to `` inside 3MF model build data (`3D/3dmodel.model`) without injecting into profile INI settings. - - **Offset X**: X translation offset for the build item. - - **Offset Y**: Y translation offset for the build item. - - **Offset Z**: Z translation offset for the build item. -- **PrusaSlicer Slice**: Exports the current mesh and configuration to a 3MF file and calls PrusaSlicer to generate G-code. - - **Executable**: Path to the PrusaSlicer executable. - - **Object**: The Blender object to slice. - - **Config**: PrusaSlicer configuration (from the **PrusaSlicer Config** node). - - **Gcode**: Output socket providing the generated G-code string. -- **PrusaSlicer Preview**: Launches PrusaSlicer's built-in G-code viewer to visualize a G-code string. - - **Executable**: Path to the PrusaSlicer executable (default: `flatpak run com.prusa3d.PrusaSlicer`). - - **Gcode**: Input socket for the G-code string to preview. - ### PDF Exporter - **Assign PDF Properties**: Assigns line width (mm) and line color PDF export properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`). - **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`). diff --git a/infra/blender/blender_mesh_capture.py b/infra/blender/blender_mesh_capture.py index e391f20..c40202a 100644 --- a/infra/blender/blender_mesh_capture.py +++ b/infra/blender/blender_mesh_capture.py @@ -108,9 +108,6 @@ class SlicingObject(): parent: str mesh: NDArray[float64] attributes: dict[str, Any] - object_type: str = "ModelPart" - modifiers: list[dict[str, Any]] = [] - extruder: int = 1 pdf_properties: PdfProperties pdf_object_properties: list[tuple[str, str]] @@ -120,7 +117,6 @@ class SlicingObject(): self.name = str(obj.name) if obj else "" self.parent = parent self.attributes = {} - self.modifiers = [] self.transforms = [] self.pdf_properties = PdfProperties() self.pdf_object_properties = [] @@ -128,11 +124,6 @@ class SlicingObject(): assert bpy.context.scene.unit_settings scene_scale: float = bpy.context.scene.unit_settings.scale_length - if obj and (ns := getattr(obj, 'nodeslicer', None)): - self.object_type = ns.object_type - self.extruder = ns.extruder - self.modifiers = [{'param_id': m.field, 'param_value': m.value_str} for m in ns.modifiers] - if obj: self.mesh, self.attributes = objects_to_tris([obj], 1000 * scene_scale) else: @@ -147,8 +138,6 @@ class SlicingObject(): # Take metadata from first object if available merged.name = objects[0].name + "_merged" merged.parent = objects[0].parent - merged.object_type = objects[0].object_type - merged.extruder = objects[0].extruder merged.pdf_properties = copy.deepcopy(objects[0].pdf_properties) if hasattr(objects[0], "pdf_properties") and objects[0].pdf_properties else PdfProperties() merged.pdf_object_properties = list(getattr(objects[0], "pdf_object_properties", [])) merged.transforms = [] @@ -215,12 +204,6 @@ class SlicingObject(): norms_len = np.where(norms_len == 0, 1.0, norms_len) self.mesh[:, 3, :] = norms / norms_len - def assign_modifiers(self, modifiers: dict[str, str]): - self.modifiers = [ - {"param_id": k, "param_value": v} - for k, v in modifiers.items() - ] - @property def checksum(self) -> int: import json @@ -391,15 +374,9 @@ class SlicingCollection(): class SlicingGroup(): collections: dict[str, SlicingCollection] - - # Metadata - wipe_tower_xy: NDArray[Any] = np.array([0, 0]) - wipe_tower_rotation_deg: float = 0 - object_offset: NDArray[Any] = np.array([0.0, 0.0, 0.0]) transforms: list[dict[str, Any]] def __init__(self, selected_objs: list[Object]) -> None: - self.object_offset = np.array([0.0, 0.0, 0.0]) self.transforms = [] family = [] @@ -407,7 +384,7 @@ class SlicingGroup(): family.append(obj) family.extend(get_all_children(obj)) - filtered_objs = [o for o in family] # if getattr(o, TYPES_NAME).object_type != 'Ignore' + filtered_objs = [o for o in family] def find_top_parent(o): current = o @@ -421,33 +398,12 @@ class SlicingGroup(): self.collections = {} for k, objects in parents.items(): self.collections[k] = SlicingCollection(objects, k) - - self._extract_metadata(family) - - def _extract_metadata(self, objs) -> None: - import math - assert bpy.context.scene and bpy.context.scene.unit_settings - scale_tx = 1000. * bpy.context.scene.unit_settings.scale_length #type: ignore - - if self.center_xy is not None: - self.wipe_tower_xy = self.center_xy[0:2] - for obj in objs: - with get_evaluated_mesh(obj) as (eval_obj, _m): - ns_prop = getattr(eval_obj, 'nodeslicer', None) - if eval_obj is not None and ns_prop and getattr(ns_prop, 'object_type', None) == 'WipeTower': - self.wipe_tower_xy = np.array(eval_obj.location[0:2]) * scale_tx - self.wipe_tower_rotation_deg = round(eval_obj.rotation_euler[2]*180/math.pi,5) - break - - return def offset(self, offset: NDArray[Any]): for k, so in self.collections.items(): so.offset(offset) - self.wipe_tower_xy = self.wipe_tower_xy + offset[0:2] def scale(self, factor: float): for k, so in self.collections.items(): so.scale(factor) - self.wipe_tower_xy *= factor @property def checksum(self): @@ -458,10 +414,6 @@ class SlicingGroup(): buf.extend(key_bytes) buf.extend(struct.pack(">I", col.checksum)) - buf.extend(struct.pack(">I", crc32_array(self.wipe_tower_xy))) - buf.extend(struct.pack(">I", crc32_array(np.array(self.wipe_tower_rotation_deg)))) - buf.extend(struct.pack(">I", crc32_array(self.object_offset))) - return zlib.crc32(buf) & 0xFFFFFFFF @property