This commit is contained in:
Nicolas Predella 2026-08-25 20:47:31 +03:00
parent 39d2530f14
commit 7535bd7a85
28 changed files with 451 additions and 324 deletions

View file

@ -25,7 +25,7 @@ def register():
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"): if not hasattr(bpy.types.Object, "nodeslicer"):
bpy.types.Object.nodeslicer = bpy.props.PointerProperty(type=SlicerObjectPropertyGroup, name="nodeslicer") #type: ignore setattr(bpy.types.Object, "nodeslicer", bpy.props.PointerProperty(type=SlicerObjectPropertyGroup, name="nodeslicer"))
_registered_nodeslicer = True _registered_nodeslicer = True
# Register Keymaps # Register Keymaps
@ -70,7 +70,7 @@ def unregister():
if _registered_nodeslicer: if _registered_nodeslicer:
try: try:
if hasattr(bpy.types.Object, "nodeslicer"): if hasattr(bpy.types.Object, "nodeslicer"):
del bpy.types.Object.nodeslicer #type: ignore delattr(bpy.types.Object, "nodeslicer")
except Exception as e: except Exception as e:
print(f"Error removing property: {e}") print(f"Error removing property: {e}")
finally: finally:

View file

@ -1,6 +1,3 @@
import numpy as np
import zlib
import numpy as np import numpy as np
import zlib import zlib
import struct import struct
@ -19,8 +16,8 @@ def crc32_list(l: list[np.ndarray]) -> int:
h = zlib.adler32(struct.pack(">I", crc32_array(a)), h) h = zlib.adler32(struct.pack(">I", crc32_array(a)), h)
return h return h
def ndarray_lru_cache(maxsize=128): def ndarray_lru_cache(maxsize: int = 128) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
def decorator(func: Callable): def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
cache = {} cache = {}
order = [] order = []
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):

View file

@ -1,3 +1,5 @@
from __future__ import annotations
from typing import Any, Callable
import bpy import bpy
from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty, EnumProperty, BoolProperty, FloatVectorProperty, CollectionProperty from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty, EnumProperty, BoolProperty, FloatVectorProperty, CollectionProperty
@ -52,16 +54,33 @@ def notify_tree_changed(self, context):
except Exception as e: except Exception as e:
print(f"Error updating node {node.name}: {e}") print(f"Error updating node {node.name}: {e}")
def NfProperty(prop_func): class MockPropertyDescriptor:
def wrapper(**kwargs): def __init__(self, default: Any = None):
self.default = default
self._values: dict[int, Any] = {}
def __get__(self, instance: Any, owner: Any = None) -> Any:
if instance is None:
return self
return self._values.get(id(instance), self.default)
def __set__(self, instance: Any, value: Any) -> None:
self._values[id(instance)] = value
def NfProperty(prop_func: Callable[..., Any]) -> Callable[..., Any]:
def wrapper(**kwargs: Any) -> Any:
kwargs.setdefault("update", notify_tree_changed) kwargs.setdefault("update", notify_tree_changed)
return prop_func(**kwargs) res = prop_func(**kwargs)
if "Mock" in type(res).__name__:
default = kwargs.get("default", None)
return MockPropertyDescriptor(default)
return res
return wrapper return wrapper
NfStringProperty = NfProperty(StringProperty) NfStringProperty: Callable[..., Any] = NfProperty(StringProperty)
NfIntProperty = NfProperty(IntProperty) NfIntProperty: Callable[..., Any] = NfProperty(IntProperty)
NfFloatProperty = NfProperty(FloatProperty) NfFloatProperty: Callable[..., Any] = NfProperty(FloatProperty)
NfEnumProperty = NfProperty(EnumProperty) NfEnumProperty: Callable[..., Any] = NfProperty(EnumProperty)
NfBoolProperty = NfProperty(BoolProperty) NfBoolProperty: Callable[..., Any] = NfProperty(BoolProperty)
NfPointerProperty = NfProperty(PointerProperty) NfPointerProperty: Callable[..., Any] = NfProperty(PointerProperty)
NfFloatVectorProperty = NfProperty(FloatVectorProperty) NfFloatVectorProperty: Callable[..., Any] = NfProperty(FloatVectorProperty)

View file

@ -61,7 +61,7 @@ class InputNode(SliceNode):
class MathNode(SliceNode): class MathNode(SliceNode):
bl_label = "Float Math" bl_label = "Float Math"
operation: NfEnumProperty( operation: str = NfEnumProperty(
name="Operation", name="Operation",
items=[ items=[
('ADD', "Add", "Add the two values"), ('ADD', "Add", "Add the two values"),
@ -81,7 +81,7 @@ class MathNode(SliceNode):
('MODULO_TRUNCATED', "Truncated Modulo", "Modulo using truncated division"), ('MODULO_TRUNCATED', "Truncated Modulo", "Modulo using truncated division"),
], ],
default='ADD' default='ADD'
) # pyright: ignore[reportInvalidTypeForm] )
math_type: TypeAlias = float math_type: TypeAlias = float
socket_label: str = "Float" socket_label: str = "Float"

View file

@ -12,7 +12,7 @@ from .base import SliceNode, SliceIntrinsics, Pather, SliceNodeJoiner, SlicePath
class SLICENODE_PathsToGcode(SliceNode): class SLICENODE_PathsToGcode(SliceNode):
bl_label = "Paths to Gcode" bl_label = "Paths to Gcode"
use_header: NfBoolProperty(name="Header", default=True) # pyright: ignore[reportInvalidTypeForm] use_header: bool = NfBoolProperty(name="Header", default=True)
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_Paths") self.add_input("SLICESOCK_Paths")
@ -119,7 +119,7 @@ class SLICENODE_GcodeRegexReplace(SliceNode):
class SLICENODE_GcodeHelper(SliceNode): class SLICENODE_GcodeHelper(SliceNode):
bl_label = "Gcode Helper" bl_label = "Gcode Helper"
command: NfEnumProperty( command: str = NfEnumProperty(
name="Command", name="Command",
items=[ items=[
('G0', "G0: Rapid Move", "Fast movement without extrusion"), ('G0', "G0: Rapid Move", "Fast movement without extrusion"),
@ -149,9 +149,9 @@ class SLICENODE_GcodeHelper(SliceNode):
('T', "T: Change Extruder", "Switch to a different extruder (e.g. T0, T1)"), ('T', "T: Change Extruder", "Switch to a different extruder (e.g. T0, T1)"),
], ],
default='G1' default='G1'
) # pyright: ignore[reportInvalidTypeForm] )
parameters: NfStringProperty(name="Params", default="") # pyright: ignore[reportInvalidTypeForm] parameters: str = NfStringProperty(name="Params", default="")
def init(self, context): def init(self, context):
self.add_output("NodeSocketString") self.add_output("NodeSocketString")

View file

@ -4,7 +4,7 @@ import bpy
import os import os
import numpy as np import numpy as np
from ..bpy_ns_types import NfStringProperty, notify_tree_changed, NfEnumProperty from ..bpy_ns_types import NfStringProperty, notify_tree_changed, NfEnumProperty, NfPointerProperty
from ....registry import register_class from ....registry import register_class
from .base import InputNode, SliceNode, SliceIntrinsics from .base import InputNode, SliceNode, SliceIntrinsics
@ -43,11 +43,11 @@ class SLICENODE_InputObject(InputNode):
class SLICENODE_CollectionObjects(SliceNode): class SLICENODE_CollectionObjects(SliceNode):
bl_label = "Collection Objects" bl_label = "Collection Objects"
collection: bpy.props.PointerProperty( collection: bpy.types.Collection | None = NfPointerProperty(
name="Collection", name="Collection",
type=bpy.types.Collection, type=bpy.types.Collection,
update=notify_tree_changed update=notify_tree_changed
) # pyright: ignore[reportInvalidTypeForm] )
def init(self, context): def init(self, context):
self._update_outputs() self._update_outputs()
@ -61,11 +61,12 @@ class SLICENODE_CollectionObjects(SliceNode):
layout.prop(self, "collection", text="") layout.prop(self, "collection", text="")
def _update_outputs(self): def _update_outputs(self):
if not self.collection: col = getattr(self, "collection", None)
if not col or not hasattr(col, "objects"):
self.outputs.clear() self.outputs.clear()
return return
objs = [obj for obj in self.collection.objects if obj.type == 'MESH'] objs = [obj for obj in col.objects if getattr(obj, "type", "") == 'MESH']
expected_names = [obj.name for obj in objs] expected_names = [obj.name for obj in objs]
# Remove outputs that are no longer in the collection # Remove outputs that are no longer in the collection
@ -96,11 +97,12 @@ class SLICENODE_WipeTower(SliceNode):
if socket_name != "Settings": if socket_name != "Settings":
return {} return {}
raw_x = self.resolve("X Position") raw_x: Any = self.resolve("X Position")
raw_y = self.resolve("Y Position") raw_y: Any = self.resolve("Y Position")
if isinstance(raw_x, (list, tuple, np.ndarray)) and len(raw_x) >= 2: if isinstance(raw_x, (list, tuple, np.ndarray)):
pos_x, pos_y = float(raw_x[0]), float(raw_x[1]) 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: else:
pos_x = float(raw_x) if raw_x is not None else 0.0 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 pos_y = float(raw_y) if raw_y is not None else 0.0
@ -123,13 +125,13 @@ class SLICENODE_OffsetObject(SliceNode):
if socket_name != "Settings": if socket_name != "Settings":
return {} return {}
raw_x = self.resolve("X Offset") raw_x: Any = self.resolve("X Offset")
raw_y = self.resolve("Y Offset") raw_y: Any = self.resolve("Y Offset")
raw_z = self.resolve("Z Offset") raw_z: Any = self.resolve("Z Offset")
if isinstance(raw_x, (list, tuple, np.ndarray)) and len(raw_x) >= 2: if isinstance(raw_x, (list, tuple, np.ndarray)):
ox = float(raw_x[0]) ox = float(raw_x[0]) if len(raw_x) >= 1 else 0.0
oy = float(raw_x[1]) 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) oz = float(raw_x[2]) if len(raw_x) >= 3 else (float(raw_z) if raw_z is not None else 0.0)
else: else:
ox = float(raw_x) if raw_x is not None else 0.0 ox = float(raw_x) if raw_x is not None else 0.0
@ -205,9 +207,9 @@ class SLICENODE_PrusaslicerConf(SliceNode):
def search_prints(self, _context, edit_text) -> list[str]: def search_prints(self, _context, edit_text) -> list[str]:
return self.search_conf(edit_text, conf_type="prints") return self.search_conf(edit_text, conf_type="prints")
printer_id: NfStringProperty(name="Printer", default='', update=update_profiles, search=search_printers) # pyright: ignore[reportInvalidTypeForm] printer_id: str = NfStringProperty(name="Printer", default='', update=update_profiles, search=search_printers)
filament_id: NfStringProperty(name="Filament", default='', search=search_filaments) # pyright: ignore[reportInvalidTypeForm] filament_id: str = NfStringProperty(name="Filament", default='', search=search_filaments)
print_id: NfStringProperty(name="Print", default='', search=search_prints) # pyright: ignore[reportInvalidTypeForm] print_id: str = NfStringProperty(name="Print", default='', search=search_prints)
def init(self, context): def init(self, context):
self.add_output("SLICESOCK_PrusaslicerConf", "Config") self.add_output("SLICESOCK_PrusaslicerConf", "Config")
@ -242,9 +244,9 @@ class SLICENODE_InputPrinter(SliceNode):
self.label = self.printer_label self.label = self.printer_label
notify_tree_changed(self, context) notify_tree_changed(self, context)
printer_label: NfStringProperty(name="Label", default="Printer", update=update_label) # pyright: ignore[reportInvalidTypeForm] printer_label: str = NfStringProperty(name="Label", default="Printer", update=update_label)
host_type: NfEnumProperty( host_type: str = NfEnumProperty(
name="Type", name="Type",
items=[ items=[
('PRUSALINK', "PrusaLink", "PrusaLink API"), ('PRUSALINK', "PrusaLink", "PrusaLink API"),
@ -252,7 +254,7 @@ class SLICENODE_InputPrinter(SliceNode):
('MOONRAKER', "Moonraker", "Moonraker (Klipper) API"), ('MOONRAKER', "Moonraker", "Moonraker (Klipper) API"),
], ],
default='PRUSALINK' default='PRUSALINK'
) # pyright: ignore[reportInvalidTypeForm] )
def init(self, context): def init(self, context):
self.add_output("SLICESOCK_Printer") self.add_output("SLICESOCK_Printer")

View file

@ -11,7 +11,7 @@ from ....infra.blender.blender_mesh_capture import PdfProperties
class SLICENODE_GetMesh(SliceNode): class SLICENODE_GetMesh(SliceNode):
bl_label = "Get Mesh" bl_label = "Get Mesh"
include_children: NfBoolProperty(name="Include Children", default=True) # pyright: ignore[reportInvalidTypeForm] include_children: bool = NfBoolProperty(name="Include Children", default=True)
def init(self, context): def init(self, context):
self.add_input("NodeSocketObject") self.add_input("NodeSocketObject")
@ -73,7 +73,7 @@ class SLICENODE_JoinMeshGroups(SliceNodeJoiner):
class SLICENODE_SeparateMesh(SliceNode): class SLICENODE_SeparateMesh(SliceNode):
bl_label = "Separate Mesh" bl_label = "Separate Mesh"
comparison: NfEnumProperty( comparison: str = NfEnumProperty(
name="Comparison", name="Comparison",
items=[ items=[
('EQUAL', "Equal", "=="), ('EQUAL', "Equal", "=="),
@ -85,7 +85,7 @@ class SLICENODE_SeparateMesh(SliceNode):
], ],
default='EQUAL', default='EQUAL',
update=notify_tree_changed update=notify_tree_changed
) # pyright: ignore[reportInvalidTypeForm] )
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_Mesh") self.add_input("SLICESOCK_Mesh")
@ -167,8 +167,8 @@ class SLICENODE_SeparateMesh(SliceNode):
class SLICENODE_SeparateMeshByRange(SliceNode): class SLICENODE_SeparateMeshByRange(SliceNode):
bl_label = "Separate Mesh by Range" bl_label = "Separate Mesh by Range"
start_val: NfIntProperty(name="Start", default=0, update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] start_val: int = NfIntProperty(name="Start", default=0, update=notify_tree_changed)
end_val: NfIntProperty(name="End", default=1, update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] end_val: int = NfIntProperty(name="End", default=1, update=notify_tree_changed)
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_Mesh") self.add_input("SLICESOCK_Mesh")
@ -181,8 +181,9 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
def _update_outputs(self): def _update_outputs(self):
# Ensure end >= start # Ensure end >= start
start = self.start_val start = int(self.start_val) if isinstance(getattr(self, "start_val", None), (int, float)) else 0
end = max(start, self.end_val) end_val = int(self.end_val) if isinstance(getattr(self, "end_val", None), (int, float)) else 1
end = max(start, end_val)
expected_ids = [str(i) for i in range(start, end + 1)] expected_ids = [str(i) for i in range(start, end + 1)]
# Remove outputs that are no longer in the range # Remove outputs that are no longer in the range
@ -222,6 +223,12 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
res = SlicingCollection([], f"{coll.name}_{target_val}") res = SlicingCollection([], f"{coll.name}_{target_val}")
start = self.start_val
end = max(start, self.end_val)
total_parts = end - start + 1
num_digits = max(2, len(str(total_parts)))
i = target_val - start
for so in coll.objects: for so in coll.objects:
if attr_name not in so.attributes: if attr_name not in so.attributes:
continue continue
@ -232,18 +239,19 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
# Attribute values might be floats or ints, we check for equality # Attribute values might be floats or ints, we check for equality
mask = vals == target_val mask = vals == target_val
num_digits = max(2, len(str(self.end_val))) if np.any(mask):
if np.all(mask):
res.objects.append(so)
elif np.any(mask):
# Split the object # Split the object
so_new = SlicingObject() so_new = SlicingObject()
so_new.name = f"{so.name}{target_val:0{num_digits}d}" base_mesh_name = so.name
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.object_type = so.object_type
so_new.extruder = so.extruder 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.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
so_new.object_properties = list(getattr(so, "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"],
@ -258,7 +266,7 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
class SLICENODE_ArrangeMesh(SliceNode): class SLICENODE_ArrangeMesh(SliceNode):
bl_label = "Arrange Slicing Group" bl_label = "Arrange Slicing Group"
method: NfEnumProperty( method: str = NfEnumProperty(
name="Method", name="Method",
items=[ items=[
('NFP', "NFP (Default)", "Nest using No-Fit Polygon placer + First-Fit selection"), ('NFP', "NFP (Default)", "Nest using No-Fit Polygon placer + First-Fit selection"),
@ -267,13 +275,13 @@ class SLICENODE_ArrangeMesh(SliceNode):
], ],
default='NFP', default='NFP',
update=notify_tree_changed update=notify_tree_changed
) # pyright: ignore[reportInvalidTypeForm] )
allow_rotations: NfBoolProperty( allow_rotations: bool = NfBoolProperty(
name="Allow Rotations", name="Allow Rotations",
default=True, default=True,
update=notify_tree_changed update=notify_tree_changed
) # pyright: ignore[reportInvalidTypeForm] )
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_Mesh", "Slicing Group", display_shape='LIST') self.add_input("SLICESOCK_Mesh", "Slicing Group", display_shape='LIST')
@ -455,7 +463,7 @@ def _transform_slice_paths(sp: SlicePaths, t_arr: np.ndarray, r_arr: np.ndarray,
if not (has_scale or has_rot or has_trans): if not (has_scale or has_rot or has_trans):
return sp.copy_with(paths=list(sp)) return sp.copy_with(paths=list(sp))
R = None rot_mat: np.ndarray | None = None
if has_rot: if has_rot:
rx, ry, rz = r_arr rx, ry, rz = r_arr
cx, sx = np.cos(rx), np.sin(rx) cx, sx = np.cos(rx), np.sin(rx)
@ -477,7 +485,7 @@ def _transform_slice_paths(sp: SlicePaths, t_arr: np.ndarray, r_arr: np.ndarray,
[sz, cz, 0.0], [sz, cz, 0.0],
[0.0, 0.0, 1.0] [0.0, 0.0, 1.0]
], dtype=np.float64) ], dtype=np.float64)
R = Rz @ (Ry @ Rx) rot_mat = Rz @ (Ry @ Rx)
for p in sp: for p in sp:
if len(p) == 0: if len(p) == 0:
@ -493,11 +501,11 @@ def _transform_slice_paths(sp: SlicePaths, t_arr: np.ndarray, r_arr: np.ndarray,
else: else:
new_p[:, :dim] *= s_arr[:dim] new_p[:, :dim] *= s_arr[:dim]
if has_rot and R is not None and dim > 0: if has_rot and rot_mat is not None and dim > 0:
if dim >= 3: if dim >= 3:
new_p[:, :3] = new_p[:, :3] @ R.T new_p[:, :3] = new_p[:, :3] @ rot_mat.T
elif dim == 2: elif dim == 2:
rot_2d = R[:2, :2] rot_2d = rot_mat[:2, :2]
new_p[:, :2] = new_p[:, :2] @ rot_2d.T new_p[:, :2] = new_p[:, :2] @ rot_2d.T
if has_trans and dim > 0: if has_trans and dim > 0:
@ -511,7 +519,7 @@ def _transform_slice_paths(sp: SlicePaths, t_arr: np.ndarray, r_arr: np.ndarray,
return sp.copy_with(paths=new_paths) return sp.copy_with(paths=new_paths)
def _transform_geometry_item(geom: Any, t_arr: np.ndarray, r_arr: np.ndarray, s_arr: np.ndarray, t_rec: dict) -> Any: def _transform_geometry_item(geom: Any, t_arr: np.ndarray, r_arr: np.ndarray, s_arr: np.ndarray, t_rec: dict[str, Any]) -> Any:
if geom is None: if geom is None:
return None return None
elif isinstance(geom, SlicePaths): elif isinstance(geom, SlicePaths):
@ -590,10 +598,12 @@ class SLICENODE_TransformGeometry(SliceNode):
self.add_output("SLICESOCK_Geometry", "Geometry") self.add_output("SLICESOCK_Geometry", "Geometry")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name not in ("Geometry"): if socket_name not in ("Geometry", "Mesh"):
return None return None
geom_data: Any = self.resolve("Geometry") geom_data: Any = self.resolve("Geometry")
if geom_data is None:
geom_data = self.resolve("Mesh")
if geom_data is None: if geom_data is None:
return None return None
@ -641,7 +651,7 @@ class SLICENODE_DropGeometry(SliceNode):
if socket_name != "Mesh": if socket_name != "Mesh":
return None return None
mesh_data: SlicingCollection | SlicingGroup | None = self.resolve("Mesh") mesh_data = self.resolve("Mesh")
if not mesh_data: if not mesh_data:
return None return None
@ -682,7 +692,7 @@ def format_name_template(template: str, so: Any, default_old_name: str = "") ->
import re import re
from ....core.pdf_exporter.metadata import get_first_attribute_value from ....core.pdf_exporter.metadata import get_first_attribute_value
def replace_match(match: re.Match) -> str: def replace_match(match: re.Match[str]) -> str:
attr_name = match.group(1).strip() attr_name = match.group(1).strip()
val = get_first_attribute_value(so, attr_name) val = get_first_attribute_value(so, attr_name)
if val is None and attr_name in ("Name", "name"): if val is None and attr_name in ("Name", "name"):
@ -699,6 +709,11 @@ def format_name_template(template: str, so: Any, default_old_name: str = "") ->
def _rename_slicing_item(geom: Any, new_name: str) -> Any: def _rename_slicing_item(geom: Any, new_name: str) -> Any:
if geom is None or not new_name: if geom is None or not new_name:
return geom return geom
elif isinstance(geom, SlicePaths):
copied = geom.copy_with(paths=list(geom))
target_name = format_name_template(new_name, copied, default_old_name=getattr(copied, "name", ""))
copied.name = target_name
return copied
elif isinstance(geom, (list, tuple)): elif isinstance(geom, (list, tuple)):
return [_rename_slicing_item(item, f"{new_name}_{i}" if (len(geom) > 1 and "{" not in new_name) else new_name) for i, item in enumerate(geom)] return [_rename_slicing_item(item, f"{new_name}_{i}" if (len(geom) > 1 and "{" not in new_name) else new_name) for i, item in enumerate(geom)]
elif isinstance(geom, SlicingGroup): elif isinstance(geom, SlicingGroup):
@ -706,40 +721,26 @@ def _rename_slicing_item(geom: Any, new_name: str) -> Any:
old_colls = dict(copied.collections) old_colls = dict(copied.collections)
new_colls = {} new_colls = {}
for i, (old_key, coll) in enumerate(old_colls.items()): for i, (old_key, coll) in enumerate(old_colls.items()):
if coll.objects: coll_old_name = getattr(coll, "name", old_key)
coll_name = format_name_template(new_name, coll.objects[0], default_old_name=old_key) target_name = format_name_template(new_name, coll, default_old_name=coll_old_name)
else: coll_new_name = f"{target_name}_{i}" if (len(old_colls) > 1 and "{" not in new_name and target_name == new_name) else target_name
coll_name = format_name_template(new_name, None, default_old_name=old_key) new_colls[coll_new_name] = _rename_slicing_item(coll, coll_new_name)
if len(old_colls) > 1 and "{" not in new_name:
coll_name = f"{coll_name}_{i}"
coll.name = coll_name
for j, obj in enumerate(coll.objects):
obj_name = format_name_template(new_name, obj, default_old_name=obj.name)
if (len(coll.objects) > 1 or len(old_colls) > 1) and "{" not in new_name:
obj_name = f"{obj_name}_{j}"
obj.name = obj_name
obj.parent = coll_name
new_colls[coll_name] = coll
copied.collections = new_colls copied.collections = new_colls
return copied return copied
elif isinstance(geom, SlicingCollection): elif isinstance(geom, SlicingCollection):
copied = _copy_slicing_collection(geom) copied = _copy_slicing_collection(geom)
old_coll_name = geom.name target_name = format_name_template(new_name, copied, default_old_name=getattr(copied, "name", ""))
if copied.objects: copied.name = target_name
coll_name = format_name_template(new_name, copied.objects[0], default_old_name=old_coll_name) for i, so in enumerate(copied.objects):
else: so.parent = target_name
coll_name = format_name_template(new_name, None, default_old_name=old_coll_name) so_old_name = getattr(so, "name", "")
copied.name = coll_name so_target = format_name_template(new_name, so, default_old_name=so_old_name)
for i, obj in enumerate(copied.objects): so.name = f"{so_target}_{i}" if (len(copied.objects) > 1 and "{" not in new_name and so_target == new_name) else so_target
obj_name = format_name_template(new_name, obj, default_old_name=obj.name)
if len(copied.objects) > 1 and "{" not in new_name:
obj_name = f"{obj_name}_{i}"
obj.name = obj_name
obj.parent = coll_name
return copied return copied
elif isinstance(geom, SlicingObject): elif isinstance(geom, SlicingObject):
copied = _copy_slicing_object(geom) copied = _copy_slicing_object(geom)
copied.name = format_name_template(new_name, copied, default_old_name=geom.name) target_name = format_name_template(new_name, copied, default_old_name=getattr(copied, "name", ""))
copied.name = target_name
return copied return copied
return geom return geom
@ -758,7 +759,7 @@ class SLICENODE_RenameMesh(SliceNode):
return None return None
geom: Any = self.resolve("Geometry") geom: Any = self.resolve("Geometry")
if not geom: if geom is None:
return None return None
name_socket: str | None = self.resolve("Name") name_socket: str | None = self.resolve("Name")
@ -769,7 +770,7 @@ class SLICENODE_RenameMesh(SliceNode):
return _rename_slicing_item(geom, target_name) return _rename_slicing_item(geom, target_name)
def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tuple[float, float, float], show_name: bool = False) -> Any: def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tuple[float, float, float] | None, show_name: bool) -> Any:
if geom is None: if geom is None:
return None return None
elif isinstance(geom, SlicePaths): elif isinstance(geom, SlicePaths):
@ -780,14 +781,14 @@ def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tup
return [_assign_pdf_properties_to_item(item, line_width, line_color, show_name) for item in geom] return [_assign_pdf_properties_to_item(item, line_width, line_color, show_name) for item in geom]
elif isinstance(geom, SlicingGroup): elif isinstance(geom, SlicingGroup):
copied = _copy_slicing_group(geom) copied = _copy_slicing_group(geom)
for coll in copied.collections.values(): new_colls = {}
for obj in coll.objects: for k, coll in copied.collections.items():
obj.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name) new_colls[k] = _assign_pdf_properties_to_item(coll, line_width, line_color, show_name)
copied.collections = new_colls
return copied return copied
elif isinstance(geom, SlicingCollection): elif isinstance(geom, SlicingCollection):
copied = _copy_slicing_collection(geom) copied = _copy_slicing_collection(geom)
for obj in copied.objects: copied.objects = [_assign_pdf_properties_to_item(so, line_width, line_color, show_name) for so in copied.objects]
obj.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name)
return copied return copied
elif isinstance(geom, SlicingObject): elif isinstance(geom, SlicingObject):
copied = _copy_slicing_object(geom) copied = _copy_slicing_object(geom)
@ -800,16 +801,16 @@ def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tup
class SLICENODE_AssignPdfProperties(SliceNode): class SLICENODE_AssignPdfProperties(SliceNode):
bl_label = "Assign PDF Properties" bl_label = "Assign PDF Properties"
line_width: NfFloatProperty(name="Line Width (mm)", default=0.0, min=0.0, max=50.0) # pyright: ignore[reportInvalidTypeForm] line_width: float = NfFloatProperty(name="Line Width (mm)", default=0.0, min=0.0, max=50.0)
line_color: NfFloatVectorProperty( line_color: tuple[float, float, float] = NfFloatVectorProperty(
name="Line Color", name="Line Color",
subtype='COLOR', subtype='COLOR',
default=(0.0, 0.0, 0.0), default=(0.0, 0.0, 0.0),
min=0.0, min=0.0,
max=1.0, max=1.0,
size=3 size=3
) # pyright: ignore[reportInvalidTypeForm] )
show_name: NfBoolProperty(name="Show Name", default=False) # pyright: ignore[reportInvalidTypeForm] show_name: bool = NfBoolProperty(name="Show Name", default=False)
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_Mesh", "Geometry") self.add_input("SLICESOCK_Mesh", "Geometry")

View file

@ -69,7 +69,7 @@ class SLICENODE_GcodeToTextBlock(ExecSliceNode):
bl_label = "Gcode to Text Block" bl_label = "Gcode to Text Block"
button_label = "Update Text Block" button_label = "Update Text Block"
text_block_name: NfStringProperty(name="Text Block Name", default="NodeSlicer_Gcode.gcode") # pyright: ignore[reportInvalidTypeForm] text_block_name: str = NfStringProperty(name="Text Block Name", default="NodeSlicer_Gcode.gcode")
def init(self, context): def init(self, context):
self._init_exec() self._init_exec()
@ -171,14 +171,15 @@ class SLICENODE_StartPrint(ExecSliceNode):
return 'ok' return 'ok'
class ExecSliceNode_DatabaseSubmit(ExecSliceNode): class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
record_id: NfStringProperty(name="Record ID", default="") # pyright: ignore[reportInvalidTypeForm] record_id: str = NfStringProperty(name="Record ID", default="")
_db_name = "Database" _db_name = "Database"
_token_prop_name = "token_path" _token_prop_name = "token_path"
def init(self, context): def init(self, context):
self._init_exec() self._init_exec()
self.add_multi_socket_group("NodeSocketString", name="Gcode") self.add_input("NodeSocketString", "Gcode")
self.add_input("SLICESOCK_Settings", "Metadata")
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
super().draw_buttons(context, layout) super().draw_buttons(context, layout)
@ -275,7 +276,7 @@ class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
def _check_config(self, token: str) -> bool: def _check_config(self, token: str) -> bool:
return False return False
def _submit_to_db(self, token: str, fields: dict, temp_path: Path) -> str: def _submit_to_db(self, token: str, fields: dict[str, Any], temp_path: Path) -> str:
return "" return ""
@register_class @register_class
@ -283,9 +284,9 @@ class SLICENODE_BaserowSubmit(ExecSliceNode_DatabaseSubmit):
bl_label = "Baserow Submit" bl_label = "Baserow Submit"
button_label = "Submit to Baserow" button_label = "Submit to Baserow"
host: NfStringProperty(name="Host", default="https://baserow.io") # pyright: ignore[reportInvalidTypeForm] host: str = NfStringProperty(name="Host", default="https://baserow.io")
token_path: NfStringProperty(name="Token File", subtype='FILE_PATH', default="") # pyright: ignore[reportInvalidTypeForm] token_path: str = NfStringProperty(name="Token File", subtype='FILE_PATH', default="")
table_id: NfStringProperty(name="Table ID", default="") # pyright: ignore[reportInvalidTypeForm] table_id: str = NfStringProperty(name="Table ID", default="")
_db_name = "Baserow" _db_name = "Baserow"
_token_prop_name = "token_path" _token_prop_name = "token_path"
@ -321,9 +322,9 @@ class SLICENODE_AirtableSubmit(ExecSliceNode_DatabaseSubmit):
bl_label = "Airtable Submit" bl_label = "Airtable Submit"
button_label = "Submit to Airtable" button_label = "Submit to Airtable"
pat_path: NfStringProperty(name="PAT File", subtype='FILE_PATH', default="") # pyright: ignore[reportInvalidTypeForm] pat_path: str = NfStringProperty(name="PAT File", subtype='FILE_PATH', default="")
base_id: NfStringProperty(name="Base ID", default="") # pyright: ignore[reportInvalidTypeForm] base_id: str = NfStringProperty(name="Base ID", default="")
table_id: NfStringProperty(name="Table ID", default="") # pyright: ignore[reportInvalidTypeForm] table_id: str = NfStringProperty(name="Table ID", default="")
_db_name = "Airtable" _db_name = "Airtable"
_token_prop_name = "pat_path" _token_prop_name = "pat_path"
@ -358,59 +359,59 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
bl_label = "Mesh to PDF" bl_label = "Mesh to PDF"
button_label = "Generate PDF" button_label = "Generate PDF"
draw_wireframe: NfBoolProperty(name="Wireframe", default=True) # pyright: ignore[reportInvalidTypeForm] draw_wireframe: bool = NfBoolProperty(name="Wireframe", default=True)
draw_boundary: NfBoolProperty(name="Boundary (1 Face)", default=False) # pyright: ignore[reportInvalidTypeForm] draw_boundary: bool = NfBoolProperty(name="Boundary (1 Face)", default=False)
draw_contour: NfBoolProperty(name="Contour", default=False) # pyright: ignore[reportInvalidTypeForm] draw_contour: bool = NfBoolProperty(name="Contour", default=False)
draw_crease: NfBoolProperty(name="Crease Angle", default=False) # pyright: ignore[reportInvalidTypeForm] draw_crease: bool = NfBoolProperty(name="Crease Angle", default=False)
crease_angle: NfFloatProperty(name="Angle (°)", default=30.0, min=0.0, max=180.0) # pyright: ignore[reportInvalidTypeForm] crease_angle: float = NfFloatProperty(name="Angle (°)", default=30.0, min=0.0, max=180.0)
draw_bbox: NfBoolProperty(name="Bounding Box", default=False) # pyright: ignore[reportInvalidTypeForm] draw_bbox: bool = NfBoolProperty(name="Bounding Box", default=False)
draw_sa2_holes: NfBoolProperty(name="Holes (Ø)", default=False) # pyright: ignore[reportInvalidTypeForm] draw_sa2_holes: bool = NfBoolProperty(name="Holes (Ø)", default=False)
show_lineart: NfBoolProperty(name="Lineart Visibility", default=False) # pyright: ignore[reportInvalidTypeForm] show_lineart: bool = NfBoolProperty(name="Lineart Visibility", default=False)
show_sa2: NfBoolProperty(name="SA2 Settings", default=False) # pyright: ignore[reportInvalidTypeForm] show_sa2: bool = NfBoolProperty(name="SA2 Settings", default=False)
draw_grid_xy: NfBoolProperty(name="XY Grid (Top)", default=True) # pyright: ignore[reportInvalidTypeForm] draw_grid_xy: bool = NfBoolProperty(name="XY Grid (Top)", default=True)
draw_grid_xz: NfBoolProperty(name="XZ Grid (Front)", default=False) # pyright: ignore[reportInvalidTypeForm] draw_grid_xz: bool = NfBoolProperty(name="XZ Grid (Front)", default=False)
draw_grid_yz: NfBoolProperty(name="YZ Grid (Side)", default=False) # pyright: ignore[reportInvalidTypeForm] draw_grid_yz: bool = NfBoolProperty(name="YZ Grid (Side)", default=False)
show_grids: NfBoolProperty(name="Grids", default=False) # pyright: ignore[reportInvalidTypeForm] show_grids: bool = NfBoolProperty(name="Grids", default=False)
draw_header: NfBoolProperty(name="Technical Header", default=True) # pyright: ignore[reportInvalidTypeForm] draw_header: bool = NfBoolProperty(name="Technical Header", default=True)
show_header: NfBoolProperty(name="Technical Drawing Header", default=False) # pyright: ignore[reportInvalidTypeForm] show_header: bool = NfBoolProperty(name="Technical Drawing Header", default=False)
header_company: NfStringProperty(name="Company", default="") # pyright: ignore[reportInvalidTypeForm] header_company: str = NfStringProperty(name="Company", default="")
header_part_name: NfStringProperty(name="Part Name", default="") # pyright: ignore[reportInvalidTypeForm] header_part_name: str = NfStringProperty(name="Part Name", default="")
header_drawing_number: NfStringProperty(name="Drawing No.", default="") # pyright: ignore[reportInvalidTypeForm] header_drawing_number: str = NfStringProperty(name="Drawing No.", default="")
header_author: NfStringProperty(name="Author", default="") # pyright: ignore[reportInvalidTypeForm] header_author: str = NfStringProperty(name="Author", default="")
header_revision: NfStringProperty(name="Revision", default="A") # pyright: ignore[reportInvalidTypeForm] header_revision: str = NfStringProperty(name="Revision", default="A")
header_material: NfStringProperty(name="Material", default="") # pyright: ignore[reportInvalidTypeForm] header_material: str = NfStringProperty(name="Material", default="")
header_tolerance: NfStringProperty(name="Tolerance", default="") # pyright: ignore[reportInvalidTypeForm] header_tolerance: str = NfStringProperty(name="Tolerance", default="")
font_size: NfFloatProperty(name="Font Size (mm)", default=0.0, min=0.0, max=100.0) # pyright: ignore[reportInvalidTypeForm] font_size: float = NfFloatProperty(name="Font Size (mm)", default=0.0, min=0.0, max=100.0)
margin: NfFloatProperty(name="Margin (mm)", default=10.0, min=0.0, max=500.0) # pyright: ignore[reportInvalidTypeForm] margin: float = NfFloatProperty(name="Margin (mm)", default=10.0, min=0.0, max=500.0)
bg_color: NfFloatVectorProperty( bg_color: tuple[float, float, float] = NfFloatVectorProperty(
name="Background", name="Background",
subtype='COLOR', subtype='COLOR',
default=(1.0, 1.0, 1.0), default=(1.0, 1.0, 1.0),
min=0.0, min=0.0,
max=1.0, max=1.0,
size=3 size=3
) # pyright: ignore[reportInvalidTypeForm] )
wire_color: NfFloatVectorProperty( wire_color: tuple[float, float, float] = NfFloatVectorProperty(
name="Wireframe", name="Wireframe",
subtype='COLOR', subtype='COLOR',
default=(0.0, 0.0, 0.0), default=(0.0, 0.0, 0.0),
min=0.0, min=0.0,
max=1.0, max=1.0,
size=3 size=3
) # pyright: ignore[reportInvalidTypeForm] )
bbox_color: NfFloatVectorProperty( bbox_color: tuple[float, float, float] = NfFloatVectorProperty(
name="BBox Color", name="BBox Color",
subtype='COLOR', subtype='COLOR',
default=(0.0, 0.0, 1.0), default=(0.0, 0.0, 1.0),
min=0.0, min=0.0,
max=1.0, max=1.0,
size=3 size=3
) # pyright: ignore[reportInvalidTypeForm] )
def init(self, context): def init(self, context):
self._init_exec() self._init_exec()

View file

@ -1,5 +1,4 @@
from __future__ import annotations from typing import Any, cast, Literal
from typing import Any
import numpy as np import numpy as np
from ..bpy_ns_types import NfEnumProperty, NfBoolProperty, notify_tree_changed from ..bpy_ns_types import NfEnumProperty, NfBoolProperty, notify_tree_changed
@ -149,14 +148,14 @@ class SLICENODE_SliceShells(SliceNode):
class SLICENODE_BufferShells(SliceNode): class SLICENODE_BufferShells(SliceNode):
bl_label = "Buffer Shells" bl_label = "Buffer Shells"
shape_option: NfEnumProperty( shape_option: str = NfEnumProperty(
name="Miter", name="Miter",
items=[ items=[
('round', "Round", ""), ('round', "Round", ""),
('square', "Square", ""), ('square', "Square", ""),
('flat', "Flat", "") ('flat', "Flat", "")
] ]
) # pyright: ignore[reportInvalidTypeForm] )
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_Paths") self.add_input("SLICESOCK_Paths")
@ -183,7 +182,7 @@ class SLICENODE_BufferShells(SliceNode):
class SLICENODE_BufferCurve(SliceNode): class SLICENODE_BufferCurve(SliceNode):
bl_label = "Buffer Curve" bl_label = "Buffer Curve"
cap_style: NfEnumProperty( cap_style: str = NfEnumProperty(
name="Cap Style", name="Cap Style",
items=[ items=[
('round', "Round", ""), ('round', "Round", ""),
@ -191,9 +190,9 @@ class SLICENODE_BufferCurve(SliceNode):
('square', "Square", "") ('square', "Square", "")
], ],
default='round' default='round'
) # pyright: ignore[reportInvalidTypeForm] )
join_style: NfEnumProperty( join_style: str = NfEnumProperty(
name="Join Style", name="Join Style",
items=[ items=[
('round', "Round", ""), ('round', "Round", ""),
@ -201,13 +200,13 @@ class SLICENODE_BufferCurve(SliceNode):
('bevel', "Bevel", "") ('bevel', "Bevel", "")
], ],
default='round' default='round'
) # pyright: ignore[reportInvalidTypeForm] )
single_sided: NfBoolProperty( single_sided: bool = NfBoolProperty(
name="Single Sided", name="Single Sided",
description="Buffer only on one side of the curve", description="Buffer only on one side of the curve",
default=False default=False
) # pyright: ignore[reportInvalidTypeForm] )
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_Paths") self.add_input("SLICESOCK_Paths")
@ -242,7 +241,7 @@ class SLICENODE_BufferCurve(SliceNode):
class SLICENODE_OffsetCurve(SliceNode): class SLICENODE_OffsetCurve(SliceNode):
bl_label = "Offset Curve" bl_label = "Offset Curve"
join_style: NfEnumProperty( join_style: str = NfEnumProperty(
name="Join Style", name="Join Style",
items=[ items=[
('round', "Round", ""), ('round', "Round", ""),
@ -250,7 +249,7 @@ class SLICENODE_OffsetCurve(SliceNode):
('bevel', "Bevel", "") ('bevel', "Bevel", "")
], ],
default='round' default='round'
) # pyright: ignore[reportInvalidTypeForm] )
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_Paths") self.add_input("SLICESOCK_Paths")
@ -273,7 +272,7 @@ class SLICENODE_OffsetCurve(SliceNode):
return paths.offset_curve( return paths.offset_curve(
distance, distance,
join_style=self.join_style, join_style=cast(Literal['round', 'mitre'], self.join_style),
rotation=rotation rotation=rotation
) )
@ -319,14 +318,14 @@ class SLICENODE_RotatePath(SliceNode):
class SLICENODE_BufferShellsMultiple(SliceNode): class SLICENODE_BufferShellsMultiple(SliceNode):
bl_label = "Buffer Shells Multiple" bl_label = "Buffer Shells Multiple"
shape_option: NfEnumProperty( shape_option: str = NfEnumProperty(
name="Miter", name="Miter",
items=[ items=[
('round', "Round", ""), ('round', "Round", ""),
('square', "Square", ""), ('square', "Square", ""),
('flat', "Flat", "") ('flat', "Flat", "")
] ]
) # pyright: ignore[reportInvalidTypeForm] )
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_Paths") self.add_input("SLICESOCK_Paths")
@ -357,7 +356,7 @@ class SLICENODE_BufferShellsMultiple(SliceNode):
class SLICENODE_Infill(SliceNode): class SLICENODE_Infill(SliceNode):
bl_label = "Infill" bl_label = "Infill"
infill_type: NfEnumProperty( infill_type: str = NfEnumProperty(
name="Type", name="Type",
items=[ items=[
('GRID', "Grid", "Cross-hatch grid"), ('GRID', "Grid", "Cross-hatch grid"),
@ -366,7 +365,7 @@ class SLICENODE_Infill(SliceNode):
], ],
default='GRID', default='GRID',
update=notify_tree_changed update=notify_tree_changed
) # pyright: ignore[reportInvalidTypeForm] )
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_Paths") self.add_input("SLICESOCK_Paths")
@ -443,7 +442,7 @@ class SLICENODE_JoinSlices(SliceNodeJoiner):
bl_label = "Boolean Paths" bl_label = "Boolean Paths"
socket_type = "SLICESOCK_Paths" socket_type = "SLICESOCK_Paths"
operation: NfEnumProperty( operation: str = NfEnumProperty(
name="Operation", name="Operation",
items=[ items=[
('UNION', "Union", "Union of the slices"), ('UNION', "Union", "Union of the slices"),
@ -452,13 +451,13 @@ class SLICENODE_JoinSlices(SliceNodeJoiner):
('XOR', "Exclusive or", "Exclusive or of the slices"), ('XOR', "Exclusive or", "Exclusive or of the slices"),
], ],
default='UNION' default='UNION'
) # pyright: ignore[reportInvalidTypeForm] )
connect_along_boundary: NfBoolProperty( connect_along_boundary: bool = NfBoolProperty(
name="Connect along boundary", name="Connect along boundary",
description="Connect clipped segments along the boundary of the clipping polygon", description="Connect clipped segments along the boundary of the clipping polygon",
default=False default=False
) # pyright: ignore[reportInvalidTypeForm] )
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
assert layout assert layout
@ -489,14 +488,14 @@ class SLICENODE_JoinPaths(SliceNodeJoiner):
bl_label = "Join Paths" bl_label = "Join Paths"
socket_type = "SLICESOCK_Paths" socket_type = "SLICESOCK_Paths"
mode: NfEnumProperty( mode: str = NfEnumProperty(
name="Mode", name="Mode",
items=[ items=[
('EXTEND', "Extend", "Append paths sequentially"), ('EXTEND', "Extend", "Append paths sequentially"),
('ZIP', "Zip", "Interleave paths from all inputs"), ('ZIP', "Zip", "Interleave paths from all inputs"),
], ],
default='EXTEND' default='EXTEND'
) # pyright: ignore[reportInvalidTypeForm] )
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
assert layout assert layout
@ -507,7 +506,7 @@ class SLICENODE_JoinPaths(SliceNodeJoiner):
inputs_data: list[SlicePaths] = [] inputs_data: list[SlicePaths] = []
for input in self.inputs: for input in self.inputs:
paths: SlicePaths = resolve_input(input) paths: SlicePaths | None = resolve_input(input)
if paths: if paths:
inputs_data.append(paths) inputs_data.append(paths)
@ -526,7 +525,7 @@ class SLICENODE_JoinPaths(SliceNodeJoiner):
class SLICENODE_SimplifyPaths(SliceNode): class SLICENODE_SimplifyPaths(SliceNode):
bl_label = "Simplify Paths" bl_label = "Simplify Paths"
preserve_topology: NfBoolProperty( # pyright: ignore[reportInvalidTypeForm] preserve_topology: bool = NfBoolProperty(
name="Preserve Topology", name="Preserve Topology",
description="Prevents swapping of contained/containing geometry during simplification", description="Prevents swapping of contained/containing geometry during simplification",
default=True default=True
@ -547,7 +546,6 @@ class SLICENODE_SimplifyPaths(SliceNode):
paths: SlicePaths = self.resolve("Paths") paths: SlicePaths = self.resolve("Paths")
tolerance: float = self.resolve("Tolerance") or 0.0 tolerance: float = self.resolve("Tolerance") or 0.0
if not paths or tolerance <= 0: if not paths or tolerance <= 0:
return paths return paths

View file

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import bpy import bpy
import os import os
@ -11,7 +12,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
bl_label = "Gcode to Preview Mesh" bl_label = "Gcode to Preview Mesh"
button_label = "Update Preview" button_label = "Update Preview"
show_e_0: NfBoolProperty(name="Show Zero Extrusions", default=False) # pyright: ignore[reportInvalidTypeForm] show_e_0: bool = NfBoolProperty(name="Show Zero Extrusions", default=False)
def init(self, context): def init(self, context):
self._init_exec() self._init_exec()
@ -201,15 +202,14 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
ng = bpy.data.node_groups.get(ng_name) ng = bpy.data.node_groups.get(ng_name)
# Get existing 'Preview' modifier or create it # Get existing 'Preview' modifier or create it
mod: Any = None
if hasattr(obj.modifiers, "get"): if hasattr(obj.modifiers, "get"):
mod = obj.modifiers.get("Preview") mod = obj.modifiers.get("Preview")
elif isinstance(obj.modifiers, list): elif isinstance(obj.modifiers, list):
mod = next((m for m in obj.modifiers if (isinstance(m, dict) and m.get("name") == "Preview") or getattr(m, "name", "Preview") == "Preview"), None) mod = next((m for m in obj.modifiers if (isinstance(m, dict) and m.get("name") == "Preview") or getattr(m, "name", "Preview") == "Preview"), None)
else:
mod = None
if (not mod or (hasattr(mod, "type") and mod.type != 'NODES')) and hasattr(obj.modifiers, "new"): if (not mod or (hasattr(mod, "type") and getattr(mod, "type", None) != 'NODES')) and hasattr(obj.modifiers, "new"):
mod = obj.modifiers.new(name="Preview", type='NODES') mod = getattr(obj.modifiers, "new")(name="Preview", type='NODES')
elif not mod and isinstance(obj.modifiers, list): elif not mod and isinstance(obj.modifiers, list):
mod = {"name": "Preview", "type": "NODES"} mod = {"name": "Preview", "type": "NODES"}
obj.modifiers.append(mod) obj.modifiers.append(mod)

View file

@ -56,7 +56,7 @@ class SLICENODE_PrinterList(ExecSliceNode):
def update_config(self, context): def update_config(self, context):
self._refresh() self._refresh()
printers_json_path: NfStringProperty(name="Config File", default="", subtype='FILE_PATH', update=update_config) # pyright: ignore[reportInvalidTypeForm] printers_json_path: str = NfStringProperty(name="Config File", default="", subtype='FILE_PATH', update=update_config)
def init(self, context): def init(self, context):
self._init_exec() self._init_exec()
@ -157,7 +157,7 @@ class SLICENODE_PrinterList(ExecSliceNode):
class SLICENODE_PrusaslicerSlice(SliceNode): class SLICENODE_PrusaslicerSlice(SliceNode):
bl_label = "PrusaSlicer Slice" bl_label = "PrusaSlicer Slice"
prusaslicer_executable: NfStringProperty(name="PrusaSlicer Path", subtype='FILE_PATH', default='flatpak run com.prusa3d.PrusaSlicer') # pyright: ignore[reportInvalidTypeForm] prusaslicer_executable: str = NfStringProperty(name="PrusaSlicer Path", subtype='FILE_PATH', default='flatpak run com.prusa3d.PrusaSlicer')
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_PrusaslicerConf", "Config") self.add_input("SLICESOCK_PrusaslicerConf", "Config")
@ -309,11 +309,11 @@ class SLICENODE_PrusaslicerPreview(ExecSliceNode):
bl_label = "PrusaSlicer Preview" bl_label = "PrusaSlicer Preview"
button_label = "Open Preview" button_label = "Open Preview"
prusaslicer_executable: NfStringProperty( prusaslicer_executable: str = NfStringProperty(
name="PrusaSlicer Path", name="PrusaSlicer Path",
subtype='FILE_PATH', subtype='FILE_PATH',
default='flatpak run com.prusa3d.PrusaSlicer' default='flatpak run com.prusa3d.PrusaSlicer'
) # pyright: ignore[reportInvalidTypeForm] )
def init(self, context): def init(self, context):
self._init_exec() self._init_exec()
@ -396,7 +396,7 @@ class SLICENODE_Executor(ExecSliceNode, SliceNodeJoiner):
button_label = "Execute All" button_label = "Execute All"
socket_type = "SLICESOCK_ExecutionLink" socket_type = "SLICESOCK_ExecutionLink"
auto_refresh: NfBoolProperty(name="Auto-refresh", default=False) # pyright: ignore[reportInvalidTypeForm] auto_refresh: bool = NfBoolProperty(name="Auto-refresh", default=False)
def init(self, context): def init(self, context):
self._init_exec() self._init_exec()

View file

@ -94,8 +94,8 @@ class SLICESOCK_PrusaslicerConf(LabelNodeSocket):
@register_class @register_class
class SettingItem(bpy.types.PropertyGroup): class SettingItem(bpy.types.PropertyGroup):
field: NfStringProperty(name="Field", default="") # pyright: ignore[reportInvalidTypeForm] field: str = NfStringProperty(name="Field", default="")
value_str: NfStringProperty(name="Value", default="") # pyright: ignore[reportInvalidTypeForm] value_str: str = NfStringProperty(name="Value", default="")
@property @property
def is_active(self): def is_active(self):
@ -137,14 +137,14 @@ class SLICESOCK_ExecutionLink(LabelNodeSocket):
class SLICESOCK_Printer(InputNodeSocket): class SLICESOCK_Printer(InputNodeSocket):
bl_label = "Printer" bl_label = "Printer"
host: NfStringProperty(name="Host", default="localhost") # pyright: ignore[reportInvalidTypeForm] host: str = NfStringProperty(name="Host", default="localhost")
port: NfIntProperty(name="Port", default=80, min=1, max=65535) # pyright: ignore[reportInvalidTypeForm] port: int = NfIntProperty(name="Port", default=80, min=1, max=65535)
prefix: NfStringProperty(name="Prefix", default="") # pyright: ignore[reportInvalidTypeForm] prefix: str = NfStringProperty(name="Prefix", default="")
label: NfStringProperty(name="Label", default="Printer") # pyright: ignore[reportInvalidTypeForm] label: str = NfStringProperty(name="Label", default="Printer")
username: NfStringProperty(name="Username", default="") # pyright: ignore[reportInvalidTypeForm] username: str = NfStringProperty(name="Username", default="")
password: NfStringProperty(name="Password", default="", subtype='PASSWORD') # pyright: ignore[reportInvalidTypeForm] password: str = NfStringProperty(name="Password", default="", subtype='PASSWORD')
host_type: NfEnumProperty( host_type: str = NfEnumProperty(
name="Type", name="Type",
items=[ items=[
('PRUSALINK', "PrusaLink", "PrusaLink API"), ('PRUSALINK', "PrusaLink", "PrusaLink API"),
@ -153,7 +153,7 @@ class SLICESOCK_Printer(InputNodeSocket):
], ],
default='PRUSALINK', default='PRUSALINK',
update=notify_tree_changed update=notify_tree_changed
) # pyright: ignore[reportInvalidTypeForm] )
bl_color = (1.0, 0.45, 0.1, 1.0) # Orange bl_color = (1.0, 0.45, 0.1, 1.0) # Orange

View file

@ -1,5 +1,4 @@
from __future__ import annotations from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING
import bpy 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
@ -179,7 +178,7 @@ class SLICENODE_OT_GroupToggle(Operator):
bl_description = "Enter or exit the selected node group" bl_description = "Enter or exit the selected node group"
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
node_name: bpy.props.StringProperty() # pyright: ignore[reportInvalidTypeForm] node_name: str = cast(Any, bpy.props.StringProperty())
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -463,7 +462,7 @@ class SLICENODE_OT_Execute(Operator):
bl_label = "Execute" bl_label = "Execute"
bl_description = "Execute" bl_description = "Execute"
node_name: bpy.props.StringProperty() # pyright: ignore[reportInvalidTypeForm] node_name: str = cast(Any, bpy.props.StringProperty())
def execute(self, context) -> set['OperatorReturnItems']: def execute(self, context) -> set['OperatorReturnItems']:
node_tree = context.space_data.edit_tree # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess] node_tree = context.space_data.edit_tree # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]

View file

@ -52,7 +52,7 @@ def transform_slice_paths(
if not (has_scale or has_rot or has_trans): if not (has_scale or has_rot or has_trans):
return sp.copy_with(paths=list(sp)) return sp.copy_with(paths=list(sp))
R = None rot_mat: np.ndarray | None = None
if has_rot: if has_rot:
rx, ry, rz = r_arr rx, ry, rz = r_arr
cx, sx = np.cos(rx), np.sin(rx) cx, sx = np.cos(rx), np.sin(rx)
@ -74,7 +74,7 @@ def transform_slice_paths(
[sz, cz, 0.0], [sz, cz, 0.0],
[0.0, 0.0, 1.0] [0.0, 0.0, 1.0]
], dtype=np.float64) ], dtype=np.float64)
R = Rz @ (Ry @ Rx) rot_mat = Rz @ (Ry @ Rx)
new_paths = [] new_paths = []
for p in sp: for p in sp:
@ -91,11 +91,11 @@ def transform_slice_paths(
else: else:
new_p[:, :dim] *= s_arr[:dim] new_p[:, :dim] *= s_arr[:dim]
if has_rot and R is not None and dim > 0: if has_rot and rot_mat is not None and dim > 0:
if dim >= 3: if dim >= 3:
new_p[:, :3] = new_p[:, :3] @ R.T new_p[:, :3] = new_p[:, :3] @ rot_mat.T
elif dim == 2: elif dim == 2:
rot_2d = R[:2, :2] rot_2d = rot_mat[:2, :2]
new_p[:, :2] = new_p[:, :2] @ rot_2d.T new_p[:, :2] = new_p[:, :2] @ rot_2d.T
if has_trans and dim > 0: if has_trans and dim > 0:
@ -752,12 +752,12 @@ def extract_silhouette_contour_edges(mesh_data: Any) -> NDArray[np.float64]:
elif isinstance(item, SlicingObject): elif isinstance(item, SlicingObject):
if item.mesh.size > 0: if item.mesh.size > 0:
verts_3d = item.mesh[:, :3, :3] verts_3d = item.mesh[:, :3, :3]
if item.mesh.shape[1] >= 4:
normals = item.mesh[:, 3, :]
else:
v0_3d = verts_3d[:, 0, :] v0_3d = verts_3d[:, 0, :]
v1_3d = verts_3d[:, 1, :] v1_3d = verts_3d[:, 1, :]
v2_3d = verts_3d[:, 2, :] v2_3d = verts_3d[:, 2, :]
if item.mesh.shape[1] >= 4:
normals = item.mesh[:, 3, :]
else:
normals = np.cross(v1_3d - v0_3d, v2_3d - v0_3d) normals = np.cross(v1_3d - v0_3d, v2_3d - v0_3d)
norms_len = np.linalg.norm(normals, axis=1, keepdims=True) norms_len = np.linalg.norm(normals, axis=1, keepdims=True)

View file

@ -38,22 +38,34 @@ def get_first_attribute_value(so_item: Any, attr_name: str) -> Any:
return val.item() return val.item()
return val return val
if hasattr(so_item, 'objects') and isinstance(so_item.objects, list):
for obj in so_item.objects:
v = get_first_attribute_value(obj, attr_name)
if v is not None:
return v
if hasattr(so_item, 'collections') and isinstance(so_item.collections, dict):
for coll in so_item.collections.values():
v = get_first_attribute_value(coll, attr_name)
if v is not None:
return v
attr_key = attr_name.lower().replace(" ", "_") attr_key = attr_name.lower().replace(" ", "_")
if hasattr(so_item, attr_key): if hasattr(so_item, attr_key):
val = getattr(so_item, attr_key) val = getattr(so_item, attr_key)
if val is not None and not callable(val): if val is not None and not callable(val):
if isinstance(val, (list, tuple, np.ndarray)) and len(val) > 0: if isinstance(val, (list, tuple, np.ndarray)) and len(val) > 0:
val = val[0] val = val[0]
if hasattr(val, 'item'): if hasattr(val, 'item') and callable(getattr(val, 'item')):
return val.item() return getattr(val, 'item')()
return val return val
elif hasattr(so_item, attr_name): elif hasattr(so_item, attr_name):
val = getattr(so_item, attr_name) val = getattr(so_item, attr_name)
if val is not None and not callable(val): if val is not None and not callable(val):
if isinstance(val, (list, tuple, np.ndarray)) and len(val) > 0: if isinstance(val, (list, tuple, np.ndarray)) and len(val) > 0:
val = val[0] val = val[0]
if hasattr(val, 'item'): if hasattr(val, 'item') and callable(getattr(val, 'item')):
return val.item() return getattr(val, 'item')()
return val return val
return None return None

View file

@ -24,7 +24,7 @@ from .pdf_utils import PdfStream, escape_pdf_text
# Geometry Utilities # Geometry Utilities
# ============================================================================= # =============================================================================
def circumcircle_3pts(p1: NDArray, p2: NDArray, p3: NDArray) -> tuple[tuple[float, float] | None, float | None]: def circumcircle_3pts(p1: NDArray[Any], p2: NDArray[Any], p3: NDArray[Any]) -> tuple[tuple[float, float] | None, float | None]:
"""Calculate center and radius of circumcircle passing through 3 2D points.""" """Calculate center and radius of circumcircle passing through 3 2D points."""
ax, ay = float(p1[0]), float(p1[1]) ax, ay = float(p1[0]), float(p1[1])
bx, by = float(p2[0]), float(p2[1]) bx, by = float(p2[0]), float(p2[1])
@ -518,7 +518,7 @@ def render_sa2_measures_pdf(
""" """
Renders ISO 129-1 compliant SA2 hole measurements (bores, concentric steps, Z-depth, pitch) on PDF stream. Renders ISO 129-1 compliant SA2 hole measurements (bores, concentric steps, Z-depth, pitch) on PDF stream.
""" """
if raw_geometry is None or not draw_sa2_holes: if raw_geometry is None or not draw_sa2_holes or to_pdf_pt is None:
return return
sa2_objects = extract_slicing_objects_bounds(raw_geometry) sa2_objects = extract_slicing_objects_bounds(raw_geometry)
@ -586,6 +586,7 @@ def render_sa2_measures_pdf(
) )
# Concentric Bores Grouping & Hole Callouts # Concentric Bores Grouping & Hole Callouts
concentric_groups: list[list[tuple[float, float, float]]] = []
if draw_sa2_holes and holes: if draw_sa2_holes and holes:
hole_color = (0.10, 0.45, 0.90) hole_color = (0.10, 0.45, 0.90)
s.stroke_color(*hole_color).fill_color(*hole_color) s.stroke_color(*hole_color).fill_color(*hole_color)
@ -611,7 +612,7 @@ def render_sa2_measures_pdf(
) )
# --- Pitch Dimensions --- # --- Pitch Dimensions ---
if draw_sa2_holes and 'concentric_groups' in locals() and len(concentric_groups) >= 2: if draw_sa2_holes and len(concentric_groups) >= 2:
pitch_color = (0.05, 0.58, 0.53) pitch_color = (0.05, 0.58, 0.53)
s.stroke_color(*pitch_color).fill_color(*pitch_color) s.stroke_color(*pitch_color).fill_color(*pitch_color)

View file

@ -2,9 +2,10 @@ from __future__ import annotations
from numpy import float64 from numpy import float64
import numpy as np import numpy as np
import pyclipper import pyclipper
from typing import Any, Literal from typing import Any, Literal, cast
import copy import copy
from shapely.geometry.base import BaseGeometry from shapely.geometry.base import BaseGeometry
from shapely.geometry import Polygon, MultiPolygon, LineString, LinearRing, MultiLineString, GeometryCollection
from ..infra.blender.blender_mesh_capture import PdfProperties from ..infra.blender.blender_mesh_capture import PdfProperties
@ -162,13 +163,13 @@ class SlicePaths(list[np.ndarray]):
extra_attributes: dict[str, list[np.ndarray]] extra_attributes: dict[str, list[np.ndarray]]
layer_height: float layer_height: float
first_layer_height: float | None first_layer_height: float | None
transforms: list[dict] transforms: list[dict[str, Any]]
object_properties: list[tuple[str, str]] object_properties: list[tuple[str, str]]
pdf_properties: PdfProperties pdf_properties: PdfProperties
name: str name: str
objects_info: dict | None objects_info: dict[str, Any] | None
def __init__(self, paths: list[np.ndarray] = [], groups: list[int] = [], extrusion_rate: list[np.ndarray] | None = None, feedrate: list[np.ndarray] | None = None, extra_attributes: dict[str, list[np.ndarray]] | None = None, layer_height: float = 0.2, first_layer_height: float | None = None, transforms: list[dict] | None = None, name: str = "", objects_info: dict | None = None): def __init__(self, paths: list[np.ndarray] = [], groups: list[int] = [], extrusion_rate: list[np.ndarray] | None = None, feedrate: list[np.ndarray] | None = None, extra_attributes: dict[str, list[np.ndarray]] | None = None, layer_height: float = 0.2, first_layer_height: float | None = None, transforms: list[dict[str, Any]] | None = None, name: str = "", objects_info: dict[str, Any] | None = None):
super().__init__(paths) super().__init__(paths)
self.groups = [] self.groups = []
@ -357,8 +358,9 @@ class SlicePaths(list[np.ndarray]):
merged_names = [] merged_names = []
merged_objs = [] merged_objs = []
for sp in paths_list: for sp in paths_list:
if hasattr(sp, "objects_info") and getattr(sp, "objects_info"): obj_info = getattr(sp, "objects_info", None)
for obj_entry in sp.objects_info.get("objects", []): if obj_info:
for obj_entry in obj_info.get("objects", []):
if obj_entry not in merged_objs: if obj_entry not in merged_objs:
merged_objs.append(dict(obj_entry)) merged_objs.append(dict(obj_entry))
n_str = obj_entry.get("name", "") n_str = obj_entry.get("name", "")
@ -1048,13 +1050,13 @@ def boolean_slice(subject: SlicePaths, object: SlicePaths, operation: str = 'UNI
return results return results
def geometry_to_paths(geom: BaseGeometry, z: float) -> list[np.ndarray]: def geometry_to_paths(geom: Any, z: float) -> list[np.ndarray]:
"""Helper to convert shapely geometries to a list of 3D numpy arrays with a given Z height.""" """Helper to convert shapely geometries to a list of 3D numpy arrays with a given Z height."""
paths = [] paths = []
if geom.is_empty: if geom.is_empty:
return paths return paths
if geom.geom_type == 'Polygon': if isinstance(geom, Polygon):
ext_coords = np.array(geom.exterior.coords) ext_coords = np.array(geom.exterior.coords)
ext_coords_3d = np.column_stack([ext_coords, np.full(len(ext_coords), z)]) ext_coords_3d = np.column_stack([ext_coords, np.full(len(ext_coords), z)])
paths.append(ext_coords_3d) paths.append(ext_coords_3d)
@ -1062,12 +1064,9 @@ def geometry_to_paths(geom: BaseGeometry, z: float) -> list[np.ndarray]:
int_coords = np.array(interior.coords) int_coords = np.array(interior.coords)
int_coords_3d = np.column_stack([int_coords, np.full(len(int_coords), z)]) int_coords_3d = np.column_stack([int_coords, np.full(len(int_coords), z)])
paths.append(int_coords_3d) paths.append(int_coords_3d)
elif geom.geom_type == 'MultiPolygon': elif isinstance(geom, (MultiPolygon, GeometryCollection)):
for poly in geom.geoms: for poly in geom.geoms:
paths.extend(geometry_to_paths(poly, z)) paths.extend(geometry_to_paths(poly, z))
elif geom.geom_type == 'GeometryCollection':
for sub_geom in geom.geoms:
paths.extend(geometry_to_paths(sub_geom, z))
return paths return paths
@ -1109,8 +1108,8 @@ def buffer_curve_slice(
poly = line.buffer( poly = line.buffer(
distance, distance,
cap_style=cap_style, cap_style=cast(Literal['round', 'square', 'flat'], cap_style),
join_style=join_style, join_style=cast(Literal['round', 'mitre', 'bevel'], join_style),
single_sided=single_sided, single_sided=single_sided,
quad_segs=16 quad_segs=16
) )
@ -1125,25 +1124,25 @@ def buffer_curve_slice(
return paths_list, groups_list return paths_list, groups_list
def offset_geometry_to_paths(geom: BaseGeometry, z: float) -> list[np.ndarray]: def offset_geometry_to_paths(geom: Any, z: float) -> list[np.ndarray]:
"""Helper to convert shapely line geometries to a list of 3D numpy arrays with a given Z height.""" """Helper to convert shapely line geometries to a list of 3D numpy arrays with a given Z height."""
paths = [] paths = []
if geom.is_empty: if geom.is_empty:
return paths return paths
if geom.geom_type in ('LineString', 'LinearRing'): if isinstance(geom, (LineString, LinearRing)):
coords = np.array(geom.coords) coords = np.array(geom.coords)
coords_3d = np.column_stack([coords, np.full(len(coords), z)]) coords_3d = np.column_stack([coords, np.full(len(coords), z)])
paths.append(coords_3d) paths.append(coords_3d)
elif geom.geom_type == 'MultiLineString': elif isinstance(geom, MultiLineString):
for line in geom.geoms: for line in geom.geoms:
coords = np.array(line.coords) coords = np.array(line.coords)
coords_3d = np.column_stack([coords, np.full(len(coords), z)]) coords_3d = np.column_stack([coords, np.full(len(coords), z)])
paths.append(coords_3d) paths.append(coords_3d)
elif geom.geom_type == 'GeometryCollection': elif isinstance(geom, GeometryCollection):
for sub_geom in geom.geoms: for sub_geom in geom.geoms:
paths.extend(offset_geometry_to_paths(sub_geom, z)) paths.extend(offset_geometry_to_paths(sub_geom, z))
elif geom.geom_type in ('Polygon', 'MultiPolygon'): elif isinstance(geom, (Polygon, MultiPolygon)):
boundary = geom.boundary boundary = geom.boundary
paths.extend(offset_geometry_to_paths(boundary, z)) paths.extend(offset_geometry_to_paths(boundary, z))
return paths return paths
@ -1288,7 +1287,6 @@ def paths_to_shapely_polygon(paths: list[np.ndarray]) -> BaseGeometry:
return Polygon() return Polygon()
import pyclipper import pyclipper
from shapely.geometry import Polygon
from shapely.ops import unary_union from shapely.ops import unary_union
pc = pyclipper.Pyclipper() # pyright: ignore[reportAttributeAccessIssue] pc = pyclipper.Pyclipper() # pyright: ignore[reportAttributeAccessIssue]
@ -1606,8 +1604,8 @@ class Pather(object):
self.retraction: float = 0.0 self.retraction: float = 0.0
self.retraction_feedrate: float = 0.0 self.retraction_feedrate: float = 0.0
self.lift: float = 0.0 self.lift: float = 0.0
self.transforms: list[dict] = list(getattr(paths, "transforms", [])) self.transforms: list[dict[str, Any]] = list(getattr(paths, "transforms", []))
self.objects_info: dict | None = getattr(paths, "objects_info", None) self.objects_info: dict[str, Any] | None = getattr(paths, "objects_info", None)
self.name: str = str(getattr(paths, "name", "")) self.name: str = str(getattr(paths, "name", ""))
@property @property

View file

@ -61,6 +61,8 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
### Geometry Processing ### Geometry Processing
- **Get Mesh**: Converts a Blender Object into internal `SlicingObject` format. - **Get Mesh**: Converts a Blender Object into internal `SlicingObject` format.
- **Separate Mesh**: Splits a `SlicingCollection` into two collections (`Selection` and `Inverted`) by comparing a specified mesh attribute against a target value with comparison operators (`==`, `!=`, `>`, `<`, `>=`, `<=`).
- **Separate Mesh by Range**: Splits a `SlicingCollection` into multiple output sockets across an integer range (`Start` to `End`), matching a specified integer attribute. Objects in each output socket are renamed with a zero-indexed padded suffix (`f"{base_mesh_name}{i:0{num_digits}d}"` where `num_digits = max(2, len(str(total_parts)))`).
- **Rename Mesh**: Overrides the internal metadata name of a `SlicingObject`, `SlicingCollection`, or `SlicingGroup`. Supports `{AttributeName}` attribute placeholders (e.g., `Rebar-L{Rebar Length}M`) that evaluate to index zero of the target attribute for each object, as well as `{Name}` to include the item's previous name (e.g., `{Name}_T{Thickness}mm`). - **Rename Mesh**: Overrides the internal metadata name of a `SlicingObject`, `SlicingCollection`, or `SlicingGroup`. Supports `{AttributeName}` attribute placeholders (e.g., `Rebar-L{Rebar Length}M`) that evaluate to index zero of the target attribute for each object, as well as `{Name}` to include the item's previous name (e.g., `{Name}_T{Thickness}mm`).
- **Geometry**: Input geometry data. - **Geometry**: Input geometry data.
- **Name**: String input socket or property for the new name template. - **Name**: String input socket or property for the new name template.

View file

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
from numpy.typing import NDArray from numpy.typing import NDArray
from pathlib import Path from pathlib import Path
@ -40,7 +41,7 @@ def write_metadata_xml(group: SlicingGroup, filepath):
for j, (k, collection) in enumerate(valid_collections.items()): for j, (k, collection) in enumerate(valid_collections.items()):
sorted_data: list[tuple[NDArray, NDArray, SlicingObject]] = sorted(zip(collection.mesh_start_ids, collection.mesh_end_ids, collection.objects), key=lambda x: object_type_order.get(x[2].object_type, 5)) sorted_data: list[tuple[NDArray[Any], NDArray[Any], SlicingObject]] = sorted(zip(collection.mesh_start_ids, collection.mesh_end_ids, collection.objects), key=lambda x: object_type_order.get(x[2].object_type, 5))
object_elem = ET.SubElement(xml_content, "object", id=str(j+1), instances_count="1") object_elem = ET.SubElement(xml_content, "object", id=str(j+1), instances_count="1")
ET.SubElement(object_elem, "metadata", type="object", key="name", value=collection.name) ET.SubElement(object_elem, "metadata", type="object", key="name", value=collection.name)

View file

@ -40,7 +40,8 @@ def get_evaluated_mesh(obj: Object):
try: try:
eval_obj = obj.evaluated_get(depsgraph) eval_obj = obj.evaluated_get(depsgraph)
m = eval_obj.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph) m = eval_obj.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
if m and len(m.vertices) > 0 and not is_hidden and (not obj.modifiers or len(m.vertices) != len(obj.data.vertices)): orig_verts = getattr(obj.data, "vertices", None)
if m and len(m.vertices) > 0 and not is_hidden and (not obj.modifiers or (orig_verts is not None and len(m.vertices) != len(orig_verts))):
mesh = m mesh = m
elif m: elif m:
eval_obj.to_mesh_clear() eval_obj.to_mesh_clear()
@ -52,7 +53,9 @@ def get_evaluated_mesh(obj: Object):
try: try:
temp_obj = obj.copy() temp_obj = obj.copy()
temp_obj.hide_viewport = False temp_obj.hide_viewport = False
bpy.context.scene.collection.objects.link(temp_obj) scene = bpy.context.scene
if scene and scene.collection:
scene.collection.objects.link(temp_obj)
depsgraph = bpy.context.evaluated_depsgraph_get() depsgraph = bpy.context.evaluated_depsgraph_get()
depsgraph.update() depsgraph.update()
@ -63,7 +66,9 @@ def get_evaluated_mesh(obj: Object):
except Exception: except Exception:
if temp_obj is not None: if temp_obj is not None:
try: try:
bpy.context.scene.collection.objects.unlink(temp_obj) scene = bpy.context.scene
if scene and scene.collection:
scene.collection.objects.unlink(temp_obj)
bpy.data.objects.remove(temp_obj, do_unlink=True) bpy.data.objects.remove(temp_obj, do_unlink=True)
except Exception: except Exception:
pass pass
@ -80,7 +85,9 @@ def get_evaluated_mesh(obj: Object):
pass pass
if is_temp and temp_obj is not None: if is_temp and temp_obj is not None:
try: try:
bpy.context.scene.collection.objects.unlink(temp_obj) scene = bpy.context.scene
if scene and scene.collection:
scene.collection.objects.unlink(temp_obj)
bpy.data.objects.remove(temp_obj, do_unlink=True) bpy.data.objects.remove(temp_obj, do_unlink=True)
bpy.context.evaluated_depsgraph_get().update() bpy.context.evaluated_depsgraph_get().update()
except Exception: except Exception:
@ -100,9 +107,9 @@ class SlicingObject():
name: str name: str
parent: str parent: str
mesh: NDArray[float64] mesh: NDArray[float64]
attributes: dict[str, dict] attributes: dict[str, Any]
object_type: str = "ModelPart" object_type: str = "ModelPart"
modifiers: list[dict] = [] modifiers: list[dict[str, Any]] = []
extruder: int = 1 extruder: int = 1
pdf_properties: PdfProperties pdf_properties: PdfProperties
object_properties: list[tuple[str, str]] object_properties: list[tuple[str, str]]
@ -176,10 +183,10 @@ class SlicingObject():
} }
return merged return merged
def offset(self, offset: NDArray): def offset(self, offset: NDArray[Any]):
self.mesh[:, :3, :] += offset self.mesh[:, :3, :] += offset
def scale(self, factor: float | NDArray): def scale(self, factor: float | NDArray[Any]):
self.mesh[:, :3, :] *= factor self.mesh[:, :3, :] *= factor
if isinstance(factor, (list, tuple, np.ndarray)): if isinstance(factor, (list, tuple, np.ndarray)):
factor_arr = np.array(factor, dtype=np.float64) factor_arr = np.array(factor, dtype=np.float64)
@ -192,7 +199,7 @@ 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 rotate(self, angles: NDArray): def rotate(self, angles: NDArray[Any]):
# angles: array-like of 3 floats (rx, ry, rz) in radians # angles: array-like of 3 floats (rx, ry, rz) in radians
angles_arr = np.array(angles, dtype=np.float64) angles_arr = np.array(angles, dtype=np.float64)
rx, ry, rz = angles_arr rx, ry, rz = angles_arr
@ -268,18 +275,18 @@ class SlicingObject():
def max_z(self) -> float: return self.mesh[:, :3, 2].max() def max_z(self) -> float: return self.mesh[:, :3, 2].max()
@property @property
def min_xy(self) -> NDArray: return np.array([self.min_x, self.min_y, 0.0]) def min_xy(self) -> NDArray[Any]: return np.array([self.min_x, self.min_y, 0.0])
@property @property
def max_xy(self) -> NDArray: return np.array([self.max_x, self.max_y, 0.0]) def max_xy(self) -> NDArray[Any]: return np.array([self.max_x, self.max_y, 0.0])
@property @property
def center_xy(self) -> NDArray: return (self.min_xy + self.max_xy) / 2.0 def center_xy(self) -> NDArray[Any]: return (self.min_xy + self.max_xy) / 2.0
class SlicingCollection(): class SlicingCollection():
name: str name: str
objects: list[SlicingObject] objects: list[SlicingObject]
transforms: list[dict] transforms: list[dict[str, Any]]
def __init__(self, objs: list[Object], parent: str): def __init__(self, objs: list[Object], parent: str):
self.objects = [SlicingObject(obj, parent) for obj in objs] self.objects = [SlicingObject(obj, parent) for obj in objs]
@ -292,15 +299,15 @@ class SlicingCollection():
if prop in self.__dict__: if prop in self.__dict__:
del self.__dict__[prop] del self.__dict__[prop]
def offset(self, offset: NDArray): def offset(self, offset: NDArray[Any]):
for so in self.objects: so.offset(offset) for so in self.objects: so.offset(offset)
self._clear_cache() self._clear_cache()
def scale(self, factor: float | NDArray): def scale(self, factor: float | NDArray[Any]):
for so in self.objects: so.scale(factor) for so in self.objects: so.scale(factor)
self._clear_cache() self._clear_cache()
def rotate(self, angles: NDArray): def rotate(self, angles: NDArray[Any]):
for so in self.objects: so.rotate(angles) for so in self.objects: so.rotate(angles)
self._clear_cache() self._clear_cache()
@ -315,26 +322,26 @@ class SlicingCollection():
return max(height_all) return max(height_all)
@property @property
def meshes(self) -> list[NDArray]: def meshes(self) -> list[NDArray[Any]]:
return [o.mesh for o in self.objects] return [o.mesh for o in self.objects]
@cached_property @cached_property
def mesh_lengths_ids(self) -> NDArray: def mesh_lengths_ids(self) -> NDArray[Any]:
return np.array([len(m) for m in self.meshes]) return np.array([len(m) for m in self.meshes])
@cached_property @cached_property
def mesh_start_ids(self) -> NDArray: def mesh_start_ids(self) -> NDArray[Any]:
lengths = self.mesh_lengths_ids lengths = self.mesh_lengths_ids
return np.insert(np.cumsum(lengths)[:-1], 0, 0) return np.insert(np.cumsum(lengths)[:-1], 0, 0)
@cached_property @cached_property
def mesh_end_ids(self) -> NDArray: def mesh_end_ids(self) -> NDArray[Any]:
starts = self.mesh_start_ids starts = self.mesh_start_ids
lengths = self.mesh_lengths_ids lengths = self.mesh_lengths_ids
return starts + lengths - 1 return starts + lengths - 1
@cached_property @cached_property
def all_verts(self) -> NDArray: def all_verts(self) -> NDArray[Any]:
all_verts = np.vstack(self.meshes)[:, :3, :] all_verts = np.vstack(self.meshes)[:, :3, :]
return all_verts.reshape(-1, 3) return all_verts.reshape(-1, 3)
@ -388,17 +395,17 @@ class SlicingCollection():
return max([so.max_z for so in self.objects if np.any(so.mesh)], default=None) return max([so.max_z for so in self.objects if np.any(so.mesh)], default=None)
@property @property
def min_xy(self) -> NDArray | None: def min_xy(self) -> NDArray[Any] | None:
if self.min_x is None or self.min_y is None: return None if self.min_x is None or self.min_y is None: return None
return np.array([self.min_x, self.min_y, 0.0]) return np.array([self.min_x, self.min_y, 0.0])
@property @property
def max_xy(self) -> NDArray | None: def max_xy(self) -> NDArray[Any] | None:
if self.max_x is None or self.max_y is None: return None if self.max_x is None or self.max_y is None: return None
return np.array([self.max_x, self.max_y, 0.0]) return np.array([self.max_x, self.max_y, 0.0])
@property @property
def center_xy(self) -> NDArray | None: def center_xy(self) -> NDArray[Any] | None:
if self.min_xy is None or self.max_xy is None: return None if self.min_xy is None or self.max_xy is None: return None
return (self.min_xy + self.max_xy) / 2.0 return (self.min_xy + self.max_xy) / 2.0
@ -407,10 +414,10 @@ class SlicingGroup():
collections: dict[str, SlicingCollection] collections: dict[str, SlicingCollection]
# Metadata # Metadata
wipe_tower_xy: NDArray = np.array([0, 0]) wipe_tower_xy: NDArray[Any] = np.array([0, 0])
wipe_tower_rotation_deg: float = 0 wipe_tower_rotation_deg: float = 0
object_offset: NDArray = np.array([0.0, 0.0, 0.0]) object_offset: NDArray[Any] = np.array([0.0, 0.0, 0.0])
transforms: list[dict] 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.object_offset = np.array([0.0, 0.0, 0.0])
@ -443,18 +450,19 @@ class SlicingGroup():
assert bpy.context.scene and bpy.context.scene.unit_settings assert bpy.context.scene and bpy.context.scene.unit_settings
scale_tx = 1000. * bpy.context.scene.unit_settings.scale_length #type: ignore 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] self.wipe_tower_xy = self.center_xy[0:2]
for obj in objs: for obj in objs:
with get_evaluated_mesh(obj) as (eval_obj, _m): with get_evaluated_mesh(obj) as (eval_obj, _m):
if eval_obj and hasattr(eval_obj, 'nodeslicer'): ns_prop = getattr(eval_obj, 'nodeslicer', None)
if eval_obj.nodeslicer.object_type == 'WipeTower': 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_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) self.wipe_tower_rotation_deg = round(eval_obj.rotation_euler[2]*180/math.pi,5)
break break
return return
def offset(self, offset: NDArray): 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] self.wipe_tower_xy = self.wipe_tower_xy + offset[0:2]
@ -499,17 +507,17 @@ class SlicingGroup():
def max_z(self) -> float: return max([so.max_z for k, so in self.collections.items() if so.max_z is not None], default=0) def max_z(self) -> float: return max([so.max_z for k, so in self.collections.items() if so.max_z is not None], default=0)
@property @property
def min_xy(self) -> NDArray: def min_xy(self) -> NDArray[Any]:
if self.min_x is None or self.min_y is None: return np.array([.0, .0, .0]) if self.min_x is None or self.min_y is None: return np.array([.0, .0, .0])
return np.array([self.min_x, self.min_y, 0.0]) return np.array([self.min_x, self.min_y, 0.0])
@property @property
def max_xy(self) -> NDArray: def max_xy(self) -> NDArray[Any]:
if self.max_x is None or self.max_y is None: return np.array([.0, .0, .0]) if self.max_x is None or self.max_y is None: return np.array([.0, .0, .0])
return np.array([self.max_x, self.max_y, 0.0]) return np.array([self.max_x, self.max_y, 0.0])
@property @property
def center_xy(self) -> NDArray: def center_xy(self) -> NDArray[Any]:
if self.min_xy is None or self.max_xy is None: return np.array([.0, .0, .0]) if self.min_xy is None or self.max_xy is None: return np.array([.0, .0, .0])
return (self.min_xy + self.max_xy) / 2.0 return (self.min_xy + self.max_xy) / 2.0
@ -525,7 +533,7 @@ class TriMesh():
self.length_tris = t self.length_tris = t
self.matrix_world = m self.matrix_world = m
def _cached_objects_to_tris(obj_names: tuple[str, ...], matrices: tuple[tuple[float, ...], ...], scale: float) -> tuple[NDArray[float64], dict[str, dict]]: def _cached_objects_to_tris(obj_names: tuple[str, ...], matrices: tuple[tuple[float, ...], ...], scale: float) -> tuple[NDArray[float64], dict[str, dict[str, Any]]]:
tris_count: int = 0 tris_count: int = 0
meshes: list[TriMesh] = [] meshes: list[TriMesh] = []
@ -656,7 +664,7 @@ def _cached_objects_to_tris(obj_names: tuple[str, ...], matrices: tuple[tuple[fl
return tris, attributes_data return tris, attributes_data
def objects_to_tris(objects: list[Object], scale) -> tuple[NDArray[float64], dict[str, dict]]: def objects_to_tris(objects: list[Object], scale: float) -> tuple[NDArray[float64], dict[str, dict[str, Any]]]:
obj_names = tuple(o.name for o in objects) obj_names = tuple(o.name for o in objects)
matrices = tuple(tuple(val for row in o.matrix_world for val in row) for o in objects) matrices = tuple(tuple(val for row in o.matrix_world for val in row) for o in objects)
return _cached_objects_to_tris(obj_names, matrices, scale) return _cached_objects_to_tris(obj_names, matrices, scale)

View file

@ -1,15 +1,15 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import field from dataclasses import field
import typing import typing
from typing import Any, cast
import bpy import bpy
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any
from bpy.types import NodeSocket, Node from bpy.types import NodeSocket, Node
class NodeException(Exception): ... class NodeException(Exception): ...
_EvalCache = dict[tuple[int, str, str], Any] _EvalCache = dict[Any, Any]
_VisitedSet = set[Any]
NATIVE_SOCKET_MAP: dict[str, str] = {} NATIVE_SOCKET_MAP: dict[str, str] = {}
@ -39,7 +39,7 @@ if hasattr(bpy.types, "NodeSocket") and not hasattr(bpy.types.NodeSocket, "value
self._value = val self._value = val
try: try:
bpy.types.NodeSocket.value = property(_get_socket_value, _set_socket_value) setattr(bpy.types.NodeSocket, "value", property(_get_socket_value, _set_socket_value))
except Exception: except Exception:
pass pass
@ -55,6 +55,17 @@ class AutoBlIdName:
if "bl_idname" not in cls.__dict__: if "bl_idname" not in cls.__dict__:
cls.bl_idname = cls.__name__ cls.bl_idname = cls.__name__
if not hasattr(cls, "__annotations__"):
cls.__annotations__ = {}
for name, val in list(cls.__dict__.items()):
val_type_name = type(val).__name__
if "PropertyDeferred" in val_type_name or val_type_name.startswith("_Property") or (isinstance(val, tuple) and len(val) > 0 and "Property" in type(val[0]).__name__):
cls.__annotations__[name] = val
try:
delattr(cls, name)
except (AttributeError, TypeError):
pass
class SliceNodeSocket(NodeSocket, AutoBlIdName): class SliceNodeSocket(NodeSocket, AutoBlIdName):
default: Any = None default: Any = None
bl_color = (1.0, 1.0, 1.0, 1.0) bl_color = (1.0, 1.0, 1.0, 1.0)
@ -64,7 +75,11 @@ class SliceNodeSocket(NodeSocket, AutoBlIdName):
from ...registry import get_label_by_name, register_class from ...registry import get_label_by_name, register_class
class SliceNode(Node, AutoBlIdName): class SliceNode(Node, AutoBlIdName):
def get(self, key: str, default: Any = None) -> Any: _current_cache: _EvalCache | None = None
_current_visited: _VisitedSet | None = None
_data: dict[str, Any] | None = None
def get(self, key: str, default: Any = None) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
try: try:
return super().get(key, default) # type: ignore return super().get(key, default) # type: ignore
except (AttributeError, TypeError): except (AttributeError, TypeError):
@ -73,7 +88,7 @@ class SliceNode(Node, AutoBlIdName):
return self._data.get(key, default) return self._data.get(key, default)
return getattr(self, key, default) return getattr(self, key, default)
def __setitem__(self, key: str, value: Any) -> None: def __setitem__(self, key: str, value: Any) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
try: try:
super().__setitem__(key, value) # type: ignore super().__setitem__(key, value) # type: ignore
except (AttributeError, TypeError): except (AttributeError, TypeError):
@ -81,7 +96,7 @@ class SliceNode(Node, AutoBlIdName):
self._data = {} self._data = {}
self._data[key] = value self._data[key] = value
def __getitem__(self, key: str) -> Any: def __getitem__(self, key: str) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
try: try:
return super().__getitem__(key) # type: ignore return super().__getitem__(key) # type: ignore
except (AttributeError, TypeError): except (AttributeError, TypeError):
@ -91,8 +106,11 @@ class SliceNode(Node, AutoBlIdName):
def __contains__(self, key: str) -> bool: def __contains__(self, key: str) -> bool:
try: try:
return super().__contains__(key) # type: ignore contains_fn = getattr(super(), "__contains__", None)
if contains_fn:
return contains_fn(key) # type: ignore
except (AttributeError, TypeError): except (AttributeError, TypeError):
pass
if hasattr(self, "_data") and isinstance(self._data, dict): if hasattr(self, "_data") and isinstance(self._data, dict):
return key in self._data return key in self._data
return False return False
@ -292,11 +310,14 @@ class SLICENODE_Group(bpy.types.NodeCustomGroup, AutoBlIdName):
self.sync_sockets() self.sync_sockets()
slicer_tree: bpy.props.PointerProperty( # pyright: ignore[reportInvalidTypeForm] slicer_tree: bpy.types.NodeTree | None = cast(
Any,
bpy.props.PointerProperty(
type=bpy.types.NodeTree, type=bpy.types.NodeTree,
name="Node Tree", name="Node Tree",
update=_tree_updated update=_tree_updated
) )
)
def sync_sockets(self): def sync_sockets(self):
try: try:
@ -375,19 +396,16 @@ class SLICENODE_Group(bpy.types.NodeCustomGroup, AutoBlIdName):
return "Group" return "Group"
def _resolve_group_output(socket: NodeSocket, cache: _EvalCache | None, visited: set[tuple[int, str]] | None): def _resolve_group_output(socket: NodeSocket, cache: _EvalCache | None, visited: _VisitedSet | None):
node = socket.node node = socket.node
assert isinstance(node, (bpy.types.NodeGroup, SLICENODE_Group)) assert node is not None
if isinstance(node, SLICENODE_Group):
tree = node.slicer_tree
else:
tree = node.node_tree
tree = getattr(node, "slicer_tree", None) or getattr(node, "node_tree", None)
if not tree: return None if not tree: return None
try: try:
idx = list(node.outputs).index(socket) outputs_list = list(node.outputs) if node.outputs is not None else []
idx = outputs_list.index(socket)
except (ValueError, IndexError): except (ValueError, IndexError):
return None return None
@ -410,14 +428,16 @@ def _resolve_group_output(socket: NodeSocket, cache: _EvalCache | None, visited:
with _group_context(node, cache=cache): with _group_context(node, cache=cache):
return resolve_input(typing.cast(Any, inner_socket), cache=cache, visited=visited) return resolve_input(typing.cast(Any, inner_socket), cache=cache, visited=visited)
def _resolve_group_input(socket: NodeSocket, cache: _EvalCache | None, visited: set[tuple[int, str]] | None): def _resolve_group_input(socket: NodeSocket, cache: _EvalCache | None, visited: _VisitedSet | None):
stack = _get_group_stack(cache) stack = _get_group_stack(cache)
if not stack: return None if not stack: return None
group_node = stack.pop() group_node = stack.pop()
# NodeGroupInput outputs usually match the group inputs by index # NodeGroupInput outputs usually match the group inputs by index
try: try:
idx = list(socket.node.outputs).index(socket) assert socket.node is not None
outputs_list = list(socket.node.outputs) if socket.node.outputs is not None else []
idx = outputs_list.index(socket)
outer_socket = group_node.inputs[idx] outer_socket = group_node.inputs[idx]
except (ValueError, IndexError): except (ValueError, IndexError):
stack.append(group_node) stack.append(group_node)
@ -428,12 +448,9 @@ def _resolve_group_input(socket: NodeSocket, cache: _EvalCache | None, visited:
finally: finally:
stack.append(group_node) stack.append(group_node)
def _follow_reroute(socket: NodeSocket) -> NodeSocket: def _follow_reroute(current: NodeSocket) -> NodeSocket:
current: NodeSocket = socket seen = set()
seen: set[int] = set() while current.node and getattr(current.node, "bl_idname", "") == "NodeReroute":
while getattr(current.node, "bl_idname", "") == "NodeReroute":
assert current.node
ptr = current.node.as_pointer() ptr = current.node.as_pointer()
if ptr in seen: if ptr in seen:
break break
@ -451,7 +468,7 @@ def resolve_input(
socket: SliceNodeSocket | None, socket: SliceNodeSocket | None,
*, *,
cache: _EvalCache | None = None, cache: _EvalCache | None = None,
visited: set[tuple[int, str]] | None = None, visited: _VisitedSet | None = None,
): ):
if socket is None: if socket is None:
raise Exception(f"The queried socket is non-existant") raise Exception(f"The queried socket is non-existant")
@ -494,7 +511,7 @@ def _resolve_output(
*, *,
default: Any = None, default: Any = None,
cache: _EvalCache | None = None, cache: _EvalCache | None = None,
visited: set[tuple[int, str]] | None = None, visited: _VisitedSet | None = None,
): ):
if cache is None: if cache is None:
cache = _current_execution_cache cache = _current_execution_cache

View file

@ -1,7 +1,8 @@
from typing import Any
import bpy import bpy
class DrawCollectionMixin(): class DrawCollectionMixin():
items: bpy.props.CollectionProperty # pyright: ignore[reportGeneralTypeIssues] items: Any = None
def draw_collection(self, items, layout): def draw_collection(self, items, layout):
col = layout.column(align=True) col = layout.column(align=True)
@ -11,7 +12,7 @@ class DrawCollectionMixin():
def _draw_prop(self, text, layout): def _draw_prop(self, text, layout):
self.draw_collection(self.items, layout) self.draw_collection(self.items, layout)
def update_collection(items: bpy.props.CollectionProperty) -> None: # pyright: ignore[reportGeneralTypeIssues] def update_collection(items: Any) -> None:
if len(items) == 0: if len(items) == 0:
items.add() items.add()
@ -25,7 +26,7 @@ def update_collection(items: bpy.props.CollectionProperty) -> None: # pyright:
break break
class UpdateCollectionMixin(): class UpdateCollectionMixin():
items: bpy.props.CollectionProperty # pyright: ignore[reportGeneralTypeIssues] items: Any = None
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View file

@ -1,5 +1,6 @@
from pathlib import Path from pathlib import Path
import requests, base64, mimetypes, os, time import requests # pyright: ignore[reportMissingModuleSource]
import base64, mimetypes, os, time
from typing import Callable, TypeAlias, TypeVar, Any from typing import Callable, TypeAlias, TypeVar, Any
F = TypeVar('F', bound=Callable[..., Any]) F = TypeVar('F', bound=Callable[..., Any])
@ -102,7 +103,7 @@ class AirtableInterface:
for i in range(0, len(records), 10): for i in range(0, len(records), 10):
batch = records[i:i+10] batch = records[i:i+10]
payload = { payload: dict[str, Any] = {
"records": [{'fields': r} for r in batch], "records": [{'fields': r} for r in batch],
"typecast": typecast, "typecast": typecast,
"returnFieldsByFieldId": returnFieldsByFieldId, "returnFieldsByFieldId": returnFieldsByFieldId,

View file

@ -11,20 +11,20 @@ class BaserowBackend:
"Content-Type": "application/json" "Content-Type": "application/json"
} }
def get_record(self, table_id: str, record_id: int) -> dict: def get_record(self, table_id: str, record_id: int) -> dict[str, Any]:
url = f"{self.host}/api/database/rows/table/{table_id}/{record_id}/" url = f"{self.host}/api/database/rows/table/{table_id}/{record_id}/"
response = requests.get(url, headers=self.headers, params={"user_field_names": "true"}) response = requests.get(url, headers=self.headers, params={"user_field_names": "true"})
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
def upsert_record(self, table_id: str, record_id: int, fields: dict[str, Any]) -> dict: def upsert_record(self, table_id: str, record_id: int, fields: dict[str, Any]) -> dict[str, Any]:
try: try:
self.get_record(table_id, record_id) self.get_record(table_id, record_id)
return self.update_record(table_id, record_id, fields) return self.update_record(table_id, record_id, fields)
except (requests.exceptions.HTTPError, ValueError): except (requests.exceptions.HTTPError, ValueError):
return self.create_record(table_id, fields) return self.create_record(table_id, fields)
def create_record(self, table_id: str, fields: dict[str, Any]) -> dict: def create_record(self, table_id: str, fields: dict[str, Any]) -> dict[str, Any]:
url = f"{self.host}/api/database/rows/table/{table_id}/" url = f"{self.host}/api/database/rows/table/{table_id}/"
response = requests.post(url, headers=self.headers, json=fields, params={"user_field_names": "true"}) response = requests.post(url, headers=self.headers, json=fields, params={"user_field_names": "true"})
try: try:
@ -34,7 +34,7 @@ class BaserowBackend:
raise e raise e
return response.json() return response.json()
def update_record(self, table_id: str, record_id: int, fields: dict[str, Any]) -> dict: def update_record(self, table_id: str, record_id: int, fields: dict[str, Any]) -> dict[str, Any]:
url = f"{self.host}/api/database/rows/table/{table_id}/{record_id}/" url = f"{self.host}/api/database/rows/table/{table_id}/{record_id}/"
response = requests.patch(url, headers=self.headers, json=fields, params={"user_field_names": "true"}) response = requests.patch(url, headers=self.headers, json=fields, params={"user_field_names": "true"})
try: try:

View file

@ -73,14 +73,14 @@ class HttpBackend:
headers = {**self.headers, **kw.pop('headers', {})} headers = {**self.headers, **kw.pop('headers', {})}
return self.session.delete(self.base + path, headers=headers, timeout=self.timeout, **kw) return self.session.delete(self.base + path, headers=headers, timeout=self.timeout, **kw)
def _json(self, resp, default: dict = {}, ok=(200, 201, 202, 204)) -> dict: def _json(self, resp: requests.Response, default: dict[str, Any] | None = None, ok: tuple[int, ...] = (200, 201, 202, 204)) -> dict[str, Any]:
if resp.status_code not in ok: if resp.status_code not in ok:
resp.raise_for_status() resp.raise_for_status()
if resp.status_code == 204 or not resp.content: if resp.status_code == 204 or not resp.content:
return default return default if default is not None else {}
return resp.json() return resp.json()
class PrinterHttpBackend(HttpBackend, PrinterBackend): class PrinterHttpBackend(HttpBackend):
pass pass
class PrusaLinkBackend(PrinterHttpBackend): class PrusaLinkBackend(PrinterHttpBackend):
@ -326,7 +326,7 @@ class MoonrakerBackend(PrinterHttpBackend):
@dataclass @dataclass
class ManagedPrinter: class ManagedPrinter:
name: str name: str
backend: PrinterHttpBackend backend: PrinterBackend
status: PrinterStatus = field(default_factory=PrinterStatus) status: PrinterStatus = field(default_factory=PrinterStatus)
last_error: str | None = None last_error: str | None = None
last_command_time: str | None = None last_command_time: str | None = None

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import operator import operator
import re import re
from typing import TYPE_CHECKING, TypeAlias, Any from typing import TYPE_CHECKING, TypeAlias, Any, cast
if TYPE_CHECKING: if TYPE_CHECKING:
Token: TypeAlias = tuple[str, str] Token: TypeAlias = tuple[str, str]
@ -109,12 +109,14 @@ class BinaryOpNode(ExprNode):
if self.op == "=~": if self.op == "=~":
if not isinstance(rv, re.Pattern): if not isinstance(rv, re.Pattern):
raise TypeError("Right operand of =~ must be a regex pattern.") raise TypeError("Right operand of =~ must be a regex pattern.")
return rv.search(str(lv)) is not None pattern_str = cast(re.Pattern[str], rv)
return pattern_str.search(str(lv)) is not None
if self.op == "!~": if self.op == "!~":
if not isinstance(rv, re.Pattern): if not isinstance(rv, re.Pattern):
raise TypeError("Right operand of !~ must be a regex pattern.") raise TypeError("Right operand of !~ must be a regex pattern.")
return rv.search(str(lv)) is None pattern_str = cast(re.Pattern[str], rv)
return pattern_str.search(str(lv)) is None
raise RuntimeError(f"Unknown binary operator: {self.op}") raise RuntimeError(f"Unknown binary operator: {self.op}")

View file

@ -5,8 +5,22 @@ from typing import Any, Callable
_bpy_class_registry: list[type] = [] _bpy_class_registry: list[type] = []
_timer_registry: list[Callable[..., int | None]] = [] _timer_registry: list[Callable[..., int | None]] = []
def setup_bpy_annotations(cls: type) -> type:
if not hasattr(cls, "__annotations__"):
cls.__annotations__ = {}
for name, val in list(cls.__dict__.items()):
val_type_name = type(val).__name__
if "PropertyDeferred" in val_type_name or val_type_name.startswith("_Property") or (isinstance(val, tuple) and len(val) > 0 and "Property" in type(val[0]).__name__):
cls.__annotations__[name] = val
try:
delattr(cls, name)
except (AttributeError, TypeError):
pass
return cls
# BPY CLASSES # BPY CLASSES
def register_class(cls: type) -> type: def register_class(cls: type) -> type:
setup_bpy_annotations(cls)
if cls not in _bpy_class_registry: if cls not in _bpy_class_registry:
_bpy_class_registry.append(cls) _bpy_class_registry.append(cls)
return cls return cls
@ -29,6 +43,7 @@ def get_class_by_name(name: str) -> type | None:
def blender_register_classes(): def blender_register_classes():
for cls in _bpy_class_registry: for cls in _bpy_class_registry:
try: try:
setup_bpy_annotations(cls)
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
except Exception as e: except Exception as e:
print(f"Nodeslicer: Failed to register {cls}: {e}") print(f"Nodeslicer: Failed to register {cls}: {e}")

View file

@ -739,6 +739,7 @@ class TestSlice(unittest.TestCase):
from NodeSlicer.infra.blender.blender_mesh_capture import SlicingCollection, SlicingObject from NodeSlicer.infra.blender.blender_mesh_capture import SlicingCollection, SlicingObject
node = SLICENODE_SeparateMeshByRange() node = SLICENODE_SeparateMeshByRange()
node.start_val = 0
node.end_val = 6 node.end_val = 6
so = SlicingObject() so = SlicingObject()
@ -758,9 +759,60 @@ class TestSlice(unittest.TestCase):
coll.transforms = [] coll.transforms = []
node.resolve = lambda name: coll if name == "Mesh" else ("part_id" if name == "Attribute" else None) node.resolve = lambda name: coll if name == "Mesh" else ("part_id" if name == "Attribute" else None)
# start_val = 0, end_val = 6 -> total_parts = 7 -> num_digits = 2
# socket "1" -> i = 1 -> CA01
res_1 = node.evaluate_output("1") res_1 = node.evaluate_output("1")
self.assertIsNotNone(res_1) self.assertIsNotNone(res_1)
self.assertEqual(res_1.objects[0].name, "CA01") self.assertEqual(res_1.objects[0].name, "CA01")
# socket "2" -> i = 2 -> CA02
res_2 = node.evaluate_output("2")
self.assertIsNotNone(res_2)
self.assertEqual(res_2.objects[0].name, "CA02")
# Now test start_val = 1, end_val = 6 -> total_parts = 6 -> num_digits = 2
# Numbering starts from zero, so socket "1" has i = 0 -> CA00, socket "2" has i = 1 -> CA01
node.start_val = 1
node.end_val = 6
res_start1_out1 = node.evaluate_output("1")
self.assertIsNotNone(res_start1_out1)
self.assertEqual(res_start1_out1.objects[0].name, "CA00")
res_start1_out2 = node.evaluate_output("2")
self.assertIsNotNone(res_start1_out2)
self.assertEqual(res_start1_out2.objects[0].name, "CA01")
# Test total_parts >= 100 -> num_digits = 3
node.start_val = 0
node.end_val = 100 # total_parts = 101 -> num_digits = 3
res_large_1 = node.evaluate_output("1")
self.assertIsNotNone(res_large_1)
self.assertEqual(res_large_1.objects[0].name, "CA001")
# Test np.all(mask) where all faces match the target
so_full = SlicingObject()
so_full.name = "FullMesh"
so_full.mesh = np.zeros((4, 4, 3))
so_full.attributes = {
"part_id": {
"domain": "FACE",
"type": "INT",
"values": np.array([2, 2, 2, 2])
}
}
coll_full = SlicingCollection.__new__(SlicingCollection)
coll_full.objects = [so_full]
coll_full.name = "FullColl"
coll_full.transforms = []
node.start_val = 2
node.end_val = 5 # total_parts = 4, num_digits = 2
node.resolve = lambda name: coll_full if name == "Mesh" else ("part_id" if name == "Attribute" else None)
res_full_2 = node.evaluate_output("2")
self.assertIsNotNone(res_full_2)
self.assertEqual(res_full_2.objects[0].name, "FullMesh00")
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()