fix transforms issues
This commit is contained in:
parent
657660ab2e
commit
6520e2b6f9
4 changed files with 237 additions and 27 deletions
|
|
@ -575,6 +575,7 @@ class SLICENODE_TransformGeometry(SliceNode):
|
|||
_transform_one,
|
||||
main_socket="Geometry",
|
||||
secondary_sockets=["Translation", "Rotation", "Scale"],
|
||||
is_vector=[True, True, True],
|
||||
socket_name=socket_name,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -311,6 +311,7 @@ class SLICENODE_SliceShells(SliceNode):
|
|||
_slice_one,
|
||||
main_socket="Mesh",
|
||||
secondary_sockets=["Layer Height", "First Layer Height", "Rotation"],
|
||||
is_vector=[False, False, True],
|
||||
socket_name=socket_name,
|
||||
default=SlicePaths(),
|
||||
)
|
||||
|
|
@ -394,6 +395,7 @@ class SLICENODE_BufferShells(SliceNode):
|
|||
_buf,
|
||||
main_socket="Paths",
|
||||
secondary_sockets=["Distance", "Rotation"],
|
||||
is_vector=[False, True],
|
||||
socket_name=socket_name,
|
||||
default=SlicePaths(),
|
||||
)
|
||||
|
|
@ -456,6 +458,7 @@ class SLICENODE_BufferCurve(SliceNode):
|
|||
_buf,
|
||||
main_socket="Paths",
|
||||
secondary_sockets=["Distance", "Rotation"],
|
||||
is_vector=[False, True],
|
||||
socket_name=socket_name,
|
||||
default=SlicePaths(),
|
||||
)
|
||||
|
|
@ -498,6 +501,7 @@ class SLICENODE_OffsetCurve(SliceNode):
|
|||
_off,
|
||||
main_socket="Paths",
|
||||
secondary_sockets=["Distance", "Rotation"],
|
||||
is_vector=[False, True],
|
||||
socket_name=socket_name,
|
||||
default=SlicePaths(),
|
||||
)
|
||||
|
|
@ -521,6 +525,7 @@ class SLICENODE_PathOffset(SliceNode):
|
|||
_off,
|
||||
main_socket="Paths",
|
||||
secondary_sockets=["Offset"],
|
||||
is_vector=[True],
|
||||
socket_name=socket_name,
|
||||
default=SlicePaths(),
|
||||
)
|
||||
|
|
@ -546,6 +551,7 @@ class SLICENODE_RotatePath(SliceNode):
|
|||
_rot,
|
||||
main_socket="Paths",
|
||||
secondary_sockets=["Angle", "Center"],
|
||||
is_vector=[False, True],
|
||||
socket_name=socket_name,
|
||||
default=SlicePaths(),
|
||||
)
|
||||
|
|
@ -599,6 +605,7 @@ class SLICENODE_BufferShellsMultiple(SliceNode):
|
|||
_buf_multi,
|
||||
main_socket="Paths",
|
||||
secondary_sockets=["Distance", "Width Step", "Count", "Rotation"],
|
||||
is_vector=[False, False, False, True],
|
||||
socket_name=socket_name,
|
||||
default=SlicePaths(),
|
||||
)
|
||||
|
|
@ -644,6 +651,7 @@ class SLICENODE_Infill(SliceNode):
|
|||
_inf,
|
||||
main_socket="Paths",
|
||||
secondary_sockets=["Density", "Infill Rotation", "Layer Rotation"],
|
||||
is_vector=[False, False, True],
|
||||
socket_name=socket_name,
|
||||
default=SlicePaths(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,27 +20,36 @@ LIST_CAPABLE_SOCKET_TYPES = {
|
|||
}
|
||||
|
||||
def is_vector_value(val: Any) -> bool:
|
||||
"""Checks if val represents a single 3D/4D vector, Euler rotation, or color,
|
||||
"""Checks if val represents a single 2D/3D/4D vector, Euler rotation, or color,
|
||||
rather than a list of such values."""
|
||||
if val is None:
|
||||
return False
|
||||
val_type = type(val).__name__
|
||||
if val_type in ("Vector", "Euler", "Quaternion", "Color"):
|
||||
if val_type in ("Vector", "Euler", "Quaternion", "Color", "Matrix") or "bpy_prop_array" in val_type:
|
||||
return True
|
||||
if isinstance(val, np.ndarray):
|
||||
if val.ndim == 1 and val.shape[0] in (3, 4):
|
||||
if val.ndim == 1 and val.shape[0] in (2, 3, 4):
|
||||
return True
|
||||
return False
|
||||
if isinstance(val, (list, tuple)):
|
||||
if len(val) in (3, 4) and all(isinstance(x, (int, float, np.number)) and not isinstance(x, bool) for x in val):
|
||||
if hasattr(val, "__len__") and not isinstance(val, (str, bytes, dict, set)):
|
||||
try:
|
||||
length = len(val)
|
||||
if length in (2, 3, 4):
|
||||
if all(isinstance(x, (int, float, np.number)) and not isinstance(x, bool) for x in val):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def _is_sequence_collection(val: Any) -> bool:
|
||||
if isinstance(val, (list, tuple)):
|
||||
return True
|
||||
if isinstance(val, np.ndarray):
|
||||
return val.ndim >= 2
|
||||
if hasattr(val, "__iter__") and not hasattr(val, "shape") and not isinstance(val, (str, bytes, dict, set)):
|
||||
cls_name = type(val).__name__
|
||||
if "bpy_prop_array" in cls_name:
|
||||
return False
|
||||
if "bpy_prop" in cls_name or "Collection" in cls_name or hasattr(val, "foreach_get"):
|
||||
return True
|
||||
import collections.abc
|
||||
|
|
@ -55,6 +64,9 @@ def is_input_a_list(val: Any, is_vector: bool = False) -> bool:
|
|||
return False
|
||||
if type(val).__name__ == "SlicePaths" or getattr(val, "is_slice_paths", False):
|
||||
return False
|
||||
val_type = type(val).__name__
|
||||
if val_type in ("Vector", "Euler", "Quaternion", "Color", "Matrix") or "bpy_prop_array" in val_type:
|
||||
return False
|
||||
if is_vector:
|
||||
if is_vector_value(val):
|
||||
return False
|
||||
|
|
@ -87,6 +99,7 @@ def prepare_indexed_inputs(
|
|||
is_vector: list[bool] | None = None,
|
||||
input_names: list[str] | None = None,
|
||||
is_atomic: list[bool] | None = None,
|
||||
main_is_vector: bool = False,
|
||||
) -> tuple[bool, list[Any], list[list[Any]]]:
|
||||
"""Prepares and validates main and secondary inputs according to the generalized list architecture:
|
||||
- Main input can be a list or a single item.
|
||||
|
|
@ -99,7 +112,7 @@ def prepare_indexed_inputs(
|
|||
Returns:
|
||||
(is_main_list, main_items, broadcasted_secondary_lists)
|
||||
"""
|
||||
main_is_list = is_input_a_list(main_data, is_vector=False)
|
||||
main_is_list = is_input_a_list(main_data, is_vector=main_is_vector)
|
||||
main_items = list(main_data) if main_is_list else [main_data]
|
||||
n = len(main_items)
|
||||
|
||||
|
|
@ -113,6 +126,13 @@ def prepare_indexed_inputs(
|
|||
is_atom = atom_flags[i] if i < len(atom_flags) else False
|
||||
name = names[i] if i < len(names) else f"input_{i+1}"
|
||||
|
||||
if not is_vec and name.lower() in ("translation", "rotation", "scale", "offset", "center", "layer rotation", "color"):
|
||||
is_vec = True
|
||||
|
||||
if is_vec and is_vector_value(sec_val):
|
||||
broadcasted_secondaries.append([sec_val] * n)
|
||||
continue
|
||||
|
||||
if is_atom:
|
||||
is_nested_list = (
|
||||
isinstance(sec_val, (list, tuple))
|
||||
|
|
@ -160,6 +180,7 @@ def eval_element_wise_multivalue(
|
|||
input_names: list[str] | None = None,
|
||||
output_index: int | None = None,
|
||||
is_atomic: list[bool] | None = None,
|
||||
main_is_vector: bool = False,
|
||||
) -> Any:
|
||||
"""Evaluates op_func element-wise with support for main and secondary list inputs:
|
||||
- If main_data is a list, returns a list of results.
|
||||
|
|
@ -175,6 +196,7 @@ def eval_element_wise_multivalue(
|
|||
is_vector=is_vector,
|
||||
input_names=input_names,
|
||||
is_atomic=is_atomic,
|
||||
main_is_vector=main_is_vector,
|
||||
)
|
||||
|
||||
def _call(item: Any, i: int) -> Any:
|
||||
|
|
@ -192,24 +214,32 @@ def eval_element_wise_multivalue(
|
|||
|
||||
def is_socket_vector_type(node: Any, socket_name: str) -> bool:
|
||||
"""Checks whether the socket on the node is a vector/rotation/color socket."""
|
||||
name_lower = socket_name.lower()
|
||||
if name_lower in ("translation", "rotation", "scale", "offset", "center", "layer rotation", "color"):
|
||||
return True
|
||||
inputs = getattr(node, "inputs", None)
|
||||
if inputs:
|
||||
sock = None
|
||||
if hasattr(inputs, "get"):
|
||||
sock = inputs.get(socket_name)
|
||||
elif isinstance(inputs, (list, tuple)):
|
||||
if sock is None:
|
||||
try:
|
||||
for s in inputs:
|
||||
if getattr(s, "name", "") == socket_name:
|
||||
if getattr(s, "name", "").lower() == name_lower:
|
||||
sock = s
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if sock is not None:
|
||||
sock_type = getattr(sock, "type", "")
|
||||
if sock_type in ("VECTOR", "ROTATION", "RGBA"):
|
||||
return True
|
||||
bl_id = getattr(sock, "bl_idname", "")
|
||||
if bl_id in ("NodeSocketVector", "NodeSocketRotation", "NodeSocketColor", "NodeSocketVectorXYZ", "NodeSocketVectorEuler"):
|
||||
if any(term in bl_id for term in ("Vector", "Rotation", "Color", "Euler")):
|
||||
return True
|
||||
if bl_id in ("NodeSocketFloat", "NodeSocketInt", "NodeSocketString", "NodeSocketBool", "SLICESOCK_Mesh", "SLICESOCK_Paths", "SLICESOCK_Settings"):
|
||||
return False
|
||||
name_lower = socket_name.lower()
|
||||
return name_lower in ("translation", "rotation", "scale", "offset", "center", "layer rotation", "color")
|
||||
return False
|
||||
|
||||
def eval_multivalue(
|
||||
node: Any,
|
||||
|
|
@ -273,7 +303,13 @@ def eval_multivalue(
|
|||
if is_vector is None:
|
||||
vec_flags = [is_socket_vector_type(node, sname) for sname in secondary_sockets]
|
||||
else:
|
||||
vec_flags = is_vector
|
||||
vec_flags = list(is_vector)
|
||||
for idx, sname in enumerate(secondary_sockets):
|
||||
if idx < len(vec_flags) and not vec_flags[idx]:
|
||||
if is_socket_vector_type(node, sname):
|
||||
vec_flags[idx] = True
|
||||
|
||||
main_is_vec = is_socket_vector_type(node, main_socket) if main_socket else False
|
||||
|
||||
try:
|
||||
return eval_element_wise_multivalue(
|
||||
|
|
@ -284,6 +320,7 @@ def eval_multivalue(
|
|||
input_names=secondary_sockets,
|
||||
output_index=output_index,
|
||||
is_atomic=is_atomic,
|
||||
main_is_vector=main_is_vec,
|
||||
)
|
||||
except Exception as e:
|
||||
from .blender_slice_nodes_generic import attach_node_failure_info
|
||||
|
|
|
|||
|
|
@ -852,27 +852,191 @@ class TestGeneralizedListArchitecture(unittest.TestCase):
|
|||
self.assertIn("2 items", str(ctx.exception))
|
||||
|
||||
def test_vector_input_distinction(self):
|
||||
# Single vector is not a list
|
||||
single_vec = (1.0, 2.0, 3.0)
|
||||
self.assertTrue(is_vector_value(single_vec))
|
||||
self.assertFalse(is_input_a_list(single_vec, is_vector=True))
|
||||
class MockBpyPropArray:
|
||||
def __init__(self, items):
|
||||
self._items = list(items)
|
||||
def __iter__(self):
|
||||
return iter(self._items)
|
||||
def __len__(self):
|
||||
return len(self._items)
|
||||
def __getitem__(self, idx):
|
||||
return self._items[idx]
|
||||
MockBpyPropArray.__name__ = "bpy_prop_array"
|
||||
|
||||
# Single vector variants (tuple, list, np.ndarray, MockBpyPropArray)
|
||||
vec_tuple = (1.0, 2.0, 3.0)
|
||||
vec_list = [1.0, 2.0, 3.0]
|
||||
vec_np = np.array([1.0, 2.0, 3.0])
|
||||
vec_bpy = MockBpyPropArray([1.0, 2.0, 3.0])
|
||||
|
||||
for v in (vec_tuple, vec_list, vec_np, vec_bpy):
|
||||
self.assertTrue(is_vector_value(v), f"Failed for {type(v).__name__}")
|
||||
self.assertFalse(is_input_a_list(v, is_vector=True), f"Failed for {type(v).__name__}")
|
||||
|
||||
# List of vectors IS a list
|
||||
vec_list = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0)]
|
||||
self.assertFalse(is_vector_value(vec_list))
|
||||
self.assertTrue(is_input_a_list(vec_list, is_vector=True))
|
||||
list_of_vecs = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0)]
|
||||
self.assertFalse(is_vector_value(list_of_vecs))
|
||||
self.assertTrue(is_input_a_list(list_of_vecs, is_vector=True))
|
||||
|
||||
# Broadcasting single vector across main list
|
||||
main = ["geom1", "geom2"]
|
||||
is_list, items, (t_list,) = prepare_indexed_inputs(main, single_vec, is_vector=[True], input_names=["Translation"])
|
||||
for v in (vec_tuple, vec_list, vec_np, vec_bpy):
|
||||
is_list, items, (t_list,) = prepare_indexed_inputs(main, v, is_vector=[True], input_names=["Translation"])
|
||||
self.assertTrue(is_list)
|
||||
self.assertEqual(t_list, [(1.0, 2.0, 3.0), (1.0, 2.0, 3.0)])
|
||||
self.assertEqual(len(t_list), 2)
|
||||
self.assertEqual(list(t_list[0]), [1.0, 2.0, 3.0])
|
||||
self.assertEqual(list(t_list[1]), [1.0, 2.0, 3.0])
|
||||
|
||||
# Single main geometry with single atomic vector must NOT be treated as a list of 3 items
|
||||
single_main = "geom1"
|
||||
for v in (vec_tuple, vec_list, vec_np, vec_bpy):
|
||||
is_list, items, (t_list,) = prepare_indexed_inputs(single_main, v, is_vector=[True], input_names=["Translation"])
|
||||
self.assertFalse(is_list)
|
||||
self.assertEqual(len(t_list), 1)
|
||||
self.assertEqual(list(t_list[0]), [1.0, 2.0, 3.0])
|
||||
|
||||
# Matching list of vectors index-per-index
|
||||
is_list, items, (t_list,) = prepare_indexed_inputs(main, vec_list, is_vector=[True], input_names=["Translation"])
|
||||
is_list, items, (t_list,) = prepare_indexed_inputs(main, list_of_vecs, is_vector=[True], input_names=["Translation"])
|
||||
self.assertTrue(is_list)
|
||||
self.assertEqual(t_list, [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0)])
|
||||
|
||||
def test_transform_geometry_atomic_vector_inputs(self):
|
||||
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_TransformGeometry
|
||||
|
||||
class MockBpyPropArray:
|
||||
def __init__(self, items):
|
||||
self._items = list(items)
|
||||
def __iter__(self):
|
||||
return iter(self._items)
|
||||
def __len__(self):
|
||||
return len(self._items)
|
||||
def __getitem__(self, idx):
|
||||
return self._items[idx]
|
||||
MockBpyPropArray.__name__ = "bpy_prop_array"
|
||||
|
||||
so1 = SlicingObject()
|
||||
so1.name = "Part1"
|
||||
so1.mesh = np.zeros((1, 4, 3), dtype=np.float64)
|
||||
c1 = SlicingCollection([so1], "Col1")
|
||||
|
||||
so2 = SlicingObject()
|
||||
so2.name = "Part2"
|
||||
so2.mesh = np.zeros((1, 4, 3), dtype=np.float64)
|
||||
c2 = SlicingCollection([so2], "Col2")
|
||||
|
||||
tf_node = SLICENODE_TransformGeometry()
|
||||
|
||||
# 1. Single geometry + atomic tuple translation
|
||||
tf_node.resolve = lambda label: {
|
||||
"Geometry": c1,
|
||||
"Translation": (10.0, 20.0, 30.0),
|
||||
"Rotation": (0.0, 0.0, 0.0),
|
||||
"Scale": (1.0, 1.0, 1.0),
|
||||
}.get(label)
|
||||
res = tf_node.evaluate_output("Geometry")
|
||||
self.assertNotIsInstance(res, list)
|
||||
self.assertIsInstance(res, SlicingCollection)
|
||||
self.assertEqual(res.transforms[-1]["translation"], (10.0, 20.0, 30.0))
|
||||
|
||||
# 2. Single geometry + atomic list translation [10, 20, 30] (NOT a list of 3 items!)
|
||||
tf_node.resolve = lambda label: {
|
||||
"Geometry": c1,
|
||||
"Translation": [10.0, 20.0, 30.0],
|
||||
"Rotation": [0.0, 0.0, 0.0],
|
||||
"Scale": [1.0, 1.0, 1.0],
|
||||
}.get(label)
|
||||
res = tf_node.evaluate_output("Geometry")
|
||||
self.assertNotIsInstance(res, list)
|
||||
self.assertIsInstance(res, SlicingCollection)
|
||||
self.assertEqual(tuple(res.transforms[-1]["translation"]), (10.0, 20.0, 30.0))
|
||||
|
||||
# 3. Single geometry + mock bpy_prop_array (unlinked socket default value in Blender)
|
||||
tf_node.resolve = lambda label: {
|
||||
"Geometry": c1,
|
||||
"Translation": MockBpyPropArray([10.0, 20.0, 30.0]),
|
||||
"Rotation": MockBpyPropArray([0.0, 0.0, 0.0]),
|
||||
"Scale": MockBpyPropArray([1.0, 1.0, 1.0]),
|
||||
}.get(label)
|
||||
res = tf_node.evaluate_output("Geometry")
|
||||
self.assertNotIsInstance(res, list)
|
||||
self.assertIsInstance(res, SlicingCollection)
|
||||
self.assertEqual(tuple(res.transforms[-1]["translation"]), (10.0, 20.0, 30.0))
|
||||
|
||||
# 4. Multiple geometries + atomic translation (broadcasted)
|
||||
tf_node.resolve = lambda label: {
|
||||
"Geometry": [c1, c2],
|
||||
"Translation": (10.0, 20.0, 30.0),
|
||||
"Rotation": (0.0, 0.0, 0.0),
|
||||
"Scale": (1.0, 1.0, 1.0),
|
||||
}.get(label)
|
||||
res_multi = tf_node.evaluate_output("Geometry")
|
||||
self.assertIsInstance(res_multi, list)
|
||||
self.assertEqual(len(res_multi), 2)
|
||||
self.assertEqual(res_multi[0].transforms[-1]["translation"], (10.0, 20.0, 30.0))
|
||||
self.assertEqual(res_multi[1].transforms[-1]["translation"], (10.0, 20.0, 30.0))
|
||||
|
||||
# 5. Multiple geometries + list of 2 translations
|
||||
tf_node.resolve = lambda label: {
|
||||
"Geometry": [c1, c2],
|
||||
"Translation": [(10.0, 0.0, 0.0), (20.0, 0.0, 0.0)],
|
||||
"Rotation": (0.0, 0.0, 0.0),
|
||||
"Scale": (1.0, 1.0, 1.0),
|
||||
}.get(label)
|
||||
res_matched = tf_node.evaluate_output("Geometry")
|
||||
self.assertIsInstance(res_matched, list)
|
||||
self.assertEqual(len(res_matched), 2)
|
||||
self.assertEqual(res_matched[0].transforms[-1]["translation"], (10.0, 0.0, 0.0))
|
||||
self.assertEqual(res_matched[1].transforms[-1]["translation"], (20.0, 0.0, 0.0))
|
||||
|
||||
# 6. Single geometry + list of 2 translations -> raises ValueError
|
||||
tf_node.resolve = lambda label: {
|
||||
"Geometry": c1,
|
||||
"Translation": [(10.0, 0.0, 0.0), (20.0, 0.0, 0.0)],
|
||||
"Rotation": (0.0, 0.0, 0.0),
|
||||
"Scale": (1.0, 1.0, 1.0),
|
||||
}.get(label)
|
||||
with self.assertRaises(ValueError):
|
||||
tf_node.evaluate_output("Geometry")
|
||||
|
||||
def test_path_nodes_atomic_vector_inputs(self):
|
||||
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathOffset, SLICENODE_RotatePath
|
||||
|
||||
class MockBpyPropArray:
|
||||
def __init__(self, items):
|
||||
self._items = list(items)
|
||||
def __iter__(self):
|
||||
return iter(self._items)
|
||||
def __len__(self):
|
||||
return len(self._items)
|
||||
def __getitem__(self, idx):
|
||||
return self._items[idx]
|
||||
MockBpyPropArray.__name__ = "bpy_prop_array"
|
||||
|
||||
p1 = SlicePaths([np.array([[0, 0, 0], [10, 0, 0]], dtype=np.int32)])
|
||||
|
||||
# Path Offset with atomic (10, 0, 0) as tuple, list, and bpy_prop_array
|
||||
node_offset = SLICENODE_PathOffset()
|
||||
for off in ((10.0, 0.0, 0.0), [10.0, 0.0, 0.0], MockBpyPropArray([10.0, 0.0, 0.0])):
|
||||
node_offset.resolve = lambda label, o=off: {
|
||||
"Paths": p1,
|
||||
"Offset": o,
|
||||
}.get(label)
|
||||
res = node_offset.evaluate_output("Paths")
|
||||
self.assertIsInstance(res, SlicePaths)
|
||||
self.assertEqual(len(res), 1)
|
||||
|
||||
# Rotate Path with atomic center
|
||||
node_rot = SLICENODE_RotatePath()
|
||||
for cent in ((5.0, 5.0, 0.0), [5.0, 5.0, 0.0], MockBpyPropArray([5.0, 5.0, 0.0])):
|
||||
node_rot.resolve = lambda label, c=cent: {
|
||||
"Paths": p1,
|
||||
"Angle": 45.0,
|
||||
"Center": c,
|
||||
}.get(label)
|
||||
res = node_rot.evaluate_output("Paths")
|
||||
self.assertIsInstance(res, SlicePaths)
|
||||
self.assertEqual(len(res), 1)
|
||||
|
||||
def test_eval_element_wise_multivalue(self):
|
||||
op = lambda item, a, b: f"{item}_{a}_{b}"
|
||||
res = eval_element_wise_multivalue(op, ["x", "y"], [1, 2], "const")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue