mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
Clear wall-edit gizmos off click targets in plan view
In plan view world-Z collapses to zero on screen, so every wall-edit icon anchored on the floor — the projected 3D cursor, wall endpoints, wall-to-wall corners, IfcRelConnectsPathElements connection points — projects onto the click target it represents. The result on a typical extend / split / unjoin action: the icon sits on top of the cursor crosshair (or the corner the user wants to click), defeating precise positioning. Add shared ``gizmo.top_down_clearance(context, billboard_rot)`` to bim/module/drawing/gizmos.py: returns a screen-up Vector in top-down view (cosine cone around world Z, matching ``is_view_top_down``) and a zero Vector elsewhere, so call sites apply it unconditionally before ``billboarded_at``. Default distance 0.4 m aligns with the inter-icon stack spacing already used by GizmoWallJoinIntersection so single icons and stack bases land at consistent screen-up positions when multiple groups render around the same wall endpoint. Apply at the seven wall-edit anchor sites: * GizmoWallEdition cursor stack (top-down branch only — non-top-down already stacks along world-Z at structural points clear of the cursor). * GizmoWallExtendVertically (single icon at wall origin endpoint, active-object Z elevation). * GizmoWallJoinIntersection corner stack base + merge midpoint. * GizmoWallUnjoinSingle link-toggle pool (one icon per IFC path connection, previously sitting exactly on the connection point). * GizmoWallFilletReedit pen icon at fillet corner. * GizmoWallFilletToggleOpenings. The clearance is a pure visual offset — bound operators still read the world-space anchor (cursor / endpoint / connection point) at execute time, so the action's target is unaffected. Also tighten GizmoWallUnjoinSingle: gate poll on ``props.is_editing`` so the link-toggle icons only surface during the wall edit lifecycle (matching every other edit-row icon), and downsize them via a new ``ICON_SCALE = 0.35`` constant since 16 of them at default scale cluttered the viewport on path-heavy walls. ruff + black clean. Wall gizmos test lane 14/14 pass. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -1702,6 +1702,33 @@ def get_screen_up(billboard_rot: Matrix) -> Vector:
|
||||
return billboard_rot @ Vector((0.0, 1.0, 0.0))
|
||||
|
||||
|
||||
# Screen-up distance lifted off floor-plane gizmo anchors in plan view. Matches
|
||||
# the inter-icon stack spacing used by wall-corner stacks so single icons and
|
||||
# stack bases sit at consistent screen-up positions when multiple groups render
|
||||
# around the same wall endpoint.
|
||||
DEFAULT_TOP_DOWN_CLEARANCE = 0.4
|
||||
|
||||
|
||||
def top_down_clearance(
|
||||
context: bpy.types.Context,
|
||||
billboard_rot: Matrix,
|
||||
distance: float = DEFAULT_TOP_DOWN_CLEARANCE,
|
||||
) -> Vector:
|
||||
"""Screen-up offset that keeps a floor-plane gizmo anchor visible in plan view.
|
||||
|
||||
In a top-down view the world-Z axis projects to ~zero on screen, so any
|
||||
icon anchored on the floor (wall endpoints, corners, connection points,
|
||||
the projected 3D cursor) sits directly on the click target it represents.
|
||||
Adding this offset before ``billboarded_at`` shifts the icon along the
|
||||
camera's up axis without changing the operator's world-space target.
|
||||
|
||||
Returns a zero vector outside the top-down cone so callers can apply it
|
||||
unconditionally."""
|
||||
if not tool.Blender.is_view_top_down(context):
|
||||
return Vector((0.0, 0.0, 0.0))
|
||||
return get_screen_up(billboard_rot) * distance
|
||||
|
||||
|
||||
# Dead-band on the screen-X delta — prevents flicker when the gizmo sits on the
|
||||
# element origin.
|
||||
EXTEND_FLIP_EPSILON = 1e-4
|
||||
|
||||
@@ -2175,11 +2175,14 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
if top_down:
|
||||
# Swap world-Z stacking for screen-up stacking so each icon stays
|
||||
# individually clickable when the camera projects world Z to zero.
|
||||
# The shared ``top_down_clearance`` lifts the whole stack off the
|
||||
# cursor so its small crosshair stays visible for precise pointing.
|
||||
screen_up = tool.Blender.get_screen_up_world(context)
|
||||
base_world = mw @ Vector((cursor_local.x, 0.0, 0.0))
|
||||
clearance = gizmo.top_down_clearance(context, billboard_rot)
|
||||
for index, (gz, _local_z) in enumerate(resolved):
|
||||
gz.hide = self.is_gizmo_hidden_by_modal(gz)
|
||||
world_pos = base_world + screen_up * (index * self.CURSOR_STACK_OFFSET)
|
||||
world_pos = base_world + clearance + screen_up * (index * self.CURSOR_STACK_OFFSET)
|
||||
gz.matrix_basis = gizmo.billboarded_at(world_pos, billboard_rot)
|
||||
_apply_wall_extend_flips(gz, self, world_pos, mw, cursor_local, props, billboard_rot)
|
||||
return
|
||||
@@ -3358,7 +3361,9 @@ class GizmoWallExtendVertically(bpy.types.GizmoGroup, _WallGeomCachedBillboardin
|
||||
# the active object's elevation — the height the wall is about to reach.
|
||||
world_pos = mw @ Vector((0.0, icon_y, 0.0))
|
||||
world_pos.z = active.matrix_world.translation.z
|
||||
self.extend_vertical_icon.matrix_basis = gizmo.billboarded_at(world_pos, gizmo.get_billboard_rotation(context))
|
||||
billboard_rot = gizmo.get_billboard_rotation(context)
|
||||
world_pos += gizmo.top_down_clearance(context, billboard_rot)
|
||||
self.extend_vertical_icon.matrix_basis = gizmo.billboarded_at(world_pos, billboard_rot)
|
||||
|
||||
|
||||
class GizmoWallJoinIntersection(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMixin):
|
||||
@@ -3463,12 +3468,13 @@ class GizmoWallJoinIntersection(bpy.types.GizmoGroup, _WallGeomCachedBillboardin
|
||||
seg_b = _wall_axis_world_segment_from_geom(selected[1], geom_b)
|
||||
billboard_rot = gizmo.get_billboard_rotation(context)
|
||||
screen_up = gizmo.get_screen_up(billboard_rot)
|
||||
clearance = gizmo.top_down_clearance(context, billboard_rot)
|
||||
anchor_z = self._stack_anchor_z(context, selected, geom_a, geom_b)
|
||||
|
||||
# State 1: walls are already joined → Unjoin (bottom) + Fillet (above).
|
||||
if _are_walls_joined(elem_a, elem_b):
|
||||
corner = _collinear_boundary_world(seg_a, seg_b)
|
||||
anchor = Vector((corner.x, corner.y, anchor_z))
|
||||
anchor = Vector((corner.x, corner.y, anchor_z)) + clearance
|
||||
self._stack_at(anchor, screen_up, billboard_rot, (self.unjoin_icon, self.fillet_icon))
|
||||
self.merge_icon.hide = True
|
||||
self.join_icon.hide = True
|
||||
@@ -3479,7 +3485,7 @@ class GizmoWallJoinIntersection(bpy.types.GizmoGroup, _WallGeomCachedBillboardin
|
||||
# at the boundary midpoint between them. No stack; single icon at the
|
||||
# geometric boundary makes the merge target unambiguous.
|
||||
if _are_walls_collinear(seg_a, seg_b, self.PARALLEL_DOT_THRESHOLD, self.COLLINEAR_LINE_TOLERANCE):
|
||||
boundary = _collinear_boundary_world(seg_a, seg_b)
|
||||
boundary = _collinear_boundary_world(seg_a, seg_b) + clearance
|
||||
self.merge_icon.matrix_basis = gizmo.billboarded_at(boundary, billboard_rot)
|
||||
self.merge_icon.hide = False
|
||||
self.unjoin_icon.hide = True
|
||||
@@ -3503,7 +3509,7 @@ class GizmoWallJoinIntersection(bpy.types.GizmoGroup, _WallGeomCachedBillboardin
|
||||
self._hide_all()
|
||||
return
|
||||
intersection = Vector(intersection_tuple)
|
||||
anchor = Vector((intersection.x, intersection.y, anchor_z))
|
||||
anchor = Vector((intersection.x, intersection.y, anchor_z)) + clearance
|
||||
self._stack_at(anchor, screen_up, billboard_rot, (self.extend_to_wall_icon, self.join_icon, self.fillet_icon))
|
||||
self.unjoin_icon.hide = True
|
||||
self.merge_icon.hide = True
|
||||
@@ -3597,6 +3603,7 @@ class GizmoWallUnjoinSingle(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMix
|
||||
# GizmoGroup to allocate gizmos inside setup() — draw_prepare / refresh-time
|
||||
# creation is forbidden — so the pool must be sized upfront for the worst case.
|
||||
POOL_SIZE = 16
|
||||
ICON_SCALE = 0.35
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
@@ -3611,6 +3618,9 @@ class GizmoWallUnjoinSingle(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMix
|
||||
element = tool.Ifc.get_entity(active)
|
||||
if not element or not tool.Parametric.is_path_connectable_wall(element):
|
||||
return False
|
||||
props = tool.Model.get_wall_props(active)
|
||||
if not props.is_editing:
|
||||
return False
|
||||
return True
|
||||
|
||||
def setup(self, context: bpy.types.Context) -> None:
|
||||
@@ -3647,6 +3657,7 @@ class GizmoWallUnjoinSingle(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMix
|
||||
return
|
||||
seg_self = _wall_axis_world_segment_from_geom(wall_obj, geom)
|
||||
billboard_rot = gizmo.get_billboard_rotation(context)
|
||||
clearance = gizmo.top_down_clearance(context, billboard_rot)
|
||||
|
||||
connections = _iter_path_connections(elem)
|
||||
if len(connections) > self.POOL_SIZE and not getattr(self, "_pool_cap_warned", False):
|
||||
@@ -3668,7 +3679,7 @@ class GizmoWallUnjoinSingle(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMix
|
||||
seg_other = _wall_axis_world_segment_from_geom(other_obj, other_geom)
|
||||
location = tool.Wall.path_connection_location_world(seg_self, self_ct, seg_other, other_ct)
|
||||
icon = self.unjoin_icons[slot_idx]
|
||||
icon.matrix_basis = gizmo.billboarded_at(location, billboard_rot)
|
||||
icon.matrix_basis = gizmo.billboarded_at(location + clearance, billboard_rot, scale=self.ICON_SCALE)
|
||||
icon.hide = False
|
||||
# Only the partner-GlobalId property is rewritten per frame; the operator
|
||||
# binding itself is the long-lived handle set up at setup() time. GlobalId
|
||||
@@ -4002,7 +4013,7 @@ class GizmoWallFilletReedit(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMix
|
||||
billboard_rot = gizmo.get_billboard_rotation(context)
|
||||
origin = corner_obj.matrix_world.translation
|
||||
top_z = origin.z + (geom.get("height") or 3.0) + self.ICON_TOP_LIFT
|
||||
anchor = Vector((origin.x, origin.y, top_z))
|
||||
anchor = Vector((origin.x, origin.y, top_z)) + gizmo.top_down_clearance(context, billboard_rot)
|
||||
self.edit_icon.matrix_basis = gizmo.billboarded_at(anchor, billboard_rot)
|
||||
self.edit_icon.hide = False
|
||||
|
||||
@@ -4064,7 +4075,7 @@ class GizmoWallFilletToggleOpenings(bpy.types.GizmoGroup, _WallGeomCachedBillboa
|
||||
billboard_rot = gizmo.get_billboard_rotation(context)
|
||||
origin = corner_obj.matrix_world.translation
|
||||
top_z = origin.z + (geom.get("height") or 3.0) + self.ICON_TOP_LIFT
|
||||
anchor = Vector((origin.x, origin.y, top_z))
|
||||
anchor = Vector((origin.x, origin.y, top_z)) + gizmo.top_down_clearance(context, billboard_rot)
|
||||
offset_x = billboard_rot @ Vector((self.ICON_OFFSET_X, 0.0, 0.0))
|
||||
self.toggle_openings_icon.matrix_basis = gizmo.billboarded_at(anchor + offset_x, billboard_rot)
|
||||
self.toggle_openings_icon.hide = False
|
||||
|
||||
Reference in New Issue
Block a user