rm prusaslicer out of scope
This commit is contained in:
parent
9137d297e5
commit
767fd08060
9 changed files with 8 additions and 647 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,5 +5,3 @@ release/
|
||||||
*.blend
|
*.blend
|
||||||
*.blend1
|
*.blend1
|
||||||
*.bgcode
|
*.bgcode
|
||||||
tests/Link to Configs
|
|
||||||
profiles/prusaslicer/Link to Configs
|
|
||||||
|
|
|
||||||
18
__init__.py
18
__init__.py
|
|
@ -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__":
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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="")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
23
doc/NODES.md
23
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.
|
- **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`).
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue