Fix parametric dimension anchor dot clicks: two-event modal + remove stale active-obj guard

ClickNearestDimensionAnchor was firing SetDimensionAnchor immediately on
LMB PRESS and returning FINISHED, which caused Blender to re-deliver the
RELEASE to view3d.select — deselecting the annotation mid-flight.  Rewrite
as a two-event modal: PRESS starts the modal, RELEASE fires SetDimensionAnchor
and exits.  SetDimensionAnchor also swallows any LMB RELEASE it receives to
prevent view3d.select from stealing the active object after hand-off.

The pre-click active-object guard (skip if dimension not active_object) caused
dots to never turn blue: view3d.select was silently replacing the dimension
with the plane underneath on every line-body click, so the dimension was
never the active object at the time of the dot click.  Removed — the operator
now selects the dimension itself before going modal, making each dot click
self-contained.

RADIUS_PX reduced from 60 to 15 to match the gizmo disc visual size and
prevent false triggers on line-body clicks near endpoints.
This commit is contained in:
Ryan Schultz
2026-06-14 15:01:20 -05:00
parent c92451e0fc
commit ffe7296973
2 changed files with 48 additions and 22 deletions
@@ -2638,10 +2638,10 @@ class ExtrusionWidget(types.GizmoGroup):
class GizmoAnchorHandle(bpy.types.Gizmo): class GizmoAnchorHandle(bpy.types.Gizmo):
"""Visual-only dot at a parametric dimension vertex. """Visual-only dot at a parametric dimension vertex.
No draw_select/invoke any draw_select entry puts the gizmo in Blender's No draw_select/invoke draw_select puts the gizmo in Blender's select buffer
select buffer, which causes the gizmo system to consume the click even and causes the gizmo system to consume clicks even without an explicit invoke,
without an explicit invoke. All click handling is done by the blocking ClickNearestDimensionAnchor from receiving them. All click handling
bim.click_nearest_dimension_anchor keymap operator. is done by the bim.click_nearest_dimension_anchor keymap operator.
""" """
bl_idname = "BIM_GT_anchor_handle" bl_idname = "BIM_GT_anchor_handle"
@@ -6710,6 +6710,13 @@ class SetDimensionAnchor(bpy.types.Operator, tool.Ifc.Operator):
self._cycle_hover(context) self._cycle_hover(context)
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
if event.type == "LEFTMOUSE" and event.value == "RELEASE":
# Swallow LMB RELEASE throughout the modal's lifetime.
# When ClickNearestDimensionAnchor hands off to this modal, Blender
# re-delivers that same RELEASE here; passing it through would let
# view3d.select steal the active object away from the annotation.
return {"RUNNING_MODAL"}
if event.type == "LEFTMOUSE" and event.value == "PRESS": if event.type == "LEFTMOUSE" and event.value == "PRESS":
if self._phase == "PICK_VERTEX": if self._phase == "PICK_VERTEX":
self._handle_vertex_pick(context, event) self._handle_vertex_pick(context, event)
@@ -7753,16 +7760,20 @@ def _find_curve_in_item(item: ifcopenshell.entity_instance) -> Optional[ifcopens
class ClickNearestDimensionAnchor(bpy.types.Operator): class ClickNearestDimensionAnchor(bpy.types.Operator):
"""LMB fallback: fire SetDimensionAnchor when cursor is within RADIUS pixels of an anchor dot. """Click handler for dimension anchor dots.
The gizmo handles exact hits; this catches near-misses where the cursor When the active dimension's anchor dots are visible, clicking within
is close to a dot but didn't land inside the gizmo hit shape. RADIUS_PX of a dot activates SetDimensionAnchor for that dot.
Implemented as a two-event modal (PRESS consumed in invoke, RELEASE
consumed in modal) so SetDimensionAnchor starts on a clean slate
no stray LMB events reach it that could trigger view3d.select.
""" """
bl_idname = "bim.click_nearest_dimension_anchor" bl_idname = "bim.click_nearest_dimension_anchor"
bl_label = "Click Nearest Dimension Anchor" bl_label = "Click Nearest Dimension Anchor"
RADIUS_PX = 60 RADIUS_PX = 15
def invoke(self, context, event): def invoke(self, context, event):
from bpy_extras.view3d_utils import location_3d_to_region_2d from bpy_extras.view3d_utils import location_3d_to_region_2d
@@ -7770,8 +7781,6 @@ class ClickNearestDimensionAnchor(bpy.types.Operator):
if not tool.Ifc.get(): if not tool.Ifc.get():
return {"PASS_THROUGH"} return {"PASS_THROUGH"}
# Always use the 3D viewport WINDOW region — context.region may be a header,
# sidebar, or toolbar depending on where the click landed in the area.
region = None region = None
rv3d = None rv3d = None
for area in context.screen.areas: for area in context.screen.areas:
@@ -7791,7 +7800,6 @@ class ClickNearestDimensionAnchor(bpy.types.Operator):
if not region or not rv3d: if not region or not rv3d:
return {"PASS_THROUGH"} return {"PASS_THROUGH"}
# Convert absolute mouse position to WINDOW region-local coordinates.
cx = event.mouse_x - region.x cx = event.mouse_x - region.x
cy = event.mouse_y - region.y cy = event.mouse_y - region.y
@@ -7802,8 +7810,6 @@ class ClickNearestDimensionAnchor(bpy.types.Operator):
best_idx = -1 best_idx = -1
best_dist_sq = float("inf") best_dist_sq = float("inf")
# Scan all visible dimension annotations — not just selected ones.
# view3d.select may deselect the annotation before this operator runs.
for obj in context.scene.objects: for obj in context.scene.objects:
if obj.type != "CURVE": if obj.type != "CURVE":
continue continue
@@ -7837,15 +7843,35 @@ class ClickNearestDimensionAnchor(bpy.types.Operator):
o.select_set(False) o.select_set(False)
best_obj.select_set(True) best_obj.select_set(True)
context.view_layer.objects.active = best_obj context.view_layer.objects.active = best_obj
from bonsai.bim.module.drawing.gizmos import set_active_anchor
set_active_anchor(best_idx, best_obj) # Store dot for the modal phase and go modal to consume the PRESS.
# Force viewport redraw so gizmo colors update before the modal starts. # The modal will also consume the RELEASE before handing off to
for area in context.screen.areas: # SetDimensionAnchor, so view3d.select never sees either event.
if area.type == "VIEW_3D": self._best_obj = best_obj
area.tag_redraw() self._best_idx = best_idx
break context.window_manager.modal_handler_add(self)
bpy.ops.bim.set_dimension_anchor("INVOKE_DEFAULT", anchor_index=best_idx) return {"RUNNING_MODAL"}
return {"FINISHED"}
def modal(self, context, event):
if event.type == "LEFTMOUSE" and event.value == "RELEASE":
# Both PRESS and RELEASE are now consumed. Start the anchor editor.
from bonsai.bim.module.drawing.gizmos import set_active_anchor
set_active_anchor(self._best_idx, self._best_obj)
for area in context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
break
bpy.ops.bim.set_dimension_anchor("INVOKE_DEFAULT", anchor_index=self._best_idx)
return {"FINISHED"}
if event.type in ("ESC", "RIGHTMOUSE"):
return {"CANCELLED"}
# Consume intermediate LMB events; pass everything else through so
# viewport navigation (MMB, scroll) still works during the brief wait.
if event.type == "LEFTMOUSE":
return {"RUNNING_MODAL"}
return {"PASS_THROUGH"}
class DebugDimensionClicks(bpy.types.Operator): class DebugDimensionClicks(bpy.types.Operator):