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 ....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
|
||||
|
||||
@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:
|
||||
if geom is 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)):
|
||||
return [_assign_pdf_properties_to_item(item, line_width, line_color, show_name) for item in geom]
|
||||
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
|
||||
if geom is 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)):
|
||||
return [_assign_properties_to_item(item, props) for item in geom]
|
||||
elif isinstance(geom, SlicingGroup):
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import bpy
|
|||
import tempfile
|
||||
import re
|
||||
import json
|
||||
import numpy as np
|
||||
|
||||
from ..bpy_ns_types import NfStringProperty, NfFloatProperty, NfEnumProperty, NfFloatVectorProperty, NfBoolProperty
|
||||
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
|
||||
|
||||
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 ""
|
||||
settings = self.resolve("Settings")
|
||||
|
||||
if geom is None:
|
||||
if operator: operator.report({'ERROR'}, "No Geometry connected")
|
||||
return 'error'
|
||||
# Collect raw geometry items
|
||||
raw_geom_items: list[Any] = []
|
||||
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]
|
||||
if not geometries:
|
||||
if operator: operator.report({'ERROR'}, "No Geometry connected")
|
||||
# Collect slice path items
|
||||
slice_paths_items: list[Any] = []
|
||||
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'
|
||||
|
||||
if not folder:
|
||||
|
|
@ -490,26 +498,29 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
|||
pass
|
||||
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:
|
||||
os.makedirs(folder_path.parent, exist_ok=True)
|
||||
target_list = [(geometries[0], str(folder_path))]
|
||||
target_list = [(primary_items[0], str(folder_path))]
|
||||
else:
|
||||
out_dir = folder_path if folder_path.suffix == "" else folder_path.parent
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
target_list = []
|
||||
for i, geom in enumerate(geometries):
|
||||
if isinstance(geom, SlicingGroup) and geom.collections:
|
||||
raw_name = "_".join(geom.collections.keys())
|
||||
elif hasattr(geom, 'name') and getattr(geom, 'name'):
|
||||
raw_name = str(getattr(geom, 'name'))
|
||||
for i, target_item in enumerate(primary_items):
|
||||
if isinstance(target_item, SlicingGroup) and target_item.collections:
|
||||
raw_name = "_".join(target_item.collections.keys())
|
||||
elif hasattr(target_item, 'name') and getattr(target_item, 'name'):
|
||||
raw_name = str(getattr(target_item, 'name'))
|
||||
else:
|
||||
raw_name = f"object_{i}"
|
||||
|
||||
name_part = raw_name.split('.')[0] if raw_name else f"object_{i}"
|
||||
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
|
||||
scale_str = "1:1"
|
||||
|
|
@ -537,8 +548,24 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
|||
"scale": scale_str,
|
||||
}
|
||||
|
||||
for geom, filepath in target_list:
|
||||
edges_2d = extract_unique_top_view_edges(geom)
|
||||
for target_item, filepath in target_list:
|
||||
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 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])),
|
||||
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])),
|
||||
raw_geometry=geom,
|
||||
settings=settings
|
||||
raw_geometry=current_raw_geom,
|
||||
slice_paths=current_slice_paths,
|
||||
)
|
||||
if operator: operator.report({'INFO'}, f"Mesh PDF saved to {filepath}")
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -140,6 +140,37 @@ class SLICESOCK_Paths(LabelNodeSocket):
|
|||
default = SlicePaths()
|
||||
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):
|
||||
bl_label = "Gcode"
|
||||
value: NfStringProperty(name="Gcode", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||
|
|
|
|||
|
|
@ -155,6 +155,13 @@ class SLICETREE_NodeTree(NodeTree):
|
|||
bl_group_idname = "SLICENODE_Group"
|
||||
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
|
||||
class SLICENODE_OT_GroupToggle(Operator):
|
||||
bl_idname = "slicenode.group_toggle"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ Modular structure:
|
|||
from .geometry import (
|
||||
deduplicate_2d_edges,
|
||||
extract_unique_top_view_edges,
|
||||
extract_slice_paths_edges,
|
||||
extract_lineart_edges,
|
||||
extract_boundary_edges,
|
||||
extract_outer_contour_edges,
|
||||
|
|
@ -85,6 +86,7 @@ __all__ = [
|
|||
# Geometry
|
||||
"deduplicate_2d_edges",
|
||||
"extract_unique_top_view_edges",
|
||||
"extract_slice_paths_edges",
|
||||
"extract_lineart_edges",
|
||||
"extract_boundary_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]
|
||||
|
||||
|
||||
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]:
|
||||
"""
|
||||
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 ..slice import SlicePaths
|
||||
|
||||
all_edge_segments: list[NDArray[np.float64]] = []
|
||||
|
||||
def process_item(item: Any):
|
||||
if item is None:
|
||||
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)):
|
||||
for sub in item:
|
||||
process_item(sub)
|
||||
|
|
|
|||
|
|
@ -9,29 +9,54 @@ from numpy.typing import NDArray
|
|||
|
||||
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):
|
||||
return None
|
||||
if hasattr(so_item, 'extra_attributes') and isinstance(so_item.extra_attributes, dict):
|
||||
if attr_name in so_item.extra_attributes:
|
||||
vals = so_item.extra_attributes[attr_name]
|
||||
if isinstance(vals, (list, tuple, np.ndarray)) and len(vals) > 0:
|
||||
val = vals[0]
|
||||
if hasattr(val, 'item'):
|
||||
return val.item()
|
||||
return val
|
||||
elif vals is not None:
|
||||
return vals
|
||||
|
||||
if attr_name not in so_item.attributes:
|
||||
return None
|
||||
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]
|
||||
if isinstance(attr_info, dict) and 'values' in attr_info:
|
||||
vals = attr_info['values']
|
||||
elif isinstance(attr_info, (list, tuple, np.ndarray)):
|
||||
vals = attr_info
|
||||
else:
|
||||
return attr_info
|
||||
|
||||
attr_info = so_item.attributes[attr_name]
|
||||
if isinstance(attr_info, dict) and 'values' in attr_info:
|
||||
vals = attr_info['values']
|
||||
elif isinstance(attr_info, (list, tuple, np.ndarray)):
|
||||
vals = attr_info
|
||||
else:
|
||||
return attr_info
|
||||
if len(vals) > 0:
|
||||
val = vals[0]
|
||||
if hasattr(val, 'item'):
|
||||
return val.item()
|
||||
return val
|
||||
|
||||
if len(vals) == 0:
|
||||
return None
|
||||
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
|
||||
|
||||
val = vals[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]]:
|
||||
|
|
@ -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]]:
|
||||
"""
|
||||
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 ..slice import SlicePaths
|
||||
|
||||
results: list[tuple[Any, str, float, float, float, float]] = []
|
||||
|
||||
def process_item(item: Any, fallback_name: str = ""):
|
||||
if item is None:
|
||||
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)):
|
||||
for i, sub in enumerate(item):
|
||||
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]]:
|
||||
"""
|
||||
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 ..slice import SlicePaths
|
||||
|
||||
results: list[tuple[Any, str, float, float, float, float]] = []
|
||||
|
||||
def process_item(item: Any, fallback_name: str = ""):
|
||||
if item is None:
|
||||
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)):
|
||||
for i, sub in enumerate(item):
|
||||
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_outer_contour_edges,
|
||||
extract_crease_edges,
|
||||
extract_slice_paths_edges,
|
||||
deduplicate_2d_edges,
|
||||
)
|
||||
from .sa2_measures import render_sa2_measures_pdf
|
||||
|
|
@ -165,7 +166,7 @@ def render_object_properties_table(
|
|||
"""
|
||||
objs_with_props = [
|
||||
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:
|
||||
return
|
||||
|
|
@ -285,10 +286,15 @@ def render_object_properties_table(
|
|||
|
||||
def get_all_slicing_objects(mesh_data: Any) -> list[Any]:
|
||||
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
||||
from ..slice import SlicePaths
|
||||
|
||||
objects = []
|
||||
|
||||
def process_item(item: Any):
|
||||
if item is None:
|
||||
return
|
||||
elif isinstance(item, (SlicingObject, SlicePaths)):
|
||||
objects.append(item)
|
||||
elif isinstance(item, (list, tuple)):
|
||||
for sub in item:
|
||||
process_item(sub)
|
||||
|
|
@ -298,8 +304,7 @@ def get_all_slicing_objects(mesh_data: Any) -> list[Any]:
|
|||
elif isinstance(item, SlicingCollection):
|
||||
for so in item.objects:
|
||||
process_item(so)
|
||||
elif isinstance(item, SlicingObject):
|
||||
objects.append(item)
|
||||
|
||||
process_item(mesh_data)
|
||||
return objects
|
||||
|
||||
|
|
@ -349,30 +354,36 @@ class PDFGridElement(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:
|
||||
exp = self.exporter
|
||||
slicing_objs = exp.slicing_objs
|
||||
from ..slice import SlicePaths
|
||||
if slicing_objs:
|
||||
for so in slicing_objs:
|
||||
so_edge_list: list[NDArray[np.float64]] = []
|
||||
if exp.draw_wireframe:
|
||||
so_w = extract_unique_top_view_edges(so)
|
||||
if len(so_w) > 0:
|
||||
so_edge_list.append(so_w)
|
||||
if exp.draw_boundary and not exp.draw_crease:
|
||||
so_b = extract_boundary_edges(so)
|
||||
if len(so_b) > 0:
|
||||
so_edge_list.append(so_b)
|
||||
if exp.draw_contour:
|
||||
so_c = extract_outer_contour_edges(so)
|
||||
if len(so_c) > 0:
|
||||
so_edge_list.append(so_c)
|
||||
if exp.draw_crease:
|
||||
so_cr = extract_crease_edges(so, max_angle_deg=exp.crease_angle)
|
||||
if len(so_cr) > 0:
|
||||
so_edge_list.append(so_cr)
|
||||
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:
|
||||
so_w = extract_unique_top_view_edges(so)
|
||||
if len(so_w) > 0:
|
||||
so_edge_list.append(so_w)
|
||||
if exp.draw_boundary and not exp.draw_crease:
|
||||
so_b = extract_boundary_edges(so)
|
||||
if len(so_b) > 0:
|
||||
so_edge_list.append(so_b)
|
||||
if exp.draw_contour:
|
||||
so_c = extract_outer_contour_edges(so)
|
||||
if len(so_c) > 0:
|
||||
so_edge_list.append(so_c)
|
||||
if exp.draw_crease:
|
||||
so_cr = extract_crease_edges(so, max_angle_deg=exp.crease_angle)
|
||||
if len(so_cr) > 0:
|
||||
so_edge_list.append(so_cr)
|
||||
|
||||
if so_edge_list:
|
||||
so_combined = np.concatenate(so_edge_list, axis=0)
|
||||
|
|
@ -561,8 +572,9 @@ class PDFObjectLabelsElement(PDFElement):
|
|||
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)
|
||||
|
||||
if (exp.show_name or has_any_obj_show_name or settings_items) and exp.raw_geometry is not None:
|
||||
label_infos = extract_object_label_info(exp.raw_geometry)
|
||||
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):
|
||||
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:
|
||||
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
|
||||
|
|
@ -710,7 +722,7 @@ class export_mesh_to_pdf:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
edges_2d: NDArray[np.float64],
|
||||
edges_2d: NDArray[np.float64] | None = None,
|
||||
filepath: str | Path | None = None,
|
||||
draw_wireframe: bool = True,
|
||||
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),
|
||||
bbox_color: tuple[float, float, float] = (0.0, 0.0, 1.0),
|
||||
raw_geometry: Any = None,
|
||||
slice_paths: Any = None,
|
||||
settings: Any = None,
|
||||
**kwargs: Any,
|
||||
) -> 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.draw_wireframe = draw_wireframe
|
||||
self.draw_boundary = draw_boundary
|
||||
|
|
@ -753,6 +766,7 @@ class export_mesh_to_pdf:
|
|||
self.bg_color = bg_color
|
||||
self.bbox_color = bbox_color
|
||||
self.raw_geometry = raw_geometry
|
||||
self.slice_paths = slice_paths
|
||||
self.settings = settings
|
||||
|
||||
self._prepare_geometry_and_layout()
|
||||
|
|
@ -773,7 +787,12 @@ class export_mesh_to_pdf:
|
|||
self.export(filepath)
|
||||
|
||||
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]] = []
|
||||
|
||||
|
|
@ -795,6 +814,11 @@ class export_mesh_to_pdf:
|
|||
if len(cr_edges) > 0:
|
||||
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:
|
||||
combined = np.concatenate(edge_list, axis=0)
|
||||
self.edges_to_draw = deduplicate_2d_edges(combined)
|
||||
|
|
@ -843,7 +867,7 @@ class export_mesh_to_pdf:
|
|||
|
||||
objs_with_props = [
|
||||
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
|
||||
|
|
@ -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.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]:
|
||||
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
|
||||
|
|
@ -1020,4 +1042,3 @@ class export_mesh_to_pdf:
|
|||
|
||||
# Alias for backward compatibility
|
||||
export_wireframe_to_pdf = export_mesh_to_pdf
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import pyclipper
|
|||
from typing import Any, Literal
|
||||
from shapely.geometry.base import BaseGeometry
|
||||
|
||||
from ..infra.blender.blender_mesh_capture import PdfProperties
|
||||
|
||||
from .geometry import ndarray_lru_cache
|
||||
|
||||
# Scaling factor for pyclipper (it works with integers)
|
||||
|
|
@ -160,6 +162,8 @@ class SlicePaths(list[np.ndarray]):
|
|||
layer_height: float
|
||||
first_layer_height: float | None
|
||||
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):
|
||||
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`).
|
||||
- **Geometry**: Input geometry data.
|
||||
- **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`.
|
||||
- **Geometry**: Input geometry socket.
|
||||
- **Assign PDF Properties**: Assigns line width (mm) and line color PDF export properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`).
|
||||
- **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 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.
|
||||
- **Geometry**: Input geometry socket.
|
||||
- **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`).
|
||||
- **Settings**: Input socket accepting configuration settings or property items.
|
||||
- **Transform Geometry**: Moves, rotates, and scales a `SlicingCollection` or `SlicingGroup` relative to the origin.
|
||||
- **Mesh**: Input mesh data.
|
||||
|
|
|
|||
|
|
@ -951,7 +951,7 @@ class TestPdfExporter(unittest.TestCase):
|
|||
pdf_file = os.path.join(tmpdir, "output.pdf")
|
||||
resolved_inputs = {
|
||||
"Geometry": so,
|
||||
"SlicePaths": None,
|
||||
"Paths": None,
|
||||
"Folder": pdf_file,
|
||||
"Settings": None,
|
||||
}
|
||||
|
|
@ -1121,6 +1121,47 @@ class TestPdfExporter(unittest.TestCase):
|
|||
for obj in coll.objects:
|
||||
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):
|
||||
so1 = SlicingObject()
|
||||
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)
|
||||
|
||||
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__':
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue