Fix parametric dimension dot clicks broken by bim.cross_select BLOCKING modal

Blender fires all matching tool-keymap entries' invoke() even after an
earlier entry returned RUNNING_MODAL.  When cross-select is enabled,
bim.cross_select (bl_options BLOCKING) goes modal on every LMB press
alongside bim.click_nearest_dimension_anchor.  Its BLOCKING flag causes
it to win the RELEASE event, selecting the background object instead of
activating the anchor dot.

Fix: CrossSelect.invoke() now calls _near_dimension_dot() before going
modal.  If a parametric dimension anchor dot is within 15 px of the
cursor it returns PASS_THROUGH, yielding to ClickNearestDimensionAnchor.

Also fixes apply_cross_select_preference to preserve pre-selection
entries (e.g. ClickNearestDimensionAnchor) when rebuilding tool keymaps:
replaces _tool_extra_keymap (which assumed the selection block starts at
index 0) with _split_tool_keymap, which searches for the selection block
by operator name and returns (pre, post) slices independently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ryan Schultz
2026-06-14 18:52:05 -05:00
parent 5f64ffee3f
commit 9f5e4d3949
2 changed files with 76 additions and 10 deletions
@@ -1650,6 +1650,14 @@ class CrossSelect(bpy.types.Operator):
DRAG_THRESHOLD = 5
def invoke(self, context, event):
# Yield to ClickNearestDimensionAnchor when a parametric dimension dot
# is near the cursor. Blender fires all matching tool-keymap entries'
# invoke() even after an earlier entry returned RUNNING_MODAL, so without
# this check our BLOCKING modal would win over click_nearest's modal and
# prevent the dot from turning blue.
if self._near_dimension_dot(context, event):
return {"PASS_THROUGH"}
self.cs_prefs = tool.Blender.get_addon_preferences().cross_select
self.enabled = self.cs_prefs.enabled
self.start_mouse = (event.mouse_region_x, event.mouse_region_y)
@@ -1662,6 +1670,47 @@ class CrossSelect(bpy.types.Operator):
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
@staticmethod
def _near_dimension_dot(context, event) -> bool:
"""Return True if the click is within 15 px of a parametric dimension anchor dot."""
try:
import ifcopenshell.util.element as _ue
from bpy_extras.view3d_utils import location_3d_to_region_2d
region = next(
(r for a in context.screen.areas if a.type == "VIEW_3D" for r in a.regions if r.type == "WINDOW"),
None,
)
rv3d = next(
(s.region_3d for a in context.screen.areas if a.type == "VIEW_3D" for s in a.spaces if s.type == "VIEW_3D"),
None,
)
if not region or not rv3d:
return False
cx = event.mouse_x - region.x
cy = event.mouse_y - region.y
r2 = 15 ** 2
for obj in context.scene.objects:
if obj.type != "CURVE" or not obj.visible_get():
continue
elem = tool.Ifc.get_entity(obj) if tool.Ifc.get() else None
if not elem or not elem.is_a("IfcAnnotation"):
continue
pset = _ue.get_pset(elem, "BBIM_Dimension")
if not pset or not pset.get("Anchors"):
continue
if not obj.data.splines:
continue
for pt in obj.data.splines[0].points:
sp = location_3d_to_region_2d(region, rv3d, obj.matrix_world @ pt.co.to_3d())
if sp and (cx - sp.x) ** 2 + (cy - sp.y) ** 2 < r2:
return True
except Exception:
pass
return False
def modal(self, context, event):
if event.shift:
self.operation = "ADD"
+27 -10
View File
@@ -823,9 +823,9 @@ class Blender(bonsai.core.tool.Blender):
"""
return cls.get_cross_select_keymap() if cls.is_cross_select_enabled() else cls.get_native_selection_keymap()
# Number of leading ``bl_keymap`` entries that make up the selection keymap, keyed by the
# operator idname of the first entry. Lets us swap the selection prefix in place while
# preserving each tool's own (hotkey) entries.
# Length of the selection block in bl_keymap, keyed by the operator idname of its
# first entry. Used to locate and replace the selection block during a rebuild
# while preserving any pre-selection or post-selection entries the tool declares.
_SELECTION_PREFIX_LENGTHS = {"bim.cross_select": 4, "view3d.select_box": 6}
@classmethod
@@ -849,11 +849,23 @@ class Blender(bonsai.core.tool.Blender):
yield (covering.workspace.CoveringTool, {"bim.wall_tool"}, False, False)
@classmethod
def _tool_extra_keymap(cls, tool_cls) -> tuple:
"""Return ``tool_cls``'s own (non-selection) ``bl_keymap`` entries."""
def _split_tool_keymap(cls, tool_cls) -> tuple[tuple, tuple]:
"""Split bl_keymap into (pre_selection, post_selection), discarding the selection block.
Locates the selection block by finding the first entry whose op is in
``_SELECTION_PREFIX_LENGTHS``, then skips the declared number of entries.
Entries before the block become *pre_selection*; entries after become
*post_selection*. The caller inserts the desired selection keymap between them.
When no selection block is found (shouldn't happen in practice) all entries
are returned as post_selection so the rebuild still produces a valid keymap.
"""
km = tuple(tool_cls.bl_keymap)
n = cls._SELECTION_PREFIX_LENGTHS.get(km[0][0], 0) if km else 0
return km[n:]
sel_start = next((i for i, entry in enumerate(km) if entry[0] in cls._SELECTION_PREFIX_LENGTHS), None)
if sel_start is None:
return (), km
sel_len = cls._SELECTION_PREFIX_LENGTHS[km[sel_start][0]]
return km[:sel_start], km[sel_start + sel_len:]
@classmethod
def apply_cross_select_preference(cls) -> None:
@@ -871,19 +883,24 @@ class Blender(bonsai.core.tool.Blender):
desired_op = selection_keymap[0][0] if selection_keymap else None
current = tuple(tools[0][0].bl_keymap)
if current and current[0][0] == desired_op:
# The selection block may be preceded by pre-selection entries (e.g. the
# AnnotationTool's ClickNearestDimensionAnchor), so search for it by op name
# rather than assuming it starts at position 0.
sel_start = next((i for i, entry in enumerate(current) if entry[0] in cls._SELECTION_PREFIX_LENGTHS), None)
if sel_start is not None and current[sel_start][0] == desired_op:
return # already applied
# Capture each tool's own entries before mutating any ``bl_keymap`` (model subclasses
# share BimTool's inherited keymap, so this must be done up front).
extras = {tool_cls: cls._tool_extra_keymap(tool_cls) for tool_cls, *_ in tools}
pre_post = {tool_cls: cls._split_tool_keymap(tool_cls) for tool_cls, *_ in tools}
for tool_cls, *_ in reversed(tools):
try:
bpy.utils.unregister_tool(tool_cls)
except Exception:
pass
for tool_cls, after, separator, group in tools:
tool_cls.bl_keymap = selection_keymap + extras[tool_cls]
pre, post = pre_post[tool_cls]
tool_cls.bl_keymap = pre + selection_keymap + post
bpy.utils.register_tool(tool_cls, after=after, separator=separator, group=group)
KEY_MODIFIERS = {