diff --git a/src/bonsai/bonsai/bim/module/drawing/__init__.py b/src/bonsai/bonsai/bim/module/drawing/__init__.py index c69a23ed6d..bf1b903e5c 100644 --- a/src/bonsai/bonsai/bim/module/drawing/__init__.py +++ b/src/bonsai/bonsai/bim/module/drawing/__init__.py @@ -114,6 +114,8 @@ classes = ( operator.DrawParametricDimension, operator.SetDimensionAnchor, operator.RegenerateDimensions, + operator.ClickNearestDimensionAnchor, + operator.DebugDimensionClicks, prop.Variable, prop.Drawing, prop.Document, @@ -175,12 +177,17 @@ classes = ( gizmos.UglyDotGizmo, gizmos.ExtrusionGuidesGizmo, gizmos.ExtrusionWidget, + gizmos.GizmoAnchorHandle, + gizmos.DimensionAnchorWidget, gizmos.DimensionLinePositionWidget, workspace.LaunchAnnotationTypeManager, workspace.Hotkey, ) +_keymaps = [] + + def menu_func(self, context): active_obj = context.active_object if active_obj: @@ -204,6 +211,15 @@ def register(): bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button) bpy.types.VIEW3D_MT_object_context_menu.append(menu_func) + wm = bpy.context.window_manager + kc = wm.keyconfigs.addon + if kc: + km = kc.keymaps.new(name="3D View", space_type="VIEW_3D") + kmi = km.keymap_items.new( + "bim.click_nearest_dimension_anchor", "LEFTMOUSE", "PRESS" + ) + _keymaps.append((km, kmi)) + def unregister(): if not bpy.app.background: @@ -217,5 +233,9 @@ def unregister(): bpy.app.handlers.load_post.remove(handler.load_post) bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler) bpy.app.handlers.depsgraph_update_post.remove(handler.depsgraph_update_post_handler) + + for km, kmi in _keymaps: + km.keymap_items.remove(kmi) + _keymaps.clear() bpy.types.VIEW3D_MT_image_add.remove(ui.add_object_button) bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func) diff --git a/src/bonsai/bonsai/bim/module/drawing/gizmos.py b/src/bonsai/bonsai/bim/module/drawing/gizmos.py index 553d486112..39cd1cd542 100644 --- a/src/bonsai/bonsai/bim/module/drawing/gizmos.py +++ b/src/bonsai/bonsai/bim/module/drawing/gizmos.py @@ -2297,6 +2297,19 @@ DISC = ( (1.0, 0.0, 0), ) +# Anchor index currently being edited by SetDimensionAnchor (-1 = none). +_active_anchor_idx: int = -1 +# The annotation curve object being edited (kept so the gizmo group stays +# visible even when SetDimensionAnchor temporarily changes the active object). +_editing_annotation_obj = None + + +def set_active_anchor(idx: int, annotation_obj=None) -> None: + global _active_anchor_idx, _editing_annotation_obj + _active_anchor_idx = idx + _editing_annotation_obj = annotation_obj if idx >= 0 else None + + X3DISC = ( (0.0, 0.0, 0.0), (1.0, 0.0, 0), @@ -2622,6 +2635,152 @@ class ExtrusionWidget(types.GizmoGroup): self.guides.target_set_prop("depth", prop, "value") +class GizmoAnchorHandle(bpy.types.Gizmo): + """Dot gizmo positioned at one vertex of a parametric dimension curve. + + Clicking it invokes ``bim.set_dimension_anchor`` pre-scoped to that vertex + index, skipping the manual vertex-pick phase of the operator. + """ + + bl_idname = "BIM_GT_anchor_handle" + + __slots__ = ("anchor_index", "custom_shape", "custom_shape_select") + + def setup(self): + self.anchor_index = 0 + self.custom_shape = self.new_custom_shape(type="TRIS", verts=X3DISC) + # 4× scaled version used only for hit-detection — bigger target, same visual size. + _sel = tuple((x * 4.0, y * 4.0, z) for x, y, z in X3DISC) + self.custom_shape_select = self.new_custom_shape(type="TRIS", verts=_sel) + + def draw(self, context): + self.draw_custom_shape(self.custom_shape) + + def draw_select(self, context, select_id): + self.draw_custom_shape(self.custom_shape_select, select_id=select_id) + + def invoke(self, context, event): + return {"RUNNING_MODAL"} + + def modal(self, context, event, tweak): + anchor_index = self.anchor_index + + def _launch(): + try: + bpy.ops.bim.set_dimension_anchor("INVOKE_DEFAULT", anchor_index=anchor_index) + except Exception as e: + print(f"[DimensionAnchorWidget] {e}") + return None + + bpy.app.timers.register(_launch, first_interval=0.0) + return {"FINISHED"} + + +class DimensionAnchorWidget(types.GizmoGroup): + """Anchor handle gizmos at each vertex of the active parametric dimension. + + Green dots indicate vertices that are anchored to an IFC element face; + orange dots are free world-point anchors. Clicking any dot fires + ``bim.set_dimension_anchor`` pre-targeted at that vertex index. + """ + + bl_idname = "BIM_GGT_dimension_anchors" + bl_label = "Dimension Anchor Handles" + bl_space_type = "VIEW_3D" + bl_region_type = "WINDOW" + bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"} + + _DIM_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE", "PLAN_LEVEL", "SECTION_LEVEL")) + _MAX_ANCHORS = 16 + + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + if not tool.Ifc.get(): + return False + # Stay visible while SetDimensionAnchor is running (active obj may be a temp element). + if _active_anchor_idx >= 0 and _editing_annotation_obj is not None: + return True + obj = context.active_object + if not obj or obj.type != "CURVE": + return False + element = tool.Ifc.get_entity(obj) + if not element or not element.is_a("IfcAnnotation"): + return False + import ifcopenshell.util.element as _ue + if _ue.get_predefined_type(element) not in cls._DIM_TYPES: + return False + pset = _ue.get_pset(element, "BBIM_Dimension") + return bool(pset and pset.get("Anchors")) + + def setup(self, context: bpy.types.Context) -> None: + self._handles: list = [] + for _ in range(self._MAX_ANCHORS): + gz = self.gizmos.new("BIM_GT_anchor_handle") + gz.scale_basis = 0.18 + gz.select_bias = -32.0 + gz.use_draw_modal = True + gz.hide = True + self._handles.append(gz) + + def refresh(self, context: bpy.types.Context) -> None: + import json + import ifcopenshell.util.element as _ue + + obj = _editing_annotation_obj if _active_anchor_idx >= 0 and _editing_annotation_obj else context.active_object + if not obj or not obj.data or not getattr(obj.data, "splines", None): + for gz in self._handles: + gz.hide = True + return + + element = tool.Ifc.get_entity(obj) + if not element: + for gz in self._handles: + gz.hide = True + return + + pset = _ue.get_pset(element, "BBIM_Dimension") + if not pset or not pset.get("Anchors"): + for gz in self._handles: + gz.hide = True + return + + try: + anchors = json.loads(pset["Anchors"]) + except Exception: + for gz in self._handles: + gz.hide = True + return + + spline = obj.data.splines[0] + n = min(len(spline.points), len(anchors), self._MAX_ANCHORS) + + for i in range(n): + gz = self._handles[i] + world_co = obj.matrix_world @ spline.points[i].co.to_3d() + gz.matrix_basis = Matrix.Translation(world_co) + gz.anchor_index = i + if i == _active_anchor_idx and obj is _editing_annotation_obj: + print(f"[refresh] setting anchor[{i}] BLUE (obj={obj.name} editing={_editing_annotation_obj.name if _editing_annotation_obj else None})") + gz.color = (0.2, 0.7, 1.0) + gz.color_highlight = (0.4, 0.85, 1.0) + elif anchors[i].get("guid"): + gz.color = (0.2, 0.85, 0.2) + gz.color_highlight = (0.4, 1.0, 0.4) + else: + gz.color = (0.9, 0.6, 0.1) + gz.color_highlight = (1.0, 0.85, 0.2) + gz.alpha = 0.85 + gz.alpha_highlight = 1.0 + gz.hide = False + + for i in range(n, self._MAX_ANCHORS): + self._handles[i].hide = True + + def draw_prepare(self, context: bpy.types.Context) -> None: + print(f"[draw_prepare] DimensionAnchorWidget _active_anchor_idx={_active_anchor_idx}") + self.refresh(context) + + class DimensionLinePositionWidget(types.GizmoGroup): """Drag handle for the LinePosition of a parametric dimension annotation. @@ -2725,6 +2884,7 @@ class DimensionLinePositionWidget(types.GizmoGroup): return self._midpoint(obj).dot(od) def _set_pos(self, value: float) -> None: + bpy.ops.ed.undo_push(message="Set Line Position") import json import numpy as np import ifcopenshell.util.element as _ue diff --git a/src/bonsai/bonsai/bim/module/drawing/handler.py b/src/bonsai/bonsai/bim/module/drawing/handler.py index 78459e61cc..61dad633fb 100644 --- a/src/bonsai/bonsai/bim/module/drawing/handler.py +++ b/src/bonsai/bonsai/bim/module/drawing/handler.py @@ -111,6 +111,80 @@ def set_active_camera_resolution(scene: bpy.types.Scene) -> None: scene_render.resolution_y = raster_y +def _sync_dimension_anchors_to_curve(file, annotation, obj) -> bool: + """Sync BBIM_Dimension.Anchors length to match the curve's spline point count. + + Called when the user adds or removes vertices from a dimension annotation in + Edit Mode. New vertices get a free WORLD-type anchor at their current world + position; removed tail vertices simply lose their anchor entries. + + Returns True if the pset was changed. + """ + import ifcopenshell.util.element + import ifcopenshell.api.pset + + if not obj.data or not getattr(obj.data, "splines", None) or not obj.data.splines: + return False + + pset_data = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension") + if not pset_data or not pset_data.get("Anchors"): + return False + + try: + anchors: list = json.loads(pset_data["Anchors"]) + except Exception: + return False + + spline = obj.data.splines[0] + spline_world = [obj.matrix_world @ p.co.to_3d() for p in spline.points] + n_pts = len(spline_world) + n_anchors = len(anchors) + + print(f"[sync_anchors] obj={obj.name} spline_pts={n_pts} anchors={n_anchors}") + + if n_pts == n_anchors: + print("[sync_anchors] counts match — no change needed") + return False + + # Match each spline point to the nearest unused anchor by proximity. + # This handles insertions (subdivide) and deletions correctly regardless + # of where in the polyline the edit happened. + _MATCH_THRESH_SQ = 1e-4 # 1 cm² — distinguishes existing pts from new midpoints + used: set = set() + new_anchors: list = [] + + for pt in spline_world: + best_idx, best_sq = None, float("inf") + for i, anc in enumerate(anchors): + if i in used: + continue + stored = anc.get("pt") + if not stored: + continue + dx, dy, dz = stored[0] - pt.x, stored[1] - pt.y, stored[2] - pt.z + sq = dx * dx + dy * dy + dz * dz + if sq < best_sq: + best_sq, best_idx = sq, i + if best_idx is not None and best_sq < _MATCH_THRESH_SQ: + new_anchors.append(anchors[best_idx]) + used.add(best_idx) + else: + new_anchors.append({ + "guid": None, + "type": "WORLD", + "addr": {}, + "hint": None, + "pt": [pt.x, pt.y, pt.z], + }) + + print(f"[sync_anchors] rebuilt {len(new_anchors)} anchors (was {n_anchors})") + + pset_entity = file.by_id(pset_data["id"]) + ifcopenshell.api.pset.edit_pset(file, pset=pset_entity, properties={"Anchors": json.dumps(new_anchors)}) + invalidate_dim_index() + return True + + @persistent def depsgraph_update_post_handler(scene, depsgraph): """Auto-regenerate parametric dimensions when referenced elements are moved.""" @@ -128,6 +202,8 @@ def depsgraph_update_post_handler(scene, depsgraph): # serialised the new mesh back to IFC via update_representation, so the # tessellation will reflect the edited shape. moved_guids: set = set() + edited_annotation_ids: set = set() + for update in depsgraph.updates: obj = update.id if not isinstance(obj, bpy.types.Object): @@ -137,18 +213,28 @@ def depsgraph_update_post_handler(scene, depsgraph): element = tool.Ifc.get_entity(obj) if element is None or not hasattr(element, "GlobalId"): continue + + # If the dimension annotation curve itself was edited (vertex added/removed), + # sync the anchor list before regenerating. + if update.is_updated_geometry and obj.type == "CURVE" and element.is_a("IfcAnnotation"): + import ifcopenshell.util.element as _ue + ptype = _ue.get_predefined_type(element) + print(f"[handler] dimension curve geometry updated: {obj.name} ptype={ptype} is_updated_geometry={update.is_updated_geometry}") + if ptype in ("DIMENSION", "RADIUS", "DIAMETER", "ANGLE", "PLAN_LEVEL", "SECTION_LEVEL"): + _sync_dimension_anchors_to_curve(file, element, obj) + edited_annotation_ids.add(element.id()) + continue + moved_guids.add(element.GlobalId) # Geometry edits invalidate the cached tessellation for this element. if update.is_updated_geometry: _dim_shape_cache.pop(element.id(), None) - if not moved_guids: - return - if _dim_index_dirty: _rebuild_dim_guid_index(file) - annotation_ids: set = set() + print(f"[handler] edited_annotation_ids={edited_annotation_ids} moved_guids={moved_guids}") + annotation_ids: set = set(edited_annotation_ids) for guid in moved_guids: for ann_id in _dim_guid_index.get(guid, []): annotation_ids.add(ann_id) @@ -196,6 +282,7 @@ def depsgraph_update_post_handler(scene, depsgraph): except Exception: pass + print(f"[handler] regenerating ann_id={ann_id} anchors_in_pset={len(json.loads(pset.get('Anchors') or '[]'))}") resolved_pts = drawing_api.regenerate_dimension( file, annotation, @@ -203,7 +290,13 @@ def depsgraph_update_post_handler(scene, depsgraph): shape_cache=_dim_shape_cache, placement_override=placement_override, ) + print(f"[handler] resolved_pts count={len(resolved_pts)} pts={[(round(p[0],3),round(p[1],3),round(p[2],3)) for p in resolved_pts]}") if resolved_pts: + obj = tool.Ifc.get_object(annotation) + if obj: + print(f"[handler] curve spline pts before update={len(obj.data.splines[0].points) if obj.data.splines else 0}") _update_blender_curve(annotation, resolved_pts) + if obj: + print(f"[handler] curve spline pts after update={len(obj.data.splines[0].points) if obj.data.splines else 0}") finally: _dim_handler_running = False diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 271fca2c09..7833afa779 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -5935,8 +5935,10 @@ class SetDimensionAnchor(bpy.types.Operator): bl_label = "Set Dimension Anchor" bl_options = {"REGISTER", "UNDO"} + anchor_index: bpy.props.IntProperty(default=-1) # -1 = begin with vertex-pick phase + if TYPE_CHECKING: - pass + anchor_index: int _annotation: Optional[ifcopenshell.entity_instance] = None _annotation_obj: Optional[bpy.types.Object] = None @@ -5981,9 +5983,15 @@ class SetDimensionAnchor(bpy.types.Operator): obj = context.active_object self._annotation = tool.Ifc.get_entity(obj) self._annotation_obj = obj - self._phase = "PICK_VERTEX" - self._active_vertex_idx = -1 self._shape_cache = {} + if self.anchor_index >= 0: + self._phase = "PICK_FACE" + self._active_vertex_idx = self.anchor_index + from bonsai.bim.module.drawing.gizmos import set_active_anchor + set_active_anchor(self.anchor_index, obj) + else: + self._phase = "PICK_VERTEX" + self._active_vertex_idx = -1 self._hover_candidates = [] self._hover_index = 0 self._hover_last_px = (-9999, -9999) @@ -6010,6 +6018,8 @@ class SetDimensionAnchor(bpy.types.Operator): if event.type == "ESC" or (event.type == "RIGHTMOUSE" and event.value == "PRESS"): self._clear_hover_highlight(context) context.workspace.status_text_set(None) + from bonsai.bim.module.drawing.gizmos import set_active_anchor + set_active_anchor(-1) return {"FINISHED"} # keep any anchors already written # Hover — recompute candidates as cursor moves (PICK_FACE phase only) @@ -6085,6 +6095,8 @@ class SetDimensionAnchor(bpy.types.Operator): self._active_vertex_idx = best_idx self._phase = "PICK_FACE" + from bonsai.bim.module.drawing.gizmos import set_active_anchor + set_active_anchor(best_idx, obj) # ------------------------------------------------------------------ # Phase 2: pick a face on an IFC element @@ -6127,6 +6139,8 @@ class SetDimensionAnchor(bpy.types.Operator): self._write_anchor(anchor, self._active_vertex_idx) self.report({"INFO"}, f"Vertex {self._active_vertex_idx} → free world point") self._phase = "PICK_VERTEX" + from bonsai.bim.module.drawing.gizmos import set_active_anchor + set_active_anchor(-1) return # Normal click — use whichever candidate is currently highlighted. @@ -6177,6 +6191,8 @@ class SetDimensionAnchor(bpy.types.Operator): self._phase = "PICK_VERTEX" self._hover_candidates = [] self._hover_index = 0 + from bonsai.bim.module.drawing.gizmos import set_active_anchor + set_active_anchor(-1) # ------------------------------------------------------------------ # Hover / cycle helpers @@ -6474,12 +6490,20 @@ class RegenerateDimensions(bpy.types.Operator, tool.Ifc.Operator): if ifcopenshell.util.element.get_pset(a, "BBIM_Dimension") ] + from bonsai.bim.module.drawing.handler import _sync_dimension_anchors_to_curve + updated = 0 for annotation in candidates: pset = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension") if not pset: continue + # Sync anchor count to curve vertex count in case the user added or + # removed vertices in Edit Mode since the last regeneration. + ann_obj = tool.Ifc.get_object(annotation) + if ann_obj and ann_obj.type == "CURVE": + _sync_dimension_anchors_to_curve(file, annotation, ann_obj) + # Build a placement override from each referenced element's current # Blender matrix_world. Bonsai only syncs ObjectPlacement to the IFC # file when the user explicitly clicks "Edit Object Placement" — so the @@ -6690,3 +6714,169 @@ def _find_curve_in_item(item: ifcopenshell.entity_instance) -> Optional[ifcopens if result is not None: return result return None + + +class ClickNearestDimensionAnchor(bpy.types.Operator): + """LMB handler: fire SetDimensionAnchor when cursor is within RADIUS pixels of an anchor dot. + + Registered as a keymap item so it runs before Blender's object-selection handler. + Returns PASS_THROUGH when the cursor is not near any anchor, so normal viewport + clicks are unaffected. + """ + + bl_idname = "bim.click_nearest_dimension_anchor" + bl_label = "Click Nearest Dimension Anchor" + + RADIUS_PX = 120 + + def invoke(self, context, event): + from bpy_extras.view3d_utils import location_3d_to_region_2d + + print(f"[AnchorClick] invoke called") + + if not tool.Ifc.get(): + print(f"[AnchorClick] PASS_THROUGH — no IFC file") + return {"PASS_THROUGH"} + + obj = context.active_object + if not obj or obj.type != "CURVE": + print(f"[AnchorClick] PASS_THROUGH — active obj is {obj} type={getattr(obj,'type',None)}") + return {"PASS_THROUGH"} + + element = tool.Ifc.get_entity(obj) + if not element or not element.is_a("IfcAnnotation"): + print(f"[AnchorClick] PASS_THROUGH — not an IfcAnnotation: {element}") + return {"PASS_THROUGH"} + + import ifcopenshell.util.element as _ue + pset = _ue.get_pset(element, "BBIM_Dimension") + if not pset or not pset.get("Anchors"): + print(f"[AnchorClick] PASS_THROUGH — no BBIM_Dimension pset or Anchors") + return {"PASS_THROUGH"} + + if not obj.data.splines: + print(f"[AnchorClick] PASS_THROUGH — no splines") + 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 + rv3d = None + for area in context.screen.areas: + if area.type != "VIEW_3D": + continue + for r in area.regions: + if r.type == "WINDOW": + region = r + break + if region: + for space in area.spaces: + if space.type == "VIEW_3D": + rv3d = space.region_3d + break + break + + if not region or not rv3d: + print(f"[AnchorClick] PASS_THROUGH — no 3D region") + return {"PASS_THROUGH"} + + # Convert absolute mouse position to WINDOW region-local coordinates. + cx = event.mouse_x - region.x + cy = event.mouse_y - region.y + + r2 = self.RADIUS_PX ** 2 + best_idx = -1 + best_dist_sq = float("inf") + for i, pt in enumerate(obj.data.splines[0].points): + world_pos = obj.matrix_world @ pt.co.to_3d() + sp = location_3d_to_region_2d(region, rv3d, world_pos) + if not sp: + continue + dx, dy = cx - sp.x, cy - sp.y + d2 = dx * dx + dy * dy + print(f"[AnchorClick] click=({cx},{cy}) anchor[{i}]=({sp.x:.0f},{sp.y:.0f}) dist={d2**0.5:.1f}px radius={self.RADIUS_PX}px") + if d2 < r2 and d2 < best_dist_sq: + best_dist_sq = d2 + best_idx = i + + if best_idx < 0: + print(f"[AnchorClick] MISS — no anchor within {self.RADIUS_PX}px") + return {"PASS_THROUGH"} + + print(f"[AnchorClick] HIT anchor[{best_idx}] dist={best_dist_sq**0.5:.1f}px") + from bonsai.bim.module.drawing.gizmos import set_active_anchor + set_active_anchor(best_idx, obj) + # Force viewport redraw so gizmo colors update before the modal starts. + 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=best_idx) + return {"FINISHED"} + + +class DebugDimensionClicks(bpy.types.Operator): + """Debug modal: logs every LMB click in the 3D viewport vs anchor gizmo positions. + + Run from the Python console: + bpy.ops.bim.debug_dimension_clicks('INVOKE_DEFAULT') + Press ESC or RMB to stop. + """ + + bl_idname = "bim.debug_dimension_clicks" + bl_label = "Debug Dimension Clicks" + + def modal(self, context, event): + if event.type == "LEFTMOUSE" and event.value == "PRESS": + # Use absolute window coords — mouse_region_x/y is relative to whichever + # region caught the event, which may differ from the 3D viewport region. + click_x = event.mouse_x + click_y = event.mouse_y + + # Find the 3D viewport region — context.region_data is None in modal. + region = None + rv3d = None + for area in context.screen.areas: + if area.type != "VIEW_3D": + continue + for r in area.regions: + if r.type == "WINDOW": + region = r + break + if region: + for space in area.spaces: + if space.type == "VIEW_3D": + rv3d = space.region_3d + break + break + + obj = context.active_object + if obj and obj.type == "CURVE" and obj.data and obj.data.splines and region and rv3d: + from bpy_extras.view3d_utils import location_3d_to_region_2d + + print(f"[ClickDebug] LMB at abs ({click_x}, {click_y}) region_offset=({region.x},{region.y})") + for i, pt in enumerate(obj.data.splines[0].points): + world_pos = obj.matrix_world @ pt.co.to_3d() + screen_pos = location_3d_to_region_2d(region, rv3d, world_pos) + if screen_pos: + # Convert region-local pos to absolute window coords for comparison. + abs_x = screen_pos.x + region.x + abs_y = screen_pos.y + region.y + dist = ((click_x - abs_x) ** 2 + (click_y - abs_y) ** 2) ** 0.5 + print(f" anchor[{i}] abs={abs_x:.0f},{abs_y:.0f} dist={dist:.1f}px") + else: + print(f" anchor[{i}] (off screen)") + else: + print(f"[ClickDebug] LMB at abs ({click_x}, {click_y}) — no active curve or no 3D region") + return {"PASS_THROUGH"} + + if event.type in {"ESC", "RIGHTMOUSE"}: + print("[ClickDebug] Stopped.") + return {"CANCELLED"} + + return {"PASS_THROUGH"} + + def invoke(self, context, event): + context.window_manager.modal_handler_add(self) + print("[ClickDebug] Dimension click logger started. Click near anchor dots; press ESC to stop.") + return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/bim/module/drawing/workspace.py b/src/bonsai/bonsai/bim/module/drawing/workspace.py index 03bcd0c033..cc99ca2983 100644 --- a/src/bonsai/bonsai/bim/module/drawing/workspace.py +++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py @@ -243,7 +243,6 @@ class AnnotationToolUI: row.prop(ann_props, "line_position") cls.layout.separator() row = cls.layout.row(align=True) - row.operator("bim.set_dimension_anchor", icon="PIVOT_CURSOR") op = row.operator("bim.regenerate_dimensions", icon="FILE_REFRESH", text="Regenerate") op.active_only = True