grid
This commit is contained in:
parent
40cc779e5d
commit
32e43c748b
5 changed files with 305 additions and 107 deletions
|
|
@ -367,6 +367,10 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
|||
draw_sa2_holes: NfBoolProperty(name="Holes (Ø)", default=False) # pyright: ignore[reportInvalidTypeForm]
|
||||
show_lineart: NfBoolProperty(name="Lineart Visibility", default=False) # pyright: ignore[reportInvalidTypeForm]
|
||||
show_sa2: NfBoolProperty(name="SA2 Settings", default=False) # pyright: ignore[reportInvalidTypeForm]
|
||||
draw_grid_xy: NfBoolProperty(name="XY Grid (Top)", default=True) # pyright: ignore[reportInvalidTypeForm]
|
||||
draw_grid_xz: NfBoolProperty(name="XZ Grid (Front)", default=False) # pyright: ignore[reportInvalidTypeForm]
|
||||
draw_grid_yz: NfBoolProperty(name="YZ Grid (Side)", default=False) # pyright: ignore[reportInvalidTypeForm]
|
||||
show_grids: NfBoolProperty(name="Grids", default=False) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
draw_header: NfBoolProperty(name="Technical Header", default=True) # pyright: ignore[reportInvalidTypeForm]
|
||||
show_header: NfBoolProperty(name="Technical Drawing Header", default=False) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
|
@ -448,6 +452,14 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
|||
box = layout.box()
|
||||
box.prop(self, "draw_sa2_holes")
|
||||
|
||||
# Grid Display subpanel
|
||||
layout.prop(self, "show_grids", text="Grids", icon='TRIA_DOWN' if self.show_grids else 'TRIA_RIGHT')
|
||||
if self.show_grids:
|
||||
box = layout.box()
|
||||
box.prop(self, "draw_grid_xy")
|
||||
box.prop(self, "draw_grid_xz")
|
||||
box.prop(self, "draw_grid_yz")
|
||||
|
||||
# Display settings
|
||||
layout.prop(self, "draw_bbox")
|
||||
if self.draw_bbox:
|
||||
|
|
@ -599,6 +611,9 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
|||
bbox_color=(float(self.bbox_color[0]), float(self.bbox_color[1]), float(self.bbox_color[2])),
|
||||
raw_geometry=current_raw_geom,
|
||||
slice_paths=current_slice_paths,
|
||||
draw_grid_xy=getattr(self, "draw_grid_xy", True),
|
||||
draw_grid_xz=getattr(self, "draw_grid_xz", False),
|
||||
draw_grid_yz=getattr(self, "draw_grid_yz", False),
|
||||
)
|
||||
if operator: operator.report({'INFO'}, f"Mesh PDF saved to {filepath}")
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -144,6 +144,26 @@ def get_first_object_rotation(
|
|||
return (0.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def map_2d_to_3d(u: float, v: float, d: float, plane: str = "XY") -> NDArray[np.float64]:
|
||||
p = plane.upper()
|
||||
if p == "XZ":
|
||||
return np.array([u, d, v], dtype=np.float64)
|
||||
elif p == "YZ":
|
||||
return np.array([d, u, v], dtype=np.float64)
|
||||
else: # XY
|
||||
return np.array([u, v, d], dtype=np.float64)
|
||||
|
||||
|
||||
def get_plane_2d_indices(plane: str = "XY") -> tuple[list[int], int]:
|
||||
p = plane.upper()
|
||||
if p == "XZ":
|
||||
return [0, 2], 1
|
||||
elif p == "YZ":
|
||||
return [1, 2], 0
|
||||
else: # XY
|
||||
return [0, 1], 2
|
||||
|
||||
|
||||
def generate_grid_slice_paths(
|
||||
grid_min_x: float,
|
||||
grid_max_x: float,
|
||||
|
|
@ -152,10 +172,11 @@ def generate_grid_slice_paths(
|
|||
minor_step: float,
|
||||
max_grid_steps: int = 2000,
|
||||
z_level: float = 0.0,
|
||||
plane: str = "XY",
|
||||
) -> tuple[Any, Any]:
|
||||
"""
|
||||
Generates 3D grid line geometry sitting at local Z=z_level as two SlicePaths objects:
|
||||
(minor_grid_slice_paths, main_grid_slice_paths).
|
||||
Generates 3D grid line geometry sitting at local depth d=z_level on specified local plane (XY, XZ, YZ)
|
||||
as two SlicePaths objects: (minor_grid_slice_paths, main_grid_slice_paths).
|
||||
"""
|
||||
from ..slice import SlicePaths
|
||||
|
||||
|
|
@ -173,19 +194,23 @@ def generate_grid_slice_paths(
|
|||
minor_paths: list[NDArray[np.float64]] = []
|
||||
main_paths: list[NDArray[np.float64]] = []
|
||||
|
||||
# Vertical grid lines (constant X)
|
||||
# Vertical grid lines (constant U)
|
||||
for k in k_x:
|
||||
gx = float(k * minor_step)
|
||||
line = np.array([[gx, grid_min_y, z_level], [gx, grid_max_y, z_level]], dtype=np.float64)
|
||||
gu = float(k * minor_step)
|
||||
p1 = map_2d_to_3d(gu, grid_min_y, z_level, plane)
|
||||
p2 = map_2d_to_3d(gu, grid_max_y, z_level, plane)
|
||||
line = np.array([p1, p2], dtype=np.float64)
|
||||
if k % 10 == 0:
|
||||
main_paths.append(line)
|
||||
else:
|
||||
minor_paths.append(line)
|
||||
|
||||
# Horizontal grid lines (constant Y)
|
||||
# Horizontal grid lines (constant V)
|
||||
for k in k_y:
|
||||
gy = float(k * minor_step)
|
||||
line = np.array([[grid_min_x, gy, z_level], [grid_max_x, gy, z_level]], dtype=np.float64)
|
||||
gv = float(k * minor_step)
|
||||
p1 = map_2d_to_3d(grid_min_x, gv, z_level, plane)
|
||||
p2 = map_2d_to_3d(grid_max_x, gv, z_level, plane)
|
||||
line = np.array([p1, p2], dtype=np.float64)
|
||||
if k % 10 == 0:
|
||||
main_paths.append(line)
|
||||
else:
|
||||
|
|
@ -203,9 +228,15 @@ def generate_legend_slice_paths(
|
|||
line_width_pt: float,
|
||||
font_size_pt: float,
|
||||
z_level: float = 0.0,
|
||||
plane: str = "XY",
|
||||
draw_x: bool = True,
|
||||
draw_y: bool = True,
|
||||
draw_z: bool = False,
|
||||
local_origin_3d: tuple[float, float, float] | None = None,
|
||||
local_max_3d: tuple[float, float, float] | None = None,
|
||||
) -> tuple[Any, list[dict[str, Any]]]:
|
||||
"""
|
||||
Generates 3D legend line geometry (axes, arrowheads, tick marks) sitting at local Z=z_level
|
||||
Generates 3D legend line geometry (axes, arrowheads, tick marks) starting at origin (min x, max y, min z)
|
||||
as a SlicePaths object, alongside tick and label metadata for text positioning.
|
||||
Returns: (legend_slice_paths, label_metadata_list)
|
||||
"""
|
||||
|
|
@ -213,74 +244,129 @@ def generate_legend_slice_paths(
|
|||
from .sizing import LegendSizing, MM_TO_PT
|
||||
|
||||
leg_sizing = LegendSizing(main_step, minor_step, line_width_pt, font_size_pt)
|
||||
x0 = grid_min_x
|
||||
y0 = grid_min_y
|
||||
|
||||
x_grid_end_mm = min(grid_max_x, grid_min_x + 10.0 * main_step)
|
||||
num_main_steps = max(1, int(np.round((x_grid_end_mm - grid_min_x) / main_step)))
|
||||
if local_origin_3d is not None:
|
||||
x0, y0, z0 = local_origin_3d
|
||||
else:
|
||||
x0 = grid_min_x
|
||||
y0 = grid_min_y
|
||||
z0 = z_level
|
||||
|
||||
max_x_val = local_max_3d[0] if local_max_3d else grid_max_x
|
||||
min_y_val = local_max_3d[1] if local_max_3d else grid_min_y
|
||||
max_z_val = local_max_3d[2] if local_max_3d else z_level
|
||||
|
||||
arrow_len_mm = leg_sizing.arrow_len_pt / MM_TO_PT
|
||||
arrow_half_w_mm = leg_sizing.arrow_half_w_pt / MM_TO_PT
|
||||
label_margin_mm = leg_sizing.label_margin_h_pt / MM_TO_PT
|
||||
|
||||
x_arrow_base = x_grid_end_mm + leg_sizing.x_axis_extension_mm
|
||||
x_arrow_tip = x_arrow_base + arrow_len_mm
|
||||
|
||||
y_arrow_base = y0 + leg_sizing.y_axis_height_mm
|
||||
y_arrow_tip = y_arrow_base + arrow_len_mm
|
||||
|
||||
legend_paths: list[NDArray[np.float64]] = []
|
||||
|
||||
# 1. Main horizontal X-axis line & Arrowhead
|
||||
legend_paths.append(np.array([[x0, y0, z_level], [x_arrow_tip, y0, z_level]], dtype=np.float64))
|
||||
legend_paths.append(np.array([[x_arrow_base, y0 + arrow_half_w_mm, z_level], [x_arrow_tip, y0, z_level]], dtype=np.float64))
|
||||
legend_paths.append(np.array([[x_arrow_tip, y0, z_level], [x_arrow_base, y0 - arrow_half_w_mm, z_level]], dtype=np.float64))
|
||||
|
||||
# 2. X-axis tick marks & label positions
|
||||
label_meta: list[dict[str, Any]] = []
|
||||
|
||||
for k in range(num_main_steps + 1):
|
||||
xk = x0 + k * main_step
|
||||
tick_h = leg_sizing.major_tick_mm if k in (0, 5, num_main_steps) else leg_sizing.minor_tick_mm
|
||||
legend_paths.append(np.array([[xk, y0, z_level], [xk, y0 + tick_h, z_level]], dtype=np.float64))
|
||||
# 1. Local X Axis (extends in +X direction from x0)
|
||||
if draw_x:
|
||||
x_grid_end_mm = min(max_x_val, x0 + 10.0 * main_step) if max_x_val > x0 else x0 + 10.0 * main_step
|
||||
num_main_steps = max(1, int(np.round(abs(x_grid_end_mm - x0) / main_step)))
|
||||
|
||||
if k > 0:
|
||||
val_mm = k * main_step
|
||||
if main_step >= 1000.0:
|
||||
val_num = val_mm / 1000.0
|
||||
unit = " m" if k == num_main_steps else ""
|
||||
tick_str = f"{val_num:g}{unit}"
|
||||
else:
|
||||
unit = " mm" if k == num_main_steps else ""
|
||||
tick_str = f"{val_mm:g}{unit}"
|
||||
x_arrow_base = x_grid_end_mm + leg_sizing.x_axis_extension_mm
|
||||
x_arrow_tip = x_arrow_base + arrow_len_mm
|
||||
|
||||
label_meta.append({
|
||||
"type": "tick",
|
||||
"text": tick_str,
|
||||
"local_pt": (xk, y0 + tick_h + label_margin_mm, z_level),
|
||||
})
|
||||
legend_paths.append(np.array([[x0, y0, z0], [x_arrow_tip, y0, z0]], dtype=np.float64))
|
||||
legend_paths.append(np.array([[x_arrow_base, y0 + arrow_half_w_mm, z0], [x_arrow_tip, y0, z0]], dtype=np.float64))
|
||||
legend_paths.append(np.array([[x_arrow_tip, y0, z0], [x_arrow_base, y0 - arrow_half_w_mm, z0]], dtype=np.float64))
|
||||
|
||||
# 3. Main vertical Y-axis line & Arrowhead
|
||||
legend_paths.append(np.array([[x0, y0, z_level], [x0, y_arrow_tip, z_level]], dtype=np.float64))
|
||||
legend_paths.append(np.array([[x0 - arrow_half_w_mm, y_arrow_base, z_level], [x0, y_arrow_tip, z_level]], dtype=np.float64))
|
||||
legend_paths.append(np.array([[x0, y_arrow_tip, z_level], [x0 + arrow_half_w_mm, y_arrow_base, z_level]], dtype=np.float64))
|
||||
for k in range(num_main_steps + 1):
|
||||
xk = x0 + k * main_step
|
||||
tick_h = leg_sizing.major_tick_mm if k in (0, 5, num_main_steps) else leg_sizing.minor_tick_mm
|
||||
legend_paths.append(np.array([[xk, y0, z0], [xk, y0 - tick_h, z0]], dtype=np.float64))
|
||||
|
||||
# 4. Axis labels meta
|
||||
label_meta.append({
|
||||
"type": "x_axis",
|
||||
"text": "X",
|
||||
"local_pt": (x_arrow_tip + label_margin_mm, y0, z_level),
|
||||
})
|
||||
label_meta.append({
|
||||
"type": "y_axis",
|
||||
"text": "Y",
|
||||
"local_pt": (x0, y_arrow_tip + label_margin_mm, z_level),
|
||||
})
|
||||
if k > 0:
|
||||
val_mm = k * main_step
|
||||
unit = " m" if (main_step >= 1000.0 and k == num_main_steps) else (" mm" if (main_step < 1000.0 and k == num_main_steps) else "")
|
||||
val_str = f"{val_mm/1000.0:g}{unit}" if main_step >= 1000.0 else f"{val_mm:g}{unit}"
|
||||
label_meta.append({
|
||||
"type": "tick",
|
||||
"text": val_str,
|
||||
"local_pt": (xk, y0 - tick_h - label_margin_mm, z0),
|
||||
})
|
||||
|
||||
label_meta.append({
|
||||
"type": "x_axis",
|
||||
"text": "X",
|
||||
"local_pt": (x_arrow_tip + label_margin_mm, y0, z0),
|
||||
})
|
||||
|
||||
# 2. Local Y Axis (extends in -Y direction from y0 towards min_y_val)
|
||||
if draw_y:
|
||||
y_grid_end_mm = max(min_y_val, y0 - 10.0 * main_step) if min_y_val < y0 else y0 - 10.0 * main_step
|
||||
num_main_steps = max(1, int(np.round(abs(y0 - y_grid_end_mm) / main_step)))
|
||||
|
||||
y_arrow_base = y_grid_end_mm - leg_sizing.y_axis_height_mm
|
||||
y_arrow_tip = y_arrow_base - arrow_len_mm
|
||||
|
||||
legend_paths.append(np.array([[x0, y0, z0], [x0, y_arrow_tip, z0]], dtype=np.float64))
|
||||
legend_paths.append(np.array([[x0 - arrow_half_w_mm, y_arrow_base, z0], [x0, y_arrow_tip, z0]], dtype=np.float64))
|
||||
legend_paths.append(np.array([[x0, y_arrow_tip, z0], [x0 + arrow_half_w_mm, y_arrow_base, z0]], dtype=np.float64))
|
||||
|
||||
if not draw_x:
|
||||
for k in range(num_main_steps + 1):
|
||||
yk = y0 - k * main_step
|
||||
tick_h = leg_sizing.major_tick_mm if k in (0, 5, num_main_steps) else leg_sizing.minor_tick_mm
|
||||
legend_paths.append(np.array([[x0, yk, z0], [x0 + tick_h, yk, z0]], dtype=np.float64))
|
||||
|
||||
if k > 0:
|
||||
val_mm = k * main_step
|
||||
unit = " m" if (main_step >= 1000.0 and k == num_main_steps) else (" mm" if (main_step < 1000.0 and k == num_main_steps) else "")
|
||||
val_str = f"{val_mm/1000.0:g}{unit}" if main_step >= 1000.0 else f"{val_mm:g}{unit}"
|
||||
label_meta.append({
|
||||
"type": "tick",
|
||||
"text": val_str,
|
||||
"local_pt": (x0 + tick_h + label_margin_mm, yk, z0),
|
||||
})
|
||||
|
||||
label_meta.append({
|
||||
"type": "y_axis",
|
||||
"text": "Y",
|
||||
"local_pt": (x0, y_arrow_tip - label_margin_mm, z0),
|
||||
})
|
||||
|
||||
# 3. Local Z Axis (extends in +Z direction from z0 towards max_z_val)
|
||||
if draw_z:
|
||||
z_grid_end_mm = min(max_z_val, z0 + 10.0 * main_step) if max_z_val > z0 else z0 + 10.0 * main_step
|
||||
num_main_steps = max(1, int(np.round(abs(z_grid_end_mm - z0) / main_step)))
|
||||
|
||||
z_arrow_base = z_grid_end_mm + leg_sizing.y_axis_height_mm
|
||||
z_arrow_tip = z_arrow_base + arrow_len_mm
|
||||
|
||||
legend_paths.append(np.array([[x0, y0, z0], [x0, y0, z_arrow_tip]], dtype=np.float64))
|
||||
legend_paths.append(np.array([[x0 - arrow_half_w_mm, y0, z_arrow_base], [x0, y0, z_arrow_tip]], dtype=np.float64))
|
||||
legend_paths.append(np.array([[x0, y0, z_arrow_tip], [x0 + arrow_half_w_mm, y0, z_arrow_base]], dtype=np.float64))
|
||||
|
||||
if not draw_x:
|
||||
for k in range(num_main_steps + 1):
|
||||
zk = z0 + k * main_step
|
||||
tick_h = leg_sizing.major_tick_mm if k in (0, 5, num_main_steps) else leg_sizing.minor_tick_mm
|
||||
legend_paths.append(np.array([[x0, y0, zk], [x0 + tick_h, y0, zk]], dtype=np.float64))
|
||||
|
||||
if k > 0:
|
||||
val_mm = k * main_step
|
||||
unit = " m" if (main_step >= 1000.0 and k == num_main_steps) else (" mm" if (main_step < 1000.0 and k == num_main_steps) else "")
|
||||
val_str = f"{val_mm/1000.0:g}{unit}" if main_step >= 1000.0 else f"{val_mm:g}{unit}"
|
||||
label_meta.append({
|
||||
"type": "tick",
|
||||
"text": val_str,
|
||||
"local_pt": (x0 + tick_h + label_margin_mm, y0, zk),
|
||||
})
|
||||
|
||||
label_meta.append({
|
||||
"type": "z_axis",
|
||||
"text": "Z",
|
||||
"local_pt": (x0, y0, z_arrow_tip + label_margin_mm),
|
||||
})
|
||||
|
||||
return SlicePaths(legend_paths), label_meta
|
||||
|
||||
|
||||
|
||||
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.
|
||||
|
|
@ -298,7 +384,8 @@ def extract_slice_paths_edges(slice_paths: Any) -> NDArray[np.float64]:
|
|||
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]
|
||||
p_arr = np.asarray(path, dtype=np.float64)
|
||||
pts = p_arr[:, :2]
|
||||
segs = np.stack([pts[:-1], pts[1:]], axis=1)
|
||||
all_segments.append(segs)
|
||||
elif isinstance(item, (list, tuple)):
|
||||
|
|
@ -440,20 +527,14 @@ def extract_lineart_edges(mesh_data: Any, max_angle_deg: float = 30.0) -> NDArra
|
|||
if num_tris == 0:
|
||||
return np.empty((0, 2, 2), dtype=np.float64)
|
||||
|
||||
# Top-view 2D vertices
|
||||
# 2D vertices projected on plane
|
||||
verts_2d = tris_3d[:, :, :2] # (N, 3, 2)
|
||||
|
||||
# For silhouette detection, categorize faces by normal.z sign
|
||||
# For view direction (0,0,-1):
|
||||
# - normal.z < 0: front-facing (facing viewer)
|
||||
# - normal.z > 0: back-facing (facing away)
|
||||
# - normal.z == 0: edge-on / vertical
|
||||
normal_z = normals_unit[:, 2]
|
||||
epsilon = 1e-3
|
||||
face_category = np.zeros(num_tris, dtype=int)
|
||||
face_category[normal_z < -epsilon] = 1 # Front-facing
|
||||
face_category[normal_z > epsilon] = 2 # Back-facing
|
||||
# face_category == 0 means vertical (|normal_z| <= epsilon)
|
||||
|
||||
# Extract all 3 edges per triangle
|
||||
e0_3d = tris_3d[:, [0, 1], :]
|
||||
|
|
@ -481,7 +562,6 @@ def extract_lineart_edges(mesh_data: Any, max_angle_deg: float = 30.0) -> NDArra
|
|||
if len(edges_3d) == 0:
|
||||
return np.empty((0, 2, 2), dtype=np.float64)
|
||||
|
||||
# Canonical undirected edge sorting in 3D (rounded to 4 decimal places)
|
||||
rounded_3d = np.round(edges_3d, decimals=4)
|
||||
pA = rounded_3d[:, 0, :]
|
||||
pB = rounded_3d[:, 1, :]
|
||||
|
|
@ -497,7 +577,6 @@ def extract_lineart_edges(mesh_data: Any, max_angle_deg: float = 30.0) -> NDArra
|
|||
canonical_2d = edges_2d.copy()
|
||||
canonical_2d[swap_mask] = canonical_2d[swap_mask][:, ::-1, :]
|
||||
|
||||
# Unique 3D edge identification using NumPy structured view
|
||||
flat_3d = canonical_3d.reshape(len(canonical_3d), 6)
|
||||
view_bytes = np.ascontiguousarray(flat_3d).view(np.dtype((np.void, 48))).ravel()
|
||||
unique_keys, first_indices, inverse_indices, counts = np.unique(
|
||||
|
|
@ -511,13 +590,9 @@ def extract_lineart_edges(mesh_data: Any, max_angle_deg: float = 30.0) -> NDArra
|
|||
np.maximum.at(max_cat, inverse_indices, edge_category)
|
||||
np.minimum.at(min_cat, inverse_indices, edge_category)
|
||||
|
||||
# Rule 1: Silhouette edge - adjacent faces have different categories
|
||||
rule1_unique = (counts >= 2) & (max_cat != min_cat)
|
||||
|
||||
# Rule 2: Boundary edge (owned by 1 face)
|
||||
rule2_unique = counts == 1
|
||||
|
||||
# Rule 3: Crease edge (owned by 2+ faces with normal angle > max_angle_deg)
|
||||
cos_threshold = math.cos(math.radians(max_angle_deg))
|
||||
rule3_unique = np.zeros(num_unique, dtype=bool)
|
||||
|
||||
|
|
@ -527,7 +602,6 @@ def extract_lineart_edges(mesh_data: Any, max_angle_deg: float = 30.0) -> NDArra
|
|||
sorted_edge_ids = inverse_indices[sort_by_edge]
|
||||
sorted_normals = edge_normals[sort_by_edge]
|
||||
|
||||
# Vectorized pair matching for adjacent faces sharing the same edge ID
|
||||
match = sorted_edge_ids[:-1] == sorted_edge_ids[1:]
|
||||
if np.any(match):
|
||||
n1 = sorted_normals[:-1][match]
|
||||
|
|
@ -688,8 +762,6 @@ def extract_silhouette_contour_edges(mesh_data: Any) -> NDArray[np.float64]:
|
|||
if num_tris == 0:
|
||||
return np.empty((0, 2, 2), dtype=np.float64)
|
||||
|
||||
# Filter for top-facing faces (normal_z > epsilon).
|
||||
# Back-facing (normal_z < -epsilon) and vertical (|normal_z| <= epsilon) faces are masked out.
|
||||
normal_z = normals_unit[:, 2]
|
||||
epsilon = 1e-3
|
||||
front_mask = normal_z > epsilon
|
||||
|
|
@ -698,7 +770,6 @@ def extract_silhouette_contour_edges(mesh_data: Any) -> NDArray[np.float64]:
|
|||
if len(tris_3d) == 0:
|
||||
return np.empty((0, 2, 2), dtype=np.float64)
|
||||
|
||||
# Extract 3 edges per front-facing triangle
|
||||
e0_3d = tris_3d[:, [0, 1], :]
|
||||
e1_3d = tris_3d[:, [1, 2], :]
|
||||
e2_3d = tris_3d[:, [2, 0], :]
|
||||
|
|
@ -710,7 +781,6 @@ def extract_silhouette_contour_edges(mesh_data: Any) -> NDArray[np.float64]:
|
|||
e2_2d = verts_2d[:, [2, 0], :]
|
||||
edges_2d = np.concatenate([e0_2d, e1_2d, e2_2d], axis=0) # (3M, 2, 2)
|
||||
|
||||
# Filter out near zero-length 2D edges
|
||||
dists_sq_2d = np.sum((edges_2d[:, 0, :] - edges_2d[:, 1, :]) ** 2, axis=1)
|
||||
valid_len_mask = dists_sq_2d >= 1e-8
|
||||
|
||||
|
|
@ -720,7 +790,6 @@ def extract_silhouette_contour_edges(mesh_data: Any) -> NDArray[np.float64]:
|
|||
if len(edges_3d) == 0:
|
||||
return np.empty((0, 2, 2), dtype=np.float64)
|
||||
|
||||
# Canonical undirected edge sorting in 3D
|
||||
rounded_3d = np.round(edges_3d, decimals=4)
|
||||
pA = rounded_3d[:, 0, :]
|
||||
pB = rounded_3d[:, 1, :]
|
||||
|
|
@ -736,14 +805,12 @@ def extract_silhouette_contour_edges(mesh_data: Any) -> NDArray[np.float64]:
|
|||
canonical_2d = edges_2d.copy()
|
||||
canonical_2d[swap_mask] = canonical_2d[swap_mask][:, ::-1, :]
|
||||
|
||||
# Unique 3D edge identification
|
||||
flat_3d = canonical_3d.reshape(len(canonical_3d), 6)
|
||||
view_bytes = np.ascontiguousarray(flat_3d).view(np.dtype((np.void, 48))).ravel()
|
||||
_, first_indices, counts = np.unique(
|
||||
view_bytes, return_index=True, return_counts=True
|
||||
)
|
||||
|
||||
# Boundary edges of the front-facing surface have count == 1
|
||||
contour_edges = canonical_2d[first_indices[counts == 1]]
|
||||
return deduplicate_2d_edges(contour_edges)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from .geometry import (
|
|||
extract_boundary_edges,
|
||||
extract_outer_contour_edges,
|
||||
extract_crease_edges,
|
||||
get_plane_2d_indices,
|
||||
)
|
||||
from .sizing import SA2Sizing, compute_effective_stroke_and_font, MM_TO_PT
|
||||
from .pdf_utils import PdfStream, escape_pdf_text
|
||||
|
|
@ -156,6 +157,7 @@ def extract_slicing_object_features(
|
|||
draw_crease: bool = False,
|
||||
crease_angle: float = 30.0,
|
||||
bounds: tuple[float, float, float, float] | None = None,
|
||||
plane: str = "XY",
|
||||
) -> list[tuple[float, float, float]]:
|
||||
"""
|
||||
Extracts circular bore / hole features (center_x, center_y, diameter_mm) from a SlicingObject
|
||||
|
|
@ -511,6 +513,7 @@ def render_sa2_measures_pdf(
|
|||
draw_contour: bool = False,
|
||||
draw_crease: bool = False,
|
||||
crease_angle: float = 30.0,
|
||||
plane: str = "XY",
|
||||
) -> None:
|
||||
"""
|
||||
Renders ISO 129-1 compliant SA2 hole measurements (bores, concentric steps, Z-depth, pitch) on PDF stream.
|
||||
|
|
|
|||
|
|
@ -484,14 +484,18 @@ class PDFLegendElement(PDFElement):
|
|||
|
||||
# Generate legend 3D SlicePaths & label metadata
|
||||
legend_sp, label_meta = generate_legend_slice_paths(
|
||||
exp.local_min_x,
|
||||
exp.local_min_y,
|
||||
exp.local_max_x,
|
||||
exp.local_min_x_3d,
|
||||
exp.local_min_y_3d,
|
||||
exp.local_max_x_3d,
|
||||
exp.main_step,
|
||||
exp.minor_step,
|
||||
exp.line_width_pt,
|
||||
exp.font_size_pt,
|
||||
z_level=getattr(exp, "local_min_z", 0.0),
|
||||
draw_x=getattr(exp, "draw_x_axis", True),
|
||||
draw_y=getattr(exp, "draw_y_axis", True),
|
||||
draw_z=getattr(exp, "draw_z_axis", False),
|
||||
local_origin_3d=getattr(exp, "legend_origin_3d", None),
|
||||
local_max_3d=getattr(exp, "legend_max_3d", None),
|
||||
)
|
||||
|
||||
# Rotate legend SlicePaths with existing transform_slice_paths
|
||||
|
|
@ -725,8 +729,22 @@ class export_mesh_to_pdf:
|
|||
raw_geometry: Any = None,
|
||||
slice_paths: Any = None,
|
||||
settings: Any = None,
|
||||
projection_plane: str | None = None,
|
||||
draw_grid_xy: bool = True,
|
||||
draw_grid_xz: bool = False,
|
||||
draw_grid_yz: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if projection_plane is not None:
|
||||
p_up = projection_plane.upper()
|
||||
draw_grid_xy = (p_up == "XY")
|
||||
draw_grid_xz = (p_up == "XZ")
|
||||
draw_grid_yz = (p_up == "YZ")
|
||||
|
||||
self.draw_grid_xy = draw_grid_xy
|
||||
self.draw_grid_xz = draw_grid_xz
|
||||
self.draw_grid_yz = draw_grid_yz
|
||||
self.projection_plane = "XY" if draw_grid_xy else ("XZ" if draw_grid_xz else "YZ")
|
||||
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
|
||||
|
|
@ -860,37 +878,77 @@ class export_mesh_to_pdf:
|
|||
l_min_x, l_max_x = float(pts_local[:, 0].min()), float(pts_local[:, 0].max())
|
||||
l_min_y, l_max_y = float(pts_local[:, 1].min()), float(pts_local[:, 1].max())
|
||||
l_min_z, l_max_z = float(pts_local[:, 2].min()), float(pts_local[:, 2].max())
|
||||
if R.T[2, 1] < -1e-6:
|
||||
l_min_z = l_max_z
|
||||
else:
|
||||
l_min_x, l_max_x, l_min_y, l_max_y = -50.0, 50.0, -50.0, 50.0
|
||||
l_min_z = 0.0
|
||||
l_min_x, l_max_x, l_min_y, l_max_y, l_min_z, l_max_z = -50.0, 50.0, -50.0, 50.0, -50.0, 50.0
|
||||
|
||||
w_geom, h_geom = max(l_max_x - l_min_x, 1.0), max(l_max_y - l_min_y, 1.0)
|
||||
if self.draw_grid_xz and not self.draw_grid_xy:
|
||||
w_geom, h_geom = max(l_max_x - l_min_x, 1.0), max(l_max_z - l_min_z, 1.0)
|
||||
elif self.draw_grid_yz and not self.draw_grid_xy and not self.draw_grid_xz:
|
||||
w_geom, h_geom = max(l_max_y - l_min_y, 1.0), max(l_max_z - l_min_z, 1.0)
|
||||
|
||||
self.local_min_z = l_min_z
|
||||
w_geom = max(l_max_x - l_min_x, 1.0)
|
||||
h_geom = max(l_max_y - l_min_y, 1.0)
|
||||
self.main_step, self.minor_step = compute_grid_steps(w_geom, h_geom)
|
||||
|
||||
self.local_min_x = math.floor((l_min_x - self.minor_step) / self.minor_step) * self.minor_step
|
||||
self.local_max_x = math.ceil((l_max_x + self.minor_step) / self.minor_step) * self.minor_step
|
||||
self.local_min_y = math.floor((l_min_y - self.minor_step) / self.minor_step) * self.minor_step
|
||||
self.local_max_y = math.ceil((l_max_y + self.minor_step) / self.minor_step) * self.minor_step
|
||||
self.grid_min_x = math.floor((l_min_x - self.minor_step) / self.minor_step) * self.minor_step
|
||||
self.grid_max_x = math.ceil((l_max_x + self.minor_step) / self.minor_step) * self.minor_step
|
||||
self.grid_min_y = math.floor((l_min_y - self.minor_step) / self.minor_step) * self.minor_step
|
||||
self.grid_max_y = math.ceil((l_max_y + self.minor_step) / self.minor_step) * self.minor_step
|
||||
|
||||
# Object sits on XY grid: exact min Z (no margin below), margin over object on max Z
|
||||
self.grid_min_z = l_min_z
|
||||
self.grid_max_z = math.ceil((l_max_z + self.minor_step) / self.minor_step) * self.minor_step
|
||||
|
||||
# Generate 3D grid line SlicePaths sitting on local XY plane at the bottom of the object (Z=l_min_z)
|
||||
minor_grid_sp, main_grid_sp = generate_grid_slice_paths(
|
||||
self.local_min_x,
|
||||
self.local_max_x,
|
||||
self.local_min_y,
|
||||
self.local_max_y,
|
||||
self.minor_step,
|
||||
z_level=l_min_z,
|
||||
)
|
||||
self.local_min_x_3d = self.grid_min_x
|
||||
self.local_max_x_3d = self.grid_max_x
|
||||
self.local_min_y_3d = self.grid_min_y
|
||||
self.local_max_y_3d = self.grid_max_y
|
||||
self.local_min_z_3d = self.grid_min_z
|
||||
self.local_max_z_3d = self.grid_max_z
|
||||
|
||||
# Shared origin point (min X, max Y, min Z)
|
||||
x0 = self.grid_min_x
|
||||
y0 = self.grid_max_y
|
||||
z0 = self.grid_min_z
|
||||
|
||||
self.legend_origin_3d = (x0, y0, z0)
|
||||
self.legend_max_3d = (self.grid_max_x, self.grid_min_y, self.grid_max_z)
|
||||
|
||||
self.draw_x_axis = self.draw_grid_xy or self.draw_grid_xz
|
||||
self.draw_y_axis = self.draw_grid_xy or self.draw_grid_yz
|
||||
self.draw_z_axis = self.draw_grid_xz or self.draw_grid_yz
|
||||
|
||||
self.local_min_x = self.grid_min_x
|
||||
self.local_max_x = self.grid_max_x
|
||||
self.local_min_y = self.grid_min_y
|
||||
self.local_max_y = self.grid_max_y
|
||||
self.local_min_z = z0
|
||||
|
||||
grid_minor_paths: list[NDArray[np.float64]] = []
|
||||
grid_main_paths: list[NDArray[np.float64]] = []
|
||||
|
||||
if self.draw_grid_xy:
|
||||
min_sp, main_sp = generate_grid_slice_paths(self.grid_min_x, self.grid_max_x, self.grid_min_y, self.grid_max_y, self.minor_step, z_level=z0, plane="XY")
|
||||
grid_minor_paths.extend(min_sp)
|
||||
grid_main_paths.extend(main_sp)
|
||||
|
||||
if self.draw_grid_xz:
|
||||
min_sp, main_sp = generate_grid_slice_paths(self.grid_min_x, self.grid_max_x, self.grid_min_z, self.grid_max_z, self.minor_step, z_level=y0, plane="XZ")
|
||||
grid_minor_paths.extend(min_sp)
|
||||
grid_main_paths.extend(main_sp)
|
||||
|
||||
if self.draw_grid_yz:
|
||||
min_sp, main_sp = generate_grid_slice_paths(self.grid_min_y, self.grid_max_y, self.grid_min_z, self.grid_max_z, self.minor_step, z_level=x0, plane="YZ")
|
||||
grid_minor_paths.extend(min_sp)
|
||||
grid_main_paths.extend(main_sp)
|
||||
|
||||
from ..slice import SlicePaths
|
||||
minor_grid_sp = SlicePaths(grid_minor_paths)
|
||||
main_grid_sp = SlicePaths(grid_main_paths)
|
||||
|
||||
# Rotate grid SlicePaths back into world space
|
||||
rot_minor_sp = transform_slice_paths(minor_grid_sp, np.zeros(3), r_arr, np.ones(3))
|
||||
rot_main_sp = transform_slice_paths(main_grid_sp, np.zeros(3), r_arr, np.ones(3))
|
||||
|
||||
# Extract 2D top-view edge segments from rotated SlicePaths
|
||||
minor_edges_2d = extract_slice_paths_edges(rot_minor_sp)
|
||||
main_edges_2d = extract_slice_paths_edges(rot_main_sp)
|
||||
|
||||
|
|
|
|||
|
|
@ -1687,6 +1687,61 @@ class TestPdfExporter(unittest.TestCase):
|
|||
self.assertGreater(len(minor_sp), 0)
|
||||
self.assertAlmostEqual(float(minor_sp[0][0, 2]), 15.0)
|
||||
|
||||
def test_projection_plane_selection_xz_and_yz(self):
|
||||
from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject
|
||||
from NodeSlicer.core.nodeslicer.nodes.output import SLICENODE_MeshToPdf
|
||||
|
||||
so = SlicingObject()
|
||||
so.name = "Box3D"
|
||||
# 3D triangle spanning X: [10, 50], Y: [20, 80], Z: [5, 40]
|
||||
tri = np.array([[10.0, 20.0, 5.0], [50.0, 20.0, 5.0], [10.0, 80.0, 40.0]], dtype=np.float64)
|
||||
so.mesh = np.array([tri])
|
||||
|
||||
# Default XY plane
|
||||
exp_xy = export_mesh_to_pdf(raw_geometry=so, draw_grid_xy=True, draw_grid_xz=False, draw_grid_yz=False)
|
||||
self.assertTrue(exp_xy.draw_grid_xy)
|
||||
self.assertFalse(exp_xy.draw_grid_xz)
|
||||
self.assertGreater(len(exp_xy.minor_lines_arr) + len(exp_xy.main_lines_arr), 0)
|
||||
|
||||
# Front view XZ plane
|
||||
exp_xz = export_mesh_to_pdf(raw_geometry=so, draw_grid_xy=False, draw_grid_xz=True, draw_grid_yz=False)
|
||||
self.assertTrue(exp_xz.draw_grid_xz)
|
||||
self.assertGreater(len(exp_xz.minor_lines_arr) + len(exp_xz.main_lines_arr), 0)
|
||||
|
||||
# Side view YZ plane
|
||||
exp_yz = export_mesh_to_pdf(raw_geometry=so, draw_grid_xy=False, draw_grid_xz=False, draw_grid_yz=True)
|
||||
self.assertTrue(exp_yz.draw_grid_yz)
|
||||
self.assertGreater(len(exp_yz.minor_lines_arr) + len(exp_yz.main_lines_arr), 0)
|
||||
|
||||
# Multi-grid plane selection (XY + YZ)
|
||||
exp_multi = export_mesh_to_pdf(raw_geometry=so, draw_grid_xy=True, draw_grid_xz=False, draw_grid_yz=True)
|
||||
self.assertTrue(exp_multi.draw_grid_xy)
|
||||
self.assertTrue(exp_multi.draw_grid_yz)
|
||||
self.assertTrue(exp_multi.draw_x_axis)
|
||||
self.assertTrue(exp_multi.draw_y_axis)
|
||||
self.assertTrue(exp_multi.draw_z_axis)
|
||||
|
||||
# Rotated object by X = 90 deg with XZ front grid
|
||||
import math
|
||||
so_rot = SlicingObject()
|
||||
so_rot.name = "RotatedFrontBox"
|
||||
so_rot.transforms = [{"rotation": [math.pi / 2.0, 0.0, 0.0]}]
|
||||
tri_rot = np.array([[0.0, 0.0, 0.0], [100.0, 0.0, 0.0], [0.0, 50.0, 0.0]], dtype=np.float64)
|
||||
so_rot.mesh = np.array([tri_rot])
|
||||
|
||||
exp_rot_front = export_mesh_to_pdf(raw_geometry=so_rot, draw_grid_xy=False, draw_grid_xz=True, draw_grid_yz=False)
|
||||
self.assertTrue(exp_rot_front.draw_grid_xz)
|
||||
self.assertGreater(len(exp_rot_front.minor_lines_arr) + len(exp_rot_front.main_lines_arr), 0)
|
||||
|
||||
# Node check
|
||||
node = SLICENODE_MeshToPdf()
|
||||
self.assertIn("draw_grid_xy", SLICENODE_MeshToPdf.__annotations__)
|
||||
self.assertIn("draw_grid_xz", SLICENODE_MeshToPdf.__annotations__)
|
||||
self.assertIn("draw_grid_yz", SLICENODE_MeshToPdf.__annotations__)
|
||||
self.assertTrue(getattr(node, "draw_grid_xy", True))
|
||||
self.assertFalse(getattr(node, "draw_grid_xz", False))
|
||||
self.assertFalse(getattr(node, "draw_grid_yz", False))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue