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

View file

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

View file

@ -1,3 +1,5 @@
from __future__ import annotations
from typing import Any, Callable
import bpy
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:
print(f"Error updating node {node.name}: {e}")
def NfProperty(prop_func):
def wrapper(**kwargs):
class MockPropertyDescriptor:
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)
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
NfStringProperty = NfProperty(StringProperty)
NfIntProperty = NfProperty(IntProperty)
NfFloatProperty = NfProperty(FloatProperty)
NfEnumProperty = NfProperty(EnumProperty)
NfBoolProperty = NfProperty(BoolProperty)
NfPointerProperty = NfProperty(PointerProperty)
NfFloatVectorProperty = NfProperty(FloatVectorProperty)
NfStringProperty: Callable[..., Any] = NfProperty(StringProperty)
NfIntProperty: Callable[..., Any] = NfProperty(IntProperty)
NfFloatProperty: Callable[..., Any] = NfProperty(FloatProperty)
NfEnumProperty: Callable[..., Any] = NfProperty(EnumProperty)
NfBoolProperty: Callable[..., Any] = NfProperty(BoolProperty)
NfPointerProperty: Callable[..., Any] = NfProperty(PointerProperty)
NfFloatVectorProperty: Callable[..., Any] = NfProperty(FloatVectorProperty)

View file

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

View file

@ -12,7 +12,7 @@ from .base import SliceNode, SliceIntrinsics, Pather, SliceNodeJoiner, SlicePath
class SLICENODE_PathsToGcode(SliceNode):
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):
self.add_input("SLICESOCK_Paths")
@ -119,7 +119,7 @@ class SLICENODE_GcodeRegexReplace(SliceNode):
class SLICENODE_GcodeHelper(SliceNode):
bl_label = "Gcode Helper"
command: NfEnumProperty(
command: str = NfEnumProperty(
name="Command",
items=[
('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)"),
],
default='G1'
) # pyright: ignore[reportInvalidTypeForm]
)
parameters: NfStringProperty(name="Params", default="") # pyright: ignore[reportInvalidTypeForm]
parameters: str = NfStringProperty(name="Params", default="")
def init(self, context):
self.add_output("NodeSocketString")

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
from __future__ import annotations
from typing import Any
import bpy
import os
@ -11,7 +12,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
bl_label = "Gcode to Preview Mesh"
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):
self._init_exec()
@ -201,15 +202,14 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
ng = bpy.data.node_groups.get(ng_name)
# Get existing 'Preview' modifier or create it
mod: Any = None
if hasattr(obj.modifiers, "get"):
mod = obj.modifiers.get("Preview")
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)
else:
mod = None
if (not mod or (hasattr(mod, "type") and mod.type != 'NODES')) and hasattr(obj.modifiers, "new"):
mod = obj.modifiers.new(name="Preview", type='NODES')
if (not mod or (hasattr(mod, "type") and getattr(mod, "type", None) != 'NODES')) and hasattr(obj.modifiers, "new"):
mod = getattr(obj.modifiers, "new")(name="Preview", type='NODES')
elif not mod and isinstance(obj.modifiers, list):
mod = {"name": "Preview", "type": "NODES"}
obj.modifiers.append(mod)

View file

@ -56,7 +56,7 @@ class SLICENODE_PrinterList(ExecSliceNode):
def update_config(self, context):
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):
self._init_exec()
@ -157,7 +157,7 @@ class SLICENODE_PrinterList(ExecSliceNode):
class SLICENODE_PrusaslicerSlice(SliceNode):
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):
self.add_input("SLICESOCK_PrusaslicerConf", "Config")
@ -309,11 +309,11 @@ class SLICENODE_PrusaslicerPreview(ExecSliceNode):
bl_label = "PrusaSlicer Preview"
button_label = "Open Preview"
prusaslicer_executable: NfStringProperty(
prusaslicer_executable: str = NfStringProperty(
name="PrusaSlicer Path",
subtype='FILE_PATH',
default='flatpak run com.prusa3d.PrusaSlicer'
) # pyright: ignore[reportInvalidTypeForm]
)
def init(self, context):
self._init_exec()
@ -396,7 +396,7 @@ class SLICENODE_Executor(ExecSliceNode, SliceNodeJoiner):
button_label = "Execute All"
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):
self._init_exec()

View file

@ -94,8 +94,8 @@ class SLICESOCK_PrusaslicerConf(LabelNodeSocket):
@register_class
class SettingItem(bpy.types.PropertyGroup):
field: NfStringProperty(name="Field", default="") # pyright: ignore[reportInvalidTypeForm]
value_str: NfStringProperty(name="Value", default="") # pyright: ignore[reportInvalidTypeForm]
field: str = NfStringProperty(name="Field", default="")
value_str: str = NfStringProperty(name="Value", default="")
@property
def is_active(self):
@ -137,14 +137,14 @@ class SLICESOCK_ExecutionLink(LabelNodeSocket):
class SLICESOCK_Printer(InputNodeSocket):
bl_label = "Printer"
host: NfStringProperty(name="Host", default="localhost") # pyright: ignore[reportInvalidTypeForm]
port: NfIntProperty(name="Port", default=80, min=1, max=65535) # pyright: ignore[reportInvalidTypeForm]
prefix: NfStringProperty(name="Prefix", default="") # pyright: ignore[reportInvalidTypeForm]
label: NfStringProperty(name="Label", default="Printer") # pyright: ignore[reportInvalidTypeForm]
username: NfStringProperty(name="Username", default="") # pyright: ignore[reportInvalidTypeForm]
password: NfStringProperty(name="Password", default="", subtype='PASSWORD') # pyright: ignore[reportInvalidTypeForm]
host: str = NfStringProperty(name="Host", default="localhost")
port: int = NfIntProperty(name="Port", default=80, min=1, max=65535)
prefix: str = NfStringProperty(name="Prefix", default="")
label: str = NfStringProperty(name="Label", default="Printer")
username: str = NfStringProperty(name="Username", default="")
password: str = NfStringProperty(name="Password", default="", subtype='PASSWORD')
host_type: NfEnumProperty(
host_type: str = NfEnumProperty(
name="Type",
items=[
('PRUSALINK', "PrusaLink", "PrusaLink API"),
@ -153,7 +153,7 @@ class SLICESOCK_Printer(InputNodeSocket):
],
default='PRUSALINK',
update=notify_tree_changed
) # pyright: ignore[reportInvalidTypeForm]
)
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
from typing import TYPE_CHECKING, Any, cast
import bpy
from bpy.types import NodeTree, Operator, Menu
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_options = {'REGISTER', 'UNDO'}
node_name: bpy.props.StringProperty() # pyright: ignore[reportInvalidTypeForm]
node_name: str = cast(Any, bpy.props.StringProperty())
@classmethod
def poll(cls, context):
@ -463,7 +462,7 @@ class SLICENODE_OT_Execute(Operator):
bl_label = "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']:
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):
return sp.copy_with(paths=list(sp))
R = None
rot_mat: np.ndarray | None = None
if has_rot:
rx, ry, rz = r_arr
cx, sx = np.cos(rx), np.sin(rx)
@ -74,7 +74,7 @@ def transform_slice_paths(
[sz, cz, 0.0],
[0.0, 0.0, 1.0]
], dtype=np.float64)
R = Rz @ (Ry @ Rx)
rot_mat = Rz @ (Ry @ Rx)
new_paths = []
for p in sp:
@ -91,11 +91,11 @@ def transform_slice_paths(
else:
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:
new_p[:, :3] = new_p[:, :3] @ R.T
new_p[:, :3] = new_p[:, :3] @ rot_mat.T
elif dim == 2:
rot_2d = R[:2, :2]
rot_2d = rot_mat[:2, :2]
new_p[:, :2] = new_p[:, :2] @ rot_2d.T
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):
if item.mesh.size > 0:
verts_3d = item.mesh[:, :3, :3]
v0_3d = verts_3d[:, 0, :]
v1_3d = verts_3d[:, 1, :]
v2_3d = verts_3d[:, 2, :]
if item.mesh.shape[1] >= 4:
normals = item.mesh[:, 3, :]
else:
v0_3d = verts_3d[:, 0, :]
v1_3d = verts_3d[:, 1, :]
v2_3d = verts_3d[:, 2, :]
normals = np.cross(v1_3d - v0_3d, v2_3d - v0_3d)
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
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(" ", "_")
if hasattr(so_item, attr_key):
val = getattr(so_item, attr_key)
if val is not None and not callable(val):
if isinstance(val, (list, tuple, np.ndarray)) and len(val) > 0:
val = val[0]
if hasattr(val, 'item'):
return val.item()
if hasattr(val, 'item') and callable(getattr(val, 'item')):
return getattr(val, 'item')()
return val
elif hasattr(so_item, attr_name):
val = getattr(so_item, attr_name)
if val is not None and not callable(val):
if isinstance(val, (list, tuple, np.ndarray)) and len(val) > 0:
val = val[0]
if hasattr(val, 'item'):
return val.item()
if hasattr(val, 'item') and callable(getattr(val, 'item')):
return getattr(val, 'item')()
return val
return None

View file

@ -24,7 +24,7 @@ from .pdf_utils import PdfStream, escape_pdf_text
# 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."""
ax, ay = float(p1[0]), float(p1[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.
"""
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
sa2_objects = extract_slicing_objects_bounds(raw_geometry)
@ -586,6 +586,7 @@ def render_sa2_measures_pdf(
)
# Concentric Bores Grouping & Hole Callouts
concentric_groups: list[list[tuple[float, float, float]]] = []
if draw_sa2_holes and holes:
hole_color = (0.10, 0.45, 0.90)
s.stroke_color(*hole_color).fill_color(*hole_color)
@ -611,7 +612,7 @@ def render_sa2_measures_pdf(
)
# --- 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)
s.stroke_color(*pitch_color).fill_color(*pitch_color)

View file

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

View file

@ -61,6 +61,8 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
### Geometry Processing
- **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`).
- **Geometry**: Input geometry data.
- **Name**: String input socket or property for the new name template.

View file

@ -1,4 +1,5 @@
from __future__ import annotations
from typing import Any
from numpy.typing import NDArray
from pathlib import Path
@ -40,7 +41,7 @@ def write_metadata_xml(group: SlicingGroup, filepath):
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")
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:
eval_obj = obj.evaluated_get(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
elif m:
eval_obj.to_mesh_clear()
@ -52,7 +53,9 @@ def get_evaluated_mesh(obj: Object):
try:
temp_obj = obj.copy()
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.update()
@ -63,7 +66,9 @@ def get_evaluated_mesh(obj: Object):
except Exception:
if temp_obj is not None:
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)
except Exception:
pass
@ -80,7 +85,9 @@ def get_evaluated_mesh(obj: Object):
pass
if is_temp and temp_obj is not None:
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.context.evaluated_depsgraph_get().update()
except Exception:
@ -100,9 +107,9 @@ class SlicingObject():
name: str
parent: str
mesh: NDArray[float64]
attributes: dict[str, dict]
attributes: dict[str, Any]
object_type: str = "ModelPart"
modifiers: list[dict] = []
modifiers: list[dict[str, Any]] = []
extruder: int = 1
pdf_properties: PdfProperties
object_properties: list[tuple[str, str]]
@ -176,10 +183,10 @@ class SlicingObject():
}
return merged
def offset(self, offset: NDArray):
def offset(self, offset: NDArray[Any]):
self.mesh[:, :3, :] += offset
def scale(self, factor: float | NDArray):
def scale(self, factor: float | NDArray[Any]):
self.mesh[:, :3, :] *= factor
if isinstance(factor, (list, tuple, np.ndarray)):
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)
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_arr = np.array(angles, dtype=np.float64)
rx, ry, rz = angles_arr
@ -268,18 +275,18 @@ class SlicingObject():
def max_z(self) -> float: return self.mesh[:, :3, 2].max()
@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
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
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():
name: str
objects: list[SlicingObject]
transforms: list[dict]
transforms: list[dict[str, Any]]
def __init__(self, objs: list[Object], parent: str):
self.objects = [SlicingObject(obj, parent) for obj in objs]
@ -292,15 +299,15 @@ class SlicingCollection():
if prop in self.__dict__:
del self.__dict__[prop]
def offset(self, offset: NDArray):
def offset(self, offset: NDArray[Any]):
for so in self.objects: so.offset(offset)
self._clear_cache()
def scale(self, factor: float | NDArray):
def scale(self, factor: float | NDArray[Any]):
for so in self.objects: so.scale(factor)
self._clear_cache()
def rotate(self, angles: NDArray):
def rotate(self, angles: NDArray[Any]):
for so in self.objects: so.rotate(angles)
self._clear_cache()
@ -315,26 +322,26 @@ class SlicingCollection():
return max(height_all)
@property
def meshes(self) -> list[NDArray]:
def meshes(self) -> list[NDArray[Any]]:
return [o.mesh for o in self.objects]
@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])
@cached_property
def mesh_start_ids(self) -> NDArray:
def mesh_start_ids(self) -> NDArray[Any]:
lengths = self.mesh_lengths_ids
return np.insert(np.cumsum(lengths)[:-1], 0, 0)
@cached_property
def mesh_end_ids(self) -> NDArray:
def mesh_end_ids(self) -> NDArray[Any]:
starts = self.mesh_start_ids
lengths = self.mesh_lengths_ids
return starts + lengths - 1
@cached_property
def all_verts(self) -> NDArray:
def all_verts(self) -> NDArray[Any]:
all_verts = np.vstack(self.meshes)[:, :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)
@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
return np.array([self.min_x, self.min_y, 0.0])
@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
return np.array([self.max_x, self.max_y, 0.0])
@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
return (self.min_xy + self.max_xy) / 2.0
@ -407,10 +414,10 @@ class SlicingGroup():
collections: dict[str, SlicingCollection]
# Metadata
wipe_tower_xy: NDArray = np.array([0, 0])
wipe_tower_xy: NDArray[Any] = np.array([0, 0])
wipe_tower_rotation_deg: float = 0
object_offset: NDArray = np.array([0.0, 0.0, 0.0])
transforms: list[dict]
object_offset: NDArray[Any] = np.array([0.0, 0.0, 0.0])
transforms: list[dict[str, Any]]
def __init__(self, selected_objs: list[Object]) -> None:
self.object_offset = np.array([0.0, 0.0, 0.0])
@ -443,18 +450,19 @@ class SlicingGroup():
assert bpy.context.scene and bpy.context.scene.unit_settings
scale_tx = 1000. * bpy.context.scene.unit_settings.scale_length #type: ignore
self.wipe_tower_xy = self.center_xy[0:2]
if self.center_xy is not None:
self.wipe_tower_xy = self.center_xy[0:2]
for obj in objs:
with get_evaluated_mesh(obj) as (eval_obj, _m):
if eval_obj and hasattr(eval_obj, 'nodeslicer'):
if eval_obj.nodeslicer.object_type == 'WipeTower':
self.wipe_tower_xy = np.array(eval_obj.location[0:2]) * scale_tx
self.wipe_tower_rotation_deg = round(eval_obj.rotation_euler[2]*180/math.pi,5)
break
ns_prop = getattr(eval_obj, 'nodeslicer', None)
if eval_obj is not None and ns_prop and getattr(ns_prop, 'object_type', None) == 'WipeTower':
self.wipe_tower_xy = np.array(eval_obj.location[0:2]) * scale_tx
self.wipe_tower_rotation_deg = round(eval_obj.rotation_euler[2]*180/math.pi,5)
break
return
def offset(self, offset: NDArray):
def offset(self, offset: NDArray[Any]):
for k, so in self.collections.items(): so.offset(offset)
self.wipe_tower_xy = self.wipe_tower_xy + offset[0:2]
@ -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)
@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])
return np.array([self.min_x, self.min_y, 0.0])
@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])
return np.array([self.max_x, self.max_y, 0.0])
@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])
return (self.min_xy + self.max_xy) / 2.0
@ -525,7 +533,7 @@ class TriMesh():
self.length_tris = t
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
meshes: list[TriMesh] = []
@ -656,7 +664,7 @@ def _cached_objects_to_tris(obj_names: tuple[str, ...], matrices: tuple[tuple[fl
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)
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)

View file

@ -1,15 +1,15 @@
from __future__ import annotations
from dataclasses import field
import typing
from typing import Any, cast
import bpy
from contextlib import contextmanager
from typing import Any
from bpy.types import NodeSocket, Node
class NodeException(Exception): ...
_EvalCache = dict[tuple[int, str, str], Any]
_EvalCache = dict[Any, Any]
_VisitedSet = set[Any]
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
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:
pass
@ -55,6 +55,17 @@ class AutoBlIdName:
if "bl_idname" not in cls.__dict__:
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):
default: Any = None
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
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:
return super().get(key, default) # type: ignore
except (AttributeError, TypeError):
@ -73,7 +88,7 @@ class SliceNode(Node, AutoBlIdName):
return self._data.get(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:
super().__setitem__(key, value) # type: ignore
except (AttributeError, TypeError):
@ -81,7 +96,7 @@ class SliceNode(Node, AutoBlIdName):
self._data = {}
self._data[key] = value
def __getitem__(self, key: str) -> Any:
def __getitem__(self, key: str) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
try:
return super().__getitem__(key) # type: ignore
except (AttributeError, TypeError):
@ -91,11 +106,14 @@ class SliceNode(Node, AutoBlIdName):
def __contains__(self, key: str) -> bool:
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):
if hasattr(self, "_data") and isinstance(self._data, dict):
return key in self._data
return False
pass
if hasattr(self, "_data") and isinstance(self._data, dict):
return key in self._data
return False
def update(self) -> None:
self._update_multi_groups()
@ -292,10 +310,13 @@ class SLICENODE_Group(bpy.types.NodeCustomGroup, AutoBlIdName):
self.sync_sockets()
slicer_tree: bpy.props.PointerProperty( # pyright: ignore[reportInvalidTypeForm]
type=bpy.types.NodeTree,
name="Node Tree",
update=_tree_updated
slicer_tree: bpy.types.NodeTree | None = cast(
Any,
bpy.props.PointerProperty(
type=bpy.types.NodeTree,
name="Node Tree",
update=_tree_updated
)
)
def sync_sockets(self):
@ -375,19 +396,16 @@ class SLICENODE_Group(bpy.types.NodeCustomGroup, AutoBlIdName):
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
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
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):
return None
@ -410,14 +428,16 @@ def _resolve_group_output(socket: NodeSocket, cache: _EvalCache | None, visited:
with _group_context(node, cache=cache):
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)
if not stack: return None
group_node = stack.pop()
# NodeGroupInput outputs usually match the group inputs by index
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]
except (ValueError, IndexError):
stack.append(group_node)
@ -428,12 +448,9 @@ def _resolve_group_input(socket: NodeSocket, cache: _EvalCache | None, visited:
finally:
stack.append(group_node)
def _follow_reroute(socket: NodeSocket) -> NodeSocket:
current: NodeSocket = socket
seen: set[int] = set()
while getattr(current.node, "bl_idname", "") == "NodeReroute":
assert current.node
def _follow_reroute(current: NodeSocket) -> NodeSocket:
seen = set()
while current.node and getattr(current.node, "bl_idname", "") == "NodeReroute":
ptr = current.node.as_pointer()
if ptr in seen:
break
@ -451,7 +468,7 @@ def resolve_input(
socket: SliceNodeSocket | None,
*,
cache: _EvalCache | None = None,
visited: set[tuple[int, str]] | None = None,
visited: _VisitedSet | None = None,
):
if socket is None:
raise Exception(f"The queried socket is non-existant")
@ -494,7 +511,7 @@ def _resolve_output(
*,
default: Any = None,
cache: _EvalCache | None = None,
visited: set[tuple[int, str]] | None = None,
visited: _VisitedSet | None = None,
):
if cache is None:
cache = _current_execution_cache

View file

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

View file

@ -1,5 +1,6 @@
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
F = TypeVar('F', bound=Callable[..., Any])
@ -102,7 +103,7 @@ class AirtableInterface:
for i in range(0, len(records), 10):
batch = records[i:i+10]
payload = {
payload: dict[str, Any] = {
"records": [{'fields': r} for r in batch],
"typecast": typecast,
"returnFieldsByFieldId": returnFieldsByFieldId,

View file

@ -11,20 +11,20 @@ class BaserowBackend:
"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}/"
response = requests.get(url, headers=self.headers, params={"user_field_names": "true"})
response.raise_for_status()
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:
self.get_record(table_id, record_id)
return self.update_record(table_id, record_id, fields)
except (requests.exceptions.HTTPError, ValueError):
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}/"
response = requests.post(url, headers=self.headers, json=fields, params={"user_field_names": "true"})
try:
@ -34,7 +34,7 @@ class BaserowBackend:
raise e
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}/"
response = requests.patch(url, headers=self.headers, json=fields, params={"user_field_names": "true"})
try:

View file

@ -73,14 +73,14 @@ class HttpBackend:
headers = {**self.headers, **kw.pop('headers', {})}
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:
resp.raise_for_status()
if resp.status_code == 204 or not resp.content:
return default
return default if default is not None else {}
return resp.json()
class PrinterHttpBackend(HttpBackend, PrinterBackend):
class PrinterHttpBackend(HttpBackend):
pass
class PrusaLinkBackend(PrinterHttpBackend):
@ -326,7 +326,7 @@ class MoonrakerBackend(PrinterHttpBackend):
@dataclass
class ManagedPrinter:
name: str
backend: PrinterHttpBackend
backend: PrinterBackend
status: PrinterStatus = field(default_factory=PrinterStatus)
last_error: str | None = None
last_command_time: str | None = None

View file

@ -1,7 +1,7 @@
from __future__ import annotations
import operator
import re
from typing import TYPE_CHECKING, TypeAlias, Any
from typing import TYPE_CHECKING, TypeAlias, Any, cast
if TYPE_CHECKING:
Token: TypeAlias = tuple[str, str]
@ -109,12 +109,14 @@ class BinaryOpNode(ExprNode):
if self.op == "=~":
if not isinstance(rv, re.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 not isinstance(rv, re.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}")

View file

@ -5,8 +5,22 @@ from typing import Any, Callable
_bpy_class_registry: list[type] = []
_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
def register_class(cls: type) -> type:
setup_bpy_annotations(cls)
if cls not in _bpy_class_registry:
_bpy_class_registry.append(cls)
return cls
@ -29,6 +43,7 @@ def get_class_by_name(name: str) -> type | None:
def blender_register_classes():
for cls in _bpy_class_registry:
try:
setup_bpy_annotations(cls)
bpy.utils.register_class(cls)
except Exception as 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
node = SLICENODE_SeparateMeshByRange()
node.start_val = 0
node.end_val = 6
so = SlicingObject()
@ -758,9 +759,60 @@ class TestSlice(unittest.TestCase):
coll.transforms = []
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")
self.assertIsNotNone(res_1)
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__':
unittest.main()