mesh 2 pdf
This commit is contained in:
parent
122f83b087
commit
5bb30b08a0
4 changed files with 156 additions and 35 deletions
|
|
@ -399,6 +399,7 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self._init_exec()
|
self._init_exec()
|
||||||
self.add_input("SLICESOCK_DirPath", "Folder")
|
self.add_input("SLICESOCK_DirPath", "Folder")
|
||||||
|
self.add_input("SLICESOCK_Settings", "Settings")
|
||||||
self.add_multi_socket_group("SLICESOCK_Mesh", name="Geometry")
|
self.add_multi_socket_group("SLICESOCK_Mesh", name="Geometry")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
|
|
@ -424,6 +425,7 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
|
|
||||||
geometries: list[Any] = [g for g in self.resolve_multi_input('Geometry') if g is not None]
|
geometries: list[Any] = [g for g in self.resolve_multi_input('Geometry') if g is not None]
|
||||||
folder: str = self.resolve("Folder") or ""
|
folder: str = self.resolve("Folder") or ""
|
||||||
|
settings = self.resolve("Settings")
|
||||||
|
|
||||||
if not geometries:
|
if not geometries:
|
||||||
if operator: operator.report({'ERROR'}, "No Geometry connected")
|
if operator: operator.report({'ERROR'}, "No Geometry connected")
|
||||||
|
|
@ -479,7 +481,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=geom,
|
||||||
|
settings=settings
|
||||||
)
|
)
|
||||||
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:
|
||||||
|
|
|
||||||
|
|
@ -459,15 +459,92 @@ def _format_pdf_lines(lines_arr: NDArray[np.float64]) -> list[str]:
|
||||||
return [f"{row[0]:.2f} {row[1]:.2f} m {row[2]:.2f} {row[3]:.2f} l" for row in flat]
|
return [f"{row[0]:.2f} {row[1]:.2f} m {row[2]:.2f} {row[3]:.2f} l" for row in flat]
|
||||||
|
|
||||||
|
|
||||||
def extract_object_label_info(mesh_data: Any) -> list[tuple[str, float, float, float, float]]:
|
def extract_settings_items(settings: Any) -> list[tuple[str, str]]:
|
||||||
"""
|
"""
|
||||||
Extracts (object_name, max_x_mm, y_at_max_x_mm, width_mm, height_mm) for each SlicingObject in mesh_data.
|
Extracts a list of (setting_name, second_field_suffix) from settings input.
|
||||||
|
The setting_name is the left field, and second_field_suffix is the right field (e.g. unit).
|
||||||
|
"""
|
||||||
|
if not settings:
|
||||||
|
return []
|
||||||
|
items: list[tuple[str, str]] = []
|
||||||
|
if isinstance(settings, dict):
|
||||||
|
for k, v in settings.items():
|
||||||
|
key_str = str(k).strip()
|
||||||
|
if key_str:
|
||||||
|
suffix_str = str(v) if v is not None else ""
|
||||||
|
items.append((key_str, suffix_str))
|
||||||
|
elif isinstance(settings, (list, tuple)):
|
||||||
|
for item in settings:
|
||||||
|
if isinstance(item, str) and item.strip():
|
||||||
|
items.append((item.strip(), ""))
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
if "field" in item and item["field"]:
|
||||||
|
key_str = str(item["field"]).strip()
|
||||||
|
val_str = str(item.get("value_str", item.get("value", "")))
|
||||||
|
items.append((key_str, val_str))
|
||||||
|
else:
|
||||||
|
for k, v in item.items():
|
||||||
|
key_str = str(k).strip()
|
||||||
|
if key_str:
|
||||||
|
suffix_str = str(v) if v is not None else ""
|
||||||
|
items.append((key_str, suffix_str))
|
||||||
|
elif hasattr(item, "field") and getattr(item, "field"):
|
||||||
|
key_str = str(getattr(item, "field")).strip()
|
||||||
|
val_str = str(getattr(item, "value_str", getattr(item, "value", "")))
|
||||||
|
items.append((key_str, val_str))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def extract_settings_keys(settings: Any) -> list[str]:
|
||||||
|
return [item[0] for item in extract_settings_items(settings)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_attribute_value(obj: Any, attr_name: str) -> Any:
|
||||||
|
"""
|
||||||
|
Accesses the first element of the attribute with attr_name on obj.
|
||||||
|
"""
|
||||||
|
val = None
|
||||||
|
if hasattr(obj, "attributes") and isinstance(obj.attributes, dict) and attr_name in obj.attributes:
|
||||||
|
attr = obj.attributes[attr_name]
|
||||||
|
if isinstance(attr, dict) and "values" in attr:
|
||||||
|
val = attr["values"]
|
||||||
|
else:
|
||||||
|
val = attr
|
||||||
|
elif hasattr(obj, attr_name):
|
||||||
|
val = getattr(obj, attr_name)
|
||||||
|
|
||||||
|
if val is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(val, np.ndarray):
|
||||||
|
if val.size > 0:
|
||||||
|
first_val = val.flat[0]
|
||||||
|
if hasattr(first_val, "item"):
|
||||||
|
return first_val.item()
|
||||||
|
return first_val
|
||||||
|
return None
|
||||||
|
elif isinstance(val, (list, tuple)):
|
||||||
|
if len(val) > 0:
|
||||||
|
first_val = val[0]
|
||||||
|
if hasattr(first_val, "item"):
|
||||||
|
return first_val.item()
|
||||||
|
return first_val
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
if hasattr(val, "item"):
|
||||||
|
return val.item()
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
max_x_mm is the X coordinate of the rightmost vertex of the object.
|
max_x_mm is the X coordinate of the rightmost vertex of the object.
|
||||||
y_at_max_x_mm is the Y coordinate of that rightmost vertex.
|
y_at_max_x_mm is the Y coordinate of that rightmost vertex.
|
||||||
"""
|
"""
|
||||||
from ..infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
from ..infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
||||||
|
|
||||||
results: list[tuple[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:
|
||||||
|
|
@ -496,7 +573,7 @@ def extract_object_label_info(mesh_data: Any) -> list[tuple[str, float, float, f
|
||||||
|
|
||||||
w_mm = max_x - min_x
|
w_mm = max_x - min_x
|
||||||
h_mm = max_y - min_y
|
h_mm = max_y - min_y
|
||||||
results.append((name, max_x, y_val, w_mm, h_mm))
|
results.append((item, name, max_x, y_val, w_mm, h_mm))
|
||||||
|
|
||||||
process_item(mesh_data)
|
process_item(mesh_data)
|
||||||
return results
|
return results
|
||||||
|
|
@ -539,19 +616,19 @@ def extract_object_bounding_boxes(mesh_data: Any) -> NDArray[np.float64]:
|
||||||
[[min_x, min_y], [max_x, min_y]],
|
[[min_x, min_y], [max_x, min_y]],
|
||||||
[[max_x, min_y], [max_x, max_y]],
|
[[max_x, min_y], [max_x, max_y]],
|
||||||
[[max_x, max_y], [min_x, max_y]],
|
[[max_x, max_y], [min_x, max_y]],
|
||||||
[[min_x, max_y], [min_x, min_y]]
|
[[min_x, max_y], [min_x, min_y]],
|
||||||
], dtype=np.float64)
|
], dtype=np.float64)
|
||||||
all_segments.append(rect)
|
all_segments.append(rect)
|
||||||
|
|
||||||
process_item(mesh_data)
|
process_item(mesh_data)
|
||||||
if all_segments:
|
if not all_segments:
|
||||||
return np.vstack(all_segments)
|
|
||||||
return np.empty((0, 2, 2), dtype=np.float64)
|
return np.empty((0, 2, 2), dtype=np.float64)
|
||||||
|
return np.concatenate(all_segments, axis=0)
|
||||||
|
|
||||||
|
|
||||||
def export_mesh_to_pdf(
|
def export_mesh_to_pdf(
|
||||||
edges_2d: NDArray[np.float64],
|
edges_2d: NDArray[np.float64],
|
||||||
filepath: str,
|
filepath: str | Path,
|
||||||
draw_wireframe: bool = True,
|
draw_wireframe: bool = True,
|
||||||
draw_boundary: bool = False,
|
draw_boundary: bool = False,
|
||||||
draw_contour: bool = False,
|
draw_contour: bool = False,
|
||||||
|
|
@ -566,6 +643,7 @@ def 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,
|
||||||
|
settings: Any = None,
|
||||||
style: str | None = None # Backward compatibility
|
style: str | None = None # Backward compatibility
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -889,8 +967,9 @@ def export_mesh_to_pdf(
|
||||||
|
|
||||||
stream_cmds.append("ET")
|
stream_cmds.append("ET")
|
||||||
|
|
||||||
# 7. Object Names and Bounding Box Sizes (to the right of the rightmost vertex)
|
# 7. Object Names, Bounding Box Sizes, and Settings Attributes (to the right of the rightmost vertex)
|
||||||
if (show_name or show_bbox_size) and raw_geometry is not None:
|
settings_items = extract_settings_items(settings)
|
||||||
|
if (show_name or show_bbox_size or settings_items) and raw_geometry is not None:
|
||||||
label_infos = extract_object_label_info(raw_geometry)
|
label_infos = extract_object_label_info(raw_geometry)
|
||||||
if label_infos:
|
if label_infos:
|
||||||
font_size_pt = max(8.0, line_width_pt * 4.0)
|
font_size_pt = max(8.0, line_width_pt * 4.0)
|
||||||
|
|
@ -900,42 +979,41 @@ def export_mesh_to_pdf(
|
||||||
stream_cmds.append("BT")
|
stream_cmds.append("BT")
|
||||||
stream_cmds.append(f"/F1 {font_size_pt:.1f} Tf")
|
stream_cmds.append(f"/F1 {font_size_pt:.1f} Tf")
|
||||||
|
|
||||||
for obj_name, max_x_mm, y_val_mm, w_obj_mm, h_obj_mm in label_infos:
|
for so_item, obj_name, max_x_mm, y_val_mm, w_obj_mm, h_obj_mm in label_infos:
|
||||||
p_pt = to_pdf_pt(np.array([[max_x_mm, y_val_mm]]))
|
p_pt = to_pdf_pt(np.array([[max_x_mm, y_val_mm]]))
|
||||||
x_text_pt = p_pt[0, 0] + (2.5 * MM_TO_PT)
|
x_text_pt = p_pt[0, 0] + (2.5 * MM_TO_PT)
|
||||||
y_text_pt = p_pt[0, 1] - (font_size_pt * 0.35)
|
y_text_pt = p_pt[0, 1] - (font_size_pt * 0.35)
|
||||||
|
|
||||||
if show_name and show_bbox_size:
|
lines_to_draw: list[str] = []
|
||||||
clean_name = str(obj_name).replace("(", "\\(").replace(")", "\\)")
|
|
||||||
stream_cmds.append(f"1 0 0 1 {x_text_pt:.2f} {y_text_pt:.2f} Tm")
|
|
||||||
stream_cmds.append(f"({clean_name}) Tj")
|
|
||||||
|
|
||||||
|
if show_name:
|
||||||
|
lines_to_draw.append(str(obj_name))
|
||||||
|
|
||||||
|
if show_bbox_size:
|
||||||
if main_step >= 1000.0:
|
if main_step >= 1000.0:
|
||||||
w_val = w_obj_mm / 1000.0
|
w_val = w_obj_mm / 1000.0
|
||||||
h_val = h_obj_mm / 1000.0
|
h_val = h_obj_mm / 1000.0
|
||||||
size_str = f"(X: {w_val:g}m Y: {h_val:g}m)"
|
size_str = f"(X: {w_val:g}m Y: {h_val:g}m)"
|
||||||
else:
|
else:
|
||||||
size_str = f"(X: {w_obj_mm:g}mm Y: {h_obj_mm:g}mm)"
|
size_str = f"(X: {w_obj_mm:g}mm Y: {h_obj_mm:g}mm)"
|
||||||
|
lines_to_draw.append(size_str)
|
||||||
|
|
||||||
clean_size = size_str.replace("(", "\\(").replace(")", "\\)")
|
for key_name, suffix in settings_items:
|
||||||
y_line2_pt = y_text_pt - line_spacing_pt
|
attr_val = get_first_attribute_value(so_item, key_name)
|
||||||
stream_cmds.append(f"1 0 0 1 {x_text_pt:.2f} {y_line2_pt:.2f} Tm")
|
if attr_val is not None:
|
||||||
stream_cmds.append(f"({clean_size}) Tj")
|
if isinstance(attr_val, float):
|
||||||
elif show_name:
|
val_str = f"{attr_val:g}"
|
||||||
clean_name = str(obj_name).replace("(", "\\(").replace(")", "\\)")
|
|
||||||
stream_cmds.append(f"1 0 0 1 {x_text_pt:.2f} {y_text_pt:.2f} Tm")
|
|
||||||
stream_cmds.append(f"({clean_name}) Tj")
|
|
||||||
elif show_bbox_size:
|
|
||||||
if main_step >= 1000.0:
|
|
||||||
w_val = w_obj_mm / 1000.0
|
|
||||||
h_val = h_obj_mm / 1000.0
|
|
||||||
size_str = f"(X: {w_val:g}m Y: {h_val:g}m)"
|
|
||||||
else:
|
else:
|
||||||
size_str = f"(X: {w_obj_mm:g}mm Y: {h_obj_mm:g}mm)"
|
val_str = str(attr_val)
|
||||||
|
if suffix:
|
||||||
|
val_str = f"{val_str}{suffix}"
|
||||||
|
lines_to_draw.append(f"{key_name}: {val_str}")
|
||||||
|
|
||||||
clean_size = size_str.replace("(", "\\(").replace(")", "\\)")
|
for line_idx, line_str in enumerate(lines_to_draw):
|
||||||
stream_cmds.append(f"1 0 0 1 {x_text_pt:.2f} {y_text_pt:.2f} Tm")
|
clean_str = line_str.replace("(", "\\(").replace(")", "\\)")
|
||||||
stream_cmds.append(f"({clean_size}) Tj")
|
curr_y_pt = y_text_pt - (line_idx * line_spacing_pt)
|
||||||
|
stream_cmds.append(f"1 0 0 1 {x_text_pt:.2f} {curr_y_pt:.2f} Tm")
|
||||||
|
stream_cmds.append(f"({clean_str}) Tj")
|
||||||
|
|
||||||
stream_cmds.append("ET")
|
stream_cmds.append("ET")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
- **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.
|
- **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.
|
||||||
- **Geometry**: Input socket accepting a `SlicingGroup`, `SlicingCollection`, or `SlicingObject`.
|
- **Geometry**: Input socket accepting a `SlicingGroup`, `SlicingCollection`, or `SlicingObject`.
|
||||||
- **Folder**: Target folder or PDF file path on disk.
|
- **Folder**: Target folder or PDF file path on disk.
|
||||||
|
- **Settings**: Input socket accepting configuration settings or attribute names to render as `<name>: <value>` next to each object.
|
||||||
- **Wireframe**: Checkbox to render all projected 3D wireframe edges.
|
- **Wireframe**: Checkbox to render all projected 3D wireframe edges.
|
||||||
- **Boundary (1 Face)**: Checkbox to render open mesh boundary edges owned by only 1 face.
|
- **Boundary (1 Face)**: Checkbox to render open mesh boundary edges owned by only 1 face.
|
||||||
- **Contour**: Checkbox to render outer boundary silhouette contour.
|
- **Contour**: Checkbox to render outer boundary silhouette contour.
|
||||||
|
|
|
||||||
|
|
@ -480,6 +480,45 @@ class TestPdfExporter(unittest.TestCase):
|
||||||
self.assertIn("(10) Tj", pdf_text)
|
self.assertIn("(10) Tj", pdf_text)
|
||||||
self.assertIn("(100 mm) Tj", pdf_text)
|
self.assertIn("(100 mm) Tj", pdf_text)
|
||||||
|
|
||||||
|
def test_export_pdf_with_settings_attributes(self):
|
||||||
|
so = SlicingObject()
|
||||||
|
so.name = "AttributedPart"
|
||||||
|
tri = np.array([
|
||||||
|
[0.0, 0.0, 0.0],
|
||||||
|
[10.0, 0.0, 0.0],
|
||||||
|
[0.0, 10.0, 0.0],
|
||||||
|
[0.0, 0.0, 1.0]
|
||||||
|
], dtype=np.float64)
|
||||||
|
so.mesh = np.array([tri])
|
||||||
|
so.attributes = {
|
||||||
|
"infill_density": {"domain": "face", "type": "float", "values": np.array([20.0, 20.0])},
|
||||||
|
"layer_height": {"domain": "face", "type": "float", "values": np.array([0.15, 0.15])},
|
||||||
|
"material": ["PLA", "PLA"]
|
||||||
|
}
|
||||||
|
|
||||||
|
edges_2d = extract_unique_top_view_edges(so)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
pdf_path = os.path.join(tmpdir, "test_settings_attr.pdf")
|
||||||
|
export_mesh_to_pdf(
|
||||||
|
edges_2d,
|
||||||
|
pdf_path,
|
||||||
|
show_name=True,
|
||||||
|
show_bbox_size=True,
|
||||||
|
raw_geometry=so,
|
||||||
|
settings={"infill_density": "%", "layer_height": " mm", "material": ""}
|
||||||
|
)
|
||||||
|
self.assertTrue(os.path.exists(pdf_path))
|
||||||
|
with open(pdf_path, "rb") as f:
|
||||||
|
content = f.read().decode("utf-8", errors="ignore")
|
||||||
|
|
||||||
|
self.assertIn("(AttributedPart) Tj", content)
|
||||||
|
self.assertIn("(infill_density: 20%) Tj", content)
|
||||||
|
self.assertIn("(layer_height: 0.15 mm) Tj", content)
|
||||||
|
self.assertIn("(material: PLA) Tj", content)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue