up
This commit is contained in:
parent
02175f379a
commit
30f9d88e4c
9 changed files with 405 additions and 54 deletions
|
|
@ -444,23 +444,157 @@ def _copy_slicing_group(sg: SlicingGroup) -> SlicingGroup:
|
||||||
return new_sg
|
return new_sg
|
||||||
|
|
||||||
|
|
||||||
|
def _transform_slice_paths(sp: SlicePaths, t_arr: np.ndarray, r_arr: np.ndarray, s_arr: np.ndarray) -> SlicePaths:
|
||||||
|
new_paths = []
|
||||||
|
has_scale = any(s != 1.0 for s in s_arr)
|
||||||
|
has_rot = any(r != 0.0 for r in r_arr)
|
||||||
|
has_trans = any(t != 0.0 for t in t_arr)
|
||||||
|
|
||||||
|
if not (has_scale or has_rot or has_trans):
|
||||||
|
return sp.copy_with(paths=list(sp))
|
||||||
|
|
||||||
|
R = None
|
||||||
|
if has_rot:
|
||||||
|
rx, ry, rz = r_arr
|
||||||
|
cx, sx = np.cos(rx), np.sin(rx)
|
||||||
|
cy, sy = np.cos(ry), np.sin(ry)
|
||||||
|
cz, sz = np.cos(rz), np.sin(rz)
|
||||||
|
|
||||||
|
Rx = np.array([
|
||||||
|
[1.0, 0.0, 0.0],
|
||||||
|
[0.0, cx, -sx],
|
||||||
|
[0.0, sx, cx]
|
||||||
|
], dtype=np.float64)
|
||||||
|
Ry = np.array([
|
||||||
|
[cy, 0.0, sy],
|
||||||
|
[0.0, 1.0, 0.0],
|
||||||
|
[-sy, 0.0, cy]
|
||||||
|
], dtype=np.float64)
|
||||||
|
Rz = np.array([
|
||||||
|
[cz, -sz, 0.0],
|
||||||
|
[sz, cz, 0.0],
|
||||||
|
[0.0, 0.0, 1.0]
|
||||||
|
], dtype=np.float64)
|
||||||
|
R = Rz @ (Ry @ Rx)
|
||||||
|
|
||||||
|
for p in sp:
|
||||||
|
if len(p) == 0:
|
||||||
|
new_paths.append(p.copy())
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_p = p.astype(np.float64, copy=True)
|
||||||
|
dim = new_p.shape[1] if new_p.ndim > 1 else 0
|
||||||
|
|
||||||
|
if has_scale and dim > 0:
|
||||||
|
if dim >= 3:
|
||||||
|
new_p[:, :3] *= s_arr
|
||||||
|
else:
|
||||||
|
new_p[:, :dim] *= s_arr[:dim]
|
||||||
|
|
||||||
|
if has_rot and R is not None and dim > 0:
|
||||||
|
if dim >= 3:
|
||||||
|
new_p[:, :3] = new_p[:, :3] @ R.T
|
||||||
|
elif dim == 2:
|
||||||
|
rot_2d = R[:2, :2]
|
||||||
|
new_p[:, :2] = new_p[:, :2] @ rot_2d.T
|
||||||
|
|
||||||
|
if has_trans and dim > 0:
|
||||||
|
if dim >= 3:
|
||||||
|
new_p[:, :3] += t_arr
|
||||||
|
else:
|
||||||
|
new_p[:, :dim] += t_arr[:dim]
|
||||||
|
|
||||||
|
new_paths.append(new_p)
|
||||||
|
|
||||||
|
return sp.copy_with(paths=new_paths)
|
||||||
|
|
||||||
|
|
||||||
|
def _transform_geometry_item(geom: Any, t_arr: np.ndarray, r_arr: np.ndarray, s_arr: np.ndarray, t_rec: dict) -> Any:
|
||||||
|
if geom is None:
|
||||||
|
return None
|
||||||
|
elif isinstance(geom, SlicePaths):
|
||||||
|
copied_data = _transform_slice_paths(geom, t_arr, r_arr, s_arr)
|
||||||
|
if not hasattr(copied_data, "transforms") or copied_data.transforms is None:
|
||||||
|
copied_data.transforms = []
|
||||||
|
else:
|
||||||
|
copied_data.transforms = [dict(t) for t in copied_data.transforms]
|
||||||
|
copied_data.transforms.append(t_rec)
|
||||||
|
return copied_data
|
||||||
|
elif isinstance(geom, (list, tuple)):
|
||||||
|
return [_transform_geometry_item(item, t_arr, r_arr, s_arr, t_rec) for item in geom]
|
||||||
|
elif isinstance(geom, SlicingGroup):
|
||||||
|
copied_data = _copy_slicing_group(geom)
|
||||||
|
for col in copied_data.collections.values():
|
||||||
|
if any(s != 1.0 for s in s_arr):
|
||||||
|
col.scale(s_arr)
|
||||||
|
if any(r != 0.0 for r in r_arr):
|
||||||
|
col.rotate(r_arr)
|
||||||
|
if any(t != 0.0 for t in t_arr):
|
||||||
|
col.offset(t_arr)
|
||||||
|
if any(t != 0.0 for t in t_arr):
|
||||||
|
copied_data.wipe_tower_xy = copied_data.wipe_tower_xy + t_arr[0:2]
|
||||||
|
if any(r != 0.0 for r in r_arr):
|
||||||
|
rz = r_arr[2]
|
||||||
|
cz, sz = np.cos(rz), np.sin(rz)
|
||||||
|
rot_mat_2d = np.array([[cz, -sz], [sz, cz]])
|
||||||
|
copied_data.wipe_tower_xy = copied_data.wipe_tower_xy @ rot_mat_2d.T
|
||||||
|
import math
|
||||||
|
copied_data.wipe_tower_rotation_deg = round(copied_data.wipe_tower_rotation_deg + rz * 180 / math.pi, 5)
|
||||||
|
if any(s != 1.0 for s in s_arr):
|
||||||
|
copied_data.wipe_tower_xy *= s_arr[0:2]
|
||||||
|
|
||||||
|
if not hasattr(copied_data, "transforms") or copied_data.transforms is None:
|
||||||
|
copied_data.transforms = []
|
||||||
|
copied_data.transforms.append(t_rec)
|
||||||
|
return copied_data
|
||||||
|
elif isinstance(geom, SlicingCollection):
|
||||||
|
copied_data = _copy_slicing_collection(geom)
|
||||||
|
if any(s != 1.0 for s in s_arr):
|
||||||
|
copied_data.scale(s_arr)
|
||||||
|
if any(r != 0.0 for r in r_arr):
|
||||||
|
copied_data.rotate(r_arr)
|
||||||
|
if any(t != 0.0 for t in t_arr):
|
||||||
|
copied_data.offset(t_arr)
|
||||||
|
|
||||||
|
if not hasattr(copied_data, "transforms") or copied_data.transforms is None:
|
||||||
|
copied_data.transforms = []
|
||||||
|
copied_data.transforms.append(t_rec)
|
||||||
|
return copied_data
|
||||||
|
elif isinstance(geom, SlicingObject):
|
||||||
|
copied_data = _copy_slicing_object(geom)
|
||||||
|
if any(s != 1.0 for s in s_arr):
|
||||||
|
copied_data.scale(s_arr)
|
||||||
|
if any(r != 0.0 for r in r_arr):
|
||||||
|
copied_data.rotate(r_arr)
|
||||||
|
if any(t != 0.0 for t in t_arr):
|
||||||
|
copied_data.offset(t_arr)
|
||||||
|
|
||||||
|
if not hasattr(copied_data, "transforms") or copied_data.transforms is None:
|
||||||
|
copied_data.transforms = []
|
||||||
|
copied_data.transforms.append(t_rec)
|
||||||
|
return copied_data
|
||||||
|
return geom
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_TransformGeometry(SliceNode):
|
class SLICENODE_TransformGeometry(SliceNode):
|
||||||
bl_label = "Transform Geometry"
|
bl_label = "Transform Geometry"
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Mesh", "Mesh")
|
self.add_input("SLICESOCK_Geometry", "Geometry")
|
||||||
self.add_input("SLICESOCK_Vector", "Translation")
|
self.add_input("SLICESOCK_Vector", "Translation")
|
||||||
self.add_input("SLICESOCK_VectorRotation", "Rotation")
|
self.add_input("SLICESOCK_VectorRotation", "Rotation")
|
||||||
self.add_input("SLICESOCK_Vector", "Scale").value = (1.0, 1.0, 1.0)
|
self.add_input("SLICESOCK_Vector", "Scale").value = (1.0, 1.0, 1.0)
|
||||||
self.add_output("SLICESOCK_Mesh", "Mesh")
|
self.add_output("SLICESOCK_Geometry", "Geometry")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | SlicingGroup | None:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
if socket_name != "Mesh":
|
if socket_name not in ("Geometry", "Mesh"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
mesh_data: SlicingCollection | SlicingGroup | None = self.resolve("Mesh")
|
geom_data: Any = self.resolve("Geometry")
|
||||||
if not mesh_data:
|
if geom_data is None:
|
||||||
|
geom_data = self.resolve("Mesh")
|
||||||
|
if geom_data is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
translation = self.resolve("Translation") or (0.0, 0.0, 0.0)
|
translation = self.resolve("Translation") or (0.0, 0.0, 0.0)
|
||||||
|
|
@ -471,47 +605,14 @@ class SLICENODE_TransformGeometry(SliceNode):
|
||||||
r_arr = np.array(rotation, dtype=np.float64)
|
r_arr = np.array(rotation, dtype=np.float64)
|
||||||
s_arr = np.array(scale, dtype=np.float64)
|
s_arr = np.array(scale, dtype=np.float64)
|
||||||
|
|
||||||
if isinstance(mesh_data, SlicingGroup):
|
|
||||||
copied_data = _copy_slicing_group(mesh_data)
|
|
||||||
for col in copied_data.collections.values():
|
|
||||||
if any(s != 1.0 for s in s_arr):
|
|
||||||
col.scale(s_arr)
|
|
||||||
if any(r != 0.0 for r in r_arr):
|
|
||||||
col.rotate(r_arr)
|
|
||||||
if any(t != 0.0 for t in t_arr):
|
|
||||||
col.offset(t_arr)
|
|
||||||
# Also offset wipe tower location
|
|
||||||
if any(t != 0.0 for t in t_arr):
|
|
||||||
copied_data.wipe_tower_xy = copied_data.wipe_tower_xy + t_arr[0:2]
|
|
||||||
if any(r != 0.0 for r in r_arr):
|
|
||||||
rz = r_arr[2]
|
|
||||||
cz, sz = np.cos(rz), np.sin(rz)
|
|
||||||
rot_mat_2d = np.array([[cz, -sz], [sz, cz]])
|
|
||||||
copied_data.wipe_tower_xy = copied_data.wipe_tower_xy @ rot_mat_2d.T
|
|
||||||
import math
|
|
||||||
copied_data.wipe_tower_rotation_deg = round(copied_data.wipe_tower_rotation_deg + rz * 180 / math.pi, 5)
|
|
||||||
if any(s != 1.0 for s in s_arr):
|
|
||||||
copied_data.wipe_tower_xy *= s_arr[0:2]
|
|
||||||
else:
|
|
||||||
copied_data = _copy_slicing_collection(mesh_data)
|
|
||||||
if any(s != 1.0 for s in s_arr):
|
|
||||||
copied_data.scale(s_arr)
|
|
||||||
if any(r != 0.0 for r in r_arr):
|
|
||||||
copied_data.rotate(r_arr)
|
|
||||||
if any(t != 0.0 for t in t_arr):
|
|
||||||
copied_data.offset(t_arr)
|
|
||||||
|
|
||||||
t_rec = {
|
t_rec = {
|
||||||
"type": "transform_geometry",
|
"type": "transform_geometry",
|
||||||
"translation": (float(t_arr[0]), float(t_arr[1]), float(t_arr[2])),
|
"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])),
|
"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])),
|
"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
|
return _transform_geometry_item(geom_data, t_arr, r_arr, s_arr, t_rec)
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
|
||||||
|
|
@ -461,10 +461,10 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
def evaluate(self, operator=None):
|
||||||
import datetime
|
import datetime
|
||||||
from ...pdf_exporter import extract_unique_top_view_edges, export_mesh_to_pdf
|
from ...pdf_exporter import extract_unique_top_view_edges, export_mesh_to_pdf, get_first_object_name
|
||||||
|
|
||||||
geom = self.resolve("Geometry")
|
geom = self.resolve("Geometry")
|
||||||
slice_paths = self.resolve("Paths") or self.resolve("SlicePaths")
|
slice_paths = self.resolve("Paths")
|
||||||
folder: str = self.resolve("Folder") or ""
|
folder: str = self.resolve("Folder") or ""
|
||||||
|
|
||||||
# Collect raw geometry items
|
# Collect raw geometry items
|
||||||
|
|
@ -556,6 +556,16 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
current_raw_geom = None
|
current_raw_geom = None
|
||||||
current_slice_paths = target_item
|
current_slice_paths = target_item
|
||||||
|
|
||||||
|
current_header_info = dict(header_info)
|
||||||
|
part_name_prop = (self.header_part_name or "").strip()
|
||||||
|
if not part_name_prop:
|
||||||
|
default_part_name = (
|
||||||
|
get_first_object_name(target_item)
|
||||||
|
or get_first_object_name(current_raw_geom)
|
||||||
|
or get_first_object_name(current_slice_paths)
|
||||||
|
)
|
||||||
|
current_header_info["part_name"] = default_part_name
|
||||||
|
|
||||||
edges_2d = np.empty((0, 2, 2), dtype=np.float64)
|
edges_2d = np.empty((0, 2, 2), dtype=np.float64)
|
||||||
if current_raw_geom is not None:
|
if current_raw_geom is not None:
|
||||||
raw_e = extract_unique_top_view_edges(current_raw_geom)
|
raw_e = extract_unique_top_view_edges(current_raw_geom)
|
||||||
|
|
@ -580,7 +590,7 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
draw_bbox=self.draw_bbox,
|
draw_bbox=self.draw_bbox,
|
||||||
draw_sa2_holes=self.draw_sa2_holes,
|
draw_sa2_holes=self.draw_sa2_holes,
|
||||||
draw_header=self.draw_header,
|
draw_header=self.draw_header,
|
||||||
header_info=header_info,
|
header_info=current_header_info,
|
||||||
crease_angle=self.crease_angle,
|
crease_angle=self.crease_angle,
|
||||||
margin_mm=self.margin,
|
margin_mm=self.margin,
|
||||||
font_size_mm=self.font_size * 2.834645669291339,
|
font_size_mm=self.font_size * 2.834645669291339,
|
||||||
|
|
|
||||||
|
|
@ -231,16 +231,21 @@ 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 = self.resolve_multi_input("Gcode")
|
gcodes: list[str] = [g for g in (self.resolve_multi_input("Gcode") or []) 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:
|
||||||
base_mesh_name = "GcodePreview"
|
base_mesh_name = "GcodePreview"
|
||||||
|
|
||||||
for i, gcode in enumerate(gcodes):
|
total_parts = len(gcodes)
|
||||||
if not gcode: continue
|
num_digits = max(2, len(str(total_parts)))
|
||||||
|
|
||||||
|
for i, gcode in enumerate(gcodes):
|
||||||
|
if total_parts > 1:
|
||||||
|
mesh_name = f"{base_mesh_name}{i+1:0{num_digits}d}"
|
||||||
|
else:
|
||||||
|
mesh_name = base_mesh_name
|
||||||
|
|
||||||
mesh_name = base_mesh_name if i == 0 else f"{base_mesh_name}_{i+1}"
|
|
||||||
try:
|
try:
|
||||||
self._process_gcode(gcode, mesh_name, operator)
|
self._process_gcode(gcode, mesh_name, operator)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ from .geometry import (
|
||||||
)
|
)
|
||||||
from .metadata import (
|
from .metadata import (
|
||||||
get_first_attribute_value,
|
get_first_attribute_value,
|
||||||
|
get_first_object_name,
|
||||||
extract_settings_items,
|
extract_settings_items,
|
||||||
extract_object_label_info,
|
extract_object_label_info,
|
||||||
extract_object_name_positions,
|
extract_object_name_positions,
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,55 @@ def get_first_attribute_value(so_item: Any, attr_name: str) -> Any:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_object_name(mesh_data: Any) -> str:
|
||||||
|
"""
|
||||||
|
Extracts the name of the first SlicingObject, SlicePaths, SlicingCollection,
|
||||||
|
or Blender Object in mesh_data.
|
||||||
|
"""
|
||||||
|
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
||||||
|
from ..slice import SlicePaths
|
||||||
|
|
||||||
|
if mesh_data is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if isinstance(mesh_data, (SlicingObject, SlicePaths)):
|
||||||
|
if getattr(mesh_data, "name", ""):
|
||||||
|
return str(mesh_data.name)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if isinstance(mesh_data, (list, tuple)) and not isinstance(mesh_data, SlicePaths):
|
||||||
|
for sub in mesh_data:
|
||||||
|
name = get_first_object_name(sub)
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if isinstance(mesh_data, SlicingGroup) and mesh_data.collections:
|
||||||
|
for key, coll in mesh_data.collections.items():
|
||||||
|
name = get_first_object_name(coll)
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
if key:
|
||||||
|
return str(key)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if isinstance(mesh_data, SlicingCollection):
|
||||||
|
if mesh_data.objects:
|
||||||
|
for so in mesh_data.objects:
|
||||||
|
name = get_first_object_name(so)
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
if getattr(mesh_data, "name", ""):
|
||||||
|
return str(mesh_data.name)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if hasattr(mesh_data, "name") and getattr(mesh_data, "name"):
|
||||||
|
return str(getattr(mesh_data, "name"))
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_object_properties(so_item: Any, props: list[tuple[str, str]]) -> list[tuple[str, str]]:
|
def resolve_object_properties(so_item: Any, props: list[tuple[str, str]]) -> list[tuple[str, str]]:
|
||||||
"""
|
"""
|
||||||
Resolves property tuples for a SlicingObject.
|
Resolves property tuples for a SlicingObject.
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,8 @@ def render_technical_header_table(
|
||||||
|
|
||||||
# Extract field values with fallbacks
|
# Extract field values with fallbacks
|
||||||
company_val = str(header_info.get("company", "") or "-")
|
company_val = str(header_info.get("company", "") or "-")
|
||||||
part_name_val = str(header_info.get("part_name", "") or obj_name or "-")
|
raw_part_name = header_info.get("part_name", "")
|
||||||
|
part_name_val = str((raw_part_name or "").strip() or obj_name or "-")
|
||||||
dwg_val = str(header_info.get("drawing_number", "") or "-")
|
dwg_val = str(header_info.get("drawing_number", "") or "-")
|
||||||
author_val = str(header_info.get("author", "") or "-")
|
author_val = str(header_info.get("author", "") or "-")
|
||||||
rev_val = str(header_info.get("revision", "") or "A")
|
rev_val = str(header_info.get("revision", "") or "A")
|
||||||
|
|
@ -647,11 +648,16 @@ class PDFHeaderTableElement(PDFElement):
|
||||||
if not exp.draw_header or not exp.header_info:
|
if not exp.draw_header or not exp.header_info:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
from .metadata import get_first_object_name
|
||||||
|
|
||||||
primary_obj_name = ""
|
primary_obj_name = ""
|
||||||
if exp.slicing_objs:
|
if exp.slicing_objs:
|
||||||
primary_obj_name = str(getattr(exp.slicing_objs[0], "name", ""))
|
for so in exp.slicing_objs:
|
||||||
elif exp.raw_geometry is not None and hasattr(exp.raw_geometry, "name"):
|
if hasattr(so, "name") and getattr(so, "name"):
|
||||||
primary_obj_name = str(getattr(exp.raw_geometry, "name", ""))
|
primary_obj_name = str(getattr(so, "name"))
|
||||||
|
break
|
||||||
|
if not primary_obj_name:
|
||||||
|
primary_obj_name = get_first_object_name(exp.raw_geometry) or get_first_object_name(exp.slice_paths)
|
||||||
|
|
||||||
render_technical_header_table(
|
render_technical_header_table(
|
||||||
s=s,
|
s=s,
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,8 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
- **Assign Property to Object**: Assigns custom settings/properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) for rendering in PDF properties tables.
|
- **Assign Property to Object**: Assigns custom settings/properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) for rendering in PDF properties tables.
|
||||||
- **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`).
|
- **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`).
|
||||||
- **Settings**: Input socket accepting configuration settings or property items.
|
- **Settings**: Input socket accepting configuration settings or property items.
|
||||||
- **Transform Geometry**: Moves, rotates, and scales a `SlicingCollection` or `SlicingGroup` relative to the origin.
|
- **Transform Geometry**: Moves, rotates, and scales geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) relative to the origin.
|
||||||
- **Mesh**: Input mesh data.
|
- **Geometry**: Input geometry data.
|
||||||
- **Translation**: 3D vector specifying the translation offset.
|
- **Translation**: 3D vector specifying the translation offset.
|
||||||
- **Rotation**: 3D vector/Euler rotation values.
|
- **Rotation**: 3D vector/Euler rotation values.
|
||||||
- **Scale**: 3D vector specifying scaling factors along the X, Y, and Z axes (defaults to `1.0, 1.0, 1.0`).
|
- **Scale**: 3D vector specifying scaling factors along the X, Y, and Z axes (defaults to `1.0, 1.0, 1.0`).
|
||||||
|
|
@ -128,7 +128,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
- **Gcode to File**: Writes G-code string to a file on disk.
|
- **Gcode to File**: Writes G-code string to a file on disk.
|
||||||
- **Automatic Binary Conversion**: If the G-code contains the comment `; binary_gcode = 1`, the node automatically converts the output to Prusa's binary G-code format (`.bgcode`) using MeatPack encoding and Heatshrink compression.
|
- **Automatic Binary Conversion**: If the G-code contains the comment `; binary_gcode = 1`, the node automatically converts the output to Prusa's binary G-code format (`.bgcode`) using MeatPack encoding and Heatshrink compression.
|
||||||
- **Mesh to PDF**: Generates a 1:1 true-scale 2D top-view (+Z orthographic) vector PDF from a `SlicingGroup` or `SlicingCollection` with a custom grid and background color. Subpanel menus are collapsed by default.
|
- **Mesh to PDF**: Generates a 1:1 true-scale 2D top-view (+Z orthographic) vector PDF from a `SlicingGroup` or `SlicingCollection` with a custom grid and background color. Subpanel menus are collapsed by default.
|
||||||
- **Technical Drawing Header**: Collapsible menu containing string inputs for technical drawing info (Company, Part Name, Drawing No., Author, Revision, Material, Tolerance). Automatically computes ISO date and scale length (`1:1`). Renders an ISO/DIN technical drawing header table on the bottom-right of the drawing.
|
- **Technical Drawing Header**: Collapsible menu containing string inputs for technical drawing info (Company, Part Name, Drawing No., Author, Revision, Material, Tolerance). If Part Name is left empty, it automatically defaults to the name of the first object being rendered. Automatically computes ISO date and scale length (`1:1`). Renders an ISO/DIN technical drawing header table on the bottom-right of the drawing.
|
||||||
- **Geometry**: Input socket accepting a single geometry (`SlicingGroup`, `SlicingCollection`, or `SlicingObject`). Join multiple geometries using Join Geometry nodes.
|
- **Geometry**: Input socket accepting a single geometry (`SlicingGroup`, `SlicingCollection`, or `SlicingObject`). Join multiple geometries using Join Geometry nodes.
|
||||||
- **SlicePaths**: Input socket accepting `SlicePaths` (reserved for path rendering).
|
- **SlicePaths**: Input socket accepting `SlicePaths` (reserved for path rendering).
|
||||||
- **Folder**: Target folder or PDF file path on disk.
|
- **Folder**: Target folder or PDF file path on disk.
|
||||||
|
|
|
||||||
|
|
@ -1427,6 +1427,83 @@ class TestPdfExporter(unittest.TestCase):
|
||||||
self.assertIn(" m ", content)
|
self.assertIn(" m ", content)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_first_object_name(self):
|
||||||
|
from NodeSlicer.core.pdf_exporter import get_first_object_name
|
||||||
|
from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject, SlicingGroup, SlicingCollection
|
||||||
|
from NodeSlicer.core.slice import SlicePaths
|
||||||
|
|
||||||
|
so = SlicingObject()
|
||||||
|
so.name = "FirstObj"
|
||||||
|
self.assertEqual(get_first_object_name(so), "FirstObj")
|
||||||
|
|
||||||
|
sp = SlicePaths()
|
||||||
|
sp.name = "PathObj"
|
||||||
|
self.assertEqual(get_first_object_name(sp), "PathObj")
|
||||||
|
|
||||||
|
self.assertEqual(get_first_object_name([None, so]), "FirstObj")
|
||||||
|
|
||||||
|
coll = SlicingCollection.__new__(SlicingCollection)
|
||||||
|
coll.objects = [so]
|
||||||
|
coll.name = "CollName"
|
||||||
|
group = SlicingGroup.__new__(SlicingGroup)
|
||||||
|
group.collections = {"GroupKey": coll}
|
||||||
|
self.assertEqual(get_first_object_name(group), "FirstObj")
|
||||||
|
|
||||||
|
def test_mesh_to_pdf_default_part_name_first_object(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.output import SLICENODE_MeshToPdf
|
||||||
|
from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject
|
||||||
|
|
||||||
|
node = SLICENODE_MeshToPdf.__new__(SLICENODE_MeshToPdf)
|
||||||
|
node.draw_wireframe = True
|
||||||
|
node.draw_boundary = False
|
||||||
|
node.draw_contour = False
|
||||||
|
node.draw_crease = False
|
||||||
|
node.crease_angle = 30.0
|
||||||
|
node.draw_bbox = False
|
||||||
|
node.draw_sa2_holes = False
|
||||||
|
node.draw_header = True
|
||||||
|
node.header_company = ""
|
||||||
|
node.header_part_name = "" # Empty field -> should default to object name
|
||||||
|
node.header_drawing_number = ""
|
||||||
|
node.header_author = ""
|
||||||
|
node.header_revision = "A"
|
||||||
|
node.header_material = ""
|
||||||
|
node.header_tolerance = ""
|
||||||
|
node.font_size = 0.0
|
||||||
|
node.margin = 10.0
|
||||||
|
node.wire_color = (0.0, 0.0, 0.0)
|
||||||
|
node.bg_color = (1.0, 1.0, 1.0)
|
||||||
|
node.bbox_color = (0.0, 0.0, 1.0)
|
||||||
|
|
||||||
|
class MockOperator:
|
||||||
|
def __init__(self):
|
||||||
|
self.reports = []
|
||||||
|
def report(self, type_set, message):
|
||||||
|
self.reports.append((type_set, message))
|
||||||
|
|
||||||
|
op = MockOperator()
|
||||||
|
so = SlicingObject()
|
||||||
|
so.name = "GearWheel"
|
||||||
|
so.mesh = np.array([[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0]]], dtype=np.float64)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
pdf_file = os.path.join(tmpdir, "output.pdf")
|
||||||
|
resolved_inputs = {
|
||||||
|
"Geometry": so,
|
||||||
|
"Paths": None,
|
||||||
|
"Folder": pdf_file,
|
||||||
|
"Settings": None,
|
||||||
|
}
|
||||||
|
node.resolve = lambda fieldname: resolved_inputs.get(fieldname)
|
||||||
|
|
||||||
|
res = node.evaluate(operator=op)
|
||||||
|
self.assertEqual(res, "ok")
|
||||||
|
self.assertTrue(os.path.exists(pdf_file))
|
||||||
|
with open(pdf_file, "rb") as f:
|
||||||
|
content = decompress_pdf_streams(f.read())
|
||||||
|
self.assertIn("(GearWheel) Tj", content)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,66 @@ class TestTransform(unittest.TestCase):
|
||||||
# v0 becomes [6.0, 22.0, 36.0]
|
# v0 becomes [6.0, 22.0, 36.0]
|
||||||
np.testing.assert_allclose(result_sc.objects[0].mesh[0, 0], [6.0, 22.0, 36.0], atol=1e-7)
|
np.testing.assert_allclose(result_sc.objects[0].mesh[0, 0], [6.0, 22.0, 36.0], atol=1e-7)
|
||||||
|
|
||||||
|
def test_transform_node_slice_paths(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_TransformGeometry
|
||||||
|
from NodeSlicer.core.slice import SlicePaths
|
||||||
|
|
||||||
|
path = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=np.float64)
|
||||||
|
sp = SlicePaths([path])
|
||||||
|
|
||||||
|
node = SLICENODE_TransformGeometry()
|
||||||
|
node.resolve = lambda f: {
|
||||||
|
"Geometry": sp,
|
||||||
|
"Translation": (10.0, 20.0, 30.0),
|
||||||
|
"Rotation": (0.0, 0.0, np.pi / 2.0),
|
||||||
|
"Scale": (2.0, 2.0, 2.0)
|
||||||
|
}.get(f)
|
||||||
|
|
||||||
|
result_sp = node.evaluate_output("Geometry")
|
||||||
|
self.assertIsNotNone(result_sp)
|
||||||
|
self.assertIsInstance(result_sp, SlicePaths)
|
||||||
|
self.assertIsNot(result_sp, sp)
|
||||||
|
|
||||||
|
# Point 1: [1, 2, 3] -> scale [2, 4, 6] -> rot Z pi/2 [-4, 2, 6] -> trans [6, 22, 36]
|
||||||
|
np.testing.assert_allclose(result_sp[0][0], [6.0, 22.0, 36.0], atol=1e-7)
|
||||||
|
# Point 2: [4, 5, 6] -> scale [8, 10, 12] -> rot Z pi/2 [-10, 8, 12] -> trans [0, 28, 42]
|
||||||
|
np.testing.assert_allclose(result_sp[0][1], [0.0, 28.0, 42.0], atol=1e-7)
|
||||||
|
|
||||||
|
# Check transforms metadata
|
||||||
|
self.assertEqual(len(result_sp.transforms), 1)
|
||||||
|
self.assertEqual(result_sp.transforms[0]["type"], "transform_geometry")
|
||||||
|
|
||||||
|
def test_transform_node_list_input(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_TransformGeometry
|
||||||
|
from NodeSlicer.core.slice import SlicePaths
|
||||||
|
|
||||||
|
so = SlicingObject()
|
||||||
|
so.mesh = np.array([[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0], [0.0, 0.0, 1.0]]], dtype=np.float64)
|
||||||
|
sc = SlicingCollection([], "coll")
|
||||||
|
sc.objects = [so]
|
||||||
|
|
||||||
|
path = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=np.float64)
|
||||||
|
sp = SlicePaths([path])
|
||||||
|
|
||||||
|
geom_list = [sc, sp]
|
||||||
|
|
||||||
|
node = SLICENODE_TransformGeometry()
|
||||||
|
node.resolve = lambda f: {
|
||||||
|
"Geometry": geom_list,
|
||||||
|
"Translation": (10.0, 0.0, 0.0),
|
||||||
|
"Rotation": (0.0, 0.0, 0.0),
|
||||||
|
"Scale": (1.0, 1.0, 1.0)
|
||||||
|
}.get(f)
|
||||||
|
|
||||||
|
result_list = node.evaluate_output("Geometry")
|
||||||
|
self.assertIsInstance(result_list, list)
|
||||||
|
self.assertEqual(len(result_list), 2)
|
||||||
|
self.assertIsInstance(result_list[0], SlicingCollection)
|
||||||
|
self.assertIsInstance(result_list[1], SlicePaths)
|
||||||
|
|
||||||
|
np.testing.assert_allclose(result_list[0].objects[0].mesh[0, 0], [11.0, 2.0, 3.0])
|
||||||
|
np.testing.assert_allclose(result_list[1][0][0], [11.0, 2.0, 3.0])
|
||||||
|
|
||||||
def test_drop_geometry_node(self):
|
def test_drop_geometry_node(self):
|
||||||
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_DropGeometry
|
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_DropGeometry
|
||||||
|
|
||||||
|
|
@ -364,6 +424,48 @@ class TestTransform(unittest.TestCase):
|
||||||
self.assertEqual(tuple(mock_mod_dict.get("Socket_6")), (0.15, 0.25, 0.35))
|
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))
|
self.assertEqual(tuple(mock_mod_dict.get("Socket_9")), (1.5, 2.0, 2.5))
|
||||||
|
|
||||||
|
def test_gcode_to_mesh_naming_convention(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.preview import SLICENODE_GcodeToMesh
|
||||||
|
node = SLICENODE_GcodeToMesh()
|
||||||
|
|
||||||
|
# 1. Single part - no number suffix
|
||||||
|
node.resolve_multi_input = MagicMock(return_value=["GCODE1"])
|
||||||
|
node.resolve = MagicMock(return_value="Part")
|
||||||
|
node._process_gcode = MagicMock()
|
||||||
|
|
||||||
|
node.evaluate()
|
||||||
|
node._process_gcode.assert_called_once_with("GCODE1", "Part", None)
|
||||||
|
|
||||||
|
# 2. Three parts - padded to 2 digits (01, 02, 03)
|
||||||
|
node.resolve_multi_input = MagicMock(return_value=["GC1", "GC2", "GC3"])
|
||||||
|
node.resolve = MagicMock(return_value="Part")
|
||||||
|
node._process_gcode = MagicMock()
|
||||||
|
|
||||||
|
node.evaluate()
|
||||||
|
self.assertEqual(node._process_gcode.call_count, 3)
|
||||||
|
node._process_gcode.assert_any_call("GC1", "Part01", None)
|
||||||
|
node._process_gcode.assert_any_call("GC2", "Part02", None)
|
||||||
|
node._process_gcode.assert_any_call("GC3", "Part03", None)
|
||||||
|
|
||||||
|
# 3. Fifteen parts - padded to 2 digits (01..15)
|
||||||
|
node.resolve_multi_input = MagicMock(return_value=[f"GC{i}" for i in range(1, 16)])
|
||||||
|
node.resolve = MagicMock(return_value="Part")
|
||||||
|
node._process_gcode = MagicMock()
|
||||||
|
|
||||||
|
node.evaluate()
|
||||||
|
self.assertEqual(node._process_gcode.call_count, 15)
|
||||||
|
node._process_gcode.assert_any_call("GC1", "Part01", None)
|
||||||
|
node._process_gcode.assert_any_call("GC15", "Part15", None)
|
||||||
|
|
||||||
|
# 4. Default base name when empty name provided
|
||||||
|
node.resolve_multi_input = MagicMock(return_value=["GC1", "GC2"])
|
||||||
|
node.resolve = MagicMock(return_value="")
|
||||||
|
node._process_gcode = MagicMock()
|
||||||
|
|
||||||
|
node.evaluate()
|
||||||
|
node._process_gcode.assert_any_call("GC1", "GcodePreview01", None)
|
||||||
|
node._process_gcode.assert_any_call("GC2", "GcodePreview02", None)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue