draw lines
This commit is contained in:
parent
5d52c936be
commit
02175f379a
12 changed files with 448 additions and 76 deletions
|
|
@ -4,7 +4,7 @@ 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
|
from .base import SliceNode, SlicingCollection, get_all_children, SliceNodeJoiner, SlicingGroup, SlicingObject, SlicePaths
|
||||||
from ....infra.blender.blender_mesh_capture import PdfProperties
|
from ....infra.blender.blender_mesh_capture import PdfProperties
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
@ -677,6 +677,10 @@ class SLICENODE_RenameMesh(SliceNode):
|
||||||
def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tuple[float, float, float], show_name: bool = False) -> Any:
|
def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tuple[float, float, float], show_name: bool = False) -> Any:
|
||||||
if geom is None:
|
if geom is None:
|
||||||
return None
|
return None
|
||||||
|
elif isinstance(geom, SlicePaths):
|
||||||
|
copied = geom.copy_with(paths=list(geom))
|
||||||
|
copied.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name)
|
||||||
|
return copied
|
||||||
elif isinstance(geom, (list, tuple)):
|
elif isinstance(geom, (list, tuple)):
|
||||||
return [_assign_pdf_properties_to_item(item, line_width, line_color, show_name) for item in geom]
|
return [_assign_pdf_properties_to_item(item, line_width, line_color, show_name) for item in geom]
|
||||||
elif isinstance(geom, SlicingGroup):
|
elif isinstance(geom, SlicingGroup):
|
||||||
|
|
@ -742,6 +746,10 @@ def _assign_properties_to_item(geom: Any, props: list[tuple[str, str]]) -> Any:
|
||||||
from ....core.pdf_exporter.metadata import resolve_object_properties
|
from ....core.pdf_exporter.metadata import resolve_object_properties
|
||||||
if geom is None:
|
if geom is None:
|
||||||
return None
|
return None
|
||||||
|
elif isinstance(geom, SlicePaths):
|
||||||
|
copied = geom.copy_with(paths=list(geom))
|
||||||
|
copied.object_properties = resolve_object_properties(copied, props)
|
||||||
|
return copied
|
||||||
elif isinstance(geom, (list, tuple)):
|
elif isinstance(geom, (list, tuple)):
|
||||||
return [_assign_properties_to_item(item, props) for item in geom]
|
return [_assign_properties_to_item(item, props) for item in geom]
|
||||||
elif isinstance(geom, SlicingGroup):
|
elif isinstance(geom, SlicingGroup):
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import bpy
|
||||||
import tempfile
|
import tempfile
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from ..bpy_ns_types import NfStringProperty, NfFloatProperty, NfEnumProperty, NfFloatVectorProperty, NfBoolProperty
|
from ..bpy_ns_types import NfStringProperty, NfFloatProperty, NfEnumProperty, NfFloatVectorProperty, NfBoolProperty
|
||||||
from ....registry import register_class
|
from ....registry import register_class
|
||||||
|
|
@ -463,17 +464,24 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
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
|
||||||
|
|
||||||
geom = self.resolve("Geometry")
|
geom = self.resolve("Geometry")
|
||||||
slice_paths = self.resolve("Paths") # Currently unused
|
slice_paths = self.resolve("Paths") or self.resolve("SlicePaths")
|
||||||
folder: str = self.resolve("Folder") or ""
|
folder: str = self.resolve("Folder") or ""
|
||||||
settings = self.resolve("Settings")
|
|
||||||
|
|
||||||
if geom is None:
|
# Collect raw geometry items
|
||||||
if operator: operator.report({'ERROR'}, "No Geometry connected")
|
raw_geom_items: list[Any] = []
|
||||||
return 'error'
|
if geom is not None:
|
||||||
|
raw_list = geom if isinstance(geom, list) else [geom]
|
||||||
|
raw_geom_items = [g for g in raw_list if g is not None]
|
||||||
|
|
||||||
geometries: list[Any] = [g for g in (geom if isinstance(geom, list) else [geom]) if g is not None]
|
# Collect slice path items
|
||||||
if not geometries:
|
slice_paths_items: list[Any] = []
|
||||||
if operator: operator.report({'ERROR'}, "No Geometry connected")
|
if slice_paths is not None:
|
||||||
|
is_sp_inst = isinstance(slice_paths, SlicePaths)
|
||||||
|
raw_list = slice_paths if (isinstance(slice_paths, list) and not is_sp_inst) else [slice_paths]
|
||||||
|
slice_paths_items = [p for p in raw_list if p is not None]
|
||||||
|
|
||||||
|
if not raw_geom_items and not slice_paths_items:
|
||||||
|
if operator: operator.report({'ERROR'}, "No Geometry or Paths connected")
|
||||||
return 'error'
|
return 'error'
|
||||||
|
|
||||||
if not folder:
|
if not folder:
|
||||||
|
|
@ -490,26 +498,29 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
pass
|
pass
|
||||||
folder_path = Path(abs_folder)
|
folder_path = Path(abs_folder)
|
||||||
|
|
||||||
is_single_pdf_file = len(geometries) == 1 and folder_path.suffix.lower() == ".pdf"
|
has_raw_geom = len(raw_geom_items) > 0
|
||||||
|
primary_items = raw_geom_items if has_raw_geom else slice_paths_items
|
||||||
|
|
||||||
|
is_single_pdf_file = len(primary_items) == 1 and folder_path.suffix.lower() == ".pdf"
|
||||||
|
|
||||||
if is_single_pdf_file:
|
if is_single_pdf_file:
|
||||||
os.makedirs(folder_path.parent, exist_ok=True)
|
os.makedirs(folder_path.parent, exist_ok=True)
|
||||||
target_list = [(geometries[0], str(folder_path))]
|
target_list = [(primary_items[0], str(folder_path))]
|
||||||
else:
|
else:
|
||||||
out_dir = folder_path if folder_path.suffix == "" else folder_path.parent
|
out_dir = folder_path if folder_path.suffix == "" else folder_path.parent
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
target_list = []
|
target_list = []
|
||||||
for i, geom in enumerate(geometries):
|
for i, target_item in enumerate(primary_items):
|
||||||
if isinstance(geom, SlicingGroup) and geom.collections:
|
if isinstance(target_item, SlicingGroup) and target_item.collections:
|
||||||
raw_name = "_".join(geom.collections.keys())
|
raw_name = "_".join(target_item.collections.keys())
|
||||||
elif hasattr(geom, 'name') and getattr(geom, 'name'):
|
elif hasattr(target_item, 'name') and getattr(target_item, 'name'):
|
||||||
raw_name = str(getattr(geom, 'name'))
|
raw_name = str(getattr(target_item, 'name'))
|
||||||
else:
|
else:
|
||||||
raw_name = f"object_{i}"
|
raw_name = f"object_{i}"
|
||||||
|
|
||||||
name_part = raw_name.split('.')[0] if raw_name else f"object_{i}"
|
name_part = raw_name.split('.')[0] if raw_name else f"object_{i}"
|
||||||
filename = f"{name_part}.pdf"
|
filename = f"{name_part}.pdf"
|
||||||
target_list.append((geom, str(out_dir / filename)))
|
target_list.append((target_item, str(out_dir / filename)))
|
||||||
|
|
||||||
# Automatic scene scale determination
|
# Automatic scene scale determination
|
||||||
scale_str = "1:1"
|
scale_str = "1:1"
|
||||||
|
|
@ -537,8 +548,24 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
"scale": scale_str,
|
"scale": scale_str,
|
||||||
}
|
}
|
||||||
|
|
||||||
for geom, filepath in target_list:
|
for target_item, filepath in target_list:
|
||||||
edges_2d = extract_unique_top_view_edges(geom)
|
if has_raw_geom:
|
||||||
|
current_raw_geom = target_item
|
||||||
|
current_slice_paths = slice_paths
|
||||||
|
else:
|
||||||
|
current_raw_geom = None
|
||||||
|
current_slice_paths = target_item
|
||||||
|
|
||||||
|
edges_2d = np.empty((0, 2, 2), dtype=np.float64)
|
||||||
|
if current_raw_geom is not None:
|
||||||
|
raw_e = extract_unique_top_view_edges(current_raw_geom)
|
||||||
|
if len(raw_e) > 0:
|
||||||
|
edges_2d = raw_e
|
||||||
|
if current_slice_paths is not None:
|
||||||
|
sp_e = extract_unique_top_view_edges(current_slice_paths)
|
||||||
|
if len(sp_e) > 0:
|
||||||
|
edges_2d = np.concatenate([edges_2d, sp_e], axis=0) if len(edges_2d) > 0 else sp_e
|
||||||
|
|
||||||
if len(edges_2d) == 0:
|
if len(edges_2d) == 0:
|
||||||
if operator: operator.report({'WARNING'}, f"Geometry for {os.path.basename(filepath)} has no valid 2D top-view wireframe edges")
|
if operator: operator.report({'WARNING'}, f"Geometry for {os.path.basename(filepath)} has no valid 2D top-view wireframe edges")
|
||||||
|
|
||||||
|
|
@ -560,8 +587,8 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
stroke_color=(float(self.wire_color[0]), float(self.wire_color[1]), float(self.wire_color[2])),
|
stroke_color=(float(self.wire_color[0]), float(self.wire_color[1]), float(self.wire_color[2])),
|
||||||
bg_color=(float(self.bg_color[0]), float(self.bg_color[1]), float(self.bg_color[2])),
|
bg_color=(float(self.bg_color[0]), float(self.bg_color[1]), float(self.bg_color[2])),
|
||||||
bbox_color=(float(self.bbox_color[0]), float(self.bbox_color[1]), float(self.bbox_color[2])),
|
bbox_color=(float(self.bbox_color[0]), float(self.bbox_color[1]), float(self.bbox_color[2])),
|
||||||
raw_geometry=geom,
|
raw_geometry=current_raw_geom,
|
||||||
settings=settings
|
slice_paths=current_slice_paths,
|
||||||
)
|
)
|
||||||
if operator: operator.report({'INFO'}, f"Mesh PDF saved to {filepath}")
|
if operator: operator.report({'INFO'}, f"Mesh PDF saved to {filepath}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,37 @@ class SLICESOCK_Paths(LabelNodeSocket):
|
||||||
default = SlicePaths()
|
default = SlicePaths()
|
||||||
bl_color = (214/255, 0, 120/255, 1.0)
|
bl_color = (214/255, 0, 120/255, 1.0)
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICESOCK_Geometry(LabelNodeSocket):
|
||||||
|
bl_label = "Geometry"
|
||||||
|
default = None
|
||||||
|
bl_color = (0.0, 0.75, 0.75, 1.0)
|
||||||
|
|
||||||
|
def draw_color(self, context, node) -> tuple[float, float, float, float]:
|
||||||
|
def _get_upstream_color(sock: Any) -> tuple[float, float, float, float]:
|
||||||
|
if not sock or not getattr(sock, "is_linked", False) or not getattr(sock, "links", None):
|
||||||
|
return self.bl_color
|
||||||
|
from_sock = sock.links[0].from_socket
|
||||||
|
if hasattr(from_sock, "draw_color"):
|
||||||
|
try:
|
||||||
|
from_node = getattr(from_sock, "node", None)
|
||||||
|
return from_sock.draw_color(context, from_node)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return getattr(from_sock, "bl_color", self.bl_color)
|
||||||
|
|
||||||
|
if self.is_output:
|
||||||
|
if node and hasattr(node, "inputs"):
|
||||||
|
sock_name = getattr(self, "name", "")
|
||||||
|
matching_in = (node.inputs.get(sock_name) if sock_name else None) or node.inputs.get("Geometry")
|
||||||
|
if matching_in:
|
||||||
|
return _get_upstream_color(matching_in)
|
||||||
|
return self.bl_color
|
||||||
|
else:
|
||||||
|
return _get_upstream_color(self)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SLICESOCK_Gcode_Common(InputNodeSocket):
|
class SLICESOCK_Gcode_Common(InputNodeSocket):
|
||||||
bl_label = "Gcode"
|
bl_label = "Gcode"
|
||||||
value: NfStringProperty(name="Gcode", default="") # pyright: ignore[reportInvalidTypeForm]
|
value: NfStringProperty(name="Gcode", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,13 @@ class SLICETREE_NodeTree(NodeTree):
|
||||||
bl_group_idname = "SLICENODE_Group"
|
bl_group_idname = "SLICENODE_Group"
|
||||||
bl_use_group_interface = True
|
bl_use_group_interface = True
|
||||||
|
|
||||||
|
def valid_link(self, from_socket: Any, to_socket: Any) -> bool:
|
||||||
|
geom_sockets = {"SLICESOCK_Mesh", "SLICESOCK_Paths", "SLICESOCK_Geometry"}
|
||||||
|
if from_socket.bl_idname in geom_sockets and to_socket.bl_idname in geom_sockets:
|
||||||
|
return True
|
||||||
|
return from_socket.bl_idname == to_socket.bl_idname
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_OT_GroupToggle(Operator):
|
class SLICENODE_OT_GroupToggle(Operator):
|
||||||
bl_idname = "slicenode.group_toggle"
|
bl_idname = "slicenode.group_toggle"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ Modular structure:
|
||||||
from .geometry import (
|
from .geometry import (
|
||||||
deduplicate_2d_edges,
|
deduplicate_2d_edges,
|
||||||
extract_unique_top_view_edges,
|
extract_unique_top_view_edges,
|
||||||
|
extract_slice_paths_edges,
|
||||||
extract_lineart_edges,
|
extract_lineart_edges,
|
||||||
extract_boundary_edges,
|
extract_boundary_edges,
|
||||||
extract_outer_contour_edges,
|
extract_outer_contour_edges,
|
||||||
|
|
@ -85,6 +86,7 @@ __all__ = [
|
||||||
# Geometry
|
# Geometry
|
||||||
"deduplicate_2d_edges",
|
"deduplicate_2d_edges",
|
||||||
"extract_unique_top_view_edges",
|
"extract_unique_top_view_edges",
|
||||||
|
"extract_slice_paths_edges",
|
||||||
"extract_lineart_edges",
|
"extract_lineart_edges",
|
||||||
"extract_boundary_edges",
|
"extract_boundary_edges",
|
||||||
"extract_outer_contour_edges",
|
"extract_outer_contour_edges",
|
||||||
|
|
|
||||||
|
|
@ -31,17 +31,54 @@ def deduplicate_2d_edges(edges_2d: NDArray[np.float64]) -> NDArray[np.float64]:
|
||||||
return edges_2d[unique_indices]
|
return edges_2d[unique_indices]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_slice_paths_edges(slice_paths: Any) -> NDArray[np.float64]:
|
||||||
|
"""
|
||||||
|
Extracts 2D line segments (shape N x 2 x 2) from SlicePaths or a list/collection of SlicePaths.
|
||||||
|
"""
|
||||||
|
from ..slice import SlicePaths
|
||||||
|
|
||||||
|
if slice_paths is None:
|
||||||
|
return np.empty((0, 2, 2), dtype=np.float64)
|
||||||
|
|
||||||
|
all_segments: list[np.ndarray] = []
|
||||||
|
|
||||||
|
def process_item(item: Any):
|
||||||
|
if item is None:
|
||||||
|
return
|
||||||
|
elif isinstance(item, SlicePaths):
|
||||||
|
for path in item:
|
||||||
|
if path is not None and len(path) >= 2:
|
||||||
|
pts = np.asarray(path, dtype=np.float64)[:, :2]
|
||||||
|
segs = np.stack([pts[:-1], pts[1:]], axis=1)
|
||||||
|
all_segments.append(segs)
|
||||||
|
elif isinstance(item, (list, tuple)):
|
||||||
|
for sub in item:
|
||||||
|
process_item(sub)
|
||||||
|
|
||||||
|
process_item(slice_paths)
|
||||||
|
|
||||||
|
if not all_segments:
|
||||||
|
return np.empty((0, 2, 2), dtype=np.float64)
|
||||||
|
|
||||||
|
return np.concatenate(all_segments, axis=0)
|
||||||
|
|
||||||
|
|
||||||
def extract_unique_top_view_edges(mesh_data: Any) -> NDArray[np.float64]:
|
def extract_unique_top_view_edges(mesh_data: Any) -> NDArray[np.float64]:
|
||||||
"""
|
"""
|
||||||
Extracts 2D top-view unique edges (X, Y projection) from mesh_data.
|
Extracts 2D top-view unique edges (X, Y projection) from mesh_data (SlicingObjects or SlicePaths).
|
||||||
"""
|
"""
|
||||||
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
||||||
|
from ..slice import SlicePaths
|
||||||
|
|
||||||
all_edge_segments: list[NDArray[np.float64]] = []
|
all_edge_segments: list[NDArray[np.float64]] = []
|
||||||
|
|
||||||
def process_item(item: Any):
|
def process_item(item: Any):
|
||||||
if item is None:
|
if item is None:
|
||||||
return
|
return
|
||||||
|
elif isinstance(item, SlicePaths):
|
||||||
|
sp_edges = extract_slice_paths_edges(item)
|
||||||
|
if len(sp_edges) > 0:
|
||||||
|
all_edge_segments.append(sp_edges)
|
||||||
elif isinstance(item, (list, tuple)):
|
elif isinstance(item, (list, tuple)):
|
||||||
for sub in item:
|
for sub in item:
|
||||||
process_item(sub)
|
process_item(sub)
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,21 @@ from numpy.typing import NDArray
|
||||||
|
|
||||||
def get_first_attribute_value(so_item: Any, attr_name: str) -> Any:
|
def get_first_attribute_value(so_item: Any, attr_name: str) -> Any:
|
||||||
"""
|
"""
|
||||||
Extracts scalar attribute value for a SlicingObject.
|
Extracts scalar attribute value for a SlicingObject or SlicePaths.
|
||||||
"""
|
"""
|
||||||
if not hasattr(so_item, 'attributes') or not isinstance(so_item.attributes, dict):
|
if hasattr(so_item, 'extra_attributes') and isinstance(so_item.extra_attributes, dict):
|
||||||
return None
|
if attr_name in so_item.extra_attributes:
|
||||||
|
vals = so_item.extra_attributes[attr_name]
|
||||||
if attr_name not in so_item.attributes:
|
if isinstance(vals, (list, tuple, np.ndarray)) and len(vals) > 0:
|
||||||
return None
|
val = vals[0]
|
||||||
|
if hasattr(val, 'item'):
|
||||||
|
return val.item()
|
||||||
|
return val
|
||||||
|
elif vals is not None:
|
||||||
|
return vals
|
||||||
|
|
||||||
|
if hasattr(so_item, 'attributes') and isinstance(so_item.attributes, dict):
|
||||||
|
if attr_name in so_item.attributes:
|
||||||
attr_info = so_item.attributes[attr_name]
|
attr_info = so_item.attributes[attr_name]
|
||||||
if isinstance(attr_info, dict) and 'values' in attr_info:
|
if isinstance(attr_info, dict) and 'values' in attr_info:
|
||||||
vals = attr_info['values']
|
vals = attr_info['values']
|
||||||
|
|
@ -25,14 +32,32 @@ def get_first_attribute_value(so_item: Any, attr_name: str) -> Any:
|
||||||
else:
|
else:
|
||||||
return attr_info
|
return attr_info
|
||||||
|
|
||||||
if len(vals) == 0:
|
if len(vals) > 0:
|
||||||
return None
|
|
||||||
|
|
||||||
val = vals[0]
|
val = vals[0]
|
||||||
if hasattr(val, 'item'):
|
if hasattr(val, 'item'):
|
||||||
return val.item()
|
return val.item()
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
attr_key = attr_name.lower().replace(" ", "_")
|
||||||
|
if hasattr(so_item, attr_key):
|
||||||
|
val = getattr(so_item, attr_key)
|
||||||
|
if val is not None and not callable(val):
|
||||||
|
if isinstance(val, (list, tuple, np.ndarray)) and len(val) > 0:
|
||||||
|
val = val[0]
|
||||||
|
if hasattr(val, 'item'):
|
||||||
|
return val.item()
|
||||||
|
return val
|
||||||
|
elif hasattr(so_item, attr_name):
|
||||||
|
val = getattr(so_item, attr_name)
|
||||||
|
if val is not None and not callable(val):
|
||||||
|
if isinstance(val, (list, tuple, np.ndarray)) and len(val) > 0:
|
||||||
|
val = val[0]
|
||||||
|
if hasattr(val, 'item'):
|
||||||
|
return val.item()
|
||||||
|
return val
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
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]]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -79,15 +104,32 @@ def extract_settings_items(settings: Any) -> list[tuple[str, str]]:
|
||||||
|
|
||||||
def extract_object_label_info(mesh_data: Any) -> list[tuple[Any, str, float, float, float, float]]:
|
def extract_object_label_info(mesh_data: Any) -> list[tuple[Any, str, float, float, float, float]]:
|
||||||
"""
|
"""
|
||||||
Extracts (slicing_object, object_name, max_x_mm, y_at_max_x_mm, width_mm, height_mm) for each SlicingObject in mesh_data.
|
Extracts (slicing_object, object_name, max_x_mm, y_at_max_x_mm, width_mm, height_mm) for each SlicingObject or SlicePaths in mesh_data.
|
||||||
"""
|
"""
|
||||||
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
||||||
|
from ..slice import SlicePaths
|
||||||
|
|
||||||
results: list[tuple[Any, str, float, float, float, float]] = []
|
results: list[tuple[Any, str, float, float, float, float]] = []
|
||||||
|
|
||||||
def process_item(item: Any, fallback_name: str = ""):
|
def process_item(item: Any, fallback_name: str = ""):
|
||||||
if item is None:
|
if item is None:
|
||||||
return
|
return
|
||||||
|
elif isinstance(item, SlicePaths):
|
||||||
|
all_pts = [p[:, :2] for p in item if p is not None and len(p) > 0]
|
||||||
|
if all_pts:
|
||||||
|
verts_2d = np.concatenate(all_pts, axis=0)
|
||||||
|
if len(verts_2d) > 0:
|
||||||
|
max_idx = np.argmax(verts_2d[:, 0])
|
||||||
|
max_x = float(verts_2d[max_idx, 0])
|
||||||
|
min_x = float(verts_2d[:, 0].min())
|
||||||
|
y_val = float(verts_2d[max_idx, 1])
|
||||||
|
min_y = float(verts_2d[:, 1].min())
|
||||||
|
max_y = float(verts_2d[:, 1].max())
|
||||||
|
|
||||||
|
w_mm = max_x - min_x
|
||||||
|
h_mm = max_y - min_y
|
||||||
|
name = getattr(item, "name", "") or fallback_name or "SlicePaths"
|
||||||
|
results.append((item, name, max_x, y_val, w_mm, h_mm))
|
||||||
elif isinstance(item, (list, tuple)):
|
elif isinstance(item, (list, tuple)):
|
||||||
for i, sub in enumerate(item):
|
for i, sub in enumerate(item):
|
||||||
process_item(sub, f"{fallback_name}_{i}" if fallback_name else f"object_{i}")
|
process_item(sub, f"{fallback_name}_{i}" if fallback_name else f"object_{i}")
|
||||||
|
|
|
||||||
|
|
@ -48,15 +48,27 @@ def extract_slicing_objects_bounds(
|
||||||
) -> list[tuple[Any, str, float, float, float, float]]:
|
) -> list[tuple[Any, str, float, float, float, float]]:
|
||||||
"""
|
"""
|
||||||
Extracts (slicing_object, object_name, min_x_mm, max_x_mm, min_y_mm, max_y_mm)
|
Extracts (slicing_object, object_name, min_x_mm, max_x_mm, min_y_mm, max_y_mm)
|
||||||
for each SlicingObject in mesh_data.
|
for each SlicingObject or SlicePaths in mesh_data.
|
||||||
"""
|
"""
|
||||||
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
||||||
|
from ..slice import SlicePaths
|
||||||
|
|
||||||
results: list[tuple[Any, str, float, float, float, float]] = []
|
results: list[tuple[Any, str, float, float, float, float]] = []
|
||||||
|
|
||||||
def process_item(item: Any, fallback_name: str = ""):
|
def process_item(item: Any, fallback_name: str = ""):
|
||||||
if item is None:
|
if item is None:
|
||||||
return
|
return
|
||||||
|
elif isinstance(item, SlicePaths):
|
||||||
|
all_pts = [p[:, :2] for p in item if p is not None and len(p) > 0]
|
||||||
|
if all_pts:
|
||||||
|
verts_2d = np.concatenate(all_pts, axis=0)
|
||||||
|
if len(verts_2d) > 0:
|
||||||
|
min_x = float(verts_2d[:, 0].min())
|
||||||
|
max_x = float(verts_2d[:, 0].max())
|
||||||
|
min_y = float(verts_2d[:, 1].min())
|
||||||
|
max_y = float(verts_2d[:, 1].max())
|
||||||
|
name = getattr(item, "name", "") or fallback_name or "SlicePaths"
|
||||||
|
results.append((item, name, min_x, max_x, min_y, max_y))
|
||||||
elif isinstance(item, (list, tuple)):
|
elif isinstance(item, (list, tuple)):
|
||||||
for i, sub in enumerate(item):
|
for i, sub in enumerate(item):
|
||||||
process_item(sub, f"{fallback_name}_{i}" if fallback_name else f"object_{i}")
|
process_item(sub, f"{fallback_name}_{i}" if fallback_name else f"object_{i}")
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ from .geometry import (
|
||||||
extract_boundary_edges,
|
extract_boundary_edges,
|
||||||
extract_outer_contour_edges,
|
extract_outer_contour_edges,
|
||||||
extract_crease_edges,
|
extract_crease_edges,
|
||||||
|
extract_slice_paths_edges,
|
||||||
deduplicate_2d_edges,
|
deduplicate_2d_edges,
|
||||||
)
|
)
|
||||||
from .sa2_measures import render_sa2_measures_pdf
|
from .sa2_measures import render_sa2_measures_pdf
|
||||||
|
|
@ -165,7 +166,7 @@ def render_object_properties_table(
|
||||||
"""
|
"""
|
||||||
objs_with_props = [
|
objs_with_props = [
|
||||||
so for so in slicing_objs
|
so for so in slicing_objs
|
||||||
if hasattr(so, "object_properties") and so.object_properties
|
if getattr(so, "object_properties", None) is not None and len(getattr(so, "object_properties", [])) > 0
|
||||||
]
|
]
|
||||||
if not objs_with_props:
|
if not objs_with_props:
|
||||||
return
|
return
|
||||||
|
|
@ -285,10 +286,15 @@ def render_object_properties_table(
|
||||||
|
|
||||||
def get_all_slicing_objects(mesh_data: Any) -> list[Any]:
|
def get_all_slicing_objects(mesh_data: Any) -> list[Any]:
|
||||||
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
||||||
|
from ..slice import SlicePaths
|
||||||
|
|
||||||
objects = []
|
objects = []
|
||||||
|
|
||||||
def process_item(item: Any):
|
def process_item(item: Any):
|
||||||
if item is None:
|
if item is None:
|
||||||
return
|
return
|
||||||
|
elif isinstance(item, (SlicingObject, SlicePaths)):
|
||||||
|
objects.append(item)
|
||||||
elif isinstance(item, (list, tuple)):
|
elif isinstance(item, (list, tuple)):
|
||||||
for sub in item:
|
for sub in item:
|
||||||
process_item(sub)
|
process_item(sub)
|
||||||
|
|
@ -298,8 +304,7 @@ def get_all_slicing_objects(mesh_data: Any) -> list[Any]:
|
||||||
elif isinstance(item, SlicingCollection):
|
elif isinstance(item, SlicingCollection):
|
||||||
for so in item.objects:
|
for so in item.objects:
|
||||||
process_item(so)
|
process_item(so)
|
||||||
elif isinstance(item, SlicingObject):
|
|
||||||
objects.append(item)
|
|
||||||
process_item(mesh_data)
|
process_item(mesh_data)
|
||||||
return objects
|
return objects
|
||||||
|
|
||||||
|
|
@ -349,14 +354,20 @@ class PDFGridElement(PDFElement):
|
||||||
|
|
||||||
class PDFGeometryElement(PDFElement):
|
class PDFGeometryElement(PDFElement):
|
||||||
"""
|
"""
|
||||||
Renders 2D vector geometry (wireframe, boundary, contour, crease lines).
|
Renders 2D vector geometry (wireframe, boundary, contour, crease lines, and slice paths).
|
||||||
"""
|
"""
|
||||||
def render(self, s: PdfStream) -> None:
|
def render(self, s: PdfStream) -> None:
|
||||||
exp = self.exporter
|
exp = self.exporter
|
||||||
slicing_objs = exp.slicing_objs
|
slicing_objs = exp.slicing_objs
|
||||||
|
from ..slice import SlicePaths
|
||||||
if slicing_objs:
|
if slicing_objs:
|
||||||
for so in slicing_objs:
|
for so in slicing_objs:
|
||||||
so_edge_list: list[NDArray[np.float64]] = []
|
so_edge_list: list[NDArray[np.float64]] = []
|
||||||
|
if isinstance(so, SlicePaths):
|
||||||
|
sp_edges = extract_slice_paths_edges(so)
|
||||||
|
if len(sp_edges) > 0:
|
||||||
|
so_edge_list.append(sp_edges)
|
||||||
|
else:
|
||||||
if exp.draw_wireframe:
|
if exp.draw_wireframe:
|
||||||
so_w = extract_unique_top_view_edges(so)
|
so_w = extract_unique_top_view_edges(so)
|
||||||
if len(so_w) > 0:
|
if len(so_w) > 0:
|
||||||
|
|
@ -561,8 +572,9 @@ class PDFObjectLabelsElement(PDFElement):
|
||||||
slicing_objs_list = exp.slicing_objs
|
slicing_objs_list = exp.slicing_objs
|
||||||
has_any_obj_show_name = any(hasattr(o, "pdf_properties") and o.pdf_properties and o.pdf_properties.show_name for o in slicing_objs_list)
|
has_any_obj_show_name = any(hasattr(o, "pdf_properties") and o.pdf_properties and o.pdf_properties.show_name for o in slicing_objs_list)
|
||||||
|
|
||||||
if (exp.show_name or has_any_obj_show_name or settings_items) and exp.raw_geometry is not None:
|
if (exp.show_name or has_any_obj_show_name or settings_items) and (exp.raw_geometry is not None or exp.slice_paths is not None):
|
||||||
label_infos = extract_object_label_info(exp.raw_geometry)
|
geom_source = [g for g in (exp.raw_geometry, exp.slice_paths) if g is not None]
|
||||||
|
label_infos = extract_object_label_info(geom_source)
|
||||||
if label_infos:
|
if label_infos:
|
||||||
eff_font_size_pt = exp.font_size_pt if exp.font_size_pt is not None and exp.font_size_pt > 0 else max(8.0, exp.line_width_pt * 4.0)
|
eff_font_size_pt = exp.font_size_pt if exp.font_size_pt is not None and exp.font_size_pt > 0 else max(8.0, exp.line_width_pt * 4.0)
|
||||||
line_spacing_pt = eff_font_size_pt * 1.25
|
line_spacing_pt = eff_font_size_pt * 1.25
|
||||||
|
|
@ -710,7 +722,7 @@ class export_mesh_to_pdf:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
edges_2d: NDArray[np.float64],
|
edges_2d: NDArray[np.float64] | None = None,
|
||||||
filepath: str | Path | None = None,
|
filepath: str | Path | None = None,
|
||||||
draw_wireframe: bool = True,
|
draw_wireframe: bool = True,
|
||||||
draw_boundary: bool = False,
|
draw_boundary: bool = False,
|
||||||
|
|
@ -729,10 +741,11 @@ class export_mesh_to_pdf:
|
||||||
bg_color: tuple[float, float, float] = (1.0, 1.0, 1.0),
|
bg_color: tuple[float, float, float] = (1.0, 1.0, 1.0),
|
||||||
bbox_color: tuple[float, float, float] = (0.0, 0.0, 1.0),
|
bbox_color: tuple[float, float, float] = (0.0, 0.0, 1.0),
|
||||||
raw_geometry: Any = None,
|
raw_geometry: Any = None,
|
||||||
|
slice_paths: Any = None,
|
||||||
settings: Any = None,
|
settings: Any = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.edges_2d = edges_2d
|
self.edges_2d = edges_2d if edges_2d is not None else np.empty((0, 2, 2), dtype=np.float64)
|
||||||
self.filepath = filepath
|
self.filepath = filepath
|
||||||
self.draw_wireframe = draw_wireframe
|
self.draw_wireframe = draw_wireframe
|
||||||
self.draw_boundary = draw_boundary
|
self.draw_boundary = draw_boundary
|
||||||
|
|
@ -753,6 +766,7 @@ class export_mesh_to_pdf:
|
||||||
self.bg_color = bg_color
|
self.bg_color = bg_color
|
||||||
self.bbox_color = bbox_color
|
self.bbox_color = bbox_color
|
||||||
self.raw_geometry = raw_geometry
|
self.raw_geometry = raw_geometry
|
||||||
|
self.slice_paths = slice_paths
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
|
||||||
self._prepare_geometry_and_layout()
|
self._prepare_geometry_and_layout()
|
||||||
|
|
@ -773,7 +787,12 @@ class export_mesh_to_pdf:
|
||||||
self.export(filepath)
|
self.export(filepath)
|
||||||
|
|
||||||
def _prepare_geometry_and_layout(self) -> None:
|
def _prepare_geometry_and_layout(self) -> None:
|
||||||
self.slicing_objs = get_all_slicing_objects(self.raw_geometry)
|
objs_raw = get_all_slicing_objects(self.raw_geometry)
|
||||||
|
objs_paths = get_all_slicing_objects(self.slice_paths)
|
||||||
|
self.slicing_objs = list(objs_raw)
|
||||||
|
for po in objs_paths:
|
||||||
|
if not any(po is existing for existing in self.slicing_objs):
|
||||||
|
self.slicing_objs.append(po)
|
||||||
|
|
||||||
edge_list: list[NDArray[np.float64]] = []
|
edge_list: list[NDArray[np.float64]] = []
|
||||||
|
|
||||||
|
|
@ -795,6 +814,11 @@ class export_mesh_to_pdf:
|
||||||
if len(cr_edges) > 0:
|
if len(cr_edges) > 0:
|
||||||
edge_list.append(cr_edges)
|
edge_list.append(cr_edges)
|
||||||
|
|
||||||
|
if self.slice_paths is not None:
|
||||||
|
sp_edges = extract_slice_paths_edges(self.slice_paths)
|
||||||
|
if len(sp_edges) > 0:
|
||||||
|
edge_list.append(sp_edges)
|
||||||
|
|
||||||
if edge_list:
|
if edge_list:
|
||||||
combined = np.concatenate(edge_list, axis=0)
|
combined = np.concatenate(edge_list, axis=0)
|
||||||
self.edges_to_draw = deduplicate_2d_edges(combined)
|
self.edges_to_draw = deduplicate_2d_edges(combined)
|
||||||
|
|
@ -843,7 +867,7 @@ class export_mesh_to_pdf:
|
||||||
|
|
||||||
objs_with_props = [
|
objs_with_props = [
|
||||||
so for so in self.slicing_objs
|
so for so in self.slicing_objs
|
||||||
if hasattr(so, "object_properties") and so.object_properties
|
if getattr(so, "object_properties", None) is not None and len(getattr(so, "object_properties", [])) > 0
|
||||||
]
|
]
|
||||||
|
|
||||||
w_table_mm = 0.0
|
w_table_mm = 0.0
|
||||||
|
|
@ -918,8 +942,6 @@ class export_mesh_to_pdf:
|
||||||
self.main_lines_arr = np.concatenate([main_v_lines, main_h_lines], axis=0) if len(main_v_lines) or len(main_h_lines) else np.empty((0, 2, 2))
|
self.main_lines_arr = np.concatenate([main_v_lines, main_h_lines], axis=0) if len(main_v_lines) or len(main_h_lines) else np.empty((0, 2, 2))
|
||||||
self.minor_lines_arr = np.concatenate([minor_v_lines, minor_h_lines], axis=0) if len(minor_v_lines) or len(minor_h_lines) else np.empty((0, 2, 2))
|
self.minor_lines_arr = np.concatenate([minor_v_lines, minor_h_lines], axis=0) if len(minor_v_lines) or len(minor_h_lines) else np.empty((0, 2, 2))
|
||||||
|
|
||||||
self.slicing_objs = get_all_slicing_objects(self.raw_geometry)
|
|
||||||
|
|
||||||
def to_pdf_pt(self, pt_mm: NDArray[np.float64]) -> NDArray[np.float64]:
|
def to_pdf_pt(self, pt_mm: NDArray[np.float64]) -> NDArray[np.float64]:
|
||||||
x_pt = (pt_mm[:, 0] - self.grid_min_x + self.total_margin_mm) * MM_TO_PT
|
x_pt = (pt_mm[:, 0] - self.grid_min_x + self.total_margin_mm) * MM_TO_PT
|
||||||
y_pt = (pt_mm[:, 1] - self.grid_min_y + self.total_margin_mm) * MM_TO_PT
|
y_pt = (pt_mm[:, 1] - self.grid_min_y + self.total_margin_mm) * MM_TO_PT
|
||||||
|
|
@ -1020,4 +1042,3 @@ class export_mesh_to_pdf:
|
||||||
|
|
||||||
# Alias for backward compatibility
|
# Alias for backward compatibility
|
||||||
export_wireframe_to_pdf = export_mesh_to_pdf
|
export_wireframe_to_pdf = export_mesh_to_pdf
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import pyclipper
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
from shapely.geometry.base import BaseGeometry
|
from shapely.geometry.base import BaseGeometry
|
||||||
|
|
||||||
|
from ..infra.blender.blender_mesh_capture import PdfProperties
|
||||||
|
|
||||||
from .geometry import ndarray_lru_cache
|
from .geometry import ndarray_lru_cache
|
||||||
|
|
||||||
# Scaling factor for pyclipper (it works with integers)
|
# Scaling factor for pyclipper (it works with integers)
|
||||||
|
|
@ -160,6 +162,8 @@ class SlicePaths(list[np.ndarray]):
|
||||||
layer_height: float
|
layer_height: float
|
||||||
first_layer_height: float | None
|
first_layer_height: float | None
|
||||||
transforms: list[dict]
|
transforms: list[dict]
|
||||||
|
object_properties: list[tuple[str, str]]
|
||||||
|
pdf_properties: PdfProperties
|
||||||
|
|
||||||
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):
|
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)
|
super().__init__(paths)
|
||||||
|
|
|
||||||
|
|
@ -63,12 +63,12 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
- **Rename Mesh**: Overrides the internal metadata name of a `SlicingObject`, `SlicingCollection`, or `SlicingGroup`. Supports `{AttributeName}` attribute placeholders (e.g., `Rebar-L{Rebar Length}M`) that evaluate to index zero of the target attribute for each object, as well as `{Name}` to include the item's previous name (e.g., `{Name}_T{Thickness}mm`).
|
- **Rename Mesh**: Overrides the internal metadata name of a `SlicingObject`, `SlicingCollection`, or `SlicingGroup`. Supports `{AttributeName}` attribute placeholders (e.g., `Rebar-L{Rebar Length}M`) that evaluate to index zero of the target attribute for each object, as well as `{Name}` to include the item's previous name (e.g., `{Name}_T{Thickness}mm`).
|
||||||
- **Geometry**: Input geometry data.
|
- **Geometry**: Input geometry data.
|
||||||
- **Name**: String input socket or property for the new name template.
|
- **Name**: String input socket or property for the new name template.
|
||||||
- **Assign PDF Properties**: Assigns line width (mm) and line color PDF export properties to all `SlicingObject` instances within a `SlicingObject`, `SlicingCollection`, or `SlicingGroup`.
|
- **Assign PDF Properties**: Assigns line width (mm) and line color PDF export properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`).
|
||||||
- **Geometry**: Input geometry socket.
|
- **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`).
|
||||||
- **Line Width**: Input socket / float property for line width in mm.
|
- **Line Width**: Input socket / float property for line width in mm.
|
||||||
- **Line Color**: Color property for stroke color.
|
- **Line Color**: Color property for stroke color.
|
||||||
- **Assign Property to Object**: Assigns custom settings/properties to each object in a `SlicingObject`, `SlicingCollection`, or `SlicingGroup` 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**: Input geometry socket.
|
- **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 a `SlicingCollection` or `SlicingGroup` relative to the origin.
|
||||||
- **Mesh**: Input mesh data.
|
- **Mesh**: Input mesh data.
|
||||||
|
|
|
||||||
|
|
@ -951,7 +951,7 @@ class TestPdfExporter(unittest.TestCase):
|
||||||
pdf_file = os.path.join(tmpdir, "output.pdf")
|
pdf_file = os.path.join(tmpdir, "output.pdf")
|
||||||
resolved_inputs = {
|
resolved_inputs = {
|
||||||
"Geometry": so,
|
"Geometry": so,
|
||||||
"SlicePaths": None,
|
"Paths": None,
|
||||||
"Folder": pdf_file,
|
"Folder": pdf_file,
|
||||||
"Settings": None,
|
"Settings": None,
|
||||||
}
|
}
|
||||||
|
|
@ -1121,6 +1121,47 @@ class TestPdfExporter(unittest.TestCase):
|
||||||
for obj in coll.objects:
|
for obj in coll.objects:
|
||||||
self.assertEqual(obj.object_properties, props)
|
self.assertEqual(obj.object_properties, props)
|
||||||
|
|
||||||
|
# Also verify SlicePaths support
|
||||||
|
from NodeSlicer.core.slice import SlicePaths
|
||||||
|
sp = SlicePaths([np.array([[0, 0, 0], [10, 10, 0]], dtype=np.float64)])
|
||||||
|
updated_sp = _assign_properties_to_item(sp, props)
|
||||||
|
self.assertEqual(updated_sp.object_properties, props)
|
||||||
|
|
||||||
|
# Verify dynamic socket color logic
|
||||||
|
from NodeSlicer.core.nodeslicer.sockets import SLICESOCK_Geometry, SLICESOCK_Paths, SLICESOCK_Mesh
|
||||||
|
class DummyLink:
|
||||||
|
def __init__(self, from_sock):
|
||||||
|
self.from_socket = from_sock
|
||||||
|
|
||||||
|
class DummyNode:
|
||||||
|
def __init__(self):
|
||||||
|
self.inputs = {}
|
||||||
|
|
||||||
|
dummy_node = DummyNode()
|
||||||
|
in_geom = SLICESOCK_Geometry()
|
||||||
|
in_geom.is_output = False
|
||||||
|
in_geom.is_linked = True
|
||||||
|
in_geom.name = "Geometry"
|
||||||
|
|
||||||
|
paths_out = SLICESOCK_Paths()
|
||||||
|
in_geom.links = [DummyLink(paths_out)]
|
||||||
|
dummy_node.inputs["Geometry"] = in_geom
|
||||||
|
|
||||||
|
out_geom = SLICESOCK_Geometry()
|
||||||
|
out_geom.is_output = True
|
||||||
|
|
||||||
|
# Input socket draws Paths color (pink)
|
||||||
|
self.assertEqual(in_geom.draw_color(None, dummy_node), paths_out.bl_color)
|
||||||
|
# Output socket on same node inherits Paths color (pink) from input socket
|
||||||
|
self.assertEqual(out_geom.draw_color(None, dummy_node), paths_out.bl_color)
|
||||||
|
|
||||||
|
# Switch upstream to Mesh (cyan)
|
||||||
|
mesh_out = SLICESOCK_Mesh()
|
||||||
|
in_geom.links = [DummyLink(mesh_out)]
|
||||||
|
self.assertEqual(in_geom.draw_color(None, dummy_node), mesh_out.bl_color)
|
||||||
|
self.assertEqual(out_geom.draw_color(None, dummy_node), mesh_out.bl_color)
|
||||||
|
|
||||||
|
|
||||||
def test_export_pdf_object_properties_table(self):
|
def test_export_pdf_object_properties_table(self):
|
||||||
so1 = SlicingObject()
|
so1 = SlicingObject()
|
||||||
so1.name = "BracketA"
|
so1.name = "BracketA"
|
||||||
|
|
@ -1245,6 +1286,146 @@ class TestPdfExporter(unittest.TestCase):
|
||||||
self.assertIn("1.000 1.000 1.000 rg", content)
|
self.assertIn("1.000 1.000 1.000 rg", content)
|
||||||
self.assertIn("1.000 1.000 1.000 RG", content)
|
self.assertIn("1.000 1.000 1.000 RG", content)
|
||||||
|
|
||||||
|
def test_slicepaths_pdf_export_default_style_and_properties(self):
|
||||||
|
from NodeSlicer.core.slice import SlicePaths
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.mesh import _assign_pdf_properties_to_item, _assign_properties_to_item
|
||||||
|
|
||||||
|
sp = SlicePaths([np.array([[0.0, 0.0], [50.0, 0.0], [50.0, 50.0], [0.0, 50.0]], dtype=np.float64)])
|
||||||
|
sp.name = "TestSlicePath"
|
||||||
|
|
||||||
|
# 1. Export with default style
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
pdf_path = os.path.join(tmpdir, "slicepath_default.pdf")
|
||||||
|
export_mesh_to_pdf(
|
||||||
|
slice_paths=sp,
|
||||||
|
filepath=pdf_path,
|
||||||
|
draw_wireframe=True,
|
||||||
|
)
|
||||||
|
self.assertTrue(os.path.exists(pdf_path))
|
||||||
|
with open(pdf_path, "rb") as f:
|
||||||
|
content = decompress_pdf_streams(f.read())
|
||||||
|
self.assertTrue(len(content) > 0)
|
||||||
|
|
||||||
|
# 2. Assign PDF properties (custom width, color, show_name)
|
||||||
|
sp_pdf_props = _assign_pdf_properties_to_item(sp, line_width=3.0, line_color=(1.0, 0.0, 0.0), show_name=True)
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
pdf_path = os.path.join(tmpdir, "slicepath_pdf_props.pdf")
|
||||||
|
export_mesh_to_pdf(
|
||||||
|
slice_paths=sp_pdf_props,
|
||||||
|
filepath=pdf_path,
|
||||||
|
draw_wireframe=True,
|
||||||
|
)
|
||||||
|
with open(pdf_path, "rb") as f:
|
||||||
|
content = decompress_pdf_streams(f.read())
|
||||||
|
self.assertIn("1.000 0.000 0.000 RG", content)
|
||||||
|
self.assertIn("(TestSlicePath) Tj", content)
|
||||||
|
|
||||||
|
# 3. Assign Property to Object (table properties)
|
||||||
|
props = [("Layer Height", "0.2mm"), ("Speed", "60mm/s")]
|
||||||
|
sp_props = _assign_properties_to_item(sp, props)
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
pdf_path = os.path.join(tmpdir, "slicepath_object_props.pdf")
|
||||||
|
export_mesh_to_pdf(
|
||||||
|
slice_paths=sp_props,
|
||||||
|
filepath=pdf_path,
|
||||||
|
draw_wireframe=True,
|
||||||
|
)
|
||||||
|
with open(pdf_path, "rb") as f:
|
||||||
|
content = decompress_pdf_streams(f.read())
|
||||||
|
self.assertIn("(Layer Height) Tj", content)
|
||||||
|
self.assertIn("(0.2mm) Tj", content)
|
||||||
|
self.assertIn("(Speed) Tj", content)
|
||||||
|
self.assertIn("(60mm/s) Tj", content)
|
||||||
|
|
||||||
|
def test_mesh_to_pdf_paths_input_only_and_combined(self):
|
||||||
|
from NodeSlicer.core.slice import SlicePaths
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.output import SLICENODE_MeshToPdf
|
||||||
|
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 = False
|
||||||
|
node.header_company = ""
|
||||||
|
node.header_part_name = ""
|
||||||
|
node.header_drawing_number = ""
|
||||||
|
node.header_author = ""
|
||||||
|
node.header_revision = ""
|
||||||
|
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))
|
||||||
|
|
||||||
|
sp = SlicePaths([np.array([[5.0, 5.0], [25.0, 25.0]], dtype=np.float64)])
|
||||||
|
sp.name = "PathOnly"
|
||||||
|
|
||||||
|
# Case A: Only Paths connected (Geometry is None)
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
pdf_file = os.path.join(tmpdir, "paths_only.pdf")
|
||||||
|
op = MockOperator()
|
||||||
|
resolved = {
|
||||||
|
"Geometry": None,
|
||||||
|
"Paths": sp,
|
||||||
|
"Folder": pdf_file,
|
||||||
|
}
|
||||||
|
node.resolve = lambda k: resolved.get(k)
|
||||||
|
res = node.evaluate(operator=op)
|
||||||
|
self.assertEqual(res, "ok", msg=f"Reports: {op.reports}")
|
||||||
|
self.assertTrue(os.path.exists(pdf_file))
|
||||||
|
with open(pdf_file, "rb") as f:
|
||||||
|
content = decompress_pdf_streams(f.read())
|
||||||
|
self.assertIn(" m ", content)
|
||||||
|
|
||||||
|
# Case B: Both Geometry and Paths connected
|
||||||
|
so = SlicingObject()
|
||||||
|
so.name = "MeshObj"
|
||||||
|
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, "combined.pdf")
|
||||||
|
op = MockOperator()
|
||||||
|
resolved = {
|
||||||
|
"Geometry": so,
|
||||||
|
"Paths": sp,
|
||||||
|
"Folder": pdf_file,
|
||||||
|
}
|
||||||
|
node.resolve = lambda k: resolved.get(k)
|
||||||
|
res = node.evaluate(operator=op)
|
||||||
|
self.assertEqual(res, "ok", msg=f"Reports: {op.reports}")
|
||||||
|
self.assertTrue(os.path.exists(pdf_file))
|
||||||
|
with open(pdf_file, "rb") as f:
|
||||||
|
content = decompress_pdf_streams(f.read())
|
||||||
|
self.assertIn(" m ", content)
|
||||||
|
|
||||||
|
# Case C: Geometry is an empty list [] and Paths is connected
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
pdf_file = os.path.join(tmpdir, "empty_geom_list.pdf")
|
||||||
|
op = MockOperator()
|
||||||
|
resolved = {
|
||||||
|
"Geometry": [],
|
||||||
|
"Paths": sp,
|
||||||
|
"Folder": pdf_file,
|
||||||
|
}
|
||||||
|
node.resolve = lambda k: resolved.get(k)
|
||||||
|
res = node.evaluate(operator=op)
|
||||||
|
self.assertEqual(res, "ok", msg=f"Reports: {op.reports}")
|
||||||
|
self.assertTrue(os.path.exists(pdf_file))
|
||||||
|
with open(pdf_file, "rb") as f:
|
||||||
|
content = decompress_pdf_streams(f.read())
|
||||||
|
self.assertIn(" m ", content)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue