This commit is contained in:
Nicolas Predella 2026-08-31 15:00:07 +03:00
parent 926da63dd8
commit bcf9334d78
3 changed files with 147 additions and 23 deletions

View file

@ -269,6 +269,10 @@ def generate_legend_slice_paths(
legend_paths: list[NDArray[np.float64]] = []
label_meta: list[dict[str, Any]] = []
p_up = plane.upper()
is_xz = (p_up == "XZ") or (draw_z and not draw_y)
is_yz = (p_up == "YZ") or (draw_z and not draw_x)
# 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
@ -278,23 +282,45 @@ def generate_legend_slice_paths(
x_arrow_tip = x_arrow_base + arrow_len_mm
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))
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))
if is_xz:
# On XZ grid plane (constant y=y0, ticks & wings extend in Z direction)
legend_paths.append(np.array([[x_arrow_base, y0, z0 + arrow_half_w_mm], [x_arrow_tip, y0, z0]], dtype=np.float64))
legend_paths.append(np.array([[x_arrow_tip, y0, z0], [x_arrow_base, y0, z0 - arrow_half_w_mm]], 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": (xk, y0 - tick_h - label_margin_mm, z0),
})
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, z0 + tick_h]], 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": (xk, y0, z0 + tick_h + label_margin_mm),
})
else:
# On XY grid plane (constant z=z0, ticks & wings extend in -Y direction)
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))
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))
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",
@ -311,14 +337,16 @@ def generate_legend_slice_paths(
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:
if is_yz:
# On YZ grid plane (constant x=x0, ticks & wings extend in Z direction)
legend_paths.append(np.array([[x0, y_arrow_base, z0 - arrow_half_w_mm], [x0, y_arrow_tip, z0]], dtype=np.float64))
legend_paths.append(np.array([[x0, y_arrow_tip, z0], [x0, y_arrow_base, z0 + arrow_half_w_mm]], dtype=np.float64))
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))
legend_paths.append(np.array([[x0, yk, z0], [x0, yk, z0 + tick_h]], dtype=np.float64))
if k > 0:
val_mm = k * main_step
@ -327,8 +355,28 @@ def generate_legend_slice_paths(
label_meta.append({
"type": "tick",
"text": val_str,
"local_pt": (x0 + tick_h + label_margin_mm, yk, z0),
"local_pt": (x0, yk, z0 + tick_h + label_margin_mm),
})
else:
# On XY grid plane (constant z=z0, ticks & wings extend in X direction)
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",
@ -345,10 +393,12 @@ def generate_legend_slice_paths(
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:
if is_xz or draw_x:
# On XZ grid plane (constant y=y0, ticks & wings extend in X direction)
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))
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
@ -363,6 +413,25 @@ def generate_legend_slice_paths(
"text": val_str,
"local_pt": (x0 + tick_h + label_margin_mm, y0, zk),
})
else:
# On YZ grid plane (constant x=x0, ticks & wings extend in Y direction)
legend_paths.append(np.array([[x0, y0 - arrow_half_w_mm, z_arrow_base], [x0, y0, z_arrow_tip]], dtype=np.float64))
legend_paths.append(np.array([[x0, y0, z_arrow_tip], [x0, y0 + arrow_half_w_mm, z_arrow_base]], dtype=np.float64))
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, y0 - tick_h, 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, y0 - tick_h - label_margin_mm, zk),
})
label_meta.append({
"type": "z_axis",

View file

@ -586,6 +586,7 @@ class PDFLegendElement(PDFElement):
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),
plane=getattr(exp, "projection_plane", "XY"),
)
# Rotate legend SlicePaths with existing transform_slice_paths
@ -629,6 +630,8 @@ class PDFLegendElement(PDFElement):
s.text_position(pt_pdf[0], pt_pdf[1] - (exp.font_size_pt * 0.35)).show(txt)
elif meta["type"] == "y_axis":
s.text_position(pt_pdf[0] - (exp.font_size_pt * 0.35), pt_pdf[1]).show(txt)
elif meta["type"] == "z_axis":
s.text_position(pt_pdf[0] - (exp.font_size_pt * 0.35), pt_pdf[1]).show(txt)
s.end_text()

View file

@ -1877,6 +1877,58 @@ class TestPdfExporter(unittest.TestCase):
self.assertTrue(generated_name.endswith(f"-{today_str}.pdf"))
self.assertIn("x", generated_name)
def test_generate_legend_slice_paths_xz_plane(self):
from NodeSlicer.core.pdf_exporter.geometry import generate_legend_slice_paths
from NodeSlicer.core.slice import SlicePaths
legend_sp, label_meta = generate_legend_slice_paths(
grid_min_x=0.0,
grid_min_y=0.0,
grid_max_x=50.0,
main_step=10.0,
minor_step=1.0,
line_width_pt=0.5,
font_size_pt=3.0,
plane="XZ",
draw_x=True,
draw_y=False,
draw_z=True,
local_origin_3d=(0.0, 0.0, 0.0),
local_max_3d=(50.0, 0.0, 50.0),
)
self.assertIsInstance(legend_sp, SlicePaths)
self.assertGreater(len(legend_sp), 0)
# Verify that all 3D line endpoints have constant y=0 (i.e. strictly in the XZ plane)
for path in legend_sp:
p_arr = np.asarray(path, dtype=np.float64)
np.testing.assert_allclose(p_arr[:, 1], 0.0, atol=1e-6, err_msg="Legend geometry for XZ plane must have Y=0")
# Verify that ticks on the X axis span in Z
x_ticks_in_z = [
path for path in legend_sp
if len(path) == 2 and abs(path[0, 0] - path[1, 0]) < 1e-6 and abs(path[0, 2] - path[1, 2]) > 0.1
]
self.assertGreater(len(x_ticks_in_z), 0, "X axis ticks must have height along Z on XZ plane")
# Verify that ticks on the Z axis span in X
z_ticks_in_x = [
path for path in legend_sp
if len(path) == 2 and abs(path[0, 2] - path[1, 2]) < 1e-6 and abs(path[0, 0] - path[1, 0]) > 0.1
]
self.assertGreater(len(z_ticks_in_x), 0, "Z axis ticks must have height along X on XZ plane")
# Verify labels
types = [m["type"] for m in label_meta]
self.assertIn("tick", types)
self.assertIn("x_axis", types)
self.assertIn("z_axis", types)
# Verify label positions are in XZ plane (Y=0)
for m in label_meta:
self.assertAlmostEqual(m["local_pt"][1], 0.0, places=5)
if __name__ == '__main__':
unittest.main()