rm prusaslicer out of scope

This commit is contained in:
Nicolas Predella 2026-09-25 13:33:01 +03:00
parent 9137d297e5
commit 767fd08060
9 changed files with 8 additions and 647 deletions

2
.gitignore vendored
View file

@ -5,5 +5,3 @@ release/
*.blend *.blend
*.blend1 *.blend1
*.bgcode *.bgcode
tests/Link to Configs
profiles/prusaslicer/Link to Configs

View file

@ -2,8 +2,6 @@ from __future__ import annotations
import bpy import bpy
from . import registry 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 .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 = { bl_info = {
"name": "NodeSlicer", "name": "NodeSlicer",
@ -15,19 +13,13 @@ bl_info = {
"category": "Node", "category": "Node",
} }
_registered_nodeslicer = False
_keymaps = [] _keymaps = []
def register(): def register():
global _registered_nodeslicer
registry.blender_register_classes() registry.blender_register_classes()
bpy.types.NODE_MT_add.append(tree.draw_add_menu) bpy.types.NODE_MT_add.append(tree.draw_add_menu)
bpy.app.handlers.depsgraph_update_post.append(tree.depsgraph_update_handler) 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 # Register Keymaps
wm = bpy.context.window_manager wm = bpy.context.window_manager
assert wm assert wm
@ -44,7 +36,6 @@ def register():
print('Nodeslicer Loaded') print('Nodeslicer Loaded')
def unregister(): def unregister():
global _registered_nodeslicer
try: try:
if tree.depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post: if tree.depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(tree.depsgraph_update_handler) bpy.app.handlers.depsgraph_update_post.remove(tree.depsgraph_update_handler)
@ -67,15 +58,6 @@ def unregister():
registry.blender_unregister_classes() 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') print('Nodeslicer Unloaded')
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -91,157 +91,11 @@ class SLICENODE_CollectionObjects(SliceNode):
return bpy.data.objects[socket_name] return bpy.data.objects[socket_name]
return None 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 @register_class
class SLICENODE_InputSettings(InputNode): class SLICENODE_InputSettings(InputNode):
input_type = "Settings" input_type = "Settings"
bl_icon = "OBJECT_DATA" 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 @register_class
class SLICENODE_InputPrinter(SliceNode): class SLICENODE_InputPrinter(SliceNode):
bl_label = "Printer" bl_label = "Printer"

View file

@ -135,9 +135,10 @@ class SLICENODE_SeparateMesh(SliceNode):
so_sel = SlicingObject() so_sel = SlicingObject()
so_sel.name = so.name + "_sel" so_sel.name = so.name + "_sel"
so_sel.parent = so.parent 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.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(): for k, v in so.attributes.items():
so_sel.attributes[k] = { so_sel.attributes[k] = {
"domain": v["domain"], "domain": v["domain"],
@ -149,9 +150,10 @@ class SLICENODE_SeparateMesh(SliceNode):
so_inv = SlicingObject() so_inv = SlicingObject()
so_inv.name = so.name + "_inv" so_inv.name = so.name + "_inv"
so_inv.parent = so.parent 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.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(): for k, v in so.attributes.items():
so_inv.attributes[k] = { so_inv.attributes[k] = {
"domain": v["domain"], "domain": v["domain"],
@ -246,13 +248,10 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
base_mesh_name = so.name base_mesh_name = so.name
so_new.name = f"{base_mesh_name}{i:0{num_digits}d}" so_new.name = f"{base_mesh_name}{i:0{num_digits}d}"
so_new.parent = so.parent 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.mesh = so.mesh[mask]
so_new.transforms = [dict(t) for t in getattr(so, "transforms", [])] 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_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.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(): for k, v in so.attributes.items():
so_new.attributes[k] = { so_new.attributes[k] = {
"domain": v["domain"], "domain": v["domain"],
@ -431,9 +430,6 @@ def _copy_slicing_object(so: SlicingObject) -> SlicingObject:
new_so.attributes[k] = list(v) new_so.attributes[k] = list(v)
else: else:
new_so.attributes[k] = copy.deepcopy(v) 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.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_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", [])) 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: def _copy_slicing_group(sg: SlicingGroup) -> SlicingGroup:
new_sg = SlicingGroup([]) new_sg = SlicingGroup([])
new_sg.collections = {k: _copy_slicing_collection(sc) for k, sc in sg.collections.items()} 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", [])] new_sg.transforms = [dict(t) for t in getattr(sg, "transforms", [])]
return new_sg 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) col.rotate(r_arr)
if any(t != 0.0 for t in t_arr): if any(t != 0.0 for t in t_arr):
col.offset(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: if not hasattr(copied_data, "transforms") or copied_data.transforms is None:
copied_data.transforms = [] copied_data.transforms = []
@ -836,124 +818,3 @@ class SLICENODE_AssignPropertyToObject(SliceNode):
return _assign_properties_to_item(geom, props) 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)

View file

@ -4,14 +4,10 @@ from pathlib import Path
import os import os
import bpy import bpy
import tempfile import tempfile
import shlex
import shutil
import subprocess
import numpy as np
from ..bpy_ns_types import NfBoolProperty, NfStringProperty from ..bpy_ns_types import NfBoolProperty, NfStringProperty
from ....registry import register_class from ....registry import register_class
from .base import ExecSliceNode, SliceNode, SliceNodeJoiner, SlicingCollection, SlicingGroup from .base import ExecSliceNode, SliceNodeJoiner
@register_class @register_class
class SLICENODE_PrinterList(ExecSliceNode): class SLICENODE_PrinterList(ExecSliceNode):
@ -153,227 +149,6 @@ class SLICENODE_PrinterList(ExecSliceNode):
return 'ok' 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 @register_class
class SLICENODE_JoinExecution(SliceNodeJoiner): class SLICENODE_JoinExecution(SliceNodeJoiner):
bl_label = "Join Execution" bl_label = "Join Execution"

View file

@ -86,12 +86,6 @@ class SLICESOCK_Intrinsics(LabelNodeSocket):
default = None default = None
bl_color = (0.2, 0.2, 0.2, 1.0) 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 @register_class
class SettingItem(bpy.types.PropertyGroup): class SettingItem(bpy.types.PropertyGroup):
field: str = NfStringProperty(name="Field", default="") field: str = NfStringProperty(name="Field", default="")

View file

@ -3,7 +3,6 @@ import bpy
from bpy.types import NodeTree, Operator, Menu from bpy.types import NodeTree, Operator, Menu
from bpy.app.handlers import persistent 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 ...infra.blender.blender_slice_nodes_generic import node_execution_feedback, resolve_input
from ...registry import register_class from ...registry import register_class
@ -114,26 +113,12 @@ def tree_update_handler(scene, depsgraph):
finally: finally:
_is_updating = False _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 @persistent
def depsgraph_update_handler(scene, depsgraph): def depsgraph_update_handler(scene, depsgraph):
global _is_updating, _tree_changed global _is_updating, _tree_changed
if _is_updating: return if _is_updating: return
tree_update_handler(scene, depsgraph) tree_update_handler(scene, depsgraph)
object_init_handler(scene, depsgraph)
# Sync SLICENODE_Group sockets if interface changed and migrate legacy sockets # Sync SLICENODE_Group sockets if interface changed and migrate legacy sockets
for tree in bpy.data.node_groups: 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="Math").type = "SLICENODE_FloatMath"
layout.operator("node.add_node", text="Integer Math").type = "SLICENODE_IntMath" 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 @register_class
class SLICENODE_MT_PdfExporter(Menu): class SLICENODE_MT_PdfExporter(Menu):
bl_idname = "SLICENODE_MT_PdfExporter" bl_idname = "SLICENODE_MT_PdfExporter"
@ -640,7 +609,6 @@ class SLICENODE_MT_Menu(Menu):
assert layout assert layout
layout.menu("SLICENODE_MT_DataInput") layout.menu("SLICENODE_MT_DataInput")
layout.menu("SLICENODE_MT_Geometry") layout.menu("SLICENODE_MT_Geometry")
layout.menu("SLICENODE_MT_PrusaSlicer")
layout.menu("SLICENODE_MT_PdfExporter") layout.menu("SLICENODE_MT_PdfExporter")
layout.menu("SLICENODE_MT_Gcode") layout.menu("SLICENODE_MT_Gcode")
layout.menu("SLICENODE_MT_Joiners") layout.menu("SLICENODE_MT_Joiners")

View file

@ -110,29 +110,6 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
- **Math**: Float math operations. - **Math**: Float math operations.
- **Integer Math**: Simple integer operations (Add, Multiply, etc.). - **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 `<item transform="...">` 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 ### PDF Exporter
- **Assign PDF Properties**: Assigns line width (mm) and line color PDF export properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`). - **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`). - **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`).

View file

@ -108,9 +108,6 @@ class SlicingObject():
parent: str parent: str
mesh: NDArray[float64] mesh: NDArray[float64]
attributes: dict[str, Any] attributes: dict[str, Any]
object_type: str = "ModelPart"
modifiers: list[dict[str, Any]] = []
extruder: int = 1
pdf_properties: PdfProperties pdf_properties: PdfProperties
pdf_object_properties: list[tuple[str, str]] pdf_object_properties: list[tuple[str, str]]
@ -120,7 +117,6 @@ class SlicingObject():
self.name = str(obj.name) if obj else "" self.name = str(obj.name) if obj else ""
self.parent = parent self.parent = parent
self.attributes = {} self.attributes = {}
self.modifiers = []
self.transforms = [] self.transforms = []
self.pdf_properties = PdfProperties() self.pdf_properties = PdfProperties()
self.pdf_object_properties = [] self.pdf_object_properties = []
@ -128,11 +124,6 @@ class SlicingObject():
assert bpy.context.scene.unit_settings assert bpy.context.scene.unit_settings
scene_scale: float = bpy.context.scene.unit_settings.scale_length 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: if obj:
self.mesh, self.attributes = objects_to_tris([obj], 1000 * scene_scale) self.mesh, self.attributes = objects_to_tris([obj], 1000 * scene_scale)
else: else:
@ -147,8 +138,6 @@ class SlicingObject():
# Take metadata from first object if available # Take metadata from first object if available
merged.name = objects[0].name + "_merged" merged.name = objects[0].name + "_merged"
merged.parent = objects[0].parent 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_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.pdf_object_properties = list(getattr(objects[0], "pdf_object_properties", []))
merged.transforms = [] merged.transforms = []
@ -215,12 +204,6 @@ class SlicingObject():
norms_len = np.where(norms_len == 0, 1.0, norms_len) norms_len = np.where(norms_len == 0, 1.0, norms_len)
self.mesh[:, 3, :] = norms / 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 @property
def checksum(self) -> int: def checksum(self) -> int:
import json import json
@ -391,15 +374,9 @@ class SlicingCollection():
class SlicingGroup(): class SlicingGroup():
collections: dict[str, SlicingCollection] 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]] transforms: list[dict[str, Any]]
def __init__(self, selected_objs: list[Object]) -> None: def __init__(self, selected_objs: list[Object]) -> None:
self.object_offset = np.array([0.0, 0.0, 0.0])
self.transforms = [] self.transforms = []
family = [] family = []
@ -407,7 +384,7 @@ class SlicingGroup():
family.append(obj) family.append(obj)
family.extend(get_all_children(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): def find_top_parent(o):
current = o current = o
@ -422,32 +399,11 @@ class SlicingGroup():
for k, objects in parents.items(): for k, objects in parents.items():
self.collections[k] = SlicingCollection(objects, k) 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]): def offset(self, offset: NDArray[Any]):
for k, so in self.collections.items(): so.offset(offset) 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): def scale(self, factor: float):
for k, so in self.collections.items(): so.scale(factor) for k, so in self.collections.items(): so.scale(factor)
self.wipe_tower_xy *= factor
@property @property
def checksum(self): def checksum(self):
@ -458,10 +414,6 @@ class SlicingGroup():
buf.extend(key_bytes) buf.extend(key_bytes)
buf.extend(struct.pack(">I", col.checksum)) 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 return zlib.crc32(buf) & 0xFFFFFFFF
@property @property