typing
This commit is contained in:
parent
39d2530f14
commit
7535bd7a85
28 changed files with 451 additions and 324 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
||||||
|
|
|
||||||
15
registry.py
15
registry.py
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue