list support
This commit is contained in:
parent
12bc6ba08b
commit
47e66bb030
16 changed files with 2045 additions and 435 deletions
|
|
@ -134,6 +134,45 @@ class SliceNodeJoiner(SliceNode):
|
||||||
self.add_output(self.output_socket_type or self.socket_type, name=self.socket_label)
|
self.add_output(self.output_socket_type or self.socket_type, name=self.socket_label)
|
||||||
self.add_multi_socket_group(self.socket_type, name=self.socket_label)
|
self.add_multi_socket_group(self.socket_type, name=self.socket_label)
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
for inp in self.inputs:
|
||||||
|
if getattr(inp, "name", "") == self.socket_label or getattr(inp, "identifier", "").startswith(f"{self.socket_label}_ms_in_"):
|
||||||
|
is_list = is_socket_linked_to_list(inp)
|
||||||
|
set_socket_shape(inp, "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
|
class SliceNodeListJoiner(SliceNode):
|
||||||
|
socket_type: str = ""
|
||||||
|
output_socket_type: str = ""
|
||||||
|
socket_label: str = ""
|
||||||
|
output_socket_name: str = ""
|
||||||
|
is_list_output: bool = True
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output(self.output_socket_type or self.socket_type, name=self.output_socket_name or self.socket_label, display_shape='LIST')
|
||||||
|
self.add_multi_socket_group(self.socket_type, name=self.socket_label)
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
for inp in self.inputs:
|
||||||
|
if getattr(inp, "name", "") == self.socket_label or getattr(inp, "identifier", "").startswith(f"{self.socket_label}_ms_in_"):
|
||||||
|
is_list = is_socket_linked_to_list(inp)
|
||||||
|
set_socket_shape(inp, "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **kwargs) -> list[Any]:
|
||||||
|
from ....infra.blender.list_utils import flatten_list, is_list_data
|
||||||
|
items: list[Any] = []
|
||||||
|
raw_inputs = self.resolve_multi_input(self.socket_label)
|
||||||
|
for val in raw_inputs:
|
||||||
|
if val is not None:
|
||||||
|
if is_list_data(val):
|
||||||
|
items.extend(flatten_list(val))
|
||||||
|
else:
|
||||||
|
items.append(val)
|
||||||
|
return items
|
||||||
|
|
||||||
class ExecSliceNode(SliceNode):
|
class ExecSliceNode(SliceNode):
|
||||||
button_label: str = 'Execute'
|
button_label: str = 'Execute'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,24 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import bpy
|
import bpy
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, NfStringProperty
|
from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, NfStringProperty
|
||||||
from ....registry import register_class
|
from ....registry import register_class
|
||||||
from .base import SliceNode, SliceIntrinsics, Pather, SliceNodeJoiner
|
from .base import SliceNode, SliceIntrinsics, Pather, SliceNodeJoiner, SliceNodeListJoiner
|
||||||
|
from ....infra.blender.list_utils import eval_element_wise, flatten_list, is_list_data
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GcodesToList(SliceNodeListJoiner):
|
||||||
|
bl_label = "Gcodes to List"
|
||||||
|
socket_type = "NodeSocketString"
|
||||||
|
socket_label = "Gcode"
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_PathsToGcode(SliceNode):
|
class SLICENODE_PathsToGcode(SliceNode):
|
||||||
bl_label = "Paths to Gcode"
|
bl_label = "Paths to Gcode"
|
||||||
|
list_socket_mapping = [("Paths", "Gcode")]
|
||||||
|
|
||||||
use_header: bool = NfBoolProperty(name="Header", default=True)
|
use_header: bool = NfBoolProperty(name="Header", default=True)
|
||||||
|
|
||||||
|
|
@ -22,30 +31,35 @@ class SLICENODE_PathsToGcode(SliceNode):
|
||||||
assert layout
|
assert layout
|
||||||
layout.prop(self, "use_header")
|
layout.prop(self, "use_header")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
paths: list[np.ndarray] = self.resolve("Paths")
|
paths: Any = self.resolve("Paths")
|
||||||
intrinsics: SliceIntrinsics | None = self.resolve("Intrinsics")
|
intrinsics: SliceIntrinsics | None = self.resolve("Intrinsics")
|
||||||
|
|
||||||
pather: Pather = Pather(paths)
|
if not paths:
|
||||||
if hasattr(paths, "layer_height"):
|
return ""
|
||||||
pather.layer_height = getattr(paths, "layer_height")
|
|
||||||
if hasattr(paths, "first_layer_height"):
|
|
||||||
pather.first_layer_height = getattr(paths, "first_layer_height")
|
|
||||||
|
|
||||||
if intrinsics:
|
def _paths_to_gcode(p: Any) -> str:
|
||||||
# Note: Pather currently uses bed_center_x/y as its internal attributes.
|
if not p:
|
||||||
# We map the bed size center to these attributes if provided.
|
return ""
|
||||||
pather.bed_center_x = intrinsics.bed_x / 2.0
|
pather: Pather = Pather(p)
|
||||||
pather.bed_center_y = intrinsics.bed_y / 2.0
|
if hasattr(p, "layer_height"):
|
||||||
pather.feedrate = intrinsics.feedrate
|
pather.layer_height = getattr(p, "layer_height")
|
||||||
pather.travel_feedrate = intrinsics.travel_feedrate
|
if hasattr(p, "first_layer_height"):
|
||||||
pather.extrusion_rate = intrinsics.extrusion_rate
|
pather.first_layer_height = getattr(p, "first_layer_height")
|
||||||
pather.retraction = intrinsics.retraction
|
|
||||||
pather.retraction_feedrate = getattr(intrinsics, "retraction_feedrate", 0.0)
|
|
||||||
pather.lift = intrinsics.lift
|
|
||||||
|
|
||||||
gcode: str = pather.to_gcode(include_header=self.use_header)
|
if intrinsics:
|
||||||
return gcode
|
pather.bed_center_x = intrinsics.bed_x / 2.0
|
||||||
|
pather.bed_center_y = intrinsics.bed_y / 2.0
|
||||||
|
pather.feedrate = intrinsics.feedrate
|
||||||
|
pather.travel_feedrate = intrinsics.travel_feedrate
|
||||||
|
pather.extrusion_rate = intrinsics.extrusion_rate
|
||||||
|
pather.retraction = intrinsics.retraction
|
||||||
|
pather.retraction_feedrate = getattr(intrinsics, "retraction_feedrate", 0.0)
|
||||||
|
pather.lift = intrinsics.lift
|
||||||
|
|
||||||
|
return pather.to_gcode(include_header=self.use_header)
|
||||||
|
|
||||||
|
return eval_element_wise(_paths_to_gcode, paths)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_GcodeBlock(SliceNode):
|
class SLICENODE_GcodeBlock(SliceNode):
|
||||||
|
|
@ -83,6 +97,7 @@ class SLICENODE_AssignMetadata(SliceNode):
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_GcodeRegexReplace(SliceNode):
|
class SLICENODE_GcodeRegexReplace(SliceNode):
|
||||||
bl_label = "Gcode Regex Replace"
|
bl_label = "Gcode Regex Replace"
|
||||||
|
list_socket_mapping = [("Gcode", "Gcode")]
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("NodeSocketString", "Gcode")
|
self.add_input("NodeSocketString", "Gcode")
|
||||||
|
|
@ -90,29 +105,37 @@ class SLICENODE_GcodeRegexReplace(SliceNode):
|
||||||
self.add_input("NodeSocketString", "Replace")
|
self.add_input("NodeSocketString", "Replace")
|
||||||
self.add_output("NodeSocketString", "Gcode")
|
self.add_output("NodeSocketString", "Gcode")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
import re
|
import re
|
||||||
gcode = self.resolve("Gcode") or ""
|
gcode = self.resolve("Gcode")
|
||||||
find = self.resolve("Find") or ""
|
find = self.resolve("Find") or ""
|
||||||
replace = self.resolve("Replace") or ""
|
replace = self.resolve("Replace") or ""
|
||||||
|
|
||||||
|
if not gcode:
|
||||||
|
return ""
|
||||||
|
|
||||||
if not find:
|
if not find:
|
||||||
return gcode
|
return gcode
|
||||||
|
|
||||||
try:
|
def _replace_one(gc: Any) -> str:
|
||||||
# Convert $1, $2, ${name} etc to Python backreferences \g<1>, \g<2>, \g<name>
|
gc_str = str(gc or "")
|
||||||
def subst(match):
|
if not find or not gc_str:
|
||||||
if match.group(1): return '$'
|
return gc_str
|
||||||
elif match.group(2): return '\\g<' + match.group(2) + '>'
|
try:
|
||||||
elif match.group(3): return '\\g<' + match.group(3) + '>'
|
# Convert $1, $2, ${name} etc to Python backreferences \g<1>, \g<2>, \g<name>
|
||||||
return match.group(0)
|
def subst(match):
|
||||||
|
if match.group(1): return '$'
|
||||||
|
elif match.group(2): return '\\g<' + match.group(2) + '>'
|
||||||
|
elif match.group(3): return '\\g<' + match.group(3) + '>'
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
replace = re.sub(r'(\$\$)|(?:\$\{([^}]+)\})|(?:\$([a-zA-Z0-9_]+))', subst, replace)
|
rep = re.sub(r'(\$\$)|(?:\$\{([^}]+)\})|(?:\$([a-zA-Z0-9_]+))', subst, replace)
|
||||||
|
return re.sub(find, rep, gc_str)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Regex error in node {self.name}: {e}")
|
||||||
|
return gc_str
|
||||||
|
|
||||||
return re.sub(find, replace, gcode)
|
return eval_element_wise(_replace_one, gcode)
|
||||||
except Exception as e:
|
|
||||||
print(f"Regex error in node {self.name}: {e}")
|
|
||||||
return gcode
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_GcodeHelper(SliceNode):
|
class SLICENODE_GcodeHelper(SliceNode):
|
||||||
|
|
@ -180,16 +203,52 @@ class SLICENODE_JoinGcode(SliceNodeJoiner):
|
||||||
output_socket_type = "NodeSocketString"
|
output_socket_type = "NodeSocketString"
|
||||||
socket_label = "Gcode"
|
socket_label = "Gcode"
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
def update(self) -> None:
|
||||||
from ....infra.blender.blender_slice_nodes_generic import resolve_input
|
super().update()
|
||||||
gcode_input: list[str] = []
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
for input in self.inputs:
|
has_list = any(
|
||||||
val = resolve_input(input)
|
is_socket_linked_to_list(inp)
|
||||||
if val is not None:
|
for inp in self.inputs
|
||||||
gc = str(val)
|
if getattr(inp, "name", "") == self.socket_label or getattr(inp, "identifier", "").startswith(f"{self.socket_label}_ms_in_")
|
||||||
if gc:
|
)
|
||||||
gcode_input.append(gc)
|
out_sock = self.outputs.get(self.socket_label)
|
||||||
return "\n".join(gcode_input)
|
if out_sock:
|
||||||
|
set_socket_shape(out_sock, "LIST" if has_list else "CIRCLE")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
raw_inputs = self.resolve_multi_input(self.socket_label)
|
||||||
|
has_list = any(is_list_data(inp) for inp in raw_inputs)
|
||||||
|
|
||||||
|
if not has_list:
|
||||||
|
flattened = flatten_list(raw_inputs)
|
||||||
|
gcode_input: list[str] = [str(gc) for gc in flattened if gc]
|
||||||
|
return "\n".join(gcode_input)
|
||||||
|
|
||||||
|
# There is at least one list input
|
||||||
|
list_lengths = [len(inp) for inp in raw_inputs if is_list_data(inp)]
|
||||||
|
if len(set(list_lengths)) > 1:
|
||||||
|
raise ValueError(
|
||||||
|
f"Join Gcode: all input lists must have the same length, got lengths {list_lengths}"
|
||||||
|
)
|
||||||
|
|
||||||
|
n_elements = list_lengths[0] if list_lengths else 0
|
||||||
|
result: list[str] = []
|
||||||
|
for i in range(n_elements):
|
||||||
|
row_parts: list[str] = []
|
||||||
|
for inp in raw_inputs:
|
||||||
|
if is_list_data(inp):
|
||||||
|
val = inp[i]
|
||||||
|
else:
|
||||||
|
val = inp
|
||||||
|
if is_list_data(val):
|
||||||
|
val_str = "\n".join(str(s) for s in flatten_list(val) if s)
|
||||||
|
if val_str:
|
||||||
|
row_parts.append(val_str)
|
||||||
|
elif val:
|
||||||
|
row_parts.append(str(val))
|
||||||
|
result.append("\n".join(row_parts))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_LoadGcode(SliceNode):
|
class SLICENODE_LoadGcode(SliceNode):
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import numpy as np
|
||||||
|
|
||||||
from ..bpy_ns_types import NfStringProperty, notify_tree_changed, NfEnumProperty, NfPointerProperty
|
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, SliceNodeListJoiner
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_InputString(InputNode):
|
class SLICENODE_InputString(InputNode):
|
||||||
|
|
@ -39,6 +39,13 @@ class SLICENODE_InputObject(InputNode):
|
||||||
socket_type = "NodeSocketObject"
|
socket_type = "NodeSocketObject"
|
||||||
bl_icon = "OBJECT_DATA"
|
bl_icon = "OBJECT_DATA"
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_ObjectsToList(SliceNodeListJoiner):
|
||||||
|
bl_label = "Objects to List"
|
||||||
|
socket_type = "NodeSocketObject"
|
||||||
|
socket_label = "Object"
|
||||||
|
output_socket_name = "Objects"
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_CollectionObjects(SliceNode):
|
class SLICENODE_CollectionObjects(SliceNode):
|
||||||
bl_label = "Collection Objects"
|
bl_label = "Collection Objects"
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,16 @@ import numpy as np
|
||||||
|
|
||||||
from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, NfIntProperty, NfFloatProperty, NfStringProperty, NfFloatVectorProperty, notify_tree_changed
|
from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, NfIntProperty, NfFloatProperty, NfStringProperty, NfFloatVectorProperty, notify_tree_changed
|
||||||
from ....registry import register_class
|
from ....registry import register_class
|
||||||
from .base import SliceNode, SlicingCollection, get_all_children, SliceNodeJoiner, SlicingGroup, SlicingObject, SlicePaths
|
from .base import SliceNode, SlicingCollection, get_all_children, SliceNodeJoiner, SliceNodeListJoiner, SlicingGroup, SlicingObject, SlicePaths
|
||||||
from ....infra.blender.blender_mesh_capture import PdfProperties
|
from ....infra.blender.blender_mesh_capture import PdfProperties
|
||||||
from ....core.pdf_exporter.geometry import transform_slice_paths
|
from ....core.pdf_exporter.geometry import transform_slice_paths
|
||||||
from ....core.geometry import update_transform
|
from ....core.geometry import update_transform
|
||||||
|
from ....infra.blender.list_utils import eval_element_wise, flatten_list, is_list_data
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_GetMesh(SliceNode):
|
class SLICENODE_GetMesh(SliceNode):
|
||||||
bl_label = "Get Mesh"
|
bl_label = "Get Mesh"
|
||||||
|
list_socket_mapping = [("Object", "Mesh")]
|
||||||
|
|
||||||
include_children: bool = NfBoolProperty(name="Include Children", default=True)
|
include_children: bool = NfBoolProperty(name="Include Children", default=True)
|
||||||
|
|
||||||
|
|
@ -23,17 +25,25 @@ class SLICENODE_GetMesh(SliceNode):
|
||||||
assert layout
|
assert layout
|
||||||
layout.prop(self, "include_children")
|
layout.prop(self, "include_children")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | None:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
if socket_name != "Mesh": return None
|
if socket_name != "Mesh": return None
|
||||||
|
|
||||||
obj: Any = self.resolve("Object")
|
obj: Any = self.resolve("Object")
|
||||||
if not obj: return None
|
if not obj: return None
|
||||||
|
|
||||||
family = [obj] + (get_all_children(obj) if self.include_children else [])
|
def _to_collection(o: Any) -> SlicingCollection | None:
|
||||||
|
if not o: return None
|
||||||
|
family = [o] + (get_all_children(o) if self.include_children else [])
|
||||||
|
return SlicingCollection(family, getattr(o, "name", "Mesh"))
|
||||||
|
|
||||||
sc: SlicingCollection = SlicingCollection(family, obj.name)
|
return eval_element_wise(_to_collection, obj)
|
||||||
|
|
||||||
return sc
|
@register_class
|
||||||
|
class SLICENODE_MeshesToList(SliceNodeListJoiner):
|
||||||
|
bl_label = "Meshes to List"
|
||||||
|
socket_type = "SLICESOCK_Mesh"
|
||||||
|
socket_label = "Mesh"
|
||||||
|
output_socket_name = "Meshes"
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_JoinMesh(SliceNodeJoiner):
|
class SLICENODE_JoinMesh(SliceNodeJoiner):
|
||||||
|
|
@ -42,19 +52,23 @@ class SLICENODE_JoinMesh(SliceNodeJoiner):
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection:
|
||||||
from ....infra.blender.blender_slice_nodes_generic import resolve_input
|
from ....infra.blender.blender_slice_nodes_generic import resolve_input
|
||||||
first_coll = resolve_input(self.inputs[0]) if self.inputs else None
|
all_colls = []
|
||||||
|
for inp in self.inputs:
|
||||||
|
if getattr(inp, "is_linked", False) or getattr(inp, "value", None):
|
||||||
|
val = resolve_input(inp)
|
||||||
|
if val:
|
||||||
|
all_colls.extend(flatten_list(val))
|
||||||
|
|
||||||
|
first_coll = all_colls[0] if all_colls else None
|
||||||
merged_coll = SlicingCollection([], first_coll.name if first_coll else '')
|
merged_coll = SlicingCollection([], first_coll.name if first_coll else '')
|
||||||
if first_coll:
|
for coll in all_colls:
|
||||||
merged_coll.objects = list(first_coll.objects)
|
if hasattr(coll, "objects"):
|
||||||
merged_coll.transforms = [dict(t) for t in getattr(first_coll, "transforms", [])]
|
|
||||||
|
|
||||||
for input in self.inputs[1:]:
|
|
||||||
coll = resolve_input(input)
|
|
||||||
if coll:
|
|
||||||
merged_coll.objects += list(coll.objects)
|
merged_coll.objects += list(coll.objects)
|
||||||
for t in getattr(coll, "transforms", []):
|
elif isinstance(coll, SlicingObject):
|
||||||
if t not in merged_coll.transforms:
|
merged_coll.objects.append(coll)
|
||||||
merged_coll.transforms.append(dict(t))
|
for t in getattr(coll, "transforms", []):
|
||||||
|
if t not in merged_coll.transforms:
|
||||||
|
merged_coll.transforms.append(dict(t))
|
||||||
|
|
||||||
return merged_coll
|
return merged_coll
|
||||||
|
|
||||||
|
|
@ -70,22 +84,28 @@ class SLICENODE_JoinMeshGroups(SliceNodeJoiner):
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup:
|
||||||
from ....infra.blender.blender_slice_nodes_generic import resolve_input
|
from ....infra.blender.blender_slice_nodes_generic import resolve_input
|
||||||
group: SlicingGroup = SlicingGroup([])
|
group: SlicingGroup = SlicingGroup([])
|
||||||
|
|
||||||
for idx, input_socket in enumerate(self.inputs):
|
all_colls = []
|
||||||
coll = resolve_input(input_socket)
|
for input_socket in self.inputs:
|
||||||
if coll:
|
if getattr(input_socket, "is_linked", False) or getattr(input_socket, "value", None):
|
||||||
col_name = getattr(coll, "name", "")
|
val = resolve_input(input_socket)
|
||||||
if not col_name and coll.objects:
|
if val:
|
||||||
col_name = coll.objects[0].name
|
all_colls.extend(flatten_list(val))
|
||||||
if not col_name:
|
|
||||||
col_name = f"collection_{idx}"
|
for idx, coll in enumerate(all_colls):
|
||||||
group.collections[col_name] = coll
|
col_name = getattr(coll, "name", "")
|
||||||
|
if not col_name and getattr(coll, "objects", None):
|
||||||
|
col_name = coll.objects[0].name
|
||||||
|
if not col_name:
|
||||||
|
col_name = f"collection_{idx}"
|
||||||
|
group.collections[col_name] = coll
|
||||||
|
|
||||||
return group
|
return group
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_SeparateMesh(SliceNode):
|
class SLICENODE_SeparateMesh(SliceNode):
|
||||||
bl_label = "Separate Mesh"
|
bl_label = "Separate Mesh"
|
||||||
|
list_socket_mapping = [("Mesh", ["Selection", "Inverted"])]
|
||||||
|
|
||||||
comparison: str = NfEnumProperty(
|
comparison: str = NfEnumProperty(
|
||||||
name="Comparison",
|
name="Comparison",
|
||||||
|
|
@ -112,69 +132,74 @@ class SLICENODE_SeparateMesh(SliceNode):
|
||||||
assert layout
|
assert layout
|
||||||
layout.prop(self, "comparison", text="")
|
layout.prop(self, "comparison", text="")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | None:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
coll: SlicingCollection | None = self.resolve("Mesh")
|
coll: Any = self.resolve("Mesh")
|
||||||
attr_name: str = self.resolve("Attribute")
|
attr_name: str = self.resolve("Attribute")
|
||||||
value: float = self.resolve("Value")
|
value: float = self.resolve("Value")
|
||||||
|
|
||||||
if not coll: return None
|
if not coll: return None
|
||||||
|
|
||||||
res_selection = SlicingCollection([], coll.name + "_selection")
|
def _split_coll(sc: SlicingCollection) -> tuple[SlicingCollection, SlicingCollection]:
|
||||||
res_inverted = SlicingCollection([], coll.name + "_inverted")
|
res_selection = SlicingCollection([], sc.name + "_selection")
|
||||||
|
res_inverted = SlicingCollection([], sc.name + "_inverted")
|
||||||
|
|
||||||
for so in coll.objects:
|
for so in sc.objects:
|
||||||
if attr_name not in so.attributes:
|
if attr_name not in so.attributes:
|
||||||
# If attribute missing, entire object goes to inverted
|
res_inverted.objects.append(so)
|
||||||
res_inverted.objects.append(so)
|
continue
|
||||||
continue
|
|
||||||
|
attr = so.attributes[attr_name]
|
||||||
attr = so.attributes[attr_name]
|
vals = attr["values"]
|
||||||
vals = attr["values"]
|
|
||||||
|
mask = np.zeros(len(so.mesh), dtype=bool)
|
||||||
mask = np.zeros(len(so.mesh), dtype=bool)
|
if self.comparison == 'EQUAL': mask = vals == value
|
||||||
if self.comparison == 'EQUAL': mask = vals == value
|
elif self.comparison == 'NOT_EQUAL': mask = vals != value
|
||||||
elif self.comparison == 'NOT_EQUAL': mask = vals != value
|
elif self.comparison == 'GREATER_THAN': mask = vals > value
|
||||||
elif self.comparison == 'GREATER_THAN': mask = vals > value
|
elif self.comparison == 'LESS_THAN': mask = vals < value
|
||||||
elif self.comparison == 'LESS_THAN': mask = vals < value
|
elif self.comparison == 'GREATER_EQUAL': mask = vals >= value
|
||||||
elif self.comparison == 'GREATER_EQUAL': mask = vals >= value
|
elif self.comparison == 'LESS_EQUAL': mask = vals <= value
|
||||||
elif self.comparison == 'LESS_EQUAL': mask = vals <= value
|
|
||||||
|
if np.all(mask):
|
||||||
if np.all(mask):
|
res_selection.objects.append(so)
|
||||||
res_selection.objects.append(so)
|
elif not np.any(mask):
|
||||||
elif not np.any(mask):
|
res_inverted.objects.append(so)
|
||||||
res_inverted.objects.append(so)
|
else:
|
||||||
else:
|
so_sel = SlicingObject()
|
||||||
# Split the object
|
so_sel.name = so.name + "_sel"
|
||||||
so_sel = SlicingObject()
|
so_sel.parent = so.parent
|
||||||
so_sel.name = so.name + "_sel"
|
so_sel.mesh = so.mesh[mask]
|
||||||
so_sel.parent = so.parent
|
so_sel.transforms = [dict(t) for t in getattr(so, "transforms", [])]
|
||||||
so_sel.mesh = so.mesh[mask]
|
so_sel.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
|
||||||
so_sel.transforms = [dict(t) for t in getattr(so, "transforms", [])]
|
so_sel.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
|
||||||
so_sel.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
|
for k, v in so.attributes.items():
|
||||||
so_sel.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
|
so_sel.attributes[k] = {
|
||||||
for k, v in so.attributes.items():
|
"domain": v["domain"],
|
||||||
so_sel.attributes[k] = {
|
"type": v["type"],
|
||||||
"domain": v["domain"],
|
"values": v["values"][mask]
|
||||||
"type": v["type"],
|
}
|
||||||
"values": v["values"][mask]
|
res_selection.objects.append(so_sel)
|
||||||
}
|
|
||||||
res_selection.objects.append(so_sel)
|
|
||||||
|
|
||||||
so_inv = SlicingObject()
|
so_inv = SlicingObject()
|
||||||
so_inv.name = so.name + "_inv"
|
so_inv.name = so.name + "_inv"
|
||||||
so_inv.parent = so.parent
|
so_inv.parent = so.parent
|
||||||
so_inv.mesh = so.mesh[~mask]
|
so_inv.mesh = so.mesh[~mask]
|
||||||
so_inv.transforms = [dict(t) for t in getattr(so, "transforms", [])]
|
so_inv.transforms = [dict(t) for t in getattr(so, "transforms", [])]
|
||||||
so_inv.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
|
so_inv.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
|
||||||
so_inv.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
|
so_inv.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
|
||||||
for k, v in so.attributes.items():
|
for k, v in so.attributes.items():
|
||||||
so_inv.attributes[k] = {
|
so_inv.attributes[k] = {
|
||||||
"domain": v["domain"],
|
"domain": v["domain"],
|
||||||
"type": v["type"],
|
"type": v["type"],
|
||||||
"values": v["values"][~mask]
|
"values": v["values"][~mask]
|
||||||
}
|
}
|
||||||
res_inverted.objects.append(so_inv)
|
res_inverted.objects.append(so_inv)
|
||||||
|
return res_selection, res_inverted
|
||||||
|
|
||||||
|
if is_list_data(coll):
|
||||||
|
results = [_split_coll(c) for c in coll]
|
||||||
|
return [r[0] for r in results] if socket_name == "Selection" else [r[1] for r in results]
|
||||||
|
|
||||||
|
res_selection, res_inverted = _split_coll(coll)
|
||||||
if socket_name == "Selection":
|
if socket_name == "Selection":
|
||||||
return res_selection
|
return res_selection
|
||||||
return res_inverted
|
return res_inverted
|
||||||
|
|
@ -202,8 +227,13 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
|
||||||
end = max(start, end_val)
|
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)]
|
||||||
|
|
||||||
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
inp = self.inputs.get("Mesh") if hasattr(self, "inputs") else None
|
||||||
|
is_list = is_socket_linked_to_list(inp) if inp is not None else False
|
||||||
|
if inp is not None:
|
||||||
|
set_socket_shape(inp, "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
# Remove outputs that are no longer in the range
|
# Remove outputs that are no longer in the range
|
||||||
# We use a list to avoid modification-during-iteration issues
|
|
||||||
to_remove = [o for o in self.outputs if o.identifier not in expected_ids]
|
to_remove = [o for o in self.outputs if o.identifier not in expected_ids]
|
||||||
for o in to_remove:
|
for o in to_remove:
|
||||||
self.outputs.remove(o)
|
self.outputs.remove(o)
|
||||||
|
|
@ -211,7 +241,9 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
|
||||||
# Add new outputs
|
# Add new outputs
|
||||||
for id_str in expected_ids:
|
for id_str in expected_ids:
|
||||||
if id_str not in self.outputs:
|
if id_str not in self.outputs:
|
||||||
self.add_output("SLICESOCK_Mesh", id_str, identifier=id_str)
|
self.add_output("SLICESOCK_Mesh", id_str, identifier=id_str, display_shape="LIST" if is_list else "")
|
||||||
|
else:
|
||||||
|
set_socket_shape(self.outputs[id_str], "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
# Re-order to match range
|
# Re-order to match range
|
||||||
for i, id_str in enumerate(expected_ids):
|
for i, id_str in enumerate(expected_ids):
|
||||||
|
|
@ -225,8 +257,8 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
|
||||||
col.prop(self, "start_val")
|
col.prop(self, "start_val")
|
||||||
col.prop(self, "end_val")
|
col.prop(self, "end_val")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | None:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
coll: SlicingCollection | None = self.resolve("Mesh")
|
coll: Any = self.resolve("Mesh")
|
||||||
attr_name: str = self.resolve("Attribute")
|
attr_name: str = self.resolve("Attribute")
|
||||||
|
|
||||||
if not coll: return None
|
if not coll: return None
|
||||||
|
|
@ -237,47 +269,50 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
res = SlicingCollection([], f"{coll.name}_{target_val}")
|
start = int(self.start_val) if isinstance(getattr(self, "start_val", None), (int, float)) else 0
|
||||||
|
end_val = int(self.end_val) if isinstance(getattr(self, "end_val", None), (int, float)) else 1
|
||||||
start = self.start_val
|
end = max(start, end_val)
|
||||||
end = max(start, self.end_val)
|
|
||||||
total_parts = end - start + 1
|
total_parts = end - start + 1
|
||||||
num_digits = max(2, len(str(total_parts)))
|
num_digits = max(2, len(str(total_parts)))
|
||||||
i = target_val - start
|
i = target_val - start
|
||||||
|
|
||||||
for so in coll.objects:
|
def _split_coll(sc: SlicingCollection) -> SlicingCollection:
|
||||||
if attr_name not in so.attributes:
|
res = SlicingCollection([], f"{sc.name}_{target_val}")
|
||||||
continue
|
for so in sc.objects:
|
||||||
|
if attr_name not in so.attributes:
|
||||||
attr = so.attributes[attr_name]
|
continue
|
||||||
vals = attr["values"]
|
|
||||||
|
attr = so.attributes[attr_name]
|
||||||
# Attribute values might be floats or ints, we check for equality
|
vals = attr["values"]
|
||||||
mask = vals == target_val
|
|
||||||
|
# Attribute values might be floats or ints, we check for equality
|
||||||
if np.any(mask):
|
mask = vals == target_val
|
||||||
# Split the object
|
|
||||||
so_new = SlicingObject()
|
if np.any(mask):
|
||||||
base_mesh_name = so.name
|
# Split the object
|
||||||
so_new.name = f"{base_mesh_name}{i:0{num_digits}d}"
|
so_new = SlicingObject()
|
||||||
so_new.parent = so.parent
|
base_mesh_name = so.name
|
||||||
so_new.mesh = so.mesh[mask]
|
so_new.name = f"{base_mesh_name}{i:0{num_digits}d}"
|
||||||
so_new.transforms = [dict(t) for t in getattr(so, "transforms", [])]
|
so_new.parent = so.parent
|
||||||
so_new.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
|
so_new.mesh = so.mesh[mask]
|
||||||
so_new.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
|
so_new.transforms = [dict(t) for t in getattr(so, "transforms", [])]
|
||||||
for k, v in so.attributes.items():
|
so_new.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
|
||||||
so_new.attributes[k] = {
|
so_new.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
|
||||||
"domain": v["domain"],
|
for k, v in so.attributes.items():
|
||||||
"type": v["type"],
|
so_new.attributes[k] = {
|
||||||
"values": v["values"][mask]
|
"domain": v["domain"],
|
||||||
}
|
"type": v["type"],
|
||||||
res.objects.append(so_new)
|
"values": v["values"][mask]
|
||||||
|
}
|
||||||
|
res.objects.append(so_new)
|
||||||
|
return res
|
||||||
|
|
||||||
return res
|
return eval_element_wise(_split_coll, coll)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_ArrangeMesh(SliceNode):
|
class SLICENODE_ArrangeMesh(SliceNode):
|
||||||
bl_label = "Arrange Slicing Group"
|
bl_label = "Arrange Slicing Group"
|
||||||
|
list_socket_mapping = [("Slicing Group", "Slicing Group")]
|
||||||
|
|
||||||
method: str = NfEnumProperty(
|
method: str = NfEnumProperty(
|
||||||
name="Method",
|
name="Method",
|
||||||
|
|
@ -297,11 +332,11 @@ class SLICENODE_ArrangeMesh(SliceNode):
|
||||||
)
|
)
|
||||||
|
|
||||||
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")
|
||||||
self.add_input("NodeSocketFloat", "Bed X").value = 250.0
|
self.add_input("NodeSocketFloat", "Bed X").value = 250.0
|
||||||
self.add_input("NodeSocketFloat", "Bed Y").value = 250.0
|
self.add_input("NodeSocketFloat", "Bed Y").value = 250.0
|
||||||
self.add_input("NodeSocketFloat", "Spacing").value = 2.0
|
self.add_input("NodeSocketFloat", "Spacing").value = 2.0
|
||||||
self.add_output("SLICESOCK_Mesh", "Slicing Group", display_shape='LIST')
|
self.add_output("SLICESOCK_Mesh", "Slicing Group")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
assert layout
|
assert layout
|
||||||
|
|
@ -320,9 +355,22 @@ class SLICENODE_ArrangeMesh(SliceNode):
|
||||||
bed_y: float = self.resolve("Bed Y") or 250.0
|
bed_y: float = self.resolve("Bed Y") or 250.0
|
||||||
spacing: float = self.resolve("Spacing") or 2.0
|
spacing: float = self.resolve("Spacing") or 2.0
|
||||||
|
|
||||||
# Convert input to SlicingGroup if it's a SlicingCollection
|
# Convert input to SlicingGroup if it's a SlicingCollection or list
|
||||||
|
is_list = is_list_data(mesh_data)
|
||||||
if isinstance(mesh_data, SlicingGroup):
|
if isinstance(mesh_data, SlicingGroup):
|
||||||
slicing_group = mesh_data
|
slicing_group = mesh_data
|
||||||
|
elif is_list:
|
||||||
|
slicing_group = SlicingGroup([])
|
||||||
|
for idx, item in enumerate(mesh_data):
|
||||||
|
if isinstance(item, SlicingGroup):
|
||||||
|
for k, sc in item.collections.items():
|
||||||
|
slicing_group.collections[f"{k}_{idx}"] = sc
|
||||||
|
elif isinstance(item, SlicingCollection):
|
||||||
|
col_name = item.name or f"mesh_{idx}"
|
||||||
|
slicing_group.collections[col_name] = item
|
||||||
|
elif isinstance(item, SlicingObject):
|
||||||
|
sc = SlicingCollection([item], item.name or f"mesh_{idx}")
|
||||||
|
slicing_group.collections[sc.name] = sc
|
||||||
else:
|
else:
|
||||||
slicing_group = SlicingGroup([])
|
slicing_group = SlicingGroup([])
|
||||||
slicing_group.collections = {mesh_data.name: mesh_data}
|
slicing_group.collections = {mesh_data.name: mesh_data}
|
||||||
|
|
@ -419,8 +467,8 @@ class SLICENODE_ArrangeMesh(SliceNode):
|
||||||
so.mesh[:, 3, 1] = ny_new
|
so.mesh[:, 3, 1] = ny_new
|
||||||
else:
|
else:
|
||||||
name, sc = collection_map[id(item)]
|
name, sc = collection_map[id(item)]
|
||||||
print(f"Warning: Collection '{name}' did not fit on the build plate (binId: {bin_id})")
|
if is_list:
|
||||||
|
return list(copied_group.collections.values())
|
||||||
return copied_group
|
return copied_group
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -515,6 +563,7 @@ def _transform_geometry_item(geom: Any, t_arr: np.ndarray, r_arr: np.ndarray, s_
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_TransformGeometry(SliceNode):
|
class SLICENODE_TransformGeometry(SliceNode):
|
||||||
bl_label = "Transform Geometry"
|
bl_label = "Transform Geometry"
|
||||||
|
list_socket_mapping = [("Geometry", "Geometry")]
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Geometry", "Geometry")
|
self.add_input("SLICESOCK_Geometry", "Geometry")
|
||||||
|
|
@ -545,6 +594,7 @@ class SLICENODE_TransformGeometry(SliceNode):
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_DropGeometry(SliceNode):
|
class SLICENODE_DropGeometry(SliceNode):
|
||||||
bl_label = "Drop Geometry"
|
bl_label = "Drop Geometry"
|
||||||
|
list_socket_mapping = [("Mesh", ["Mesh", "Translation"])]
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Mesh", "Mesh")
|
self.add_input("SLICESOCK_Mesh", "Mesh")
|
||||||
|
|
@ -552,23 +602,8 @@ class SLICENODE_DropGeometry(SliceNode):
|
||||||
self.add_output("SLICESOCK_Mesh", "Mesh")
|
self.add_output("SLICESOCK_Mesh", "Mesh")
|
||||||
self.add_output("NodeSocketVector", "Translation")
|
self.add_output("NodeSocketVector", "Translation")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | SlicingGroup | tuple[float, float, float] | None:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
if socket_name == "Translation":
|
mesh_data: Any = self.resolve("Mesh")
|
||||||
mesh_data: SlicingCollection | SlicingGroup | None = self.resolve("Mesh")
|
|
||||||
if not mesh_data:
|
|
||||||
return None
|
|
||||||
z = self.resolve('Z')
|
|
||||||
if z is None:
|
|
||||||
z = 0.0
|
|
||||||
assert mesh_data.min_z is not None
|
|
||||||
min_z = round(mesh_data.min_z, 6)
|
|
||||||
offset_z = float(z - min_z)
|
|
||||||
return (0.0, 0.0, offset_z)
|
|
||||||
|
|
||||||
if socket_name != "Mesh":
|
|
||||||
return None
|
|
||||||
|
|
||||||
mesh_data = self.resolve("Mesh")
|
|
||||||
if not mesh_data:
|
if not mesh_data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -576,29 +611,38 @@ class SLICENODE_DropGeometry(SliceNode):
|
||||||
if z is None:
|
if z is None:
|
||||||
z = 0.0
|
z = 0.0
|
||||||
|
|
||||||
assert mesh_data.min_z is not None
|
def _drop_item(item: Any) -> tuple[Any, tuple[float, float, float]]:
|
||||||
min_z = round(mesh_data.min_z, 6)
|
if not item or getattr(item, "min_z", None) is None:
|
||||||
offset_z = float(z - min_z)
|
return item, (0.0, 0.0, 0.0)
|
||||||
|
min_z = round(item.min_z, 6)
|
||||||
if isinstance(mesh_data, SlicingGroup):
|
offset_z = float(z - min_z)
|
||||||
copied_data = _copy_slicing_group(mesh_data)
|
if isinstance(item, SlicingGroup):
|
||||||
else:
|
copied_data = _copy_slicing_group(item)
|
||||||
copied_data = _copy_slicing_collection(mesh_data)
|
else:
|
||||||
|
copied_data = _copy_slicing_collection(item)
|
||||||
drop_vec = np.array([0.0, 0.0, offset_z], dtype=np.float64)
|
drop_vec = np.array([0.0, 0.0, offset_z], dtype=np.float64)
|
||||||
copied_data.offset(drop_vec)
|
copied_data.offset(drop_vec)
|
||||||
|
copied_data.transforms = update_transform(getattr(copied_data, "transforms", []), t_arr=drop_vec)
|
||||||
copied_data.transforms = update_transform(getattr(copied_data, "transforms", []), t_arr=drop_vec)
|
if isinstance(copied_data, SlicingGroup):
|
||||||
if isinstance(copied_data, SlicingGroup):
|
for col in copied_data.collections.values():
|
||||||
for col in copied_data.collections.values():
|
col.transforms = update_transform(getattr(col, "transforms", []), t_arr=drop_vec)
|
||||||
col.transforms = update_transform(getattr(col, "transforms", []), t_arr=drop_vec)
|
for so in col.objects:
|
||||||
for so in col.objects:
|
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
|
||||||
|
elif isinstance(copied_data, SlicingCollection):
|
||||||
|
for so in copied_data.objects:
|
||||||
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
|
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
|
||||||
elif isinstance(copied_data, SlicingCollection):
|
return copied_data, (0.0, 0.0, offset_z)
|
||||||
for so in copied_data.objects:
|
|
||||||
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
|
|
||||||
|
|
||||||
return copied_data
|
if is_list_data(mesh_data):
|
||||||
|
results = [_drop_item(m) for m in mesh_data]
|
||||||
|
if socket_name == "Translation":
|
||||||
|
return [r[1] for r in results]
|
||||||
|
return [r[0] for r in results]
|
||||||
|
|
||||||
|
dropped, trans = _drop_item(mesh_data)
|
||||||
|
if socket_name == "Translation":
|
||||||
|
return trans
|
||||||
|
return dropped
|
||||||
|
|
||||||
|
|
||||||
def format_name_template(template: str, so: Any, default_old_name: str = "") -> str:
|
def format_name_template(template: str, so: Any, default_old_name: str = "") -> str:
|
||||||
|
|
@ -664,6 +708,7 @@ def _rename_slicing_item(geom: Any, new_name: str) -> Any:
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_RenameMesh(SliceNode):
|
class SLICENODE_RenameMesh(SliceNode):
|
||||||
bl_label = "Rename Mesh"
|
bl_label = "Rename Mesh"
|
||||||
|
list_socket_mapping = [("Geometry", "Geometry")]
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Mesh", "Geometry")
|
self.add_input("SLICESOCK_Mesh", "Geometry")
|
||||||
|
|
@ -716,6 +761,7 @@ def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tup
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_AssignPdfProperties(SliceNode):
|
class SLICENODE_AssignPdfProperties(SliceNode):
|
||||||
bl_label = "Assign PDF Properties"
|
bl_label = "Assign PDF Properties"
|
||||||
|
list_socket_mapping = [("Geometry", "Geometry")]
|
||||||
|
|
||||||
line_width: float = NfFloatProperty(name="Line Width (mm)", default=0.0, min=0.0, max=50.0)
|
line_width: float = NfFloatProperty(name="Line Width (mm)", default=0.0, min=0.0, max=50.0)
|
||||||
line_color: tuple[float, float, float] = NfFloatVectorProperty(
|
line_color: tuple[float, float, float] = NfFloatVectorProperty(
|
||||||
|
|
@ -777,7 +823,7 @@ def _assign_properties_to_item(geom: Any, props: list[tuple[str, str]]) -> Any:
|
||||||
return copied
|
return copied
|
||||||
elif isinstance(geom, SlicingObject):
|
elif isinstance(geom, SlicingObject):
|
||||||
copied = _copy_slicing_object(geom)
|
copied = _copy_slicing_object(geom)
|
||||||
copied.pdf_object_properties = resolve_object_properties(copied, props)
|
copied.pdf_object_properties = resolve_object_properties(geom, props)
|
||||||
return copied
|
return copied
|
||||||
return geom
|
return geom
|
||||||
|
|
||||||
|
|
@ -785,6 +831,7 @@ def _assign_properties_to_item(geom: Any, props: list[tuple[str, str]]) -> Any:
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_AssignPropertyToObject(SliceNode):
|
class SLICENODE_AssignPropertyToObject(SliceNode):
|
||||||
bl_label = "Assign PDF Object Properties"
|
bl_label = "Assign PDF Object Properties"
|
||||||
|
list_socket_mapping = [("Geometry", "Geometry")]
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Mesh", "Geometry")
|
self.add_input("SLICESOCK_Mesh", "Geometry")
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,22 @@ class SLICENODE_GcodeToFile(ExecSliceNode):
|
||||||
self.add_input("NodeSocketString", "Name")
|
self.add_input("NodeSocketString", "Name")
|
||||||
self.add_multi_socket_group("NodeSocketString", name="Gcode")
|
self.add_multi_socket_group("NodeSocketString", name="Gcode")
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
for inp in self.inputs:
|
||||||
|
if getattr(inp, "name", "") == "Gcode" or getattr(inp, "identifier", "").startswith("Gcode_ms_in_"):
|
||||||
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
super().draw_buttons(context, layout)
|
super().draw_buttons(context, layout)
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
def evaluate(self, operator=None):
|
||||||
from ....infra.networking.gcode_utils import get_gcode_name_from_metadata, prepare_gcode_file
|
from ....infra.networking.gcode_utils import get_gcode_name_from_metadata, prepare_gcode_file
|
||||||
|
from ....infra.blender.list_utils import flatten_list
|
||||||
gcodes: list[str] = [gc for gc in self.resolve_multi_input('Gcode') if gc]
|
|
||||||
|
raw_gcodes = self.resolve_multi_input('Gcode')
|
||||||
|
gcodes: list[str] = [str(gc) for gc in flatten_list(raw_gcodes) if gc]
|
||||||
|
|
||||||
folder: str = self.resolve("Folder")
|
folder: str = self.resolve("Folder")
|
||||||
if not gcodes:
|
if not gcodes:
|
||||||
|
|
@ -86,21 +95,38 @@ class SLICENODE_GcodeToTextBlock(ExecSliceNode):
|
||||||
self._init_exec()
|
self._init_exec()
|
||||||
self.add_input("NodeSocketString", "Gcode")
|
self.add_input("NodeSocketString", "Gcode")
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
inp = self.inputs.get("Gcode")
|
||||||
|
if inp:
|
||||||
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
super().draw_buttons(context, layout)
|
super().draw_buttons(context, layout)
|
||||||
layout.prop(self, "text_block_name")
|
layout.prop(self, "text_block_name")
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
def evaluate(self, operator=None):
|
||||||
gcode: str = self.resolve("Gcode")
|
from ....infra.blender.list_utils import flatten_list, is_list_data
|
||||||
if not gcode:
|
raw = self.resolve("Gcode")
|
||||||
|
if not raw:
|
||||||
return 'ok'
|
return 'ok'
|
||||||
|
|
||||||
name = self.text_block_name
|
gcodes = [str(g) for g in flatten_list(raw) if g] if is_list_data(raw) else ([str(raw)] if raw else [])
|
||||||
text_block = bpy.data.texts.get(name)
|
if not gcodes:
|
||||||
if text_block is None:
|
return 'ok'
|
||||||
text_block = bpy.data.texts.new(name=name)
|
|
||||||
|
|
||||||
text_block.from_string(gcode)
|
base_name = self.text_block_name
|
||||||
|
name_root, ext = os.path.splitext(base_name)
|
||||||
|
total_parts = len(gcodes)
|
||||||
|
num_digits = max(2, len(str(total_parts)))
|
||||||
|
|
||||||
|
for i, gcode in enumerate(gcodes):
|
||||||
|
name = f"{name_root}_{i:0{num_digits}d}{ext}" if total_parts > 1 else base_name
|
||||||
|
text_block = bpy.data.texts.get(name)
|
||||||
|
if text_block is None:
|
||||||
|
text_block = bpy.data.texts.new(name=name)
|
||||||
|
text_block.from_string(gcode)
|
||||||
|
|
||||||
# Attempt to show in an open text editor
|
# Attempt to show in an open text editor
|
||||||
assert bpy.context.window_manager
|
assert bpy.context.window_manager
|
||||||
|
|
@ -124,14 +150,21 @@ class SLICENODE_StartPrint(ExecSliceNode):
|
||||||
self.add_input("SLICESOCK_Printer")
|
self.add_input("SLICESOCK_Printer")
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
def evaluate(self, operator=None):
|
||||||
|
from ....infra.blender.list_utils import is_list_data
|
||||||
|
|
||||||
|
raw_gcode = self.resolve("Gcode")
|
||||||
|
if is_list_data(raw_gcode):
|
||||||
|
if operator: operator.report({'ERROR'}, "Start Print does not accept a list of Gcode")
|
||||||
|
return 'error'
|
||||||
|
|
||||||
from ....infra.networking.printer_backends import (
|
from ....infra.networking.printer_backends import (
|
||||||
PrinterConfig,
|
PrinterConfig,
|
||||||
PrusaLinkBackend,
|
PrusaLinkBackend,
|
||||||
CrealityBackend,
|
CrealityBackend,
|
||||||
MoonrakerBackend
|
MoonrakerBackend
|
||||||
)
|
)
|
||||||
|
|
||||||
gcode: str = self.resolve("Gcode")
|
gcode: str = str(raw_gcode or "")
|
||||||
config: PrinterConfig | None = self.resolve("Printer")
|
config: PrinterConfig | None = self.resolve("Printer")
|
||||||
|
|
||||||
if not gcode:
|
if not gcode:
|
||||||
|
|
@ -192,6 +225,13 @@ class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
|
||||||
self.add_input("NodeSocketString", "Gcode")
|
self.add_input("NodeSocketString", "Gcode")
|
||||||
self.add_input("SLICESOCK_Settings", "Metadata")
|
self.add_input("SLICESOCK_Settings", "Metadata")
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
inp = self.inputs.get("Gcode")
|
||||||
|
if inp:
|
||||||
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
super().draw_buttons(context, layout)
|
super().draw_buttons(context, layout)
|
||||||
layout.prop(self, "record_id")
|
layout.prop(self, "record_id")
|
||||||
|
|
@ -199,8 +239,9 @@ class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
|
||||||
def evaluate(self, operator=None):
|
def evaluate(self, operator=None):
|
||||||
from ....infra.networking.bgcode import convert_to_bgcode
|
from ....infra.networking.bgcode import convert_to_bgcode
|
||||||
from ....infra.networking.gcode_utils import get_gcode_name_from_metadata, parse_gcode_metadata
|
from ....infra.networking.gcode_utils import get_gcode_name_from_metadata, parse_gcode_metadata
|
||||||
|
from ....infra.blender.list_utils import flatten_list
|
||||||
gcodes: list[str] = self.resolve_multi_input("Gcode")
|
|
||||||
|
gcodes: list[str] = [str(gc) for gc in flatten_list(self.resolve_multi_input("Gcode")) if gc]
|
||||||
|
|
||||||
for gc in gcodes:
|
for gc in gcodes:
|
||||||
if not gc:
|
if not gc:
|
||||||
|
|
@ -413,6 +454,14 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
self.add_input("SLICESOCK_Mesh", "Geometry")
|
self.add_input("SLICESOCK_Mesh", "Geometry")
|
||||||
self.add_input("SLICESOCK_Paths", "Paths")
|
self.add_input("SLICESOCK_Paths", "Paths")
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
for name in ("Geometry", "Paths"):
|
||||||
|
inp = self.inputs.get(name)
|
||||||
|
if inp:
|
||||||
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
super().draw_buttons(context, layout)
|
super().draw_buttons(context, layout)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,20 @@ import numpy as np
|
||||||
|
|
||||||
from ..bpy_ns_types import NfEnumProperty, NfBoolProperty, NfFloatProperty, notify_tree_changed
|
from ..bpy_ns_types import NfEnumProperty, NfBoolProperty, NfFloatProperty, notify_tree_changed
|
||||||
from ....registry import register_class
|
from ....registry import register_class
|
||||||
from .base import SliceNode, SlicePaths, SlicingObject, SlicingCollection, slice_at_range, SliceNodeJoiner, SlicingGroup
|
from .base import SliceNode, SlicePaths, SlicingObject, SlicingCollection, slice_at_range, SliceNodeJoiner, SliceNodeListJoiner, SlicingGroup
|
||||||
from ...slice import align_path_seam_sharpest, is_closed_path
|
from ...slice import align_path_seam_sharpest, is_closed_path
|
||||||
|
from ....infra.blender.list_utils import eval_element_wise, is_list_data, set_socket_shape, is_socket_linked_to_list
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_PathsToList(SliceNodeListJoiner):
|
||||||
|
bl_label = "Paths to List"
|
||||||
|
socket_type = "SLICESOCK_Paths"
|
||||||
|
socket_label = "Paths"
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_GetPaths(SliceNode):
|
class SLICENODE_GetPaths(SliceNode):
|
||||||
bl_label = "Get Paths"
|
bl_label = "Get Paths"
|
||||||
|
list_socket_mapping = [("Object", "Paths")]
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("NodeSocketObject")
|
self.add_input("NodeSocketObject")
|
||||||
|
|
@ -21,27 +29,143 @@ class SLICENODE_GetPaths(SliceNode):
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
obj: Any = self.resolve("Object")
|
obj: Any = self.resolve("Object")
|
||||||
e_attr_name: str = self.resolve("Extrusion Multiplier Attribute")
|
e_attr_name: str = self.resolve("Extrusion Multiplier Attribute")
|
||||||
f_attr_name: str = self.resolve("Feedrate Attribute")
|
f_attr_name: str = self.resolve("Feedrate Attribute")
|
||||||
extra_attrs: dict[str, str] = self.resolve("Extra Attributes")
|
extra_attrs: dict[str, str] = self.resolve("Extra Attributes")
|
||||||
|
|
||||||
from ....infra.blender.blender_mesh_capture import capture_mesh_edges
|
from ....infra.blender.blender_mesh_capture import capture_mesh_edges
|
||||||
so, e_rates, f_rates, extra_attrs_data = capture_mesh_edges(obj, e_attr_name, f_attr_name, extra_attrs)
|
|
||||||
|
|
||||||
sp = SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
def _get_single_path(o: Any) -> SlicePaths:
|
||||||
if hasattr(obj, "transforms"):
|
if not o:
|
||||||
sp.transforms = list(getattr(obj, "transforms", []))
|
return SlicePaths()
|
||||||
if hasattr(obj, "name") and getattr(obj, "name"):
|
so, e_rates, f_rates, extra_attrs_data = capture_mesh_edges(o, e_attr_name, f_attr_name, extra_attrs)
|
||||||
name_val = str(getattr(obj, "name"))
|
sp = SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
||||||
sp.name = name_val
|
if hasattr(o, "transforms"):
|
||||||
sp.objects_info = {"objects": [{"name": name_val, "polygon": []}]}
|
sp.transforms = list(getattr(o, "transforms", []))
|
||||||
return sp
|
if hasattr(o, "name") and getattr(o, "name"):
|
||||||
|
name_val = str(getattr(o, "name"))
|
||||||
|
sp.name = name_val
|
||||||
|
sp.objects_info = {"objects": [{"name": name_val, "polygon": []}]}
|
||||||
|
return sp
|
||||||
|
|
||||||
|
return eval_element_wise(_get_single_path, obj)
|
||||||
|
|
||||||
|
def _slice_single_mesh_item(coll: Any, layer_height: float, fl_height: float | None, rotation: Any, ignore_holes: bool) -> SlicePaths:
|
||||||
|
if not coll:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
# Handle SlicingGroup, SlicingCollection, SlicingObject
|
||||||
|
if isinstance(coll, SlicingGroup):
|
||||||
|
objects = []
|
||||||
|
for c in coll.collections.values():
|
||||||
|
objects.extend(c.objects)
|
||||||
|
elif isinstance(coll, SlicingCollection):
|
||||||
|
objects = list(coll.objects)
|
||||||
|
elif isinstance(coll, SlicingObject):
|
||||||
|
objects = [coll]
|
||||||
|
else:
|
||||||
|
objects = getattr(coll, "objects", [coll] if hasattr(coll, "mesh") else [])
|
||||||
|
|
||||||
|
merged_object = SlicingObject.merge(objects)
|
||||||
|
if merged_object.mesh is None or merged_object.mesh.size == 0:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
has_rot = bool(rotation and any(rotation))
|
||||||
|
if has_rot:
|
||||||
|
from mathutils import Euler
|
||||||
|
mat = np.array(Euler(rotation).to_matrix().inverted())
|
||||||
|
merged_object.mesh[:, :3, :] = merged_object.mesh[:, :3, :] @ mat.T
|
||||||
|
merged_object.mesh[:, 3:4, :] = merged_object.mesh[:, 3:4, :] @ mat.T
|
||||||
|
|
||||||
|
min_z = float(merged_object.mesh[:, :3, 2].min())
|
||||||
|
if abs(min_z) < 1e-6:
|
||||||
|
min_z = 0.0
|
||||||
|
max_z = float(merged_object.mesh[:, :3, 2].max())
|
||||||
|
|
||||||
|
h_first = fl_height if fl_height is not None else layer_height
|
||||||
|
|
||||||
|
min_z_um = int(round(min_z * 1000))
|
||||||
|
lh_um = int(round(layer_height * 1000))
|
||||||
|
flh_um = int(round(h_first * 1000))
|
||||||
|
max_z_um = int(round(max_z * 1000))
|
||||||
|
|
||||||
|
z_first_um = min_z_um + flh_um
|
||||||
|
z_rest_start_um = z_first_um + lh_um
|
||||||
|
|
||||||
|
if z_first_um <= max_z_um:
|
||||||
|
if z_rest_start_um <= max_z_um and lh_um > 0:
|
||||||
|
z_um_values = np.arange(z_rest_start_um, max_z_um + 1, lh_um, dtype=np.int64)
|
||||||
|
z_values = np.concatenate(([z_first_um], z_um_values)) / 1000.0
|
||||||
|
else:
|
||||||
|
z_values = np.array([z_first_um], dtype=np.float64) / 1000.0
|
||||||
|
else:
|
||||||
|
z_values = np.array([], dtype=np.float64)
|
||||||
|
|
||||||
|
slices_group: list[list[np.ndarray]] | None = slice_at_range(
|
||||||
|
[merged_object.mesh],
|
||||||
|
z_values,
|
||||||
|
ignore_holes=ignore_holes
|
||||||
|
)
|
||||||
|
if not slices_group or not slices_group[0]:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
paths = slices_group[0]
|
||||||
|
z_to_group: dict[int, int] = {}
|
||||||
|
groups = []
|
||||||
|
for p in paths:
|
||||||
|
if len(p) > 0:
|
||||||
|
z_k = int(round(p[0, 2] * 1000))
|
||||||
|
if z_k not in z_to_group:
|
||||||
|
z_to_group[z_k] = len(z_to_group)
|
||||||
|
groups.append(z_to_group[z_k])
|
||||||
|
else:
|
||||||
|
groups.append(0)
|
||||||
|
|
||||||
|
slices = SlicePaths(paths, groups=groups)
|
||||||
|
slices.layer_height = layer_height
|
||||||
|
if fl_height is not None:
|
||||||
|
slices.first_layer_height = fl_height
|
||||||
|
slices.transforms = list(getattr(coll, "transforms", [])) or list(getattr(merged_object, "transforms", []))
|
||||||
|
|
||||||
|
names = []
|
||||||
|
if objects:
|
||||||
|
for o in objects:
|
||||||
|
n = getattr(o, "name", "")
|
||||||
|
if n:
|
||||||
|
if n.endswith("_merged"):
|
||||||
|
n = n[:-7]
|
||||||
|
if n and n not in names:
|
||||||
|
names.append(n)
|
||||||
|
if not names and hasattr(coll, "name") and getattr(coll, "name"):
|
||||||
|
names.append(str(getattr(coll, "name")))
|
||||||
|
|
||||||
|
if names:
|
||||||
|
slices.name = names[0]
|
||||||
|
slices.objects_info = {"objects": [{"name": n, "polygon": []} for n in names]}
|
||||||
|
|
||||||
|
if len(z_values) > 0 and fl_height is not None and layer_height > 0:
|
||||||
|
mult = fl_height / layer_height
|
||||||
|
z_first_um = int(round(z_values[0] * 1000))
|
||||||
|
for i, p in enumerate(slices):
|
||||||
|
if len(p) > 0 and int(round(p[0, 2] * 1000)) == z_first_um:
|
||||||
|
slices.extrusion_rate[i] = slices.extrusion_rate[i] * mult
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if has_rot:
|
||||||
|
from mathutils import Euler
|
||||||
|
mat_inv = np.array(Euler(rotation).to_matrix())
|
||||||
|
slices = slices.transform(mat_inv)
|
||||||
|
slices = slices.align_seams(mode="sharpest")
|
||||||
|
|
||||||
|
return slices
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_SliceShells(SliceNode):
|
class SLICENODE_SliceShells(SliceNode):
|
||||||
bl_label = "Slice Shells"
|
bl_label = "Slice Shells"
|
||||||
|
list_socket_mapping = [("Mesh", "Paths")]
|
||||||
|
|
||||||
ignore_holes: bool = NfBoolProperty(
|
ignore_holes: bool = NfBoolProperty(
|
||||||
name="Ignore Holes",
|
name="Ignore Holes",
|
||||||
|
|
@ -61,7 +185,7 @@ class SLICENODE_SliceShells(SliceNode):
|
||||||
if hasattr(self, "ignore_holes"):
|
if hasattr(self, "ignore_holes"):
|
||||||
layout.prop(self, "ignore_holes")
|
layout.prop(self, "ignore_holes")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
if socket_name != "Paths": return SlicePaths()
|
if socket_name != "Paths": return SlicePaths()
|
||||||
|
|
||||||
coll: Any = self.resolve("Mesh")
|
coll: Any = self.resolve("Mesh")
|
||||||
|
|
@ -92,126 +216,15 @@ class SLICENODE_SliceShells(SliceNode):
|
||||||
|
|
||||||
if not coll: return SlicePaths()
|
if not coll: return SlicePaths()
|
||||||
|
|
||||||
# Handle SlicingGroup, SlicingCollection, SlicingObject, or list
|
def _slice_one(item: Any) -> SlicePaths:
|
||||||
if isinstance(coll, SlicingGroup):
|
return _slice_single_mesh_item(item, layer_height, fl_height, rotation, ignore_holes)
|
||||||
objects = []
|
|
||||||
for c in coll.collections.values():
|
|
||||||
objects.extend(c.objects)
|
|
||||||
elif isinstance(coll, SlicingCollection):
|
|
||||||
objects = list(coll.objects)
|
|
||||||
elif isinstance(coll, SlicingObject):
|
|
||||||
objects = [coll]
|
|
||||||
elif isinstance(coll, list):
|
|
||||||
objects = []
|
|
||||||
for item in coll:
|
|
||||||
if isinstance(item, SlicingGroup):
|
|
||||||
for c in item.collections.values():
|
|
||||||
objects.extend(c.objects)
|
|
||||||
elif isinstance(item, SlicingCollection):
|
|
||||||
objects.extend(item.objects)
|
|
||||||
elif isinstance(item, SlicingObject):
|
|
||||||
objects.append(item)
|
|
||||||
else:
|
|
||||||
objects = getattr(coll, "objects", [coll] if hasattr(coll, "mesh") else [])
|
|
||||||
|
|
||||||
merged_object = SlicingObject.merge(objects)
|
return eval_element_wise(_slice_one, coll)
|
||||||
if merged_object.mesh is None or merged_object.mesh.size == 0:
|
|
||||||
return SlicePaths()
|
|
||||||
|
|
||||||
has_rot = bool(rotation and any(rotation))
|
|
||||||
if has_rot:
|
|
||||||
from mathutils import Euler
|
|
||||||
mat = np.array(Euler(rotation).to_matrix().inverted())
|
|
||||||
merged_object.mesh[:, :3, :] = merged_object.mesh[:, :3, :] @ mat.T
|
|
||||||
merged_object.mesh[:, 3:4, :] = merged_object.mesh[:, 3:4, :] @ mat.T
|
|
||||||
|
|
||||||
min_z = float(merged_object.mesh[:, :3, 2].min())
|
|
||||||
if abs(min_z) < 1e-6:
|
|
||||||
min_z = 0.0
|
|
||||||
max_z = float(merged_object.mesh[:, :3, 2].max())
|
|
||||||
|
|
||||||
h_first = fl_height if fl_height is not None else layer_height
|
|
||||||
|
|
||||||
min_z_um = int(round(min_z * 1000))
|
|
||||||
lh_um = int(round(layer_height * 1000))
|
|
||||||
flh_um = int(round(h_first * 1000))
|
|
||||||
max_z_um = int(round(max_z * 1000))
|
|
||||||
|
|
||||||
z_first_um = min_z_um + flh_um
|
|
||||||
z_rest_start_um = z_first_um + lh_um
|
|
||||||
|
|
||||||
if z_first_um <= max_z_um:
|
|
||||||
if z_rest_start_um <= max_z_um and lh_um > 0:
|
|
||||||
z_um_values = np.arange(z_rest_start_um, max_z_um + 1, lh_um, dtype=np.int64)
|
|
||||||
z_values = np.concatenate(([z_first_um], z_um_values)) / 1000.0
|
|
||||||
else:
|
|
||||||
z_values = np.array([z_first_um], dtype=np.float64) / 1000.0
|
|
||||||
else:
|
|
||||||
z_values = np.array([], dtype=np.float64)
|
|
||||||
|
|
||||||
slices_group: list[list[np.ndarray]] | None = slice_at_range(
|
|
||||||
[merged_object.mesh],
|
|
||||||
z_values,
|
|
||||||
ignore_holes=ignore_holes
|
|
||||||
)
|
|
||||||
if not slices_group or not slices_group[0]:
|
|
||||||
return SlicePaths()
|
|
||||||
|
|
||||||
paths = slices_group[0]
|
|
||||||
# Assign layer-consistent group indices based on Z height in integer microns
|
|
||||||
z_to_group: dict[int, int] = {}
|
|
||||||
groups = []
|
|
||||||
for p in paths:
|
|
||||||
if len(p) > 0:
|
|
||||||
z_k = int(round(p[0, 2] * 1000))
|
|
||||||
if z_k not in z_to_group:
|
|
||||||
z_to_group[z_k] = len(z_to_group)
|
|
||||||
groups.append(z_to_group[z_k])
|
|
||||||
else:
|
|
||||||
groups.append(0)
|
|
||||||
|
|
||||||
slices = SlicePaths(paths, groups=groups)
|
|
||||||
slices.layer_height = layer_height
|
|
||||||
if fl_height is not None:
|
|
||||||
slices.first_layer_height = fl_height
|
|
||||||
slices.transforms = list(getattr(coll, "transforms", [])) or list(getattr(merged_object, "transforms", []))
|
|
||||||
|
|
||||||
names = []
|
|
||||||
if objects:
|
|
||||||
for o in objects:
|
|
||||||
n = getattr(o, "name", "")
|
|
||||||
if n:
|
|
||||||
if n.endswith("_merged"):
|
|
||||||
n = n[:-7]
|
|
||||||
if n and n not in names:
|
|
||||||
names.append(n)
|
|
||||||
if not names and hasattr(coll, "name") and getattr(coll, "name"):
|
|
||||||
names.append(str(getattr(coll, "name")))
|
|
||||||
|
|
||||||
if names:
|
|
||||||
slices.name = names[0]
|
|
||||||
slices.objects_info = {"objects": [{"name": n, "polygon": []} for n in names]}
|
|
||||||
|
|
||||||
if len(z_values) > 0 and fl_height is not None and layer_height > 0:
|
|
||||||
mult = fl_height / layer_height
|
|
||||||
z_first_um = int(round(z_values[0] * 1000))
|
|
||||||
for i, p in enumerate(slices):
|
|
||||||
if len(p) > 0 and int(round(p[0, 2] * 1000)) == z_first_um:
|
|
||||||
slices.extrusion_rate[i] = slices.extrusion_rate[i] * mult
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
if has_rot:
|
|
||||||
from mathutils import Euler
|
|
||||||
mat_inv = np.array(Euler(rotation).to_matrix())
|
|
||||||
slices = slices.transform(mat_inv)
|
|
||||||
slices = slices.align_seams(mode="sharpest")
|
|
||||||
|
|
||||||
return slices
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_BufferShells(SliceNode):
|
class SLICENODE_BufferShells(SliceNode):
|
||||||
bl_label = "Buffer Shells"
|
bl_label = "Buffer Shells"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
shape_option: str = NfEnumProperty(
|
shape_option: str = NfEnumProperty(
|
||||||
name="Miter",
|
name="Miter",
|
||||||
|
|
@ -232,21 +245,26 @@ class SLICENODE_BufferShells(SliceNode):
|
||||||
assert layout
|
assert layout
|
||||||
layout.prop(self, "shape_option")
|
layout.prop(self, "shape_option")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
if socket_name != "Paths": return SlicePaths()
|
if socket_name != "Paths": return SlicePaths()
|
||||||
|
|
||||||
paths: SlicePaths = self.resolve("Paths")
|
paths = self.resolve("Paths")
|
||||||
distance_val = self.resolve("Distance")
|
distance_val = self.resolve("Distance")
|
||||||
distance: float = float(distance_val) if distance_val is not None else 0.0
|
distance: float = float(distance_val) if distance_val is not None else 0.0
|
||||||
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
if not paths: return SlicePaths()
|
if not paths: return SlicePaths()
|
||||||
|
|
||||||
return paths.buffer(distance, miter=self.shape_option, rotation=rotation)
|
def _buf(p: Any) -> SlicePaths:
|
||||||
|
if not p: return SlicePaths()
|
||||||
|
return p.buffer(distance, miter=self.shape_option, rotation=rotation)
|
||||||
|
|
||||||
|
return eval_element_wise(_buf, paths)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_BufferCurve(SliceNode):
|
class SLICENODE_BufferCurve(SliceNode):
|
||||||
bl_label = "Buffer Curve"
|
bl_label = "Buffer Curve"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
cap_style: str = NfEnumProperty(
|
cap_style: str = NfEnumProperty(
|
||||||
name="Cap Style",
|
name="Cap Style",
|
||||||
|
|
@ -286,26 +304,31 @@ class SLICENODE_BufferCurve(SliceNode):
|
||||||
layout.prop(self, "join_style")
|
layout.prop(self, "join_style")
|
||||||
layout.prop(self, "single_sided")
|
layout.prop(self, "single_sided")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
if socket_name != "Paths": return SlicePaths()
|
if socket_name != "Paths": return SlicePaths()
|
||||||
|
|
||||||
paths: SlicePaths = self.resolve("Paths")
|
paths = self.resolve("Paths")
|
||||||
distance: float = self.resolve("Distance") or 0.0
|
distance: float = self.resolve("Distance") or 0.0
|
||||||
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
if not paths: return SlicePaths()
|
if not paths: return SlicePaths()
|
||||||
|
|
||||||
return paths.buffer_curve(
|
def _buf(p: Any) -> SlicePaths:
|
||||||
distance,
|
if not p: return SlicePaths()
|
||||||
cap_style=self.cap_style,
|
return p.buffer_curve(
|
||||||
join_style=self.join_style,
|
distance,
|
||||||
single_sided=self.single_sided,
|
cap_style=self.cap_style,
|
||||||
rotation=rotation
|
join_style=self.join_style,
|
||||||
)
|
single_sided=self.single_sided,
|
||||||
|
rotation=rotation
|
||||||
|
)
|
||||||
|
|
||||||
|
return eval_element_wise(_buf, paths)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_OffsetCurve(SliceNode):
|
class SLICENODE_OffsetCurve(SliceNode):
|
||||||
bl_label = "Offset Curve"
|
bl_label = "Offset Curve"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
join_style: str = NfEnumProperty(
|
join_style: str = NfEnumProperty(
|
||||||
name="Join Style",
|
name="Join Style",
|
||||||
|
|
@ -327,24 +350,29 @@ class SLICENODE_OffsetCurve(SliceNode):
|
||||||
assert layout
|
assert layout
|
||||||
layout.prop(self, "join_style")
|
layout.prop(self, "join_style")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
if socket_name != "Paths": return SlicePaths()
|
if socket_name != "Paths": return SlicePaths()
|
||||||
|
|
||||||
paths: SlicePaths = self.resolve("Paths")
|
paths = self.resolve("Paths")
|
||||||
distance: float = self.resolve("Distance") or 0.0
|
distance: float = self.resolve("Distance") or 0.0
|
||||||
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
if not paths: return SlicePaths()
|
if not paths: return SlicePaths()
|
||||||
|
|
||||||
return paths.offset_curve(
|
def _off(p: Any) -> SlicePaths:
|
||||||
distance,
|
if not p: return SlicePaths()
|
||||||
join_style=cast(Literal['round', 'mitre'], self.join_style),
|
return p.offset_curve(
|
||||||
rotation=rotation
|
distance,
|
||||||
)
|
join_style=cast(Literal['round', 'mitre'], self.join_style),
|
||||||
|
rotation=rotation
|
||||||
|
)
|
||||||
|
|
||||||
|
return eval_element_wise(_off, paths)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_PathOffset(SliceNode):
|
class SLICENODE_PathOffset(SliceNode):
|
||||||
bl_label = "Path Offset"
|
bl_label = "Path Offset"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Paths", "Paths")
|
self.add_input("SLICESOCK_Paths", "Paths")
|
||||||
|
|
@ -352,16 +380,21 @@ class SLICENODE_PathOffset(SliceNode):
|
||||||
self.add_output("SLICESOCK_Paths", "Paths")
|
self.add_output("SLICESOCK_Paths", "Paths")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
paths: SlicePaths = self.resolve("Paths")
|
paths = self.resolve("Paths")
|
||||||
offset: Any = self.resolve("Offset") or (0.0, 0.0, 0.0)
|
offset: Any = self.resolve("Offset") or (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
if not paths: return SlicePaths()
|
if not paths: return SlicePaths()
|
||||||
|
|
||||||
return paths.offset(np.array(offset, dtype=np.float64))
|
def _off(p: Any) -> SlicePaths:
|
||||||
|
if not p: return SlicePaths()
|
||||||
|
return p.offset(np.array(offset, dtype=np.float64))
|
||||||
|
|
||||||
|
return eval_element_wise(_off, paths)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_RotatePath(SliceNode):
|
class SLICENODE_RotatePath(SliceNode):
|
||||||
bl_label = "Rotate Path"
|
bl_label = "Rotate Path"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Paths", "Paths")
|
self.add_input("SLICESOCK_Paths", "Paths")
|
||||||
|
|
@ -370,19 +403,24 @@ class SLICENODE_RotatePath(SliceNode):
|
||||||
self.add_output("SLICESOCK_Paths", "Paths")
|
self.add_output("SLICESOCK_Paths", "Paths")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
paths: SlicePaths = self.resolve("Paths")
|
paths = self.resolve("Paths")
|
||||||
angle: float = self.resolve("Angle") or 0.0
|
angle: float = self.resolve("Angle") or 0.0
|
||||||
center: Any = self.resolve("Center")
|
center: Any = self.resolve("Center")
|
||||||
|
|
||||||
if not paths: return SlicePaths()
|
if not paths: return SlicePaths()
|
||||||
|
|
||||||
center_vec = np.array(center, dtype=np.float64) if center and any(center) else None
|
center_vec = np.array(center, dtype=np.float64) if center and any(center) else None
|
||||||
|
|
||||||
return paths.rotate(angle, center=center_vec)
|
def _rot(p: Any) -> SlicePaths:
|
||||||
|
if not p: return SlicePaths()
|
||||||
|
return p.rotate(angle, center=center_vec)
|
||||||
|
|
||||||
|
return eval_element_wise(_rot, paths)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_BufferShellsMultiple(SliceNode):
|
class SLICENODE_BufferShellsMultiple(SliceNode):
|
||||||
bl_label = "Buffer Shells Multiple"
|
bl_label = "Buffer Shells Multiple"
|
||||||
|
list_socket_mapping = [("Paths", ["Paths", "Outermost Path", "Innermost Path"])]
|
||||||
|
|
||||||
shape_option: str = NfEnumProperty(
|
shape_option: str = NfEnumProperty(
|
||||||
name="Miter",
|
name="Miter",
|
||||||
|
|
@ -407,8 +445,8 @@ class SLICENODE_BufferShellsMultiple(SliceNode):
|
||||||
assert layout
|
assert layout
|
||||||
layout.prop(self, "shape_option")
|
layout.prop(self, "shape_option")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
paths: SlicePaths = self.resolve("Paths")
|
paths = self.resolve("Paths")
|
||||||
base_distance_val = self.resolve("Distance")
|
base_distance_val = self.resolve("Distance")
|
||||||
base_distance: float = float(base_distance_val) if base_distance_val is not None else 0.0
|
base_distance: float = float(base_distance_val) if base_distance_val is not None else 0.0
|
||||||
width_step_val = self.resolve("Width Step")
|
width_step_val = self.resolve("Width Step")
|
||||||
|
|
@ -420,20 +458,25 @@ class SLICENODE_BufferShellsMultiple(SliceNode):
|
||||||
if not paths or count <= 0:
|
if not paths or count <= 0:
|
||||||
return SlicePaths()
|
return SlicePaths()
|
||||||
|
|
||||||
if socket_name == "Outermost Path":
|
def _buf_multi(p: Any) -> SlicePaths:
|
||||||
outer_dist = max(base_distance, base_distance + (count - 1) * width_step) if count > 1 else base_distance
|
if not p or count <= 0:
|
||||||
return paths.buffer(outer_dist, miter=self.shape_option, rotation=rotation)
|
return SlicePaths()
|
||||||
elif socket_name == "Innermost Path":
|
if socket_name == "Outermost Path":
|
||||||
inner_dist = min(base_distance, base_distance + (count - 1) * width_step) if count > 1 else base_distance
|
outer_dist = max(base_distance, base_distance + (count - 1) * width_step) if count > 1 else base_distance
|
||||||
return paths.buffer(inner_dist, miter=self.shape_option, rotation=rotation)
|
return p.buffer(outer_dist, miter=self.shape_option, rotation=rotation)
|
||||||
elif socket_name == "Paths":
|
elif socket_name == "Innermost Path":
|
||||||
return paths.buffer_multiple(base_distance, count, width_step, miter=self.shape_option, rotation=rotation)
|
inner_dist = min(base_distance, base_distance + (count - 1) * width_step) if count > 1 else base_distance
|
||||||
|
return p.buffer(inner_dist, miter=self.shape_option, rotation=rotation)
|
||||||
|
elif socket_name == "Paths":
|
||||||
|
return p.buffer_multiple(base_distance, count, width_step, miter=self.shape_option, rotation=rotation)
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
return SlicePaths()
|
return eval_element_wise(_buf_multi, paths)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_Infill(SliceNode):
|
class SLICENODE_Infill(SliceNode):
|
||||||
bl_label = "Infill"
|
bl_label = "Infill"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
infill_type: str = NfEnumProperty(
|
infill_type: str = NfEnumProperty(
|
||||||
name="Type",
|
name="Type",
|
||||||
|
|
@ -457,26 +500,31 @@ class SLICENODE_Infill(SliceNode):
|
||||||
assert layout
|
assert layout
|
||||||
layout.prop(self, "infill_type", text="")
|
layout.prop(self, "infill_type", text="")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
if socket_name != "Paths": return SlicePaths()
|
if socket_name != "Paths": return SlicePaths()
|
||||||
|
|
||||||
paths: SlicePaths = self.resolve("Paths")
|
paths = self.resolve("Paths")
|
||||||
density: float = self.resolve("Density") or 2.0
|
density: float = self.resolve("Density") or 2.0
|
||||||
infill_rotation: float = self.resolve("Infill Rotation") or 0.0
|
infill_rotation: float = self.resolve("Infill Rotation") or 0.0
|
||||||
rotation: Any = self.resolve("Layer Rotation") or (0.0, 0.0, 0.0)
|
rotation: Any = self.resolve("Layer Rotation") or (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
if not paths: return SlicePaths()
|
if not paths: return SlicePaths()
|
||||||
|
|
||||||
return paths.infill(
|
def _inf(p: Any) -> SlicePaths:
|
||||||
density,
|
if not p: return SlicePaths()
|
||||||
rotation_deg=infill_rotation,
|
return p.infill(
|
||||||
infill_type=self.infill_type, # pyright: ignore[reportOptionalMemberAccess]
|
density,
|
||||||
layer_rotation=rotation
|
rotation_deg=infill_rotation,
|
||||||
)
|
infill_type=self.infill_type,
|
||||||
|
layer_rotation=rotation
|
||||||
|
)
|
||||||
|
|
||||||
|
return eval_element_wise(_inf, paths)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_OverhangMask(SliceNode):
|
class SLICENODE_OverhangMask(SliceNode):
|
||||||
bl_label = "Overhang Mask"
|
bl_label = "Overhang Mask"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Paths")
|
self.add_input("SLICESOCK_Paths")
|
||||||
|
|
@ -486,15 +534,19 @@ class SLICENODE_OverhangMask(SliceNode):
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
if socket_name != "Paths": return SlicePaths()
|
if socket_name != "Paths": return SlicePaths()
|
||||||
|
|
||||||
paths: SlicePaths = self.resolve("Paths")
|
paths = self.resolve("Paths")
|
||||||
tolerance: float = self.resolve("Tolerance") or 0.05
|
tolerance: float = self.resolve("Tolerance") or 0.05
|
||||||
|
|
||||||
if not paths: return SlicePaths()
|
if not paths: return SlicePaths()
|
||||||
|
|
||||||
return paths.get_overhang_mask(tolerance=tolerance)
|
def _mask(p: Any) -> SlicePaths:
|
||||||
|
if not p: return SlicePaths()
|
||||||
|
return p.get_overhang_mask(tolerance=tolerance)
|
||||||
|
|
||||||
|
return eval_element_wise(_mask, paths)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_PathOperations(SliceNodeJoiner):
|
class SLICENODE_PathOperations(SliceNodeJoiner):
|
||||||
|
|
@ -522,37 +574,56 @@ class SLICENODE_PathOperations(SliceNodeJoiner):
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
if self.inputs:
|
||||||
|
first_inp = self.inputs[0]
|
||||||
|
is_list = is_socket_linked_to_list(first_inp)
|
||||||
|
set_socket_shape(first_inp, "LIST" if is_list else "CIRCLE")
|
||||||
|
if self.outputs:
|
||||||
|
set_socket_shape(self.outputs[0], "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
assert layout
|
assert layout
|
||||||
layout.prop(self, "operation", text="")
|
layout.prop(self, "operation", text="")
|
||||||
if self.operation in ['MIN_COVERAGE', 'INTERSECTION', 'SUBTRACT']:
|
if self.operation in ['MIN_COVERAGE', 'INTERSECTION', 'SUBTRACT']:
|
||||||
layout.prop(self, "connect_along_boundary")
|
layout.prop(self, "connect_along_boundary")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
from ....infra.blender.blender_slice_nodes_generic import resolve_input
|
from ....infra.blender.blender_slice_nodes_generic import resolve_input
|
||||||
|
|
||||||
inputs_data: list[SlicePaths] = []
|
if not self.inputs:
|
||||||
for input_socket in self.inputs:
|
|
||||||
if hasattr(input_socket, "is_linked") and not input_socket.is_linked:
|
|
||||||
continue
|
|
||||||
paths: Any = resolve_input(input_socket)
|
|
||||||
if isinstance(paths, SlicePaths) and len(paths) > 0:
|
|
||||||
inputs_data.append(paths)
|
|
||||||
|
|
||||||
if not inputs_data:
|
|
||||||
return SlicePaths()
|
return SlicePaths()
|
||||||
|
|
||||||
output = SlicePaths.path_operations(
|
first_val = resolve_input(self.inputs[0]) if (self.inputs[0].is_linked or getattr(self.inputs[0], 'value', None)) else None
|
||||||
inputs_data,
|
other_paths: list[SlicePaths] = []
|
||||||
operation=self.operation,
|
for input_socket in self.inputs[1:]:
|
||||||
connect_along_boundary=self.connect_along_boundary
|
if getattr(input_socket, "is_linked", False):
|
||||||
)
|
val = resolve_input(input_socket)
|
||||||
return output
|
if isinstance(val, SlicePaths) and len(val) > 0:
|
||||||
|
other_paths.append(val)
|
||||||
|
|
||||||
|
def _run_op(primary_path: Any) -> SlicePaths:
|
||||||
|
inputs_data = [p for p in [primary_path] + other_paths if isinstance(p, SlicePaths) and len(p) > 0]
|
||||||
|
if not inputs_data:
|
||||||
|
return SlicePaths()
|
||||||
|
return SlicePaths.path_operations(
|
||||||
|
inputs_data,
|
||||||
|
operation=self.operation,
|
||||||
|
connect_along_boundary=self.connect_along_boundary
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_list_data(first_val):
|
||||||
|
return [_run_op(item) for item in first_val]
|
||||||
|
elif isinstance(first_val, SlicePaths) or other_paths:
|
||||||
|
return _run_op(first_val)
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_SimplifyPaths(SliceNode):
|
class SLICENODE_SimplifyPaths(SliceNode):
|
||||||
bl_label = "Simplify Paths"
|
bl_label = "Simplify Paths"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
preserve_topology: bool = NfBoolProperty(
|
preserve_topology: bool = NfBoolProperty(
|
||||||
name="Preserve Topology",
|
name="Preserve Topology",
|
||||||
|
|
@ -569,21 +640,27 @@ class SLICENODE_SimplifyPaths(SliceNode):
|
||||||
assert layout
|
assert layout
|
||||||
layout.prop(self, "preserve_topology")
|
layout.prop(self, "preserve_topology")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
if socket_name != "Paths":
|
if socket_name != "Paths":
|
||||||
return SlicePaths()
|
return SlicePaths()
|
||||||
|
|
||||||
paths: SlicePaths = self.resolve("Paths")
|
paths = 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
|
||||||
|
|
||||||
return paths.simplify(tolerance, preserve_topology=self.preserve_topology)
|
def _simp(p: Any) -> SlicePaths:
|
||||||
|
if not p or tolerance <= 0:
|
||||||
|
return p
|
||||||
|
return p.simplify(tolerance, preserve_topology=self.preserve_topology)
|
||||||
|
|
||||||
|
return eval_element_wise(_simp, paths)
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_SeamPlacement(SliceNode):
|
class SLICENODE_SeamPlacement(SliceNode):
|
||||||
bl_label = "Seam Placement"
|
bl_label = "Seam Placement"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
mode: str = NfEnumProperty(
|
mode: str = NfEnumProperty(
|
||||||
name="Mode",
|
name="Mode",
|
||||||
|
|
@ -614,11 +691,11 @@ class SLICENODE_SeamPlacement(SliceNode):
|
||||||
layout.prop(self, "mode", text="")
|
layout.prop(self, "mode", text="")
|
||||||
layout.prop(self, "acquire_transforms")
|
layout.prop(self, "acquire_transforms")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
if socket_name != "Paths":
|
if socket_name != "Paths":
|
||||||
return SlicePaths()
|
return SlicePaths()
|
||||||
|
|
||||||
paths: SlicePaths = self.resolve("Paths")
|
paths = self.resolve("Paths")
|
||||||
mesh_input: Any = self.resolve("Mesh")
|
mesh_input: Any = self.resolve("Mesh")
|
||||||
stagger_val = self.resolve("Seam Staggering")
|
stagger_val = self.resolve("Seam Staggering")
|
||||||
stagger_mm: float = float(stagger_val) if stagger_val is not None else 0.0
|
stagger_mm: float = float(stagger_val) if stagger_val is not None else 0.0
|
||||||
|
|
@ -626,10 +703,15 @@ class SLICENODE_SeamPlacement(SliceNode):
|
||||||
if not paths:
|
if not paths:
|
||||||
return SlicePaths()
|
return SlicePaths()
|
||||||
|
|
||||||
if not mesh_input and abs(stagger_mm) < 1e-6:
|
|
||||||
return paths
|
|
||||||
|
|
||||||
acquire_t = bool(getattr(self, "acquire_transforms", True))
|
acquire_t = bool(getattr(self, "acquire_transforms", True))
|
||||||
return paths.place_seams(mesh_input, mode=self.mode.lower(), acquire_transforms=acquire_t, stagger_mm=stagger_mm)
|
|
||||||
|
def _place(p: Any) -> SlicePaths:
|
||||||
|
if not p:
|
||||||
|
return SlicePaths()
|
||||||
|
if not mesh_input and abs(stagger_mm) < 1e-6:
|
||||||
|
return p
|
||||||
|
return p.place_seams(mesh_input, mode=self.mode.lower(), acquire_transforms=acquire_t, stagger_mm=stagger_mm)
|
||||||
|
|
||||||
|
return eval_element_wise(_place, paths)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,13 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
self.add_input("NodeSocketString", "Name")
|
self.add_input("NodeSocketString", "Name")
|
||||||
self.add_multi_socket_group("NodeSocketString", name="Gcode")
|
self.add_multi_socket_group("NodeSocketString", name="Gcode")
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
for inp in self.inputs:
|
||||||
|
if getattr(inp, "name", "") == "Gcode" or getattr(inp, "identifier", "").startswith("Gcode_ms_in_"):
|
||||||
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
super().draw_buttons(context, layout)
|
super().draw_buttons(context, layout)
|
||||||
layout.prop(self, 'show_e_0')
|
layout.prop(self, 'show_e_0')
|
||||||
|
|
@ -229,7 +236,9 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
mod["Socket_9"] = tuple(map(float, prep_scale))
|
mod["Socket_9"] = tuple(map(float, prep_scale))
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
def evaluate(self, operator=None):
|
||||||
gcodes: list[str] = [g for g in (self.resolve_multi_input("Gcode") or []) if g]
|
from ....infra.blender.list_utils import flatten_list
|
||||||
|
raw_gcodes = self.resolve_multi_input("Gcode") or []
|
||||||
|
gcodes: list[str] = [str(g) for g in flatten_list(raw_gcodes) if g]
|
||||||
base_mesh_name: str = self.resolve("Name")
|
base_mesh_name: str = self.resolve("Name")
|
||||||
|
|
||||||
if not base_mesh_name:
|
if not base_mesh_name:
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,11 @@ class SLICENODE_PrinterList(ExecSliceNode):
|
||||||
if not gcode:
|
if not gcode:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
from ....infra.blender.list_utils import is_list_data
|
||||||
|
if is_list_data(gcode):
|
||||||
|
if operator: operator.report({'ERROR'}, f"Printer List does not accept a list of Gcode on socket {sock.name}")
|
||||||
|
continue
|
||||||
|
|
||||||
p_data = printer_configs[sock.name]
|
p_data = printer_configs[sock.name]
|
||||||
host = str(p_data.get("ip", "localhost"))
|
host = str(p_data.get("ip", "localhost"))
|
||||||
port = int(p_data.get("port", 80))
|
port = int(p_data.get("port", 80))
|
||||||
|
|
|
||||||
|
|
@ -121,12 +121,23 @@ def depsgraph_update_handler(scene, depsgraph):
|
||||||
# Sync SLICENODE_Group sockets if interface changed and migrate legacy sockets
|
# Sync SLICENODE_Group sockets if interface changed and migrate legacy sockets
|
||||||
for tree in bpy.data.node_groups:
|
for tree in bpy.data.node_groups:
|
||||||
if tree.bl_idname == "SLICETREE_NodeTree":
|
if tree.bl_idname == "SLICETREE_NodeTree":
|
||||||
|
try:
|
||||||
|
from ...infra.blender.list_utils import sync_group_tree_internal_sockets
|
||||||
|
sync_group_tree_internal_sockets(tree)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
for node in tree.nodes:
|
for node in tree.nodes:
|
||||||
if node.bl_idname == "SLICENODE_Group" and hasattr(node, "sync_sockets"):
|
if node.bl_idname == "SLICENODE_Group":
|
||||||
try:
|
if hasattr(node, "sync_sockets"):
|
||||||
node.sync_sockets()
|
try:
|
||||||
except Exception:
|
node.sync_sockets()
|
||||||
pass
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif hasattr(node, "sync_socket_shapes"):
|
||||||
|
try:
|
||||||
|
node.sync_socket_shapes()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
elif node.bl_idname == "SLICENODE_CollectionObjects" and hasattr(node, "_update_outputs"):
|
elif node.bl_idname == "SLICENODE_CollectionObjects" and hasattr(node, "_update_outputs"):
|
||||||
try:
|
try:
|
||||||
node._update_outputs()
|
node._update_outputs()
|
||||||
|
|
@ -149,9 +160,35 @@ class SLICETREE_NodeTree(NodeTree):
|
||||||
|
|
||||||
def valid_link(self, from_socket: Any, to_socket: Any) -> bool:
|
def valid_link(self, from_socket: Any, to_socket: Any) -> bool:
|
||||||
geom_sockets = {"SLICESOCK_Mesh", "SLICESOCK_Paths", "SLICESOCK_Geometry"}
|
geom_sockets = {"SLICESOCK_Mesh", "SLICESOCK_Paths", "SLICESOCK_Geometry"}
|
||||||
if from_socket.bl_idname in geom_sockets and to_socket.bl_idname in geom_sockets:
|
compatible = (
|
||||||
return True
|
(from_socket.bl_idname in geom_sockets and to_socket.bl_idname in geom_sockets)
|
||||||
return from_socket.bl_idname == to_socket.bl_idname
|
or from_socket.bl_idname == to_socket.bl_idname
|
||||||
|
)
|
||||||
|
if not compatible:
|
||||||
|
return False
|
||||||
|
|
||||||
|
from ...infra.blender.list_utils import is_output_socket_a_list, group_input_can_accept_list
|
||||||
|
if is_output_socket_a_list(from_socket):
|
||||||
|
to_node = getattr(to_socket, "node", None)
|
||||||
|
if to_node is not None:
|
||||||
|
# Start Print and Printer List reject lists
|
||||||
|
if to_node.bl_idname in ("SLICENODE_StartPrint", "SLICENODE_PrinterList"):
|
||||||
|
return False
|
||||||
|
# Path Operations: only input 0 can accept a list; secondary inputs reject lists
|
||||||
|
if to_node.bl_idname == "SLICENODE_PathOperations":
|
||||||
|
if getattr(to_node, "inputs", None) and to_socket != to_node.inputs[0]:
|
||||||
|
return False
|
||||||
|
# Seam Placement: secondary input (Mesh) rejects lists
|
||||||
|
if to_node.bl_idname == "SLICENODE_SeamPlacement":
|
||||||
|
if getattr(to_socket, "name", "") == "Mesh":
|
||||||
|
return False
|
||||||
|
# Group node: check if input can accept list
|
||||||
|
if to_node.bl_idname == "SLICENODE_Group":
|
||||||
|
tree = getattr(to_node, "slicer_tree", None) or getattr(to_node, "node_tree", None)
|
||||||
|
if tree and not group_input_can_accept_list(tree, to_socket):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
@ -590,6 +627,11 @@ class SLICENODE_MT_Joiners(Menu):
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
assert layout
|
assert layout
|
||||||
|
layout.operator("node.add_node", text="Objects to List").type = "SLICENODE_ObjectsToList"
|
||||||
|
layout.operator("node.add_node", text="Meshes to List").type = "SLICENODE_MeshesToList"
|
||||||
|
layout.operator("node.add_node", text="Paths to List").type = "SLICENODE_PathsToList"
|
||||||
|
layout.operator("node.add_node", text="Gcodes to List").type = "SLICENODE_GcodesToList"
|
||||||
|
layout.separator()
|
||||||
layout.operator("node.add_node", text="Join Mesh Parts").type = "SLICENODE_JoinMesh"
|
layout.operator("node.add_node", text="Join Mesh Parts").type = "SLICENODE_JoinMesh"
|
||||||
layout.operator("node.add_node", text="Join Mesh Groups").type = "SLICENODE_JoinMeshGroups"
|
layout.operator("node.add_node", text="Join Mesh Groups").type = "SLICENODE_JoinMeshGroups"
|
||||||
layout.operator("node.add_node", text="Path Operations").type = "SLICENODE_PathOperations"
|
layout.operator("node.add_node", text="Path Operations").type = "SLICENODE_PathOperations"
|
||||||
|
|
|
||||||
|
|
@ -1060,6 +1060,7 @@ def shift_path_seam_by_distance(
|
||||||
|
|
||||||
|
|
||||||
class SlicePaths(list[np.ndarray]):
|
class SlicePaths(list[np.ndarray]):
|
||||||
|
is_slice_paths: bool = True
|
||||||
groups: list[int]
|
groups: list[int]
|
||||||
extrusion_rate: list[np.ndarray]
|
extrusion_rate: list[np.ndarray]
|
||||||
feedrate: list[np.ndarray]
|
feedrate: list[np.ndarray]
|
||||||
|
|
|
||||||
|
|
@ -72,3 +72,9 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
||||||
- `get_last_debug_dump()` / `get_last_debug_text()` / `load_last_inputs()`: Utilities to review and reload the most recent execution dump for debugging.
|
- `get_last_debug_dump()` / `get_last_debug_text()` / `load_last_inputs()`: Utilities to review and reload the most recent execution dump for debugging.
|
||||||
- `debug_session()`: Context manager enabling execution capture across node evaluations.
|
- `debug_session()`: Context manager enabling execution capture across node evaluations.
|
||||||
- `SLICENODE_OT_DebugDump`: Blender operator (`slicenode.debug_dump`) to dump the active node's inputs on demand.
|
- `SLICENODE_OT_DebugDump`: Blender operator (`slicenode.debug_dump`) to dump the active node's inputs on demand.
|
||||||
|
|
||||||
|
- `list_utils.py`:
|
||||||
|
- `is_list_data()`: Type discrimination distinguishing list/tuple collections from atomic objects (such as `SlicePaths`).
|
||||||
|
- `flatten_list()`: Recursive flattener for multi-input joiners and batch sinks.
|
||||||
|
- `eval_element_wise()`: Vectorized applicator executing operations element-wise on lists or directly on single items.
|
||||||
|
- `set_socket_shape()` / `is_socket_linked_to_list()` / `is_output_socket_a_list()` / `sync_node_list_sockets()`: Dynamic socket visual appearance management (`LIST` vs `CIRCLE`).
|
||||||
|
|
|
||||||
12
doc/NODES.md
12
doc/NODES.md
|
|
@ -168,10 +168,14 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
- **Load Gcode**: Reads a G-code file from disk.
|
- **Load Gcode**: Reads a G-code file from disk.
|
||||||
|
|
||||||
### Utility & Joiners
|
### Utility & Joiners
|
||||||
- **Join Mesh Parts**: Merges multiple `SlicingObject` instances into one `SlicingCollection`.
|
- **Objects to List**: Multi-input list joiner that bundles objects into a `list[bpy.types.Object]` with LIST socket shape. Flattens incoming lists.
|
||||||
- **Join Mesh Groups**: Combines multiple `SlicingCollection` instances into a `SlicingGroup`.
|
- **Meshes to List**: Multi-input list joiner that bundles meshes into a `list[SlicingCollection]` with LIST socket shape. Flattens incoming lists.
|
||||||
- **Path Operations**: Multi-input joiner performing Zip, Extend, Maximum coverage, Minimum coverage, Intersection, Subtract, and XOR operations.
|
- **Paths to List**: Multi-input list joiner that bundles toolpaths into a `list[SlicePaths]` with LIST socket shape. Flattens incoming lists.
|
||||||
- **Join Gcode**: Concatenates multiple G-code strings.
|
- **Gcodes to List**: Multi-input list joiner that bundles G-code strings into a `list[str]` with LIST socket shape. Flattens incoming lists.
|
||||||
|
- **Join Mesh Parts**: Merges multiple `SlicingObject` instances into one `SlicingCollection`. Flattens incoming lists.
|
||||||
|
- **Join Mesh Groups**: Combines multiple `SlicingCollection` instances into a `SlicingGroup`. Flattens incoming lists.
|
||||||
|
- **Path Operations**: Multi-input joiner performing Zip, Extend, Maximum coverage, Minimum coverage, Intersection, Subtract, and XOR operations. The first input can be a list, in which case the operation runs per-element against the same secondary inputs.
|
||||||
|
- **Join Gcode**: Concatenates multiple G-code inputs. If no input is a list, concatenates scalar strings into a single string (CIRCLE). If one or more inputs are lists (which must all be the same length), outputs a list of strings (LIST) where each element at index $i$ is joined with static elements broadcast across all indices.
|
||||||
- **Join Settings**: Sequentially merges multiple override dictionaries. Later inputs overwrite earlier ones for matching keys.
|
- **Join Settings**: Sequentially merges multiple override dictionaries. Later inputs overwrite earlier ones for matching keys.
|
||||||
- **Join Execution**: Synchronizes sequential execution flows.
|
- **Join Execution**: Synchronizes sequential execution flows.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
Type,Category,Node Name,Class Name,File,Archetype,First Input Socket,Other Input Sockets,Output Socket(s),First Input Shape,Output Shape,Single Item Behavior,List Behavior
|
Type,Category,Node Name,Class Name,File,Archetype,First Input Socket,Other Input Sockets,Output Socket(s),First Input Shape,Output Shape,Single Item Behavior,List Behavior
|
||||||
Object,List Joiners (New),Objects to List,SLICENODE_ObjectsToList,core/nodeslicer/nodes/inputs.py,List Joiner,Multi-input NodeSocketObject (Object),None,NodeSocketObject (Objects),Standard / LIST,Fixed LIST,Wraps single item into a 1-element list [Obj1],Flattens incoming lists and combines all objects into list[bpy.types.Object]. Output appearance is LIST.
|
Object,List Joiners (New),Objects to List,SLICENODE_ObjectsToList,core/nodeslicer/nodes/inputs.py,List Joiner,Multi-input NodeSocketObject (Object),None,NodeSocketObject (Objects),Standard / LIST,Fixed LIST,Wraps single item into a 1-element list [Obj1],Flattens incoming lists and combines all objects into list[bpy.types.Object]. Output appearance is LIST.
|
||||||
Mesh,List Joiners (New),Meshes to List,SLICENODE_MeshesToList,core/nodeslicer/nodes/mesh.py,List Joiner,Multi-input SLICESOCK_Mesh (Mesh),None,SLICESOCK_Mesh (Meshes),Standard / LIST,Fixed LIST,Wraps single mesh into a 1-element list [Mesh1],Flattens incoming lists and bundles all meshes into list[SlicingCollection]. Output appearance is LIST.
|
Mesh,List Joiners (New),Meshes to List,SLICENODE_MeshesToList,core/nodeslicer/nodes/mesh.py,List Joiner,Multi-input SLICESOCK_Mesh (Mesh),None,SLICESOCK_Mesh (Meshes),Standard / LIST,Fixed LIST,Wraps single mesh into a 1-element list [Mesh1],Flattens incoming lists and bundles all meshes into list[SlicingCollection]. Output appearance is LIST.
|
||||||
Path,List Joiners (New),Paths to List,SLICENODE_PathsToList,core/nodeslicer/nodes/path.py,List Joiner,Multi-input SLICESOCK_Paths (Paths),None,SLICESOCK_Paths (Paths),Standard / LIST,Fixed LIST,Wraps single path into a 1-element list [Path1],Flattens incoming lists and bundles all toolpaths into list[SlicePaths]. Output appearance is LIST.
|
Path,List Joiners (New),Paths to List,SLICENODE_PathsToList,core/nodeslicer/nodes/path.py,List Joiner,Multi-input SLICESOCK_Paths (Paths),None,SLICESOCK_Paths (Paths),Standard / LIST,Fixed LIST,Wraps single path into a 1-element list [Path1],Flattens incoming lists and bundles all toolpaths into list[SlicePaths]. Output appearance is LIST.
|
||||||
|
|
@ -33,8 +33,8 @@ Path,G-code Processing,Paths to Gcode,SLICENODE_PathsToGcode,core/nodeslicer/nod
|
||||||
Gcode,G-code Processing,Gcode Regex Replace,SLICENODE_GcodeRegexReplace,core/nodeslicer/nodes/gcode.py,Element-wise Operator,NodeSocketString (Gcode),"Find, Replace (cannot be list)",NodeSocketString (Gcode),Dynamic (Changes to LIST if input is list),Dynamic (Changes to LIST if input is list),Runs regex find & replace on G-code string,"First input is list [G1, G2, ...]. Runs regex search and replace on each G-code string using the same pattern. Output is [G1', G2', ...]. Sockets switch to LIST."
|
Gcode,G-code Processing,Gcode Regex Replace,SLICENODE_GcodeRegexReplace,core/nodeslicer/nodes/gcode.py,Element-wise Operator,NodeSocketString (Gcode),"Find, Replace (cannot be list)",NodeSocketString (Gcode),Dynamic (Changes to LIST if input is list),Dynamic (Changes to LIST if input is list),Runs regex find & replace on G-code string,"First input is list [G1, G2, ...]. Runs regex search and replace on each G-code string using the same pattern. Output is [G1', G2', ...]. Sockets switch to LIST."
|
||||||
Gcode,Action & I/O,Gcode to File,SLICENODE_GcodeToFile,core/nodeslicer/nodes/output.py,Batch Sink,NodeSocketString (Gcode) [Can be a list or multi-input],"Folder, Name (cannot be list)",Execution Link,Dynamic (Displays LIST if input is list),Standard Execution Link,Saves G-code string to .gcode or .bgcode file on disk,"First input is list [G1, G2, ...]. Saves a separate file for each element using the same folder/name template with zero-padded numbering (Part00.gcode, Part01.gcode) or metadata names."
|
Gcode,Action & I/O,Gcode to File,SLICENODE_GcodeToFile,core/nodeslicer/nodes/output.py,Batch Sink,NodeSocketString (Gcode) [Can be a list or multi-input],"Folder, Name (cannot be list)",Execution Link,Dynamic (Displays LIST if input is list),Standard Execution Link,Saves G-code string to .gcode or .bgcode file on disk,"First input is list [G1, G2, ...]. Saves a separate file for each element using the same folder/name template with zero-padded numbering (Part00.gcode, Part01.gcode) or metadata names."
|
||||||
Gcode,Action & I/O,Gcode to Text Block,SLICENODE_GcodeToTextBlock,core/nodeslicer/nodes/output.py,Batch Sink,NodeSocketString (Gcode) [Can be a list],Text Block Name (cannot be list),Execution Link,Dynamic (Displays LIST if input is list),Standard Execution Link,Writes G-code string to an active Blender text editor data-block,"First input is list [G1, G2, ...]. Writes separate numbered Blender text blocks (NodeSlicer_00.gcode, NodeSlicer_01.gcode) using the base name."
|
Gcode,Action & I/O,Gcode to Text Block,SLICENODE_GcodeToTextBlock,core/nodeslicer/nodes/output.py,Batch Sink,NodeSocketString (Gcode) [Can be a list],Text Block Name (cannot be list),Execution Link,Dynamic (Displays LIST if input is list),Standard Execution Link,Writes G-code string to an active Blender text editor data-block,"First input is list [G1, G2, ...]. Writes separate numbered Blender text blocks (NodeSlicer_00.gcode, NodeSlicer_01.gcode) using the base name."
|
||||||
Gcode,Action & I/O,Start Print,SLICENODE_StartPrint,core/nodeslicer/nodes/output.py,Batch Sink,NodeSocketString (Gcode) [Can be a list],SLICESOCK_Printer (cannot be list),Execution Link,Dynamic (Displays LIST if input is list),Standard Execution Link,Uploads and starts print on network printer,"First input is list [G1, G2, ...]. Sequentially queues and sends each G-code print job to the target printer using the same printer configuration."
|
Gcode,Action & I/O,Start Print,SLICENODE_StartPrint,core/nodeslicer/nodes/output.py,Single Item Sink,NodeSocketString (Gcode) [Single item only],SLICESOCK_Printer (Single item only),Execution Link,Standard (CIRCLE),Standard Execution Link,Uploads and starts print on network printer,List is not accepted. Sockets reject list connections and evaluate() reports an error if a list is received.
|
||||||
Gcode,Action & I/O,Baserow Submit,SLICENODE_BaserowSubmit,core/nodeslicer/nodes/output.py,Batch Sink,NodeSocketString (Gcode) [Can be a list or multi-input],"Settings, Host, Token, Table ID, Record ID (cannot be list)",Execution Link,Dynamic (Displays LIST if input is list),Standard Execution Link,Uploads G-code attachment and creates/updates row in Baserow database,"First input is list [G1, G2, ...]. Submits each G-code as a separate record and attachment in Baserow using the same database credentials."
|
Gcode,Action & I/O,Baserow Submit,SLICENODE_BaserowSubmit,core/nodeslicer/nodes/output.py,Batch Sink,NodeSocketString (Gcode) [Can be a list or multi-input],"Settings, Host, Token, Table ID, Record ID (cannot be list)",Execution Link,Dynamic (Displays LIST if input is list),Standard Execution Link,Uploads G-code attachment and creates/updates row in Baserow database,"First input is list [G1, G2, ...]. Submits each G-code as a separate record and attachment in Baserow using the same database credentials."
|
||||||
Gcode,Action & I/O,Airtable Submit,SLICENODE_AirtableSubmit,core/nodeslicer/nodes/output.py,Batch Sink,NodeSocketString (Gcode) [Can be a list or multi-input],"Settings, PAT File, Base ID, Table ID, Record ID (cannot be list)",Execution Link,Dynamic (Displays LIST if input is list),Standard Execution Link,Uploads G-code attachment and creates/updates row in Airtable base,"First input is list [G1, G2, ...]. Submits each G-code as a separate record and attachment in Airtable using the same database credentials."
|
Gcode,Action & I/O,Airtable Submit,SLICENODE_AirtableSubmit,core/nodeslicer/nodes/output.py,Batch Sink,NodeSocketString (Gcode) [Can be a list or multi-input],"Settings, PAT File, Base ID, Table ID, Record ID (cannot be list)",Execution Link,Dynamic (Displays LIST if input is list),Standard Execution Link,Uploads G-code attachment and creates/updates row in Airtable base,"First input is list [G1, G2, ...]. Submits each G-code as a separate record and attachment in Airtable using the same database credentials."
|
||||||
Gcode,Action & I/O,Gcode to Preview Mesh,SLICENODE_GcodeToMesh,core/nodeslicer/nodes/preview.py,Batch Sink,NodeSocketString (Gcode) [Can be a list or multi-input],"Name, show_e_0 (cannot be list)",Execution Link,Dynamic (Displays LIST if input is list),Standard Execution Link,Decodes toolpaths and reconstructs 3D wireframe preview mesh object in Blender,"First input is list [G1, G2, ...]. Generates a separate preview mesh object in the scene (Preview00, Preview01, ...) for each G-code in the list."
|
Gcode,Action & I/O,Gcode to Preview Mesh,SLICENODE_GcodeToMesh,core/nodeslicer/nodes/preview.py,Batch Sink,NodeSocketString (Gcode) [Can be a list or multi-input],"Name, show_e_0 (cannot be list)",Execution Link,Dynamic (Displays LIST if input is list),Standard Execution Link,Decodes toolpaths and reconstructs 3D wireframe preview mesh object in Blender,"First input is list [G1, G2, ...]. Generates a separate preview mesh object in the scene (Preview00, Preview01, ...) for each G-code in the list."
|
||||||
Gcode,Action & I/O,Printer List,SLICENODE_PrinterList,core/nodeslicer/nodes/system.py,Batch Sink,Dynamic G-code sockets per printer [Can be a list],Config File JSON (cannot be list),Execution Link,Dynamic (Displays LIST if input is list),Standard Execution Link,Sends G-code to corresponding printer defined in config JSON,"If a printer socket receives a list [G1, G2, ...], queues and dispatches all jobs for that printer."
|
Gcode,Action & I/O,Printer List,SLICENODE_PrinterList,core/nodeslicer/nodes/system.py,Single Item Sink,Dynamic G-code sockets per printer [Single item only],Config File JSON (cannot be list),Execution Link,Standard (CIRCLE),Standard Execution Link,Sends G-code to corresponding printer defined in config JSON,List is not accepted. Sockets reject list connections and evaluate() ignores list inputs.
|
||||||
|
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ from bpy.types import NodeSocket, Node
|
||||||
|
|
||||||
class NodeException(Exception): ...
|
class NodeException(Exception): ...
|
||||||
|
|
||||||
|
from .list_utils import is_list_data, flatten_list, eval_element_wise, set_socket_shape, is_socket_linked_to_list, sync_node_list_sockets
|
||||||
|
|
||||||
_EvalCache = dict[Any, Any]
|
_EvalCache = dict[Any, Any]
|
||||||
_VisitedSet = set[Any]
|
_VisitedSet = set[Any]
|
||||||
|
|
||||||
|
|
@ -116,8 +118,17 @@ class SliceNode(Node, AutoBlIdName):
|
||||||
return key in self._data
|
return key in self._data
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
list_socket_mapping: list[tuple[str, list[str] | str]] = []
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
self._update_multi_groups()
|
self._update_multi_groups()
|
||||||
|
self._sync_list_socket_shapes()
|
||||||
|
|
||||||
|
def _sync_list_socket_shapes(self) -> None:
|
||||||
|
mapping = getattr(self, "list_socket_mapping", None)
|
||||||
|
if mapping:
|
||||||
|
from .list_utils import sync_node_list_sockets
|
||||||
|
sync_node_list_sockets(self, mapping)
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ...
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ...
|
||||||
|
|
||||||
|
|
@ -475,6 +486,21 @@ class SLICENODE_Group(bpy.types.NodeCustomGroup, AutoBlIdName):
|
||||||
# 3. Sync Outputs
|
# 3. Sync Outputs
|
||||||
self._sync_socket_collection(self.outputs, expected_outputs)
|
self._sync_socket_collection(self.outputs, expected_outputs)
|
||||||
|
|
||||||
|
# 4. Sync Socket Shapes (LIST vs CIRCLE)
|
||||||
|
self.sync_socket_shapes()
|
||||||
|
|
||||||
|
def sync_socket_shapes(self):
|
||||||
|
try:
|
||||||
|
if not self.name or not self.slicer_tree:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
from .list_utils import sync_group_node_socket_shapes
|
||||||
|
sync_group_node_socket_shapes(self)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self.sync_socket_shapes()
|
||||||
|
|
||||||
def _sync_socket_collection(self, collection, expected):
|
def _sync_socket_collection(self, collection, expected):
|
||||||
# Remove sockets that shouldn't be there
|
# Remove sockets that shouldn't be there
|
||||||
expected_ids = {e[0] for e in expected}
|
expected_ids = {e[0] for e in expected}
|
||||||
|
|
|
||||||
563
infra/blender/list_utils.py
Normal file
563
infra/blender/list_utils.py
Normal file
|
|
@ -0,0 +1,563 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
LIST_CAPABLE_SOCKET_TYPES = {
|
||||||
|
"SLICESOCK_Mesh",
|
||||||
|
"SLICESOCK_Paths",
|
||||||
|
"SLICESOCK_Object",
|
||||||
|
"SLICESOCK_Geometry",
|
||||||
|
"NodeSocketObject",
|
||||||
|
"NodeSocketString",
|
||||||
|
}
|
||||||
|
|
||||||
|
def is_list_data(val: Any) -> bool:
|
||||||
|
"""Checks if val represents a list of items (Object, Mesh, Path, Gcode).
|
||||||
|
Note: SlicePaths is a list subclass representing an individual toolpath collection,
|
||||||
|
not a list of toolpaths, so it is treated as an atomic item (not list data)."""
|
||||||
|
if val is None or isinstance(val, (str, bytes)):
|
||||||
|
return False
|
||||||
|
if type(val).__name__ == "SlicePaths" or getattr(val, "is_slice_paths", False):
|
||||||
|
return False
|
||||||
|
return isinstance(val, (list, tuple))
|
||||||
|
|
||||||
|
def flatten_list(items: Any) -> list[Any]:
|
||||||
|
"""Recursively flattens nested lists/tuples, keeping atomic items intact."""
|
||||||
|
result: list[Any] = []
|
||||||
|
if not is_list_data(items):
|
||||||
|
if items is not None:
|
||||||
|
result.append(items)
|
||||||
|
return result
|
||||||
|
for item in items:
|
||||||
|
if is_list_data(item):
|
||||||
|
result.extend(flatten_list(item))
|
||||||
|
elif item is not None:
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def eval_element_wise(op_func: Callable[[Any], Any], data: Any) -> Any:
|
||||||
|
"""If data is a list, applies op_func to each element and returns a list.
|
||||||
|
Otherwise applies op_func to data directly."""
|
||||||
|
if is_list_data(data):
|
||||||
|
return [op_func(item) for item in data]
|
||||||
|
return op_func(data)
|
||||||
|
|
||||||
|
def set_socket_shape(sock: Any, shape: str) -> None:
|
||||||
|
"""Safely updates socket display_shape property ('LIST', 'CIRCLE', etc.)."""
|
||||||
|
if sock is not None:
|
||||||
|
try:
|
||||||
|
sock.display_shape = shape
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _follow_reroute_upstream(sock: Any, visited: set | None = None) -> Any:
|
||||||
|
"""Follows reroute nodes backwards to find the original source output socket."""
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
current = sock
|
||||||
|
while (
|
||||||
|
current is not None
|
||||||
|
and getattr(current, "node", None) is not None
|
||||||
|
and getattr(current.node, "bl_idname", "") == "NodeReroute"
|
||||||
|
and getattr(current.node, "inputs", None)
|
||||||
|
and current.node.inputs[0].is_linked
|
||||||
|
and current.node.inputs[0].links
|
||||||
|
):
|
||||||
|
ptr = current.as_pointer() if hasattr(current, "as_pointer") else id(current)
|
||||||
|
if ptr in visited:
|
||||||
|
break
|
||||||
|
visited.add(ptr)
|
||||||
|
current = current.node.inputs[0].links[0].from_socket
|
||||||
|
return current
|
||||||
|
|
||||||
|
def _follow_reroute_downstream(reroute_node: Any, visited: set | None = None) -> list[Any]:
|
||||||
|
"""Follows reroute nodes forwards to find all downstream target input sockets."""
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
node_ptr = reroute_node.as_pointer() if hasattr(reroute_node, "as_pointer") else id(reroute_node)
|
||||||
|
if node_ptr in visited:
|
||||||
|
return []
|
||||||
|
visited.add(node_ptr)
|
||||||
|
|
||||||
|
targets: list[Any] = []
|
||||||
|
for outp in getattr(reroute_node, "outputs", []):
|
||||||
|
for link in getattr(outp, "links", []):
|
||||||
|
to_sock = link.to_socket
|
||||||
|
to_node = getattr(to_sock, "node", None)
|
||||||
|
if to_node and getattr(to_node, "bl_idname", "") == "NodeReroute":
|
||||||
|
targets.extend(_follow_reroute_downstream(to_node, visited))
|
||||||
|
else:
|
||||||
|
targets.append(to_sock)
|
||||||
|
return targets
|
||||||
|
|
||||||
|
def socket_accepts_list(sock: Any, visited: set | None = None) -> bool:
|
||||||
|
"""Determines whether a given input socket can accept a list input."""
|
||||||
|
if sock is None:
|
||||||
|
return False
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
sock_ptr = sock.as_pointer() if hasattr(sock, "as_pointer") else id(sock)
|
||||||
|
if sock_ptr in visited:
|
||||||
|
return False
|
||||||
|
visited.add(sock_ptr)
|
||||||
|
|
||||||
|
node = getattr(sock, "node", None)
|
||||||
|
if node is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
bl_idname = getattr(node, "bl_idname", "")
|
||||||
|
|
||||||
|
# Strictly forbidden nodes
|
||||||
|
if bl_idname in ("SLICENODE_StartPrint", "SLICENODE_PrinterList"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Path Operations: only input 0 can accept a list
|
||||||
|
if bl_idname == "SLICENODE_PathOperations":
|
||||||
|
inputs = getattr(node, "inputs", None)
|
||||||
|
if inputs and sock != inputs[0]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Seam Placement: "Mesh" input rejects lists; "Paths" accepts lists
|
||||||
|
if bl_idname == "SLICENODE_SeamPlacement":
|
||||||
|
if getattr(sock, "name", "") == "Mesh":
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Socket type check
|
||||||
|
sock_type = getattr(sock, "bl_idname", "")
|
||||||
|
if sock_type not in LIST_CAPABLE_SOCKET_TYPES:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# String sockets: only Gcode accepts lists
|
||||||
|
if sock_type == "NodeSocketString":
|
||||||
|
sock_name = getattr(sock, "name", "")
|
||||||
|
if bl_idname == "SLICENODE_JoinGcode":
|
||||||
|
return True
|
||||||
|
if bl_idname in ("SLICENODE_WriteGcode", "SLICENODE_ExecuteGcode"):
|
||||||
|
return sock_name == "Gcode"
|
||||||
|
if bl_idname == "SLICENODE_GcodeRegexReplace":
|
||||||
|
return sock_name == "Gcode"
|
||||||
|
if bl_idname == "SLICENODE_GcodesToList":
|
||||||
|
return True
|
||||||
|
if bl_idname == "NodeGroupOutput":
|
||||||
|
return sock_name.lower().startswith("gcode")
|
||||||
|
if bl_idname == "SLICENODE_Group":
|
||||||
|
inner_tree = getattr(node, "slicer_tree", None) or getattr(node, "node_tree", None)
|
||||||
|
if inner_tree:
|
||||||
|
return group_input_can_accept_list(inner_tree, sock, visited)
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Reroute node: check downstream targets
|
||||||
|
if bl_idname == "NodeReroute":
|
||||||
|
targets = _follow_reroute_downstream(node, visited)
|
||||||
|
if not targets:
|
||||||
|
return False
|
||||||
|
return all(socket_accepts_list(t, visited) for t in targets)
|
||||||
|
|
||||||
|
# NodeGroupOutput
|
||||||
|
if bl_idname == "NodeGroupOutput":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Nested SLICENODE_Group
|
||||||
|
if bl_idname == "SLICENODE_Group":
|
||||||
|
inner_tree = getattr(node, "slicer_tree", None) or getattr(node, "node_tree", None)
|
||||||
|
if inner_tree:
|
||||||
|
return group_input_can_accept_list(inner_tree, sock, visited)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# List joiners accept lists
|
||||||
|
if getattr(node, "is_list_output", False):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Data joiners accept lists
|
||||||
|
if bl_idname in ("SLICENODE_JoinMesh", "SLICENODE_JoinMeshGroups", "SLICENODE_JoinGcode"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Nodes with list_socket_mapping
|
||||||
|
mapping = getattr(node, "list_socket_mapping", None)
|
||||||
|
if mapping:
|
||||||
|
mapped_in_names = {item[0] for item in mapping}
|
||||||
|
if getattr(sock, "name", "") in mapped_in_names:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_input_interface_item(tree: Any, sock_or_idx_or_ident: Any) -> Any:
|
||||||
|
if not hasattr(tree, "interface"):
|
||||||
|
inputs = getattr(tree, "inputs", [])
|
||||||
|
if isinstance(sock_or_idx_or_ident, int) and 0 <= sock_or_idx_or_ident < len(inputs):
|
||||||
|
return inputs[sock_or_idx_or_ident]
|
||||||
|
ident = getattr(sock_or_idx_or_ident, "identifier", str(sock_or_idx_or_ident))
|
||||||
|
for s in inputs:
|
||||||
|
if s.identifier == ident or s.name == ident:
|
||||||
|
return s
|
||||||
|
return None
|
||||||
|
|
||||||
|
input_items = [
|
||||||
|
item for item in getattr(tree.interface, "items_tree", [])
|
||||||
|
if getattr(item, "item_type", "") == 'SOCKET' and getattr(item, "in_out", "") == 'INPUT'
|
||||||
|
]
|
||||||
|
if isinstance(sock_or_idx_or_ident, int):
|
||||||
|
if 0 <= sock_or_idx_or_ident < len(input_items):
|
||||||
|
return input_items[sock_or_idx_or_ident]
|
||||||
|
return None
|
||||||
|
ident = getattr(sock_or_idx_or_ident, "identifier", None)
|
||||||
|
name = getattr(sock_or_idx_or_ident, "name", None)
|
||||||
|
if isinstance(sock_or_idx_or_ident, str):
|
||||||
|
ident = sock_or_idx_or_ident
|
||||||
|
name = sock_or_idx_or_ident
|
||||||
|
for item in input_items:
|
||||||
|
if ident and getattr(item, "identifier", None) == ident:
|
||||||
|
return item
|
||||||
|
for item in input_items:
|
||||||
|
if name and getattr(item, "name", None) == name:
|
||||||
|
return item
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_output_interface_item(tree: Any, sock_or_idx_or_ident: Any) -> Any:
|
||||||
|
if not hasattr(tree, "interface"):
|
||||||
|
outputs = getattr(tree, "outputs", [])
|
||||||
|
if isinstance(sock_or_idx_or_ident, int) and 0 <= sock_or_idx_or_ident < len(outputs):
|
||||||
|
return outputs[sock_or_idx_or_ident]
|
||||||
|
ident = getattr(sock_or_idx_or_ident, "identifier", str(sock_or_idx_or_ident))
|
||||||
|
for s in outputs:
|
||||||
|
if s.identifier == ident or s.name == ident:
|
||||||
|
return s
|
||||||
|
return None
|
||||||
|
|
||||||
|
output_items = [
|
||||||
|
item for item in getattr(tree.interface, "items_tree", [])
|
||||||
|
if getattr(item, "item_type", "") == 'SOCKET' and getattr(item, "in_out", "") == 'OUTPUT'
|
||||||
|
]
|
||||||
|
if isinstance(sock_or_idx_or_ident, int):
|
||||||
|
if 0 <= sock_or_idx_or_ident < len(output_items):
|
||||||
|
return output_items[sock_or_idx_or_ident]
|
||||||
|
return None
|
||||||
|
ident = getattr(sock_or_idx_or_ident, "identifier", None)
|
||||||
|
name = getattr(sock_or_idx_or_ident, "name", None)
|
||||||
|
if isinstance(sock_or_idx_or_ident, str):
|
||||||
|
ident = sock_or_idx_or_ident
|
||||||
|
name = sock_or_idx_or_ident
|
||||||
|
for item in output_items:
|
||||||
|
if ident and getattr(item, "identifier", None) == ident:
|
||||||
|
return item
|
||||||
|
for item in output_items:
|
||||||
|
if name and getattr(item, "name", None) == name:
|
||||||
|
return item
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _find_group_input_socket(tree: Any, item: Any) -> Any:
|
||||||
|
if not item or not hasattr(tree, "nodes"):
|
||||||
|
return None
|
||||||
|
ident = getattr(item, "identifier", "")
|
||||||
|
name = getattr(item, "name", "")
|
||||||
|
for n in tree.nodes:
|
||||||
|
if getattr(n, "bl_idname", "") == "NodeGroupInput":
|
||||||
|
for outp in getattr(n, "outputs", []):
|
||||||
|
if outp.identifier == ident or outp.name == name:
|
||||||
|
return outp
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _find_group_output_socket(tree: Any, item: Any) -> Any:
|
||||||
|
if not item or not hasattr(tree, "nodes"):
|
||||||
|
return None
|
||||||
|
ident = getattr(item, "identifier", "")
|
||||||
|
name = getattr(item, "name", "")
|
||||||
|
for n in tree.nodes:
|
||||||
|
if getattr(n, "bl_idname", "") == "NodeGroupOutput":
|
||||||
|
for inp in getattr(n, "inputs", []):
|
||||||
|
if inp.identifier == ident or inp.name == name:
|
||||||
|
return inp
|
||||||
|
return None
|
||||||
|
|
||||||
|
def group_input_can_accept_list(tree: Any, sock_or_idx_or_ident: Any, visited: set | None = None) -> bool:
|
||||||
|
"""Determines whether a group tree input socket can accept a list input."""
|
||||||
|
if not tree:
|
||||||
|
return False
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
item = _get_input_interface_item(tree, sock_or_idx_or_ident)
|
||||||
|
if not item:
|
||||||
|
return False
|
||||||
|
|
||||||
|
item_key = (tree.as_pointer() if hasattr(tree, "as_pointer") else id(tree), getattr(item, "identifier", id(item)))
|
||||||
|
if item_key in visited:
|
||||||
|
return False
|
||||||
|
visited.add(item_key)
|
||||||
|
|
||||||
|
stype = getattr(item, "socket_type", "") or getattr(item, "bl_idname", "")
|
||||||
|
if stype not in LIST_CAPABLE_SOCKET_TYPES:
|
||||||
|
return False
|
||||||
|
|
||||||
|
gin_sock = _find_group_input_socket(tree, item)
|
||||||
|
if not gin_sock or not getattr(gin_sock, "is_linked", False) or not gin_sock.links:
|
||||||
|
return False
|
||||||
|
|
||||||
|
targets: list[Any] = []
|
||||||
|
for link in gin_sock.links:
|
||||||
|
to_sock = link.to_socket
|
||||||
|
to_node = getattr(to_sock, "node", None)
|
||||||
|
if to_node and getattr(to_node, "bl_idname", "") == "NodeReroute":
|
||||||
|
targets.extend(_follow_reroute_downstream(to_node, visited))
|
||||||
|
else:
|
||||||
|
targets.append(to_sock)
|
||||||
|
|
||||||
|
if not targets:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return all(socket_accepts_list(t, visited) for t in targets)
|
||||||
|
|
||||||
|
def is_internal_socket_a_list(group_node: Any, tree: Any, in_sock: Any, visited: set | None = None) -> bool:
|
||||||
|
"""Determines whether an internal input socket receives a list within a group context."""
|
||||||
|
if not in_sock or not getattr(in_sock, "is_linked", False) or not in_sock.links:
|
||||||
|
return False
|
||||||
|
from_sock = in_sock.links[0].from_socket
|
||||||
|
from_sock = _follow_reroute_upstream(from_sock, visited)
|
||||||
|
if not from_sock:
|
||||||
|
return False
|
||||||
|
from_node = getattr(from_sock, "node", None)
|
||||||
|
if not from_node:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if getattr(from_node, "is_list_output", False):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if getattr(from_node, "bl_idname", "") == "NodeGroupInput":
|
||||||
|
in_item = _get_input_interface_item(tree, from_sock)
|
||||||
|
if not in_item:
|
||||||
|
return False
|
||||||
|
if group_node is not None:
|
||||||
|
for grp_in in getattr(group_node, "inputs", []):
|
||||||
|
if grp_in.identifier == getattr(in_item, "identifier", None) or grp_in.name == getattr(in_item, "name", None):
|
||||||
|
return is_socket_linked_to_list(grp_in)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return group_input_can_accept_list(tree, in_item, visited)
|
||||||
|
|
||||||
|
if getattr(from_node, "bl_idname", "") == "SLICENODE_Group":
|
||||||
|
inner = getattr(from_node, "slicer_tree", None) or getattr(from_node, "node_tree", None)
|
||||||
|
if inner:
|
||||||
|
return is_group_output_a_list(from_node, inner, from_sock, visited)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if getattr(from_node, "bl_idname", "") == "SLICENODE_JoinGcode":
|
||||||
|
for inp in getattr(from_node, "inputs", []):
|
||||||
|
if getattr(inp, "name", "") == "Gcode" or getattr(inp, "identifier", "").startswith("Gcode_ms_in_"):
|
||||||
|
if is_internal_socket_a_list(group_node, tree, inp, visited):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
mapping = getattr(from_node, "list_socket_mapping", None)
|
||||||
|
if mapping:
|
||||||
|
sock_name = getattr(from_sock, "name", "")
|
||||||
|
for m_in_name, m_out_names in mapping:
|
||||||
|
targets = [m_out_names] if isinstance(m_out_names, str) else list(m_out_names)
|
||||||
|
if sock_name in targets:
|
||||||
|
mapped_inp = getattr(from_node, "inputs", None) and from_node.inputs.get(m_in_name)
|
||||||
|
if mapped_inp:
|
||||||
|
return is_internal_socket_a_list(group_node, tree, mapped_inp, visited)
|
||||||
|
|
||||||
|
return getattr(from_sock, "display_shape", "") == "LIST"
|
||||||
|
|
||||||
|
def is_group_output_a_list(
|
||||||
|
group_node: Any,
|
||||||
|
tree: Any,
|
||||||
|
sock_or_idx_or_ident: Any,
|
||||||
|
visited: set | None = None
|
||||||
|
) -> bool:
|
||||||
|
"""Determines whether a group tree output socket produces a list."""
|
||||||
|
if not tree:
|
||||||
|
return False
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
item = _get_output_interface_item(tree, sock_or_idx_or_ident)
|
||||||
|
if not item:
|
||||||
|
return False
|
||||||
|
|
||||||
|
item_key = (
|
||||||
|
tree.as_pointer() if hasattr(tree, "as_pointer") else id(tree),
|
||||||
|
getattr(item, "identifier", id(item)),
|
||||||
|
id(group_node) if group_node else 0,
|
||||||
|
)
|
||||||
|
if item_key in visited:
|
||||||
|
return False
|
||||||
|
visited.add(item_key)
|
||||||
|
|
||||||
|
gout_sock = _find_group_output_socket(tree, item)
|
||||||
|
if not gout_sock or not getattr(gout_sock, "is_linked", False) or not gout_sock.links:
|
||||||
|
return False
|
||||||
|
|
||||||
|
src_sock = gout_sock.links[0].from_socket
|
||||||
|
src_sock = _follow_reroute_upstream(src_sock, visited)
|
||||||
|
if not src_sock:
|
||||||
|
return False
|
||||||
|
|
||||||
|
src_node = getattr(src_sock, "node", None)
|
||||||
|
if not src_node:
|
||||||
|
return False
|
||||||
|
|
||||||
|
src_bl_id = getattr(src_node, "bl_idname", "")
|
||||||
|
|
||||||
|
if getattr(src_node, "is_list_output", False):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if src_bl_id == "SLICENODE_JoinGcode":
|
||||||
|
for inp in getattr(src_node, "inputs", []):
|
||||||
|
if getattr(inp, "name", "") == "Gcode" or getattr(inp, "identifier", "").startswith("Gcode_ms_in_"):
|
||||||
|
if is_internal_socket_a_list(group_node, tree, inp, visited):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
if src_bl_id == "SLICENODE_Group":
|
||||||
|
inner_tree = getattr(src_node, "slicer_tree", None) or getattr(src_node, "node_tree", None)
|
||||||
|
if inner_tree:
|
||||||
|
return is_group_output_a_list(src_node, inner_tree, src_sock, visited)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if src_bl_id == "NodeGroupInput":
|
||||||
|
in_item = _get_input_interface_item(tree, src_sock)
|
||||||
|
if not in_item:
|
||||||
|
return False
|
||||||
|
if group_node is not None:
|
||||||
|
for grp_in in getattr(group_node, "inputs", []):
|
||||||
|
if grp_in.identifier == getattr(in_item, "identifier", None) or grp_in.name == getattr(in_item, "name", None):
|
||||||
|
return is_socket_linked_to_list(grp_in)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return group_input_can_accept_list(tree, in_item, visited)
|
||||||
|
|
||||||
|
mapping = getattr(src_node, "list_socket_mapping", None)
|
||||||
|
if mapping:
|
||||||
|
sock_name = getattr(src_sock, "name", "")
|
||||||
|
for in_name, out_names in mapping:
|
||||||
|
targets = [out_names] if isinstance(out_names, str) else list(out_names)
|
||||||
|
if sock_name in targets:
|
||||||
|
inp = getattr(src_node, "inputs", None) and src_node.inputs.get(in_name)
|
||||||
|
if inp:
|
||||||
|
return is_internal_socket_a_list(group_node, tree, inp, visited)
|
||||||
|
|
||||||
|
return getattr(src_sock, "display_shape", "") == "LIST"
|
||||||
|
|
||||||
|
def sync_group_node_socket_shapes(group_node: Any) -> None:
|
||||||
|
"""Updates display_shape property on a SLICENODE_Group node's inputs and outputs."""
|
||||||
|
tree = getattr(group_node, "slicer_tree", None) or getattr(group_node, "node_tree", None)
|
||||||
|
if not tree:
|
||||||
|
return
|
||||||
|
for inp in getattr(group_node, "inputs", []):
|
||||||
|
can_list = group_input_can_accept_list(tree, inp)
|
||||||
|
set_socket_shape(inp, "LIST" if can_list else "CIRCLE")
|
||||||
|
for outp in getattr(group_node, "outputs", []):
|
||||||
|
is_list = is_group_output_a_list(group_node, tree, outp)
|
||||||
|
set_socket_shape(outp, "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
|
def sync_group_tree_internal_sockets(tree: Any) -> None:
|
||||||
|
"""Updates display_shape property on NodeGroupInput and NodeGroupOutput within a group tree."""
|
||||||
|
if not tree or not hasattr(tree, "nodes"):
|
||||||
|
return
|
||||||
|
for node in tree.nodes:
|
||||||
|
bl_id = getattr(node, "bl_idname", "")
|
||||||
|
if bl_id == "NodeGroupInput":
|
||||||
|
for outp in getattr(node, "outputs", []):
|
||||||
|
if outp.name == "" or outp.identifier == "__extend__":
|
||||||
|
continue
|
||||||
|
can_list = group_input_can_accept_list(tree, outp)
|
||||||
|
set_socket_shape(outp, "LIST" if can_list else "CIRCLE")
|
||||||
|
elif bl_id == "NodeGroupOutput":
|
||||||
|
for inp in getattr(node, "inputs", []):
|
||||||
|
if inp.name == "" or inp.identifier == "__extend__":
|
||||||
|
continue
|
||||||
|
is_list = is_group_output_a_list(None, tree, inp)
|
||||||
|
set_socket_shape(inp, "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
|
def is_output_socket_a_list(sock: Any, visited: set | None = None) -> bool:
|
||||||
|
"""Determines whether an output socket produces a list."""
|
||||||
|
if sock is None:
|
||||||
|
return False
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
sock_ptr = sock.as_pointer() if hasattr(sock, "as_pointer") else id(sock)
|
||||||
|
if sock_ptr in visited:
|
||||||
|
return False
|
||||||
|
visited.add(sock_ptr)
|
||||||
|
|
||||||
|
if getattr(sock, "display_shape", "") == "LIST":
|
||||||
|
return True
|
||||||
|
|
||||||
|
node = getattr(sock, "node", None)
|
||||||
|
if node is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
bl_idname = getattr(node, "bl_idname", "")
|
||||||
|
|
||||||
|
if getattr(node, "is_list_output", False):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if bl_idname == "NodeReroute":
|
||||||
|
inputs = getattr(node, "inputs", None)
|
||||||
|
if inputs and inputs[0].is_linked and inputs[0].links:
|
||||||
|
return is_output_socket_a_list(inputs[0].links[0].from_socket, visited)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if bl_idname == "SLICENODE_JoinGcode":
|
||||||
|
inputs = getattr(node, "inputs", [])
|
||||||
|
return any(
|
||||||
|
is_socket_linked_to_list(inp)
|
||||||
|
for inp in inputs
|
||||||
|
if getattr(inp, "name", "") == "Gcode" or getattr(inp, "identifier", "").startswith("Gcode_ms_in_")
|
||||||
|
)
|
||||||
|
|
||||||
|
if bl_idname == "SLICENODE_Group":
|
||||||
|
tree = getattr(node, "slicer_tree", None) or getattr(node, "node_tree", None)
|
||||||
|
if tree:
|
||||||
|
return is_group_output_a_list(node, tree, sock, visited)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if bl_idname == "NodeGroupInput":
|
||||||
|
tree = getattr(node, "id_data", None)
|
||||||
|
if tree:
|
||||||
|
return group_input_can_accept_list(tree, sock, visited)
|
||||||
|
return False
|
||||||
|
|
||||||
|
mapping = getattr(node, "list_socket_mapping", None)
|
||||||
|
if mapping:
|
||||||
|
sock_name = getattr(sock, "name", "")
|
||||||
|
for in_name, out_names in mapping:
|
||||||
|
targets = [out_names] if isinstance(out_names, str) else list(out_names)
|
||||||
|
if sock_name in targets:
|
||||||
|
inp = getattr(node, "inputs", None) and node.inputs.get(in_name)
|
||||||
|
if inp and is_socket_linked_to_list(inp):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_socket_linked_to_list(sock: Any) -> bool:
|
||||||
|
"""Determines whether an input socket is linked to an upstream list output."""
|
||||||
|
if not getattr(sock, "is_linked", False) or not getattr(sock, "links", None):
|
||||||
|
return False
|
||||||
|
from_socket = sock.links[0].from_socket
|
||||||
|
from_socket = _follow_reroute_upstream(from_socket)
|
||||||
|
return is_output_socket_a_list(from_socket)
|
||||||
|
|
||||||
|
def sync_node_list_sockets(node: Any, mapping: list[tuple[str, list[str] | str]]) -> None:
|
||||||
|
"""Updates display_shape on input and output sockets based on mapping.
|
||||||
|
mapping is a list of tuples: (input_socket_name, output_socket_names)
|
||||||
|
e.g. [('Paths', ['Paths']), ('Mesh', ['Selection', 'Inverted'])]
|
||||||
|
"""
|
||||||
|
for in_name, out_names in mapping:
|
||||||
|
inp = getattr(node, "inputs", None) and node.inputs.get(in_name)
|
||||||
|
if inp is None:
|
||||||
|
continue
|
||||||
|
is_list = is_socket_linked_to_list(inp)
|
||||||
|
set_socket_shape(inp, "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
|
if isinstance(out_names, str):
|
||||||
|
targets = [out_names]
|
||||||
|
else:
|
||||||
|
targets = list(out_names)
|
||||||
|
for out_name in targets:
|
||||||
|
out_sock = getattr(node, "outputs", None) and node.outputs.get(out_name)
|
||||||
|
if out_sock is not None:
|
||||||
|
set_socket_shape(out_sock, "LIST" if is_list else "CIRCLE")
|
||||||
671
tests/test_lists.py
Normal file
671
tests/test_lists.py
Normal file
|
|
@ -0,0 +1,671 @@
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from pathlib import Path
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.append(str(PROJECT_ROOT.parent))
|
||||||
|
|
||||||
|
# Ensure bpy mocks
|
||||||
|
class DummyObject:
|
||||||
|
def __init__(self, name="TestObj"):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
class DummyMesh: pass
|
||||||
|
class DummyBase: pass
|
||||||
|
|
||||||
|
mock_types = MagicMock()
|
||||||
|
mock_types.NodeSocket = type("NodeSocket", (DummyBase,), {})
|
||||||
|
mock_types.Node = type("Node", (DummyBase,), {})
|
||||||
|
mock_types.NodeCustomGroup = type("NodeCustomGroup", (DummyBase,), {})
|
||||||
|
mock_types.NodeTree = type("NodeTree", (DummyBase,), {})
|
||||||
|
mock_types.PropertyGroup = type("PropertyGroup", (DummyBase,), {})
|
||||||
|
mock_types.Operator = type("Operator", (DummyBase,), {})
|
||||||
|
mock_types.Panel = type("Panel", (DummyBase,), {})
|
||||||
|
mock_types.Object = DummyObject
|
||||||
|
mock_types.Mesh = DummyMesh
|
||||||
|
|
||||||
|
mock_bpy = MagicMock()
|
||||||
|
mock_bpy.context.scene = MagicMock()
|
||||||
|
mock_bpy.context.scene.unit_settings.scale_length = 0.001
|
||||||
|
mock_bpy.types = mock_types
|
||||||
|
mock_bpy.path.abspath = lambda path: path.replace("//", "/abs/")
|
||||||
|
|
||||||
|
sys.modules['bpy'] = mock_bpy
|
||||||
|
sys.modules['bpy.types'] = mock_types
|
||||||
|
sys.modules['bpy.props'] = MagicMock()
|
||||||
|
sys.modules['bpy.utils'] = MagicMock()
|
||||||
|
sys.modules['bpy.utils.previews'] = MagicMock()
|
||||||
|
sys.modules['bpy.app'] = MagicMock()
|
||||||
|
sys.modules['bpy.app.handlers'] = MagicMock()
|
||||||
|
|
||||||
|
from NodeSlicer.infra.blender.list_utils import (
|
||||||
|
is_list_data,
|
||||||
|
flatten_list,
|
||||||
|
eval_element_wise,
|
||||||
|
set_socket_shape,
|
||||||
|
is_socket_linked_to_list,
|
||||||
|
is_output_socket_a_list,
|
||||||
|
sync_node_list_sockets,
|
||||||
|
group_input_can_accept_list,
|
||||||
|
is_group_output_a_list,
|
||||||
|
sync_group_node_socket_shapes,
|
||||||
|
)
|
||||||
|
from NodeSlicer.core.slice import SlicePaths
|
||||||
|
from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject, SlicingCollection, SlicingGroup
|
||||||
|
from NodeSlicer.core.nodeslicer.tree import SLICETREE_NodeTree
|
||||||
|
|
||||||
|
|
||||||
|
class TestListUtils(unittest.TestCase):
|
||||||
|
def test_is_list_data(self):
|
||||||
|
self.assertFalse(is_list_data(None))
|
||||||
|
self.assertFalse(is_list_data("a string"))
|
||||||
|
self.assertFalse(is_list_data(b"bytes"))
|
||||||
|
self.assertFalse(is_list_data(123))
|
||||||
|
self.assertFalse(is_list_data(12.34))
|
||||||
|
|
||||||
|
# SlicePaths subclasses list, but represents an atomic toolpath set, NOT a list of toolpaths
|
||||||
|
sp = SlicePaths()
|
||||||
|
self.assertFalse(is_list_data(sp))
|
||||||
|
|
||||||
|
# Normal lists and tuples
|
||||||
|
self.assertTrue(is_list_data([]))
|
||||||
|
self.assertTrue(is_list_data([1, 2, 3]))
|
||||||
|
self.assertTrue(is_list_data((1, 2)))
|
||||||
|
self.assertTrue(is_list_data([sp, sp]))
|
||||||
|
|
||||||
|
def test_flatten_list(self):
|
||||||
|
sp1 = SlicePaths()
|
||||||
|
sp2 = SlicePaths()
|
||||||
|
nested = [sp1, [sp2, None], [["gcode1"], "gcode2"]]
|
||||||
|
flattened = flatten_list(nested)
|
||||||
|
self.assertEqual(flattened, [sp1, sp2, "gcode1", "gcode2"])
|
||||||
|
|
||||||
|
def test_eval_element_wise(self):
|
||||||
|
def double(x):
|
||||||
|
return x * 2
|
||||||
|
|
||||||
|
self.assertEqual(eval_element_wise(double, 5), 10)
|
||||||
|
self.assertEqual(eval_element_wise(double, [1, 2, 3]), [2, 4, 6])
|
||||||
|
|
||||||
|
|
||||||
|
class DummySocket:
|
||||||
|
def __init__(self, name="Socket", bl_idname="NodeSocketFloat", display_shape="CIRCLE"):
|
||||||
|
self.name = name
|
||||||
|
self.identifier = name
|
||||||
|
self.bl_idname = bl_idname
|
||||||
|
self.display_shape = display_shape
|
||||||
|
self.is_linked = False
|
||||||
|
self.links = []
|
||||||
|
self.node = None
|
||||||
|
self.value = None
|
||||||
|
|
||||||
|
|
||||||
|
class DummyLink:
|
||||||
|
def __init__(self, from_socket, to_socket):
|
||||||
|
self.from_socket = from_socket
|
||||||
|
self.to_socket = to_socket
|
||||||
|
|
||||||
|
|
||||||
|
class DummySocketCollection(list):
|
||||||
|
def get(self, name, default=None):
|
||||||
|
for s in self:
|
||||||
|
if getattr(s, "name", "") == name:
|
||||||
|
return s
|
||||||
|
return default
|
||||||
|
|
||||||
|
class DummyNode:
|
||||||
|
def __init__(self, bl_idname="DummyNode"):
|
||||||
|
self.bl_idname = bl_idname
|
||||||
|
self.inputs = DummySocketCollection()
|
||||||
|
self.outputs = DummySocketCollection()
|
||||||
|
self.is_list_output = False
|
||||||
|
|
||||||
|
def add_in(self, sock):
|
||||||
|
sock.node = self
|
||||||
|
self.inputs.append(sock)
|
||||||
|
|
||||||
|
def add_out(self, sock):
|
||||||
|
sock.node = self
|
||||||
|
self.outputs.append(sock)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSocketShapesAndLinks(unittest.TestCase):
|
||||||
|
def test_socket_linked_to_list(self):
|
||||||
|
upstream_node = DummyNode("SLICENODE_MeshesToList")
|
||||||
|
upstream_node.is_list_output = True
|
||||||
|
out_sock = DummySocket("Meshes", "SLICESOCK_Mesh", display_shape="LIST")
|
||||||
|
upstream_node.add_out(out_sock)
|
||||||
|
|
||||||
|
downstream_node = DummyNode("SLICENODE_SliceShells")
|
||||||
|
in_sock = DummySocket("Mesh", "SLICESOCK_Mesh", display_shape="CIRCLE")
|
||||||
|
downstream_node.add_in(in_sock)
|
||||||
|
|
||||||
|
# Before linking
|
||||||
|
self.assertFalse(is_socket_linked_to_list(in_sock))
|
||||||
|
self.assertTrue(is_output_socket_a_list(out_sock))
|
||||||
|
|
||||||
|
# Link them
|
||||||
|
link = DummyLink(out_sock, in_sock)
|
||||||
|
in_sock.is_linked = True
|
||||||
|
in_sock.links = [link]
|
||||||
|
|
||||||
|
self.assertTrue(is_socket_linked_to_list(in_sock))
|
||||||
|
|
||||||
|
def test_sync_node_list_sockets(self):
|
||||||
|
upstream_node = DummyNode("SLICENODE_MeshesToList")
|
||||||
|
out_sock = DummySocket("Meshes", "SLICESOCK_Mesh", display_shape="LIST")
|
||||||
|
upstream_node.add_out(out_sock)
|
||||||
|
|
||||||
|
node = DummyNode("SLICENODE_SliceShells")
|
||||||
|
in_sock = DummySocket("Mesh", "SLICESOCK_Mesh", display_shape="CIRCLE")
|
||||||
|
out_path = DummySocket("Paths", "SLICESOCK_Paths", display_shape="CIRCLE")
|
||||||
|
node.add_in(in_sock)
|
||||||
|
node.add_out(out_path)
|
||||||
|
|
||||||
|
link = DummyLink(out_sock, in_sock)
|
||||||
|
in_sock.is_linked = True
|
||||||
|
in_sock.links = [link]
|
||||||
|
|
||||||
|
mapping = [("Mesh", "Paths")]
|
||||||
|
sync_node_list_sockets(node, mapping)
|
||||||
|
|
||||||
|
self.assertEqual(in_sock.display_shape, "LIST")
|
||||||
|
self.assertEqual(out_path.display_shape, "LIST")
|
||||||
|
|
||||||
|
|
||||||
|
class TestTreeValidLinkRules(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tree = SLICETREE_NodeTree()
|
||||||
|
|
||||||
|
def test_valid_link_start_print_rejects_list(self):
|
||||||
|
list_out = DummySocket("Gcode", "NodeSocketString", display_shape="LIST")
|
||||||
|
start_print = DummyNode("SLICENODE_StartPrint")
|
||||||
|
sp_in = DummySocket("Gcode", "NodeSocketString")
|
||||||
|
start_print.add_in(sp_in)
|
||||||
|
|
||||||
|
# Connection should be rejected
|
||||||
|
self.assertFalse(self.tree.valid_link(list_out, sp_in))
|
||||||
|
|
||||||
|
# Standard non-list connection should be allowed
|
||||||
|
normal_out = DummySocket("Gcode", "NodeSocketString", display_shape="CIRCLE")
|
||||||
|
self.assertTrue(self.tree.valid_link(normal_out, sp_in))
|
||||||
|
|
||||||
|
def test_valid_link_printer_list_rejects_list(self):
|
||||||
|
list_out = DummySocket("Gcode", "NodeSocketString", display_shape="LIST")
|
||||||
|
printer_list = DummyNode("SLICENODE_PrinterList")
|
||||||
|
pl_in = DummySocket("Printer1", "NodeSocketString")
|
||||||
|
printer_list.add_in(pl_in)
|
||||||
|
|
||||||
|
self.assertFalse(self.tree.valid_link(list_out, pl_in))
|
||||||
|
|
||||||
|
def test_valid_link_path_operations_secondary_inputs_reject_list(self):
|
||||||
|
list_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="LIST")
|
||||||
|
path_ops = DummyNode("SLICENODE_PathOperations")
|
||||||
|
p0 = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
p1 = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
path_ops.add_in(p0)
|
||||||
|
path_ops.add_in(p1)
|
||||||
|
|
||||||
|
# Primary input p0 accepts list
|
||||||
|
self.assertTrue(self.tree.valid_link(list_out, p0))
|
||||||
|
# Secondary input p1 rejects list
|
||||||
|
self.assertFalse(self.tree.valid_link(list_out, p1))
|
||||||
|
|
||||||
|
def test_valid_link_seam_placement_cutting_mesh_rejects_list(self):
|
||||||
|
list_mesh_out = DummySocket("Mesh", "SLICESOCK_Mesh", display_shape="LIST")
|
||||||
|
seam_node = DummyNode("SLICENODE_SeamPlacement")
|
||||||
|
p_in = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
m_in = DummySocket("Mesh", "SLICESOCK_Mesh")
|
||||||
|
seam_node.add_in(p_in)
|
||||||
|
seam_node.add_in(m_in)
|
||||||
|
|
||||||
|
# Secondary cutting mesh rejects list
|
||||||
|
self.assertFalse(self.tree.valid_link(list_mesh_out, m_in))
|
||||||
|
|
||||||
|
|
||||||
|
class TestNodeEvaluationsWithLists(unittest.TestCase):
|
||||||
|
def test_path_operations_with_first_input_list(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathOperations
|
||||||
|
|
||||||
|
node = SLICENODE_PathOperations()
|
||||||
|
node.operation = 'EXTEND'
|
||||||
|
|
||||||
|
p1 = SlicePaths([np.array([[0, 0, 0], [1, 1, 0]], dtype=np.float64)])
|
||||||
|
p2 = SlicePaths([np.array([[2, 2, 0], [3, 3, 0]], dtype=np.float64)])
|
||||||
|
q = SlicePaths([np.array([[10, 10, 0], [11, 11, 0]], dtype=np.float64)])
|
||||||
|
|
||||||
|
# Mock inputs
|
||||||
|
inp0 = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
inp0.is_linked = True
|
||||||
|
inp1 = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
inp1.is_linked = True
|
||||||
|
|
||||||
|
node.inputs = [inp0, inp1]
|
||||||
|
|
||||||
|
# resolve input mocking
|
||||||
|
def mock_resolve_input(sock, **kwargs):
|
||||||
|
if sock is inp0:
|
||||||
|
return [p1, p2] # List input
|
||||||
|
elif sock is inp1:
|
||||||
|
return q
|
||||||
|
return None
|
||||||
|
|
||||||
|
import NodeSlicer.infra.blender.blender_slice_nodes_generic as b_gen
|
||||||
|
original_resolve = b_gen.resolve_input
|
||||||
|
b_gen.resolve_input = mock_resolve_input
|
||||||
|
try:
|
||||||
|
res = node.evaluate_output("Paths")
|
||||||
|
self.assertIsInstance(res, list)
|
||||||
|
self.assertEqual(len(res), 2)
|
||||||
|
# res[0] is EXTEND of p1 and q
|
||||||
|
self.assertEqual(len(res[0]), 2)
|
||||||
|
# res[1] is EXTEND of p2 and q
|
||||||
|
self.assertEqual(len(res[1]), 2)
|
||||||
|
finally:
|
||||||
|
b_gen.resolve_input = original_resolve
|
||||||
|
|
||||||
|
def test_join_gcode_no_lists(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode
|
||||||
|
|
||||||
|
node = SLICENODE_JoinGcode()
|
||||||
|
node.resolve_multi_input = lambda label: ["G28", "G90", "M104 S200"]
|
||||||
|
res = node.evaluate_output("Gcode")
|
||||||
|
self.assertEqual(res, "G28\nG90\nM104 S200")
|
||||||
|
|
||||||
|
def test_join_gcode_one_list(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode
|
||||||
|
|
||||||
|
node = SLICENODE_JoinGcode()
|
||||||
|
node.resolve_multi_input = lambda label: ["START", ["LAYER1", "LAYER2", "LAYER3"], "END"]
|
||||||
|
res = node.evaluate_output("Gcode")
|
||||||
|
self.assertEqual(res, [
|
||||||
|
"START\nLAYER1\nEND",
|
||||||
|
"START\nLAYER2\nEND",
|
||||||
|
"START\nLAYER3\nEND",
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_join_gcode_multiple_lists_zipped(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode
|
||||||
|
|
||||||
|
node = SLICENODE_JoinGcode()
|
||||||
|
node.resolve_multi_input = lambda label: [
|
||||||
|
"INIT",
|
||||||
|
["A1", "A2", "A3"],
|
||||||
|
"MID",
|
||||||
|
["B1", "B2", "B3"],
|
||||||
|
"FINAL",
|
||||||
|
]
|
||||||
|
res = node.evaluate_output("Gcode")
|
||||||
|
self.assertEqual(res, [
|
||||||
|
"INIT\nA1\nMID\nB1\nFINAL",
|
||||||
|
"INIT\nA2\nMID\nB2\nFINAL",
|
||||||
|
"INIT\nA3\nMID\nB3\nFINAL",
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_join_gcode_mismatched_lengths_raises_error(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode
|
||||||
|
|
||||||
|
node = SLICENODE_JoinGcode()
|
||||||
|
node.resolve_multi_input = lambda label: [["A1", "A2"], ["B1", "B2", "B3"]]
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
node.evaluate_output("Gcode")
|
||||||
|
|
||||||
|
def test_join_gcode_socket_shape_updates(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode
|
||||||
|
|
||||||
|
node = SLICENODE_JoinGcode()
|
||||||
|
in0 = DummySocket("Gcode", "NodeSocketString", display_shape="CIRCLE")
|
||||||
|
out0 = DummySocket("Gcode", "NodeSocketString", display_shape="CIRCLE")
|
||||||
|
node.inputs = DummySocketCollection([in0])
|
||||||
|
node.outputs = DummySocketCollection([out0])
|
||||||
|
|
||||||
|
# Initially without list link
|
||||||
|
node.update()
|
||||||
|
self.assertEqual(out0.display_shape, "CIRCLE")
|
||||||
|
|
||||||
|
# Linked to list
|
||||||
|
upstream = DummyNode("SLICENODE_GcodesToList")
|
||||||
|
upstream.is_list_output = True
|
||||||
|
up_out = DummySocket("Gcode", "NodeSocketString", display_shape="LIST")
|
||||||
|
upstream.add_out(up_out)
|
||||||
|
|
||||||
|
link = DummyLink(up_out, in0)
|
||||||
|
in0.is_linked = True
|
||||||
|
in0.links = [link]
|
||||||
|
|
||||||
|
node.update()
|
||||||
|
self.assertEqual(out0.display_shape, "LIST")
|
||||||
|
|
||||||
|
def test_gcode_regex_replace_list(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_GcodeRegexReplace
|
||||||
|
|
||||||
|
node = SLICENODE_GcodeRegexReplace()
|
||||||
|
node.resolve = lambda field: {
|
||||||
|
"Gcode": ["G1 X10 Y10", "G1 X20 Y20"],
|
||||||
|
"Find": "G1",
|
||||||
|
"Replace": "G0"
|
||||||
|
}.get(field)
|
||||||
|
|
||||||
|
res = node.evaluate_output("Gcode")
|
||||||
|
self.assertEqual(res, ["G0 X10 Y10", "G0 X20 Y20"])
|
||||||
|
|
||||||
|
def test_list_joiners_evaluation(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_ObjectsToList
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_MeshesToList
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathsToList
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_GcodesToList
|
||||||
|
|
||||||
|
# ObjectsToList
|
||||||
|
obj1 = DummyObject("Obj1")
|
||||||
|
obj2 = DummyObject("Obj2")
|
||||||
|
o_node = SLICENODE_ObjectsToList()
|
||||||
|
o_node.resolve_multi_input = lambda label: [[obj1], obj2]
|
||||||
|
self.assertEqual(o_node.evaluate_output("Objects"), [obj1, obj2])
|
||||||
|
|
||||||
|
# MeshesToList
|
||||||
|
sc1 = SlicingCollection([], "M1")
|
||||||
|
sc2 = SlicingCollection([], "M2")
|
||||||
|
m_node = SLICENODE_MeshesToList()
|
||||||
|
m_node.resolve_multi_input = lambda label: [sc1, [sc2]]
|
||||||
|
self.assertEqual(m_node.evaluate_output("Meshes"), [sc1, sc2])
|
||||||
|
|
||||||
|
# PathsToList
|
||||||
|
sp1 = SlicePaths([np.array([[0, 0, 0], [1, 1, 0]], dtype=np.float64)])
|
||||||
|
sp2 = SlicePaths([np.array([[2, 2, 0], [3, 3, 0]], dtype=np.float64)])
|
||||||
|
p_node = SLICENODE_PathsToList()
|
||||||
|
p_node.resolve_multi_input = lambda label: [sp1, [sp2]]
|
||||||
|
self.assertEqual(p_node.evaluate_output("Paths"), [sp1, sp2])
|
||||||
|
|
||||||
|
# GcodesToList
|
||||||
|
g_node = SLICENODE_GcodesToList()
|
||||||
|
g_node.resolve_multi_input = lambda label: [["G1 X1"], "G1 X2"]
|
||||||
|
self.assertEqual(g_node.evaluate_output("Gcode"), ["G1 X1", "G1 X2"])
|
||||||
|
|
||||||
|
def test_join_mesh_flattens_lists(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_JoinMesh
|
||||||
|
|
||||||
|
node = SLICENODE_JoinMesh()
|
||||||
|
so1 = SlicingObject()
|
||||||
|
so1.name = "part1"
|
||||||
|
so1.mesh = np.ones((1, 4, 3), dtype=np.float64)
|
||||||
|
sc1 = SlicingCollection([], "Coll1")
|
||||||
|
sc1.objects = [so1]
|
||||||
|
|
||||||
|
so2 = SlicingObject()
|
||||||
|
so2.name = "part2"
|
||||||
|
so2.mesh = np.ones((2, 4, 3), dtype=np.float64)
|
||||||
|
sc2 = SlicingCollection([], "Coll2")
|
||||||
|
sc2.objects = [so2]
|
||||||
|
|
||||||
|
so3 = SlicingObject()
|
||||||
|
so3.name = "part3"
|
||||||
|
so3.mesh = np.ones((3, 4, 3), dtype=np.float64)
|
||||||
|
sc3 = SlicingCollection([], "Coll3")
|
||||||
|
sc3.objects = [so3]
|
||||||
|
|
||||||
|
# Mock inputs delivering [ [sc1, sc2], sc3 ]
|
||||||
|
mock_in1 = DummySocket("Mesh", "SLICESOCK_Mesh")
|
||||||
|
mock_in1.is_linked = True
|
||||||
|
mock_in2 = DummySocket("Mesh", "SLICESOCK_Mesh")
|
||||||
|
mock_in2.is_linked = True
|
||||||
|
node.inputs = [mock_in1, mock_in2]
|
||||||
|
|
||||||
|
import NodeSlicer.infra.blender.blender_slice_nodes_generic as b_gen
|
||||||
|
original_resolve = b_gen.resolve_input
|
||||||
|
b_gen.resolve_input = lambda sock, **kw: [sc1, sc2] if sock is mock_in1 else sc3
|
||||||
|
try:
|
||||||
|
res = node.evaluate_output("Mesh")
|
||||||
|
self.assertIsInstance(res, SlicingCollection)
|
||||||
|
self.assertEqual(len(res.objects), 3)
|
||||||
|
self.assertEqual({o.name for o in res.objects}, {"part1", "part2", "part3"})
|
||||||
|
finally:
|
||||||
|
b_gen.resolve_input = original_resolve
|
||||||
|
|
||||||
|
def test_drop_geometry_list_evaluation(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_DropGeometry
|
||||||
|
|
||||||
|
node = SLICENODE_DropGeometry()
|
||||||
|
so1 = SlicingObject()
|
||||||
|
so1.name = "part1"
|
||||||
|
so1.mesh = np.array([[[0, 0, 5], [1, 0, 5], [0, 1, 5], [0, 0, 1]]], dtype=np.float64)
|
||||||
|
sc1 = SlicingCollection([], "M1")
|
||||||
|
sc1.objects = [so1]
|
||||||
|
|
||||||
|
so2 = SlicingObject()
|
||||||
|
so2.name = "part2"
|
||||||
|
so2.mesh = np.array([[[0, 0, 2], [1, 0, 2], [0, 1, 2], [0, 0, 1]]], dtype=np.float64)
|
||||||
|
sc2 = SlicingCollection([], "M2")
|
||||||
|
sc2.objects = [so2]
|
||||||
|
|
||||||
|
node.resolve = lambda field: [sc1, sc2] if field == "Mesh" else 0.0
|
||||||
|
|
||||||
|
res_mesh = node.evaluate_output("Mesh")
|
||||||
|
res_trans = node.evaluate_output("Translation")
|
||||||
|
|
||||||
|
self.assertIsInstance(res_mesh, list)
|
||||||
|
self.assertEqual(len(res_mesh), 2)
|
||||||
|
self.assertIsInstance(res_trans, list)
|
||||||
|
self.assertEqual(res_trans, [(0.0, 0.0, -5.0), (0.0, 0.0, -2.0)])
|
||||||
|
|
||||||
|
def test_start_print_rejects_list_in_evaluate(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.output import SLICENODE_StartPrint
|
||||||
|
|
||||||
|
node = SLICENODE_StartPrint()
|
||||||
|
node.resolve = lambda field: ["G1 X0", "G1 X10"] if field == "Gcode" else None
|
||||||
|
|
||||||
|
op = MagicMock()
|
||||||
|
res = node.evaluate(operator=op)
|
||||||
|
self.assertEqual(res, 'error')
|
||||||
|
op.report.assert_called_with({'ERROR'}, "Start Print does not accept a list of Gcode")
|
||||||
|
|
||||||
|
|
||||||
|
class DummyInterfaceItem:
|
||||||
|
def __init__(self, name, identifier, in_out="INPUT", socket_type="SLICESOCK_Paths"):
|
||||||
|
self.name = name
|
||||||
|
self.identifier = identifier
|
||||||
|
self.in_out = in_out
|
||||||
|
self.socket_type = socket_type
|
||||||
|
self.item_type = 'SOCKET'
|
||||||
|
|
||||||
|
|
||||||
|
class DummyInterface:
|
||||||
|
def __init__(self, items):
|
||||||
|
self.items_tree = items
|
||||||
|
|
||||||
|
|
||||||
|
class DummyGroupTree:
|
||||||
|
def __init__(self, items=None, nodes=None):
|
||||||
|
self.interface = DummyInterface(items or [])
|
||||||
|
self.nodes = nodes or []
|
||||||
|
|
||||||
|
|
||||||
|
class DummyGroupNode(DummyNode):
|
||||||
|
def __init__(self, name="Group", tree=None):
|
||||||
|
super().__init__("SLICENODE_Group")
|
||||||
|
self.name = name
|
||||||
|
self.slicer_tree = tree
|
||||||
|
|
||||||
|
|
||||||
|
class TestGroupNodeListShapes(unittest.TestCase):
|
||||||
|
def test_group_input_can_accept_list_detection(self):
|
||||||
|
item_paths = DummyInterfaceItem("Paths", "sock_paths", in_out="INPUT", socket_type="SLICESOCK_Paths")
|
||||||
|
item_float = DummyInterfaceItem("FloatVal", "sock_float", in_out="INPUT", socket_type="NodeSocketFloat")
|
||||||
|
|
||||||
|
gin = DummyNode("NodeGroupInput")
|
||||||
|
gin_out_paths = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
gin_out_paths.identifier = "sock_paths"
|
||||||
|
gin_out_float = DummySocket("FloatVal", "NodeSocketFloat")
|
||||||
|
gin_out_float.identifier = "sock_float"
|
||||||
|
gin.add_out(gin_out_paths)
|
||||||
|
gin.add_out(gin_out_float)
|
||||||
|
|
||||||
|
target_node = DummyNode("SLICENODE_BufferShells")
|
||||||
|
target_node.list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
target_in_paths = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
target_node.add_in(target_in_paths)
|
||||||
|
|
||||||
|
link = DummyLink(gin_out_paths, target_in_paths)
|
||||||
|
gin_out_paths.is_linked = True
|
||||||
|
gin_out_paths.links = [link]
|
||||||
|
|
||||||
|
tree = DummyGroupTree([item_paths, item_float], [gin, target_node])
|
||||||
|
|
||||||
|
# Paths input can accept list because target is BufferShells
|
||||||
|
self.assertTrue(group_input_can_accept_list(tree, item_paths))
|
||||||
|
# FloatVal cannot accept list because socket type is Float
|
||||||
|
self.assertFalse(group_input_can_accept_list(tree, item_float))
|
||||||
|
|
||||||
|
def test_group_input_connected_to_forbidden_node_rejects_list(self):
|
||||||
|
item_gcode = DummyInterfaceItem("Gcode", "sock_gcode", in_out="INPUT", socket_type="NodeSocketString")
|
||||||
|
|
||||||
|
gin = DummyNode("NodeGroupInput")
|
||||||
|
gin_out_gcode = DummySocket("Gcode", "NodeSocketString")
|
||||||
|
gin_out_gcode.identifier = "sock_gcode"
|
||||||
|
gin.add_out(gin_out_gcode)
|
||||||
|
|
||||||
|
sp_node = DummyNode("SLICENODE_StartPrint")
|
||||||
|
sp_in = DummySocket("Gcode", "NodeSocketString")
|
||||||
|
sp_node.add_in(sp_in)
|
||||||
|
|
||||||
|
link = DummyLink(gin_out_gcode, sp_in)
|
||||||
|
gin_out_gcode.is_linked = True
|
||||||
|
gin_out_gcode.links = [link]
|
||||||
|
|
||||||
|
tree = DummyGroupTree([item_gcode], [gin, sp_node])
|
||||||
|
|
||||||
|
# StartPrint forbids lists, so group input must NOT accept list
|
||||||
|
self.assertFalse(group_input_can_accept_list(tree, item_gcode))
|
||||||
|
|
||||||
|
def test_group_output_inherent_list(self):
|
||||||
|
item_out = DummyInterfaceItem("Paths", "sock_out_paths", in_out="OUTPUT", socket_type="SLICESOCK_Paths")
|
||||||
|
|
||||||
|
gout = DummyNode("NodeGroupOutput")
|
||||||
|
gout_in = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
gout_in.identifier = "sock_out_paths"
|
||||||
|
gout.add_in(gout_in)
|
||||||
|
|
||||||
|
list_joiner = DummyNode("SLICENODE_PathsToList")
|
||||||
|
list_joiner.is_list_output = True
|
||||||
|
lj_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="LIST")
|
||||||
|
list_joiner.add_out(lj_out)
|
||||||
|
|
||||||
|
link = DummyLink(lj_out, gout_in)
|
||||||
|
gout_in.is_linked = True
|
||||||
|
gout_in.links = [link]
|
||||||
|
|
||||||
|
tree = DummyGroupTree([item_out], [gout, list_joiner])
|
||||||
|
|
||||||
|
# Output fed from PathsToList is always a list
|
||||||
|
self.assertTrue(is_group_output_a_list(None, tree, item_out))
|
||||||
|
|
||||||
|
def test_group_output_element_wise_propagation_and_sync(self):
|
||||||
|
item_in = DummyInterfaceItem("Paths", "sock_in", in_out="INPUT", socket_type="SLICESOCK_Paths")
|
||||||
|
item_out = DummyInterfaceItem("Paths", "sock_out", in_out="OUTPUT", socket_type="SLICESOCK_Paths")
|
||||||
|
|
||||||
|
gin = DummyNode("NodeGroupInput")
|
||||||
|
gin_out = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
gin_out.identifier = "sock_in"
|
||||||
|
gin.add_out(gin_out)
|
||||||
|
|
||||||
|
buf_node = DummyNode("SLICENODE_BufferShells")
|
||||||
|
buf_node.list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
buf_in = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
buf_out = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
buf_node.add_in(buf_in)
|
||||||
|
buf_node.add_out(buf_out)
|
||||||
|
|
||||||
|
gout = DummyNode("NodeGroupOutput")
|
||||||
|
gout_in = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
gout_in.identifier = "sock_out"
|
||||||
|
gout.add_in(gout_in)
|
||||||
|
|
||||||
|
link1 = DummyLink(gin_out, buf_in)
|
||||||
|
gin_out.is_linked = True
|
||||||
|
gin_out.links = [link1]
|
||||||
|
buf_in.is_linked = True
|
||||||
|
buf_in.links = [link1]
|
||||||
|
|
||||||
|
link2 = DummyLink(buf_out, gout_in)
|
||||||
|
buf_out.is_linked = True
|
||||||
|
buf_out.links = [link2]
|
||||||
|
gout_in.is_linked = True
|
||||||
|
gout_in.links = [link2]
|
||||||
|
|
||||||
|
tree = DummyGroupTree([item_in, item_out], [gin, buf_node, gout])
|
||||||
|
|
||||||
|
grp_node = DummyGroupNode("MyGroup", tree)
|
||||||
|
grp_in = DummySocket("Paths", "SLICESOCK_Paths", display_shape="CIRCLE")
|
||||||
|
grp_in.identifier = "sock_in"
|
||||||
|
grp_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="CIRCLE")
|
||||||
|
grp_out.identifier = "sock_out"
|
||||||
|
grp_node.add_in(grp_in)
|
||||||
|
grp_node.add_out(grp_out)
|
||||||
|
|
||||||
|
# Before external link:
|
||||||
|
# Input should be LIST (because list input is possible)
|
||||||
|
# Output should be CIRCLE (because no list is fed)
|
||||||
|
sync_group_node_socket_shapes(grp_node)
|
||||||
|
self.assertEqual(grp_in.display_shape, "LIST")
|
||||||
|
self.assertEqual(grp_out.display_shape, "CIRCLE")
|
||||||
|
|
||||||
|
# Now link a list to the group's input externally:
|
||||||
|
up_node = DummyNode("SLICENODE_PathsToList")
|
||||||
|
up_node.is_list_output = True
|
||||||
|
up_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="LIST")
|
||||||
|
up_node.add_out(up_out)
|
||||||
|
|
||||||
|
ext_link = DummyLink(up_out, grp_in)
|
||||||
|
grp_in.is_linked = True
|
||||||
|
grp_in.links = [ext_link]
|
||||||
|
|
||||||
|
# After external list link:
|
||||||
|
# Output should now become LIST!
|
||||||
|
sync_group_node_socket_shapes(grp_node)
|
||||||
|
self.assertEqual(grp_in.display_shape, "LIST")
|
||||||
|
self.assertEqual(grp_out.display_shape, "LIST")
|
||||||
|
|
||||||
|
def test_valid_link_with_group_nodes(self):
|
||||||
|
item_paths = DummyInterfaceItem("Paths", "sock_paths", in_out="INPUT", socket_type="SLICESOCK_Paths")
|
||||||
|
item_float = DummyInterfaceItem("FloatVal", "sock_float", in_out="INPUT", socket_type="NodeSocketFloat")
|
||||||
|
|
||||||
|
gin = DummyNode("NodeGroupInput")
|
||||||
|
gin_out_paths = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
gin_out_paths.identifier = "sock_paths"
|
||||||
|
gin_out_float = DummySocket("FloatVal", "NodeSocketFloat")
|
||||||
|
gin_out_float.identifier = "sock_float"
|
||||||
|
gin.add_out(gin_out_paths)
|
||||||
|
gin.add_out(gin_out_float)
|
||||||
|
|
||||||
|
target_node = DummyNode("SLICENODE_BufferShells")
|
||||||
|
target_node.list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
target_in_paths = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
target_node.add_in(target_in_paths)
|
||||||
|
|
||||||
|
link = DummyLink(gin_out_paths, target_in_paths)
|
||||||
|
gin_out_paths.is_linked = True
|
||||||
|
gin_out_paths.links = [link]
|
||||||
|
|
||||||
|
tree = DummyGroupTree([item_paths, item_float], [gin, target_node])
|
||||||
|
|
||||||
|
grp_node = DummyGroupNode("Group", tree)
|
||||||
|
grp_in_paths = DummySocket("Paths", "SLICESOCK_Paths")
|
||||||
|
grp_in_paths.identifier = "sock_paths"
|
||||||
|
grp_in_float = DummySocket("FloatVal", "NodeSocketFloat")
|
||||||
|
grp_in_float.identifier = "sock_float"
|
||||||
|
grp_node.add_in(grp_in_paths)
|
||||||
|
grp_node.add_in(grp_in_float)
|
||||||
|
|
||||||
|
list_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="LIST")
|
||||||
|
validator = SLICETREE_NodeTree()
|
||||||
|
|
||||||
|
# Linking list to Paths group input (which accepts list): allowed
|
||||||
|
self.assertTrue(validator.valid_link(list_out, grp_in_paths))
|
||||||
|
|
||||||
|
# Linking list to FloatVal group input: rejected
|
||||||
|
self.assertFalse(validator.valid_link(list_out, grp_in_float))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Loading…
Add table
Reference in a new issue