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):
|
||||
self._init_exec()
|
||||
self.add_input("SLICESOCK_DirPath", "Folder")
|
||||
self.add_input("SLICESOCK_Settings", "Settings")
|
||||
self.add_multi_socket_group("SLICESOCK_Mesh", name="Geometry")
|
||||
|
||||
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]
|
||||
folder: str = self.resolve("Folder") or ""
|
||||
settings = self.resolve("Settings")
|
||||
|
||||
if not geometries:
|
||||
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])),
|
||||
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
|
||||
raw_geometry=geom,
|
||||
settings=settings
|
||||
)
|
||||
if operator: operator.report({'INFO'}, f"Mesh PDF saved to {filepath}")
|
||||
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]
|
||||
|
||||
|
||||
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.
|
||||
y_at_max_x_mm is the Y coordinate of that rightmost vertex.
|
||||
"""
|
||||
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 = ""):
|
||||
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
|
||||
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)
|
||||
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]],
|
||||
[[max_x, min_y], [max_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)
|
||||
all_segments.append(rect)
|
||||
|
||||
process_item(mesh_data)
|
||||
if all_segments:
|
||||
return np.vstack(all_segments)
|
||||
if not all_segments:
|
||||
return np.empty((0, 2, 2), dtype=np.float64)
|
||||
return np.concatenate(all_segments, axis=0)
|
||||
|
||||
|
||||
def export_mesh_to_pdf(
|
||||
edges_2d: NDArray[np.float64],
|
||||
filepath: str,
|
||||
filepath: str | Path,
|
||||
draw_wireframe: bool = True,
|
||||
draw_boundary: 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),
|
||||
bbox_color: tuple[float, float, float] = (0.0, 0.0, 1.0),
|
||||
raw_geometry: Any = None,
|
||||
settings: Any = None,
|
||||
style: str | None = None # Backward compatibility
|
||||
) -> None:
|
||||
"""
|
||||
|
|
@ -889,8 +967,9 @@ def export_mesh_to_pdf(
|
|||
|
||||
stream_cmds.append("ET")
|
||||
|
||||
# 7. Object Names and Bounding Box Sizes (to the right of the rightmost vertex)
|
||||
if (show_name or show_bbox_size) and raw_geometry is not None:
|
||||
# 7. Object Names, Bounding Box Sizes, and Settings Attributes (to the right of the rightmost vertex)
|
||||
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)
|
||||
if label_infos:
|
||||
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(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]]))
|
||||
x_text_pt = p_pt[0, 0] + (2.5 * MM_TO_PT)
|
||||
y_text_pt = p_pt[0, 1] - (font_size_pt * 0.35)
|
||||
|
||||
if show_name and show_bbox_size:
|
||||
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")
|
||||
lines_to_draw: list[str] = []
|
||||
|
||||
if show_name:
|
||||
lines_to_draw.append(str(obj_name))
|
||||
|
||||
if 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:
|
||||
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(")", "\\)")
|
||||
y_line2_pt = y_text_pt - line_spacing_pt
|
||||
stream_cmds.append(f"1 0 0 1 {x_text_pt:.2f} {y_line2_pt:.2f} Tm")
|
||||
stream_cmds.append(f"({clean_size}) Tj")
|
||||
elif show_name:
|
||||
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)"
|
||||
for key_name, suffix in settings_items:
|
||||
attr_val = get_first_attribute_value(so_item, key_name)
|
||||
if attr_val is not None:
|
||||
if isinstance(attr_val, float):
|
||||
val_str = f"{attr_val:g}"
|
||||
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(")", "\\)")
|
||||
stream_cmds.append(f"1 0 0 1 {x_text_pt:.2f} {y_text_pt:.2f} Tm")
|
||||
stream_cmds.append(f"({clean_size}) Tj")
|
||||
for line_idx, line_str in enumerate(lines_to_draw):
|
||||
clean_str = line_str.replace("(", "\\(").replace(")", "\\)")
|
||||
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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
- **Geometry**: Input socket accepting a `SlicingGroup`, `SlicingCollection`, or `SlicingObject`.
|
||||
- **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.
|
||||
- **Boundary (1 Face)**: Checkbox to render open mesh boundary edges owned by only 1 face.
|
||||
- **Contour**: Checkbox to render outer boundary silhouette contour.
|
||||
|
|
|
|||
|
|
@ -480,6 +480,45 @@ class TestPdfExporter(unittest.TestCase):
|
|||
self.assertIn("(10) 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__':
|
||||
unittest.main()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue