mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
Add link-toggle hover gizmo for wall junctions
The previous single-wall unjoin gizmo used a bracket-pair icon (VIEW3D_GT_unjoin) that reads as "unjoin" only after you know what it is, with no clear "linked" inverse — closing the brackets to suggest the connected state collapses to a hollow square that doesn't read as a link at all. Add GizmoLinkToggle (VIEW3D_GT_link_toggle): two filled dots joined by a horizontal connector in the default state. On hover the two halves shear vertically apart — left dot+stub slip down as a unit, right dot+stub slip up — with a horizontal gap at the centre, signalling that a click will sever the underlying connection. The glyph lives next to the generic icon classes (GizmoLockOpen/Closed, GizmoArc) so any path / link / pair-of-connected-items context can reuse it; it isn't wall-specific despite the first caller. The class keeps its own per-state GPUBatch cache so the shape swap on hover doesn't allocate per frame. The hit-shape is sourced from the broken form (the larger bbox of the two states) so the cursor doesn't lose hover at the offset dots' outer edges and flicker between states. GizmoWallUnjoinSingle.setup() now requests VIEW3D_GT_link_toggle. The operator binding (bim.unjoin_wall_path_connection), the POOL_SIZE, and the per-frame partner-GUID write are unchanged. Generated with the assistance of an AI coding tool.
This commit is contained in:
committed by
Thomas Krijnen
parent
49fe0756fa
commit
a48f326ab2
@@ -141,6 +141,7 @@ classes = (
|
||||
gizmos.GizmoLockOpen,
|
||||
gizmos.GizmoLockClosed,
|
||||
gizmos.GizmoArc,
|
||||
gizmos.GizmoLinkToggle,
|
||||
gizmos.GizmoFillet,
|
||||
gizmos.GizmoWallCornerIcon,
|
||||
gizmos.GizmoWallTeeIcon,
|
||||
|
||||
@@ -3215,6 +3215,114 @@ class GizmoArc(StaticTrisGizmoMixin, bpy.types.Gizmo):
|
||||
tris = ARC_TRIS_DEFAULT
|
||||
|
||||
|
||||
def _link_toggle_icon_tris(broken: bool) -> tuple[tuple[float, float, float], ...]:
|
||||
"""Two filled dots joined by a horizontal connector. ``broken=False``
|
||||
draws a single continuous bar between the dots' inner edges;
|
||||
``broken=True`` shears the two halves vertically — the left dot AND
|
||||
its stub slip down as a unit, the right dot AND its stub slip up,
|
||||
with a horizontal gap at the centre.
|
||||
|
||||
Each half moves as a cohesive piece so the stub stays attached to its
|
||||
dot at the same y, reading as a snapped link whose two halves slid
|
||||
apart rather than as bent stubs jutting out of stationary dots."""
|
||||
dot_cx = 0.30
|
||||
dot_r = 0.10
|
||||
bar_half_thickness = 0.04
|
||||
# Inner edge of each dot — the intact bar joins the dot edges, not the
|
||||
# centres, so the dot + bar reads as one continuous shape.
|
||||
bar_inner_x = dot_cx - dot_r
|
||||
segments = 12
|
||||
# Vertical shear applied to each half when broken. Zero in the intact
|
||||
# form keeps both halves on the centerline.
|
||||
half_offset_y = 0.08 if broken else 0.0
|
||||
|
||||
tris: list[tuple[float, float, float]] = []
|
||||
for sign in (-1, 1):
|
||||
cx = sign * dot_cx
|
||||
cy = sign * half_offset_y
|
||||
for i in range(segments):
|
||||
a1 = (2.0 * math.pi) * (i / segments)
|
||||
a2 = (2.0 * math.pi) * ((i + 1) / segments)
|
||||
p1 = (cx + dot_r * math.cos(a1), cy + dot_r * math.sin(a1))
|
||||
p2 = (cx + dot_r * math.cos(a2), cy + dot_r * math.sin(a2))
|
||||
tris.append((cx, cy, 0.0))
|
||||
tris.append((p1[0], p1[1], 0.0))
|
||||
tris.append((p2[0], p2[1], 0.0))
|
||||
|
||||
if broken:
|
||||
# Stubs reach inward into the dot's interior so they read as rooted
|
||||
# in the dot rather than floating off its edge after the slip.
|
||||
stub_outer_x = 0.27
|
||||
stub_inner_x = 0.06
|
||||
tris.extend(
|
||||
rect_tris(
|
||||
-stub_outer_x,
|
||||
-half_offset_y - bar_half_thickness,
|
||||
-stub_inner_x,
|
||||
-half_offset_y + bar_half_thickness,
|
||||
)
|
||||
)
|
||||
tris.extend(
|
||||
rect_tris(
|
||||
stub_inner_x,
|
||||
half_offset_y - bar_half_thickness,
|
||||
stub_outer_x,
|
||||
half_offset_y + bar_half_thickness,
|
||||
)
|
||||
)
|
||||
else:
|
||||
tris.extend(rect_tris(-bar_inner_x, -bar_half_thickness, bar_inner_x, bar_half_thickness))
|
||||
|
||||
return tuple(tris)
|
||||
|
||||
|
||||
LINK_TRIS_INTACT = _link_toggle_icon_tris(broken=False)
|
||||
LINK_TRIS_BROKEN = _link_toggle_icon_tris(broken=True)
|
||||
|
||||
|
||||
class GizmoLinkToggle(StaticTrisGizmoMixin, bpy.types.Gizmo):
|
||||
"""Two-state link glyph: default reads as a connected link (two dots +
|
||||
intact connector); hover swaps to a broken link (same dots + severed
|
||||
connector) to signal that a click will sever the underlying connection.
|
||||
|
||||
Single-click gizmo — the target operator is bound via
|
||||
``target_set_operator`` by the owning group. The hover swap is purely
|
||||
visual; the click target is the same in both states. The glyph is
|
||||
feature-agnostic — any path / link / pair-of-connected-items context can
|
||||
reuse it for a sever-this-connection affordance."""
|
||||
|
||||
bl_idname = "VIEW3D_GT_link_toggle"
|
||||
__slots__ = ("custom_shape",)
|
||||
# Bbox source for the hit shape. The broken form's vertically-sheared
|
||||
# halves give it the larger bbox of the two states, so using it as the
|
||||
# hit-shape source guarantees the clickable area covers either form —
|
||||
# the cursor doesn't lose hover at the offset dots' outer edges.
|
||||
tris = LINK_TRIS_BROKEN
|
||||
|
||||
# Per-class batch cache: one entry per highlight state. The mixin parent
|
||||
# caches one batch per class via ``_get_static_tris_batch``; the per-state
|
||||
# swap needs a second batch, so this class keeps its own cache.
|
||||
_batch_cache: ClassVar[dict[bool, "gpu.types.GPUBatch"]] = {}
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
broken = bool(self.is_highlight)
|
||||
batch = type(self)._batch_cache.get(broken)
|
||||
if batch is None:
|
||||
tris = LINK_TRIS_BROKEN if broken else LINK_TRIS_INTACT
|
||||
batch = batch_for_shader(_get_static_tris_shader(), "TRIS", {"pos": tris})
|
||||
type(self)._batch_cache[broken] = batch
|
||||
# Icon body forced fully opaque so the dark outline behind doesn't
|
||||
# bleed through and grey out the glyph.
|
||||
color = (*self.color_highlight, 1.0) if broken else (*self.color, 1.0)
|
||||
draw_tris_with_outline(
|
||||
batch,
|
||||
self.matrix_basis @ self.matrix_offset,
|
||||
color,
|
||||
self.outline_width,
|
||||
self.outline_alpha,
|
||||
)
|
||||
|
||||
|
||||
def _fillet_icon_tris() -> tuple[tuple[float, float, float], ...]:
|
||||
"""Filled L-glyph with a smoothly rounded corner — two perpendicular
|
||||
wall bars joined by a constant-thickness arc band."""
|
||||
|
||||
@@ -3630,7 +3630,7 @@ class GizmoWallUnjoinSingle(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMix
|
||||
self.unjoin_op_props = []
|
||||
for _ in range(self.POOL_SIZE):
|
||||
icon = self.setup_icon_gizmo(
|
||||
"VIEW3D_GT_unjoin", default_color, highlight_color, "bim.unjoin_wall_path_connection"
|
||||
"VIEW3D_GT_link_toggle", default_color, highlight_color, "bim.unjoin_wall_path_connection"
|
||||
)
|
||||
icon.hide = True
|
||||
self.unjoin_icons.append(icon)
|
||||
|
||||
Reference in New Issue
Block a user