Align extend gizmo arrow with segment axis

The extend icon used a pure screen-space billboard that always
pointed +X across the screen — the arrow ran horizontally
regardless of the pipe / duct's orientation. The new
billboarded_along_axis helper rotates the gizmo about the camera-
forward axis so its local +X aligns with the segment's local +Z
projected onto the screen, keeping the icon camera-facing but
visually following the extrusion direction. The flip-mirror branch
now reads from cursor-vs-current-end along the segment axis (not
screen-X), so the arrow points away from the current endpoint
regardless of viewport orientation. The split icon stacks
perpendicular to the rotated extend arrow in screen space so the
two don't overlap.

The decorator's green preview line no longer clamps the cursor
projection to min_projected_length — it follows the raw projection
so the line stays visible when the cursor crosses behind the
segment origin (the user still sees where they're pointing even
though the operator floors the actual commit).

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-09 11:51:37 +02:00
parent becbcfdfe7
commit d7dd8ecf57
4 changed files with 68 additions and 31 deletions
@@ -332,7 +332,6 @@ def test_extend_preview_line_returns_none_for_degenerate_segment():
matrix_world=Matrix.Identity(4),
cursor_world=Vector((0.0, 0.0, 1.0)),
current_length=0.0,
min_projected_length=0.01,
)
assert result is None
@@ -346,7 +345,6 @@ def test_extend_preview_line_returns_none_when_cursor_at_current_end():
matrix_world=Matrix.Identity(4),
cursor_world=Vector((0.0, 0.0, 1.5)),
current_length=1.5,
min_projected_length=0.01,
)
assert result is None
@@ -361,7 +359,6 @@ def test_extend_preview_line_renders_extension_when_cursor_past_end():
matrix_world=Matrix.Identity(4),
cursor_world=Vector((0.0, 0.0, 3.0)),
current_length=1.0,
min_projected_length=0.01,
)
assert result is not None
start, end = result
@@ -378,7 +375,6 @@ def test_extend_preview_line_renders_trim_when_cursor_inside_segment():
matrix_world=Matrix.Identity(4),
cursor_world=Vector((0.0, 0.0, 0.4)),
current_length=1.0,
min_projected_length=0.01,
)
assert result is not None
start, end = result
@@ -386,23 +382,23 @@ def test_extend_preview_line_renders_trim_when_cursor_inside_segment():
assert tuple(end) == pytest.approx((0.0, 0.0, 0.4))
def test_extend_preview_line_clamps_cursor_projection_to_minimum():
"""When the cursor's projected Z is negative (behind segment origin) or
near zero, the extend operator clamps to ``min_projected_length``. The
preview must match the same clamp so the line lands where the operator
would actually commit, not at the raw cursor position."""
def test_extend_preview_line_follows_raw_projection_behind_segment_origin():
"""When the cursor's projected Z is negative (behind segment origin),
the preview line must follow the raw cursor projection — the user is
pointing somewhere and expects to see where, even though the operator
would floor the actual commit. Matching the operator's clamp would
hide the line whenever the cursor crossed the segment origin."""
from bonsai.bim.module.model.decorator import MEPSegmentExtendPreviewDecorator
result = MEPSegmentExtendPreviewDecorator._compute_extend_preview_line(
matrix_world=Matrix.Identity(4),
cursor_world=Vector((0.0, 0.0, -2.0)),
current_length=1.0,
min_projected_length=0.01,
)
assert result is not None
start, end = result
assert tuple(start) == pytest.approx((0.0, 0.0, 1.0))
assert tuple(end) == pytest.approx((0.0, 0.0, 0.01))
assert tuple(end) == pytest.approx((0.0, 0.0, -2.0))
def test_extend_preview_line_respects_object_rotation():
@@ -418,7 +414,6 @@ def test_extend_preview_line_respects_object_rotation():
matrix_world=rotation,
cursor_world=Vector((3.0, 0.0, 0.0)),
current_length=1.0,
min_projected_length=0.01,
)
assert result is not None
start, end = result