mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
Show wall cursor gizmos outside edit mode + axis previews
Four concerns that together make the cursor-anchored gizmos
(extend_x_gizmo, extend_z_gizmo, split_gizmo on GizmoWallEdition)
fully functional and visually informative without entering parametric
edit mode first:
* Drop the props.is_editing gate in _update_cursor_gizmos. The three
bound operators (bim.extend_wall_to_cursor,
bim.extend_wall_height_to_cursor, bim.split_wall_at_cursor) already
poll on wall-selected and commit any pending wall edit before
acting, so single-click without entering edit mode is now the
canonical flow. Matches gizmos-8088's always-on behaviour.
* Register GizmoWallEdition instances in a per-region weakref map
(_active_instances) populated at setup_element_specific_gizmos
time. The WallGizmoPreviewDecorator dereferences this map to read
live is_highlight state off the cursor icons. Without the
registration its _cursor_icon_hovered always returned False and
the hover-gated GPU previews silently never drew. Mirrors the
same pattern already in place on GizmoWallJoinIntersection.
* Add post-operator resync to all three cursor operators
(_maybe_resync_wall_props_from_ifc for the single-wall split /
extend-height paths, _resync_walls_after_mutation for the
selection-wide extend-X path). Without this, props.length /
props.height stayed stale after the operator ran, so the
orientation flips _apply_wall_extend_flips computes from
cursor_local vs wall dimensions kept using the pre-extend values
until the next selection change. Matches gizmos-8088's pattern.
* Hover-gated GPU previews per icon:
- extend-X: filled Z=0 floor quads spanning the wall's offset to
offset+thickness Y band, visible from plan view without side-
view clutter. Grow case (cursor beyond either endpoint): one
green decorator_color_selected quad over the extension. Shrink
case (cursor inside extent): green quad for the portion that
REMAINS + red decorator_color_error quad for the portion the
operator REMOVES.
- extend-Z: vertical lines at the cursor's projected X in the
wall's y=0 reference-line plane. Grow case (cursor above wall
top): one green segment from z=height to z=cursor.z. Shrink
case: green from z=0 to z=cursor.z (REMAINS) + red from
z=cursor.z to z=height (REMOVES).
- split: one red vertical line at the cursor's projected X from
base to wall top — the cut plane.
Quads use QUAD_ALPHA=0.25 so the underlying wall body stays
visible.
* New module-level _fill_quads_alpha helper next to
_stroke_lines_alpha, plus a per-decorator _fill convenience method
and a _wall_floor_quad corner builder.
Modal-active gizmo hiding (is_gizmo_hidden_by_modal) is preserved.
Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -2069,6 +2069,47 @@ def _stroke_lines_alpha(
|
||||
gpu.state.blend_set("NONE")
|
||||
|
||||
|
||||
def _fill_quads_alpha(
|
||||
context: bpy.types.Context,
|
||||
quads: list[
|
||||
tuple[
|
||||
tuple[float, float, float],
|
||||
tuple[float, float, float],
|
||||
tuple[float, float, float],
|
||||
tuple[float, float, float],
|
||||
]
|
||||
],
|
||||
color_rgb: tuple[float, float, float],
|
||||
alpha: float,
|
||||
) -> None:
|
||||
"""Render ``quads`` (each a 4-tuple of world-space corner verts in CCW
|
||||
order) as one TRIS batch with two triangles per quad. Companion to
|
||||
``_stroke_lines_alpha`` for filled previews."""
|
||||
if not quads:
|
||||
return
|
||||
verts: list[tuple[float, float, float]] = []
|
||||
indices: list[tuple[int, int, int]] = []
|
||||
for quad in quads:
|
||||
if len(quad) != 4:
|
||||
continue
|
||||
base = len(verts)
|
||||
verts.extend(tuple(v) for v in quad)
|
||||
indices.append((base, base + 1, base + 2))
|
||||
indices.append((base, base + 2, base + 3))
|
||||
if not tool.Blender.validate_shader_batch_data(verts, indices):
|
||||
return
|
||||
region = getattr(context, "region", None)
|
||||
if region is None:
|
||||
return
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
shader.bind()
|
||||
shader.uniform_float("color", (*color_rgb, alpha))
|
||||
batch = batch_for_shader(shader, "TRIS", {"pos": verts}, indices=indices)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
batch.draw(shader)
|
||||
gpu.state.blend_set("NONE")
|
||||
|
||||
|
||||
class WallFilletPreviewDecorator(tool.Blender.ViewportDecorator):
|
||||
"""GPU preview lines for the wall-fillet flow.
|
||||
|
||||
@@ -2543,6 +2584,9 @@ class WallGizmoPreviewDecorator(tool.Blender.ViewportDecorator):
|
||||
|
||||
LINE_WIDTH = 1.5
|
||||
LINE_ALPHA = 0.8
|
||||
# Semi-transparent so the wall body and surrounding geometry stay visible
|
||||
# under the preview quads.
|
||||
QUAD_ALPHA = 0.25
|
||||
|
||||
def draw_lines(self, context: bpy.types.Context) -> None:
|
||||
if not tool.Blender.are_viewport_gizmos_enabled():
|
||||
@@ -2563,6 +2607,38 @@ class WallGizmoPreviewDecorator(tool.Blender.ViewportDecorator):
|
||||
) -> None:
|
||||
_stroke_lines_alpha(context, segments, color_rgb, self.LINE_WIDTH, self.LINE_ALPHA)
|
||||
|
||||
def _fill(
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
quads: list[
|
||||
tuple[
|
||||
tuple[float, float, float],
|
||||
tuple[float, float, float],
|
||||
tuple[float, float, float],
|
||||
tuple[float, float, float],
|
||||
]
|
||||
],
|
||||
color_rgb: tuple[float, float, float],
|
||||
) -> None:
|
||||
_fill_quads_alpha(context, quads, color_rgb, self.QUAD_ALPHA)
|
||||
|
||||
@staticmethod
|
||||
def _wall_floor_quad(mw: Matrix, x0: float, x1: float, y0: float, y1: float) -> tuple[
|
||||
tuple[float, float, float],
|
||||
tuple[float, float, float],
|
||||
tuple[float, float, float],
|
||||
tuple[float, float, float],
|
||||
]:
|
||||
"""4 world-space corners of a Z=0 wall-local rectangle, CCW when
|
||||
viewed from +Z. Used for top-down floor-projection quads so the
|
||||
extend / split previews stay legible from plan view."""
|
||||
return (
|
||||
tuple(mw @ Vector((x0, y0, 0.0))),
|
||||
tuple(mw @ Vector((x1, y0, 0.0))),
|
||||
tuple(mw @ Vector((x1, y1, 0.0))),
|
||||
tuple(mw @ Vector((x0, y1, 0.0))),
|
||||
)
|
||||
|
||||
def _draw_join_preview(self, context: bpy.types.Context, prefs: Any) -> None:
|
||||
"""Render four preview lines per wall pair — two at each wall's base
|
||||
Z, two at each wall's top Z — extending each axis to the projected
|
||||
@@ -2710,11 +2786,18 @@ class WallGizmoPreviewDecorator(tool.Blender.ViewportDecorator):
|
||||
return active
|
||||
|
||||
def _draw_cursor_extend_preview(self, context: bpy.types.Context, prefs: Any) -> None:
|
||||
"""Render the extend-X preview line only while the cursor-anchored
|
||||
extend-X icon gizmo is hovered. The line runs from the wall's nearer
|
||||
axis endpoint to the cursor's projected X on the wall axis, both
|
||||
clamped to wall-local Y=0 Z=0 so the line stays on the wall's floor
|
||||
edge regardless of cursor Z."""
|
||||
"""Hover-gated floor-plane preview for the extend-X icon. Quads sit
|
||||
on the Z=0 plane spanning the wall's ``offset`` to
|
||||
``offset + thickness`` Y band so the operator's effect reads from
|
||||
plan view without side-view clutter:
|
||||
|
||||
- **Cursor outside ``[anchor_x, anchor_x+length]`` (grow)**: one
|
||||
green ``decorator_color_selected`` quad over the extension
|
||||
(nearer endpoint → cursor X).
|
||||
- **Cursor inside the wall extent (shrink)**: green quad for the
|
||||
portion that REMAINS (cursor X → farther endpoint) + red
|
||||
``decorator_color_error`` quad for the portion the operator
|
||||
REMOVES (nearer endpoint → cursor X)."""
|
||||
active = self._active_layer2_wall_for_gizmo_preview(context, prefs)
|
||||
if active is None:
|
||||
return
|
||||
@@ -2727,21 +2810,35 @@ class WallGizmoPreviewDecorator(tool.Blender.ViewportDecorator):
|
||||
return
|
||||
anchor_x = geom.get("anchor_x", 0.0)
|
||||
length = geom.get("length", 0.0)
|
||||
if length <= 0:
|
||||
offset = geom.get("offset", 0.0)
|
||||
thickness = geom.get("thickness", 0.0)
|
||||
if length <= 0 or thickness <= 0:
|
||||
return
|
||||
mw = active.matrix_world
|
||||
cursor_local = mw.inverted() @ context.scene.cursor.location
|
||||
y_floor_0 = offset
|
||||
y_floor_1 = offset + thickness
|
||||
start_x = anchor_x
|
||||
end_x = anchor_x + length
|
||||
keep_color = tuple(prefs.decorator_color_selected[:3])
|
||||
nearest_x = start_x if abs(cursor_local.x - start_x) < abs(cursor_local.x - end_x) else end_x
|
||||
start_world = mw @ Vector((nearest_x, 0.0, 0.0))
|
||||
end_world = mw @ Vector((cursor_local.x, 0.0, 0.0))
|
||||
if (end_world - start_world).length < 1e-6:
|
||||
|
||||
def emit(x0: float, x1: float, color: tuple[float, float, float]) -> None:
|
||||
if abs(x1 - x0) < 1e-6:
|
||||
return
|
||||
lo, hi = (x0, x1) if x0 < x1 else (x1, x0)
|
||||
self._fill(context, [self._wall_floor_quad(mw, lo, hi, y_floor_0, y_floor_1)], color)
|
||||
|
||||
if start_x < cursor_local.x < end_x:
|
||||
remove_color = tuple(prefs.decorator_color_error[:3])
|
||||
farthest_x = end_x if nearest_x == start_x else start_x
|
||||
emit(nearest_x, cursor_local.x, remove_color)
|
||||
emit(cursor_local.x, farthest_x, keep_color)
|
||||
return
|
||||
self._stroke(context, [(tuple(start_world), tuple(end_world))], tuple(prefs.decorator_color_selected[:3]))
|
||||
emit(nearest_x, cursor_local.x, keep_color)
|
||||
|
||||
def _draw_cursor_split_preview(self, context: bpy.types.Context, prefs: Any) -> None:
|
||||
"""Render one line at the cursor's projected X, from wall base to wall top
|
||||
"""Render one red line at the cursor's projected X, from wall base to wall top
|
||||
along the wall's local Z — the cut plane the split operator would commit.
|
||||
Hover-gated on the split icon; coloured with the destructive-action warning
|
||||
red to match the icon's own hover signal."""
|
||||
@@ -2769,9 +2866,18 @@ class WallGizmoPreviewDecorator(tool.Blender.ViewportDecorator):
|
||||
self._stroke(context, [(tuple(bottom_world), tuple(top_world))], tuple(prefs.decorator_color_error[:3]))
|
||||
|
||||
def _draw_cursor_extend_z_preview(self, context: bpy.types.Context, prefs: Any) -> None:
|
||||
"""Render one preview line at the cursor's projected X on the wall axis,
|
||||
from the wall base to the gizmo's local Z — the new total height the
|
||||
extend-Z operator would commit. Hover-gated on the extend-Z icon."""
|
||||
"""Hover-gated vertical-line preview for the extend-Z icon at the
|
||||
cursor's projected X on the wall axis (y=0 reference-line plane).
|
||||
|
||||
Two cases by cursor Z relative to the wall's current height:
|
||||
|
||||
- **Cursor Z above the wall top (grow)**: one green
|
||||
``decorator_color_selected`` segment from z=height to z=cursor.z
|
||||
(the new vertical material).
|
||||
- **Cursor Z inside ``(0, height)`` (shrink)**: two segments —
|
||||
green from z=0 to z=cursor.z (the portion that REMAINS), red
|
||||
``decorator_color_error`` from z=cursor.z to z=height (the
|
||||
portion the operator REMOVES)."""
|
||||
active = self._active_layer2_wall_for_gizmo_preview(context, prefs)
|
||||
if active is None:
|
||||
return
|
||||
@@ -2793,6 +2899,17 @@ class WallGizmoPreviewDecorator(tool.Blender.ViewportDecorator):
|
||||
return
|
||||
if abs(cursor_local.z - height) < 1e-6:
|
||||
return
|
||||
base_world = mw @ Vector((cursor_local.x, 0.0, 0.0))
|
||||
top_world = mw @ Vector((cursor_local.x, 0.0, cursor_local.z))
|
||||
self._stroke(context, [(tuple(base_world), tuple(top_world))], tuple(prefs.decorator_color_selected[:3]))
|
||||
keep_color = tuple(prefs.decorator_color_selected[:3])
|
||||
cursor_x = cursor_local.x
|
||||
|
||||
def stroke(z0: float, z1: float, color: tuple[float, float, float]) -> None:
|
||||
a = mw @ Vector((cursor_x, 0.0, z0))
|
||||
b = mw @ Vector((cursor_x, 0.0, z1))
|
||||
self._stroke(context, [(tuple(a), tuple(b))], color)
|
||||
|
||||
if cursor_local.z > height:
|
||||
stroke(height, cursor_local.z, keep_color)
|
||||
return
|
||||
remove_color = tuple(prefs.decorator_color_error[:3])
|
||||
stroke(0.0, cursor_local.z, keep_color)
|
||||
stroke(cursor_local.z, height, remove_color)
|
||||
|
||||
@@ -2011,6 +2011,13 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
(0, 0, 1),
|
||||
)
|
||||
|
||||
# Per-region weakref map populated in ``setup_element_specific_gizmos``.
|
||||
# The ``WallGizmoPreviewDecorator`` dereferences this each draw to read
|
||||
# live ``is_highlight`` state off the cursor icons (extend-X / extend-Z
|
||||
# / split) in the same region it's currently drawing in, so the GPU
|
||||
# axis-preview lines only render while the matching icon is hovered.
|
||||
_active_instances: ClassVar["dict[int, weakref.ReferenceType[GizmoWallEdition]]"] = {}
|
||||
|
||||
# Row layout: validate / cancel / baseline-triplet / rotate / array.
|
||||
# Wall has no ``cycle_type_operator``, so the cycle slot collapses and
|
||||
# the baseline triplet takes the cycle X position (0.87). Rotate
|
||||
@@ -2073,6 +2080,8 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
highlight_color,
|
||||
)
|
||||
self.setup_pen_row_toggle_openings_icon()
|
||||
if context.region is not None:
|
||||
type(self)._active_instances[context.region.as_pointer()] = weakref.ref(self)
|
||||
|
||||
def _refresh_element_specific(self, context: bpy.types.Context, mw: Matrix, props: "BIMWallProperties") -> None:
|
||||
"""Position cursor-anchored gizmos and the wall-specific icon-row extras."""
|
||||
@@ -2087,6 +2096,14 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
"""Position the cursor-anchored icons (extend-X / extend-Z / split) on the wall
|
||||
axis at the cursor's projected X.
|
||||
|
||||
Always visible when a parametric wall is selected — not gated on edit
|
||||
mode. The three operators (``bim.extend_wall_to_cursor`` /
|
||||
``bim.extend_wall_height_to_cursor`` / ``bim.split_wall_at_cursor``)
|
||||
all poll on wall-selected and commit any pending wall edit before
|
||||
acting, so single-click without entering edit mode is the canonical
|
||||
flow. The ``WallGizmoPreviewDecorator`` keeps the viewport clean by
|
||||
only drawing the action's guide line on hover.
|
||||
|
||||
Two branches by view orientation:
|
||||
|
||||
- **Non-top-down**: world-Z stacking. Each icon sits at the Z its
|
||||
@@ -2101,10 +2118,6 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
if not hasattr(self, "split_gizmo"):
|
||||
return
|
||||
all_gizmos = (self.extend_x_gizmo, self.extend_z_gizmo, self.split_gizmo)
|
||||
if not props.is_editing:
|
||||
for gz in all_gizmos:
|
||||
gz.hide = True
|
||||
return
|
||||
cursor_world = context.scene.cursor.location
|
||||
cursor_local = mw.inverted() @ cursor_world
|
||||
in_range = props.anchor_x < cursor_local.x < props.anchor_x + props.length
|
||||
@@ -2252,9 +2265,11 @@ class SplitWallAtCursor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
# Applies any pending wall edit first so the split operates on the committed
|
||||
# geometry rather than the draft preview box.
|
||||
if _commit_active_wall_edit_if_any(context) is None:
|
||||
obj = _commit_active_wall_edit_if_any(context)
|
||||
if obj is None:
|
||||
return {"CANCELLED"}
|
||||
bpy.ops.bim.split_wall()
|
||||
_maybe_resync_wall_props_from_ifc(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -2282,6 +2297,7 @@ class ExtendWallToCursor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Model,
|
||||
context.scene.cursor.location,
|
||||
)
|
||||
_resync_walls_after_mutation(tool.Blender.get_selected_objects())
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -2313,6 +2329,7 @@ class ExtendWallHeightToCursor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"CANCELLED"}
|
||||
with bpy.context.temp_override(active_object=obj, selected_objects=[obj]):
|
||||
bpy.ops.bim.change_extrusion_depth(depth=new_height)
|
||||
_maybe_resync_wall_props_from_ifc(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user