transforms
This commit is contained in:
parent
4fe4f4e75c
commit
fda0042ba0
7 changed files with 351 additions and 52 deletions
|
|
@ -415,11 +415,13 @@ def _copy_slicing_object(so: SlicingObject) -> SlicingObject:
|
|||
new_so.object_type = so.object_type
|
||||
new_so.modifiers = so.modifiers.copy() if so.modifiers else []
|
||||
new_so.extruder = so.extruder
|
||||
new_so.transforms = [dict(t) for t in getattr(so, "transforms", [])]
|
||||
return new_so
|
||||
|
||||
def _copy_slicing_collection(sc: SlicingCollection) -> SlicingCollection:
|
||||
new_sc = SlicingCollection([], sc.name)
|
||||
new_sc.objects = [_copy_slicing_object(so) for so in sc.objects]
|
||||
new_sc.transforms = [dict(t) for t in getattr(sc, "transforms", [])]
|
||||
return new_sc
|
||||
|
||||
def _copy_slicing_group(sg: SlicingGroup) -> SlicingGroup:
|
||||
|
|
@ -428,6 +430,7 @@ def _copy_slicing_group(sg: SlicingGroup) -> SlicingGroup:
|
|||
new_sg.wipe_tower_xy = sg.wipe_tower_xy.copy()
|
||||
new_sg.wipe_tower_rotation_deg = sg.wipe_tower_rotation_deg
|
||||
new_sg.object_offset = sg.object_offset.copy()
|
||||
new_sg.transforms = [dict(t) for t in getattr(sg, "transforms", [])]
|
||||
return new_sg
|
||||
|
||||
|
||||
|
|
@ -488,6 +491,16 @@ class SLICENODE_TransformGeometry(SliceNode):
|
|||
if any(t != 0.0 for t in t_arr):
|
||||
copied_data.offset(t_arr)
|
||||
|
||||
t_rec = {
|
||||
"type": "transform_geometry",
|
||||
"translation": (float(t_arr[0]), float(t_arr[1]), float(t_arr[2])),
|
||||
"rotation": (float(r_arr[0]), float(r_arr[1]), float(r_arr[2])),
|
||||
"scale": (float(s_arr[0]), float(s_arr[1]), float(s_arr[2])),
|
||||
}
|
||||
if not hasattr(copied_data, "transforms"):
|
||||
copied_data.transforms = []
|
||||
copied_data.transforms.append(t_rec)
|
||||
|
||||
return copied_data
|
||||
|
||||
|
||||
|
|
@ -501,8 +514,6 @@ class SLICENODE_DropGeometry(SliceNode):
|
|||
self.add_output("SLICESOCK_Mesh", "Mesh")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | SlicingGroup | None:
|
||||
|
||||
z=self.resolve('Z')
|
||||
if socket_name != "Mesh":
|
||||
return None
|
||||
|
||||
|
|
@ -510,8 +521,31 @@ class SLICENODE_DropGeometry(SliceNode):
|
|||
if not mesh_data:
|
||||
return None
|
||||
|
||||
assert mesh_data.min_z is not None
|
||||
min_z = round(mesh_data.min_z , 6)
|
||||
mesh_data.offset(np.array([0.0, 0.0, z-min_z], dtype=np.float64))
|
||||
z = self.resolve('Z')
|
||||
if z is None:
|
||||
z = 0.0
|
||||
|
||||
return mesh_data
|
||||
assert mesh_data.min_z is not None
|
||||
min_z = round(mesh_data.min_z, 6)
|
||||
offset_z = float(z - min_z)
|
||||
|
||||
if isinstance(mesh_data, SlicingGroup):
|
||||
copied_data = _copy_slicing_group(mesh_data)
|
||||
else:
|
||||
copied_data = _copy_slicing_collection(mesh_data)
|
||||
|
||||
copied_data.offset(np.array([0.0, 0.0, offset_z], dtype=np.float64))
|
||||
|
||||
drop_rec = {
|
||||
"type": "drop_geometry",
|
||||
"target_z": float(z),
|
||||
"offset_z": float(offset_z),
|
||||
"translation": (0.0, 0.0, float(offset_z)),
|
||||
"rotation": (0.0, 0.0, 0.0),
|
||||
"scale": (1.0, 1.0, 1.0),
|
||||
}
|
||||
if not hasattr(copied_data, "transforms"):
|
||||
copied_data.transforms = []
|
||||
copied_data.transforms.append(drop_rec)
|
||||
|
||||
return copied_data
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@ class SLICENODE_GetPaths(SliceNode):
|
|||
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)
|
||||
|
||||
return SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
||||
sp = SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
||||
if hasattr(obj, "transforms"):
|
||||
sp.transforms = list(getattr(obj, "transforms", []))
|
||||
return sp
|
||||
|
||||
@register_class
|
||||
class SLICENODE_SliceShells(SliceNode):
|
||||
|
|
@ -104,6 +107,7 @@ class SLICENODE_SliceShells(SliceNode):
|
|||
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", []))
|
||||
|
||||
if len(z_values) > 0 and fl_height is not None and layer_height > 0:
|
||||
mult = fl_height / layer_height
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
def segment_length_3d(p1, p2) -> float:
|
||||
return ((p2[0]-p1[0])**2 + (p2[1]-p1[1])**2 + (p2[2]-p1[2])**2)**0.5
|
||||
|
||||
prep_translation = [0.0, 0.0, 0.0]
|
||||
prep_rotation = [0.0, 0.0, 0.0]
|
||||
prep_scale = [1.0, 1.0, 1.0]
|
||||
|
||||
for line in gcode.splitlines():
|
||||
# Parse metadata from comments
|
||||
if ';' in line:
|
||||
|
|
@ -72,6 +76,21 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
elif comment.startswith("DEFAULT_LAYER_WIDTH="):
|
||||
try: default_layer_width = float(comment.split('=')[1])
|
||||
except ValueError: pass
|
||||
elif comment.startswith("PREPARATION_TOTAL_TRANSLATION="):
|
||||
try:
|
||||
vals = [float(v) for v in comment.split('=')[1].split(',')]
|
||||
if len(vals) >= 3: prep_translation = vals[:3]
|
||||
except ValueError: pass
|
||||
elif comment.startswith("PREPARATION_TOTAL_ROTATION="):
|
||||
try:
|
||||
vals = [float(v) for v in comment.split('=')[1].split(',')]
|
||||
if len(vals) >= 3: prep_rotation = vals[:3]
|
||||
except ValueError: pass
|
||||
elif comment.startswith("PREPARATION_TOTAL_SCALE="):
|
||||
try:
|
||||
vals = [float(v) for v in comment.split('=')[1].split(',')]
|
||||
if len(vals) >= 3: prep_scale = vals[:3]
|
||||
except ValueError: pass
|
||||
|
||||
line_no_comment = line.split(';')[0].strip()
|
||||
if not line_no_comment: continue
|
||||
|
|
@ -97,7 +116,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
new_e = params.get('E', last_e)
|
||||
|
||||
show = False
|
||||
if self.show_e_0: show = True
|
||||
if getattr(self, "show_e_0", False): show = True
|
||||
if cmd == 'G1' and new_e > 0: show = True
|
||||
|
||||
if show:
|
||||
|
|
@ -140,16 +159,19 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
if not vertices:
|
||||
return
|
||||
|
||||
import sys
|
||||
real_bpy = sys.modules.get('bpy', bpy)
|
||||
|
||||
# Create or find "Debug" mesh object
|
||||
if mesh_name in bpy.data.objects:
|
||||
obj = bpy.data.objects[mesh_name]
|
||||
if mesh_name in real_bpy.data.objects:
|
||||
obj = real_bpy.data.objects[mesh_name]
|
||||
mesh = obj.data
|
||||
mesh.clear_geometry()
|
||||
else:
|
||||
mesh = bpy.data.meshes.new(mesh_name)
|
||||
obj = bpy.data.objects.new(mesh_name, mesh)
|
||||
assert bpy.context.collection
|
||||
bpy.context.collection.objects.link(obj)
|
||||
mesh = real_bpy.data.meshes.new(mesh_name)
|
||||
obj = real_bpy.data.objects.new(mesh_name, mesh)
|
||||
assert real_bpy.context.collection
|
||||
real_bpy.context.collection.objects.link(obj)
|
||||
|
||||
mesh.from_pydata(vertices, edges, [])
|
||||
|
||||
|
|
@ -164,60 +186,126 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
|
||||
# Assign Geometry Nodes nodegroup for preview
|
||||
ng_name = "Nonplanar Slicing Preview"
|
||||
if ng_name not in bpy.data.node_groups:
|
||||
if ng_name not in real_bpy.data.node_groups:
|
||||
# Try to load it from nodes.blend in the addon root
|
||||
addon_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||
blend_path = os.path.join(addon_dir, "nodes.blend")
|
||||
if os.path.exists(blend_path):
|
||||
try:
|
||||
with bpy.data.libraries.load(blend_path) as (data_from, data_to):
|
||||
with real_bpy.data.libraries.load(blend_path) as (data_from, data_to):
|
||||
if ng_name in data_from.node_groups:
|
||||
data_to.node_groups = [ng_name]
|
||||
except Exception as e:
|
||||
if operator: operator.report({'WARNING'}, f"Failed to load preview node group: {e}")
|
||||
|
||||
if ng_name in bpy.data.node_groups:
|
||||
ng = bpy.data.node_groups[ng_name]
|
||||
# Check if already assigned
|
||||
mod = None
|
||||
for m in obj.modifiers:
|
||||
if m.type == 'NODES' and m.node_group == ng: # type: ignore
|
||||
mod = m
|
||||
break
|
||||
# Find or assign Preview modifier
|
||||
mod = None
|
||||
ng = real_bpy.data.node_groups.get(ng_name) if hasattr(real_bpy.data, "node_groups") and hasattr(real_bpy.data.node_groups, "get") else None
|
||||
|
||||
if not mod:
|
||||
mod = obj.modifiers.new(name="Preview", type='NODES') # type: ignore
|
||||
mod.node_group = ng
|
||||
if hasattr(obj, "modifiers"):
|
||||
if "Preview" in obj.modifiers:
|
||||
mod = obj.modifiers["Preview"]
|
||||
else:
|
||||
for m in obj.modifiers:
|
||||
if getattr(m, "type", "") == 'NODES':
|
||||
if ng and getattr(m, "node_group", None) == ng:
|
||||
mod = m
|
||||
break
|
||||
elif mod is None:
|
||||
mod = m
|
||||
|
||||
# Helper to find socket identifiers by name, falling back to specified key
|
||||
def find_socket_key(node_group, name: str, fallback: str) -> str:
|
||||
if hasattr(node_group, "interface") and node_group.interface:
|
||||
if not mod and ng:
|
||||
mod = obj.modifiers.new(name="Preview", type='NODES') # type: ignore
|
||||
mod.node_group = ng
|
||||
|
||||
if mod:
|
||||
ng_ref = getattr(mod, "node_group", ng)
|
||||
# Helper to find socket identifiers by name, falling back to specified key
|
||||
def find_socket_key(node_group, name: str, fallback: str) -> str:
|
||||
if node_group and hasattr(node_group, "interface") and node_group.interface:
|
||||
try:
|
||||
items = getattr(node_group.interface, "items", [])
|
||||
if not isinstance(items, (list, tuple)):
|
||||
items = []
|
||||
for item in items:
|
||||
if getattr(item, "item_type", None) == 'INPUT' and getattr(item, "name", None) == name:
|
||||
ident = getattr(item, "identifier", None)
|
||||
if isinstance(ident, str):
|
||||
return ident
|
||||
except Exception:
|
||||
pass
|
||||
if node_group and hasattr(node_group, "inputs") and node_group.inputs:
|
||||
try:
|
||||
inputs = getattr(node_group, "inputs", [])
|
||||
if not isinstance(inputs, (list, tuple)):
|
||||
inputs = []
|
||||
for inp in inputs:
|
||||
if getattr(inp, "name", None) == name:
|
||||
ident = getattr(inp, "identifier", None)
|
||||
if isinstance(ident, str):
|
||||
return ident
|
||||
except Exception:
|
||||
pass
|
||||
return fallback
|
||||
|
||||
height_key = find_socket_key(ng_ref, "Layer Height", "Socket_4")
|
||||
param_key = find_socket_key(ng_ref, "Parameter", "Socket_2")
|
||||
trans_key = find_socket_key(ng_ref, "Translation", "Socket_8")
|
||||
rot_key = find_socket_key(ng_ref, "Rotation", "Socket_6")
|
||||
scale_key = find_socket_key(ng_ref, "Scale", "Socket_9")
|
||||
|
||||
# Always overwrite layer height and parameter values on modifier
|
||||
if height_key:
|
||||
try: mod[height_key] = default_layer_height * scale_factor
|
||||
except Exception: pass
|
||||
if param_key:
|
||||
try: mod[param_key] = 10000.0 / scale_length
|
||||
except Exception: pass
|
||||
|
||||
# Helper to assign 3D vector values to modifier socket properties
|
||||
def _set_vector(socket_key: str | None, val: tuple[float, float, float]):
|
||||
if not socket_key:
|
||||
return
|
||||
written = False
|
||||
# Try updating items if property is an existing sequence/array (e.g. IDPropertyArray or Vector)
|
||||
try:
|
||||
prop = mod.get(socket_key) if hasattr(mod, "get") else None
|
||||
if prop is not None and hasattr(prop, "__getitem__"):
|
||||
try:
|
||||
for item in node_group.interface.items:
|
||||
if getattr(item, "item_type", None) == 'INPUT' and item.name == name:
|
||||
return item.identifier
|
||||
prop[0] = float(val[0])
|
||||
prop[1] = float(val[1])
|
||||
prop[2] = float(val[2])
|
||||
written = True
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(node_group, "inputs") and node_group.inputs:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Try direct tuple/sequence assignment
|
||||
if not written:
|
||||
try:
|
||||
mod[socket_key] = val
|
||||
written = True
|
||||
except Exception:
|
||||
try:
|
||||
for inp in node_group.inputs:
|
||||
if inp.name == name:
|
||||
return inp.identifier
|
||||
mod[socket_key] = [val[0], val[1], val[2]]
|
||||
written = True
|
||||
except Exception:
|
||||
pass
|
||||
return fallback
|
||||
|
||||
height_key = find_socket_key(ng, "Layer Height", "Socket_4")
|
||||
width_key = find_socket_key(ng, "Layer Width", "Socket_3")
|
||||
param_key = find_socket_key(ng, "Parameter", "Socket_2")
|
||||
# Always overwrite Translation (Socket_8), Rotation (Socket_6), Scale (Socket_9)
|
||||
translation_val = (float(prep_translation[0]) * 0.001, float(prep_translation[1]) * 0.001, float(prep_translation[2]) * 0.001)
|
||||
rotation_val = (float(prep_rotation[0]), float(prep_rotation[1]), float(prep_rotation[2]))
|
||||
scale_val = (float(prep_scale[0]), float(prep_scale[1]), float(prep_scale[2]))
|
||||
|
||||
# Set the values on the modifier
|
||||
if height_key:
|
||||
mod[height_key] = default_layer_height * scale_factor
|
||||
if width_key:
|
||||
mod[width_key] = default_layer_width * scale_factor
|
||||
if param_key:
|
||||
mod[param_key] = 10000.0 / scale_length
|
||||
_set_vector(trans_key, translation_val)
|
||||
_set_vector("Socket_8", translation_val)
|
||||
|
||||
_set_vector(rot_key, rotation_val)
|
||||
_set_vector("Socket_6", rotation_val)
|
||||
|
||||
_set_vector(scale_key, scale_val)
|
||||
_set_vector("Socket_9", scale_val)
|
||||
|
||||
def evaluate(self, operator=None):
|
||||
gcodes = self.resolve_multi_input("Gcode")
|
||||
|
|
|
|||
|
|
@ -159,8 +159,9 @@ class SlicePaths(list[np.ndarray]):
|
|||
extra_attributes: dict[str, list[np.ndarray]]
|
||||
layer_height: float
|
||||
first_layer_height: float | None
|
||||
transforms: list[dict]
|
||||
|
||||
def __init__(self, paths: list[np.ndarray] = [], groups: list[int] = [], extrusion_rate: list[np.ndarray] | None = None, feedrate: list[np.ndarray] | None = None, extra_attributes: dict[str, list[np.ndarray]] | None = None, layer_height: float = 0.2, first_layer_height: float | None = None):
|
||||
def __init__(self, paths: list[np.ndarray] = [], groups: list[int] = [], extrusion_rate: list[np.ndarray] | None = None, feedrate: list[np.ndarray] | None = None, extra_attributes: dict[str, list[np.ndarray]] | None = None, layer_height: float = 0.2, first_layer_height: float | None = None, transforms: list[dict] | None = None):
|
||||
super().__init__(paths)
|
||||
|
||||
self.groups = []
|
||||
|
|
@ -182,6 +183,7 @@ class SlicePaths(list[np.ndarray]):
|
|||
self.extra_attributes = extra_attributes or {}
|
||||
self.layer_height = layer_height
|
||||
self.first_layer_height = first_layer_height
|
||||
self.transforms = list(transforms) if transforms is not None else []
|
||||
|
||||
def copy_with(self, paths: list[np.ndarray], groups: list[int] | None = None, extrusion_rate: list[np.ndarray] | None = None, feedrate: list[np.ndarray] | None = None, extra_attributes: dict[str, list[np.ndarray]] | None = None) -> SlicePaths:
|
||||
"""Returns a new SlicePaths object with new paths/groups, preserving all metadata and instance attributes."""
|
||||
|
|
@ -306,8 +308,13 @@ class SlicePaths(list[np.ndarray]):
|
|||
base = primary_source if primary_source is not None else paths_list[0]
|
||||
merged = base.copy_with([])
|
||||
|
||||
merged_transforms = []
|
||||
for sp in paths_list:
|
||||
merged.extend_with_groups(sp)
|
||||
for t in getattr(sp, "transforms", []):
|
||||
if t not in merged_transforms:
|
||||
merged_transforms.append(t)
|
||||
merged.transforms = merged_transforms
|
||||
|
||||
return merged
|
||||
|
||||
|
|
@ -337,6 +344,13 @@ class SlicePaths(list[np.ndarray]):
|
|||
for path, ext, feed, extra in items:
|
||||
merged.append_with_group(path, group=g, extrusion_rate=ext, feedrate=feed, extra_attributes=extra)
|
||||
|
||||
merged_transforms = []
|
||||
for sp in paths_list:
|
||||
for t in getattr(sp, "transforms", []):
|
||||
if t not in merged_transforms:
|
||||
merged_transforms.append(t)
|
||||
merged.transforms = merged_transforms
|
||||
|
||||
return merged
|
||||
|
||||
def get_infill_mask(self) -> SlicePaths:
|
||||
|
|
@ -1562,6 +1576,7 @@ class Pather(object):
|
|||
self.retraction: float = 0.0
|
||||
self.retraction_feedrate: float = 0.0
|
||||
self.lift: float = 0.0
|
||||
self.transforms: list[dict] = list(getattr(paths, "transforms", []))
|
||||
|
||||
@property
|
||||
def layer_width(self) -> float:
|
||||
|
|
@ -1630,6 +1645,47 @@ class Pather(object):
|
|||
f";DEFAULT_LAYER_WIDTH={self.layer_width:.6f}",
|
||||
f";OFFSET_X={offset_x:.6f}",
|
||||
f";OFFSET_Y={offset_y:.6f}",
|
||||
])
|
||||
|
||||
if hasattr(self, "transforms") and self.transforms:
|
||||
import json
|
||||
header.append(f";PREPARATION_TRANSFORMS={json.dumps(self.transforms)}")
|
||||
|
||||
total_tx, total_ty, total_tz = 0.0, 0.0, 0.0
|
||||
total_rx, total_ry, total_rz = 0.0, 0.0, 0.0
|
||||
total_sx, total_sy, total_sz = 1.0, 1.0, 1.0
|
||||
|
||||
for i, t in enumerate(self.transforms):
|
||||
t_type = t.get("type", "transform")
|
||||
tr = t.get("translation", (0.0, 0.0, 0.0))
|
||||
rot = t.get("rotation", (0.0, 0.0, 0.0))
|
||||
sc = t.get("scale", (1.0, 1.0, 1.0))
|
||||
|
||||
total_tx += tr[0]
|
||||
total_ty += tr[1]
|
||||
total_tz += tr[2]
|
||||
total_rx += rot[0]
|
||||
total_ry += rot[1]
|
||||
total_rz += rot[2]
|
||||
total_sx *= sc[0]
|
||||
total_sy *= sc[1]
|
||||
total_sz *= sc[2]
|
||||
|
||||
if t_type == "transform_geometry":
|
||||
header.append(f";TRANSFORM_GEOMETRY_{i}_TRANSLATION={tr[0]:.6f},{tr[1]:.6f},{tr[2]:.6f}")
|
||||
header.append(f";TRANSFORM_GEOMETRY_{i}_ROTATION={rot[0]:.6f},{rot[1]:.6f},{rot[2]:.6f}")
|
||||
header.append(f";TRANSFORM_GEOMETRY_{i}_SCALE={sc[0]:.6f},{sc[1]:.6f},{sc[2]:.6f}")
|
||||
elif t_type == "drop_geometry":
|
||||
target_z = t.get("target_z", 0.0)
|
||||
off_z = t.get("offset_z", 0.0)
|
||||
header.append(f";DROP_GEOMETRY_{i}_TARGET_Z={target_z:.6f}")
|
||||
header.append(f";DROP_GEOMETRY_{i}_OFFSET_Z={off_z:.6f}")
|
||||
|
||||
header.append(f";PREPARATION_TOTAL_TRANSLATION={total_tx:.6f},{total_ty:.6f},{total_tz:.6f}")
|
||||
header.append(f";PREPARATION_TOTAL_ROTATION={total_rx:.6f},{total_ry:.6f},{total_rz:.6f}")
|
||||
header.append(f";PREPARATION_TOTAL_SCALE={total_sx:.6f},{total_sy:.6f},{total_sz:.6f}")
|
||||
|
||||
header.extend([
|
||||
"G21 ; set units to mm",
|
||||
"G90 ; absolute XYZ positioning",
|
||||
"M83 ; relative extrusion mode",
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ class SlicingObject():
|
|||
self.parent = parent
|
||||
self.attributes = {}
|
||||
self.modifiers = []
|
||||
self.transforms = []
|
||||
|
||||
assert bpy.context.scene.unit_settings
|
||||
scene_scale: float = bpy.context.scene.unit_settings.scale_length
|
||||
|
|
@ -127,6 +128,11 @@ class SlicingObject():
|
|||
merged.parent = objects[0].parent
|
||||
merged.object_type = objects[0].object_type
|
||||
merged.extruder = objects[0].extruder
|
||||
merged.transforms = []
|
||||
for o in objects:
|
||||
for t in getattr(o, "transforms", []):
|
||||
if t not in merged.transforms:
|
||||
merged.transforms.append(dict(t))
|
||||
|
||||
valid_meshes = [o.mesh for o in objects if o.mesh.size]
|
||||
merged.mesh = np.concatenate(valid_meshes, axis=0) if valid_meshes else np.empty((0, 4, 3))
|
||||
|
|
@ -257,11 +263,13 @@ class SlicingObject():
|
|||
class SlicingCollection():
|
||||
name: str
|
||||
objects: list[SlicingObject]
|
||||
transforms: list[dict]
|
||||
|
||||
def __init__(self, objs: list[Object], parent: str):
|
||||
self.objects = [SlicingObject(obj, parent) for obj in objs]
|
||||
self.objects = [o for o in self.objects if o.mesh.size]
|
||||
self.name = parent
|
||||
self.transforms = []
|
||||
|
||||
def _clear_cache(self):
|
||||
for prop in ["all_verts", "unique_verts"]:
|
||||
|
|
@ -386,9 +394,11 @@ class SlicingGroup():
|
|||
wipe_tower_xy: NDArray = np.array([0, 0])
|
||||
wipe_tower_rotation_deg: float = 0
|
||||
object_offset: NDArray = np.array([0.0, 0.0, 0.0])
|
||||
transforms: list[dict]
|
||||
|
||||
def __init__(self, selected_objs: list[Object]) -> None:
|
||||
self.object_offset = np.array([0.0, 0.0, 0.0])
|
||||
self.transforms = []
|
||||
|
||||
family = []
|
||||
for obj in selected_objs:
|
||||
|
|
|
|||
BIN
nodes.blend
BIN
nodes.blend
Binary file not shown.
|
|
@ -34,12 +34,16 @@ mock_types.Menu = DummyMenu
|
|||
mock_types.Panel = DummyPanel
|
||||
mock_types.PropertyGroup = DummyPropertyGroup
|
||||
|
||||
mock_bpy = MagicMock()
|
||||
if 'bpy' in sys.modules and isinstance(sys.modules['bpy'], MagicMock):
|
||||
mock_bpy = sys.modules['bpy']
|
||||
else:
|
||||
mock_bpy = MagicMock()
|
||||
sys.modules['bpy'] = mock_bpy
|
||||
|
||||
mock_bpy.context.scene = MagicMock()
|
||||
mock_bpy.context.scene.unit_settings.scale_length = 0.001
|
||||
mock_bpy.types = mock_types
|
||||
|
||||
sys.modules['bpy'] = mock_bpy
|
||||
sys.modules['bpy.types'] = mock_types
|
||||
sys.modules['bpy.props'] = MagicMock()
|
||||
sys.modules['bpy.utils'] = MagicMock()
|
||||
|
|
@ -258,6 +262,109 @@ class TestTransform(unittest.TestCase):
|
|||
self.assertEqual(result_sc_2.min_z, 10.0)
|
||||
np.testing.assert_allclose(result_sc_2.objects[0].mesh[0, 0], [1.0, 2.0, 10.0])
|
||||
|
||||
def test_gcode_transform_metadata(self):
|
||||
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_TransformGeometry, SLICENODE_DropGeometry
|
||||
from NodeSlicer.core.slice import SlicePaths, Pather
|
||||
|
||||
so = SlicingObject()
|
||||
so.mesh = np.array([[[0.0, 0.0, 5.0],
|
||||
[10.0, 0.0, 5.0],
|
||||
[5.0, 10.0, 10.0],
|
||||
[0.0, 0.0, 1.0]]], dtype=np.float64)
|
||||
|
||||
sc = SlicingCollection([], "coll")
|
||||
sc.objects = [so]
|
||||
|
||||
# 1. Transform Geometry
|
||||
t_node = SLICENODE_TransformGeometry()
|
||||
t_node.resolve = lambda f: {
|
||||
"Mesh": sc,
|
||||
"Translation": (10.0, 20.0, 0.0),
|
||||
"Rotation": (0.0, 0.0, 0.0),
|
||||
"Scale": (1.0, 1.0, 1.0)
|
||||
}.get(f)
|
||||
|
||||
sc_transformed = t_node.evaluate_output("Mesh")
|
||||
self.assertIsNotNone(sc_transformed)
|
||||
self.assertEqual(len(sc_transformed.transforms), 1)
|
||||
self.assertEqual(sc_transformed.transforms[0]["type"], "transform_geometry")
|
||||
|
||||
# 2. Drop Geometry
|
||||
d_node = SLICENODE_DropGeometry()
|
||||
d_node.resolve = lambda f: {
|
||||
"Mesh": sc_transformed,
|
||||
"Z": 0.0
|
||||
}.get(f)
|
||||
|
||||
sc_dropped = d_node.evaluate_output("Mesh")
|
||||
self.assertIsNotNone(sc_dropped)
|
||||
self.assertEqual(len(sc_dropped.transforms), 2)
|
||||
self.assertEqual(sc_dropped.transforms[1]["type"], "drop_geometry")
|
||||
|
||||
# 3. Create SlicePaths from geometry
|
||||
path = np.array([[10.0, 20.0, 0.0], [20.0, 20.0, 0.0], [15.0, 30.0, 0.0]])
|
||||
sp = SlicePaths([path], transforms=sc_dropped.transforms)
|
||||
|
||||
# 4. Generate G-code with Pather
|
||||
pather = Pather(sp)
|
||||
gcode = pather.to_gcode(include_header=True)
|
||||
|
||||
self.assertIn(";PREPARATION_TRANSFORMS=", gcode)
|
||||
self.assertIn(";TRANSFORM_GEOMETRY_0_TRANSLATION=10.000000,20.000000,0.000000", gcode)
|
||||
self.assertIn(";DROP_GEOMETRY_1_OFFSET_Z=-5.000000", gcode)
|
||||
self.assertIn(";PREPARATION_TOTAL_TRANSLATION=10.000000,20.000000,-5.000000", gcode)
|
||||
|
||||
# 5. Process G-code with GcodeToMesh preview node
|
||||
from NodeSlicer.core.nodeslicer.nodes.preview import SLICENODE_GcodeToMesh
|
||||
preview_node = SLICENODE_GcodeToMesh()
|
||||
preview_node.show_e_0 = True
|
||||
|
||||
mock_mod_dict = {}
|
||||
class MockModifier:
|
||||
type = 'NODES'
|
||||
node_group = MagicMock()
|
||||
def __getitem__(self, k): return mock_mod_dict.get(k)
|
||||
def __setitem__(self, k, v): mock_mod_dict[k] = v
|
||||
def get(self, k, default=None): return mock_mod_dict.get(k, default)
|
||||
|
||||
mock_obj = MagicMock()
|
||||
mock_obj.type = 'MESH'
|
||||
mock_obj.modifiers = [MockModifier()]
|
||||
|
||||
import sys
|
||||
sys.modules['bpy'].data.objects = {"PreviewObj": mock_obj}
|
||||
sys.modules['bpy'].data.node_groups = {"Nonplanar Slicing Preview": mock_obj.modifiers[0].node_group}
|
||||
|
||||
preview_node._process_gcode(gcode, "PreviewObj")
|
||||
|
||||
# Verify modifier sockets Socket_8, Socket_6, Socket_9
|
||||
self.assertEqual(mock_mod_dict.get("Socket_8"), (0.01, 0.02, -0.005))
|
||||
self.assertEqual(mock_mod_dict.get("Socket_6"), (0.0, 0.0, 0.0))
|
||||
|
||||
# Test overwriting on an existing mesh object with pre-existing modifier values
|
||||
mock_mod_dict["Socket_8"] = [0.0, 0.0, 0.0]
|
||||
mock_mod_dict["Socket_6"] = [1.0, 1.0, 1.0]
|
||||
mock_mod_dict["Socket_9"] = [2.0, 2.0, 2.0]
|
||||
|
||||
preview_node._process_gcode(gcode, "PreviewObj")
|
||||
|
||||
self.assertEqual(tuple(mock_mod_dict.get("Socket_8")), (0.01, 0.02, -0.005))
|
||||
self.assertEqual(tuple(mock_mod_dict.get("Socket_6")), (0.0, 0.0, 0.0))
|
||||
self.assertEqual(tuple(mock_mod_dict.get("Socket_9")), (1.0, 1.0, 1.0))
|
||||
|
||||
# Test G-code with non-zero 3D rotation and non-uniform scale
|
||||
gcode_rot = (
|
||||
";PREPARATION_TOTAL_TRANSLATION=10.000000,20.000000,30.000000\n"
|
||||
";PREPARATION_TOTAL_ROTATION=0.150000,0.250000,0.350000\n"
|
||||
";PREPARATION_TOTAL_SCALE=1.500000,2.000000,2.500000\n"
|
||||
"G1 X10 Y20 Z30 E1\n"
|
||||
)
|
||||
preview_node._process_gcode(gcode_rot, "PreviewObj")
|
||||
self.assertEqual(tuple(mock_mod_dict.get("Socket_8")), (0.01, 0.02, 0.03))
|
||||
self.assertEqual(tuple(mock_mod_dict.get("Socket_6")), (0.15, 0.25, 0.35))
|
||||
self.assertEqual(tuple(mock_mod_dict.get("Socket_9")), (1.5, 2.0, 2.5))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue