diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py index 81c8f70592..b1e105a079 100644 --- a/src/bonsai/bonsai/bim/__init__.py +++ b/src/bonsai/bonsai/bim/__init__.py @@ -31,18 +31,11 @@ from . import handler, operator, prop, ui def _parametric_gizmo_preference_classes() -> list[type]: - """Lazy resolution. ``bonsai.tool/__init__.py`` transitively loads - ``tool/ifc.py`` (and several siblings) which import ``from bonsai.bim.ifc - import IfcStore`` at module top — that ``tool → bim`` cycle means - ``bonsai.tool`` cannot be imported here before ``from . import handler, …`` - above has primed the bim partial-import dance through ``handler``'s own - ``import bonsai.tool``. By the time this function runs (during the - classes-tuple build below), ``handler`` has fully loaded and ``bonsai.tool`` - is safely importable. - - The architectural root cause is ``IfcStore`` living in ``bim/ifc.py``; - moving it to ``tool/ifc.py`` would let ``tool/`` stop reaching into ``bim/`` - and eliminate the need for this indirection. Tracked separately.""" + """Resolves the registry-driven ``GizmoPreferences`` classes for the + ``classes`` list below. ``import bonsai.tool`` is kept local to surface + the load-order constraint: it relies on ``from . import handler, …`` + above having primed the + ``tool/ifc.py → bim/ifc.py → bim/handler.py → bonsai.tool`` cycle.""" import bonsai.tool as tool return tool.Parametric.iter_gizmo_preference_classes(ui) diff --git a/src/bonsai/bonsai/bim/module/drawing/gizmos.py b/src/bonsai/bonsai/bim/module/drawing/gizmos.py index dd9882cc65..31a0be5c80 100644 --- a/src/bonsai/bonsai/bim/module/drawing/gizmos.py +++ b/src/bonsai/bonsai/bim/module/drawing/gizmos.py @@ -3168,7 +3168,7 @@ class GizmoMerge(TrisGizmoMixin, bpy.types.Gizmo): class GizmoSplit(TrisGizmoMixin, bpy.types.Gizmo): """Two arrows pointing outward away from each other — conveys splitting/cutting - one element into two. Visual inverse of :class:`GizmoMerge`.""" + one element into two. Visual inverse of `GizmoMerge`.""" bl_idname = "VIEW3D_GT_split" @@ -3216,7 +3216,7 @@ class GizmoExtend(TrisGizmoMixin, bpy.types.Gizmo): class GizmoExtendVertical(TrisGizmoMixin, bpy.types.Gizmo): - """Vertical sibling of :class:`GizmoExtend` — arrow pointing UP into a horizontal + """Vertical sibling of `GizmoExtend` — arrow pointing UP into a horizontal bar. Conveys extending an element's height to a target Z.""" bl_idname = "VIEW3D_GT_extend_vertical" @@ -4211,7 +4211,7 @@ class BillboardingGizmoGroupMixin: operator: str, alpha: float = 0.8, ) -> bpy.types.Gizmo: - """Convenience wrapper over :func:`setup_icon_gizmo` for subclasses.""" + """Convenience wrapper over `setup_icon_gizmo` for subclasses.""" return setup_icon_gizmo(self, gizmo_type, color, highlight_color, operator, alpha) def position_gizmos(self, context: bpy.types.Context) -> None: @@ -4445,7 +4445,7 @@ class BaseParametricGizmoGroup: ) -> float: """Y coordinate just outside the camera-facing face of an element. - Generalises :meth:`get_y_position_for_view` for elements whose near face + Generalises `get_y_position_for_view` for elements whose near face isn't at the local origin. ``near_y`` is the local-Y of the -Y face; ``far_y`` is the local-Y of the +Y face. Returns the Y just *outside* the face the camera is currently looking at, pushed by ``gizmo_offset`` (use @@ -4670,11 +4670,10 @@ class BaseParametricGizmoGroup: """ pass - # Frame-scoped caches populated by :meth:`_prime_frame_caches` at the top of - # ``refresh()`` and ``draw_prepare()``. Every per-frame helper — preferences - # access, view-direction lookup, billboard rotation — reads these instead of - # re-deriving the same values, since each gizmo group ends up needing them - # 2–5× per frame across its position helpers. + # Frame-scoped caches primed at the top of ``refresh()`` and ``draw_prepare()``. + # Every per-frame helper — preferences access, view-direction lookup, billboard + # rotation — reads these instead of re-deriving the same values, since each + # gizmo group ends up needing them 2–5× per frame across its position helpers. _frame_prefs: Any = None _frame_view_dir: tuple[bool, bool] | None = None _frame_billboard_rot: "Matrix | None" = None @@ -4939,7 +4938,7 @@ class BaseParametricGizmoGroup: ) -> bpy.types.Gizmo: """Create and configure an icon gizmo with standard settings. - Thin wrapper over :func:`setup_icon_gizmo` that defaults ``highlight_color`` + Thin wrapper over `setup_icon_gizmo` that defaults ``highlight_color`` to the addon-prefs selection color via ``get_decoration_colors``. """ if highlight_color is None: @@ -5112,33 +5111,39 @@ class BaseParametricGizmoGroup: icon_z = self.get_element_height(props) + self.ICON_Z_OFFSET icon_y = self.get_icon_y_offset(context, mw) billboard_rot = self._frame_billboard_rot - - # This ensures icons face camera regardless of object rotation - local_pos_validate = Vector((self.ICON_VALIDATE_X, icon_y, icon_z)) - world_pos_validate = mw @ local_pos_validate - - icon_matrix_base = Matrix.Translation(world_pos_validate) @ billboard_rot @ Matrix.Scale(0.5, 4) - + # set_icon_gizmo_position no-ops on hidden gizmos (via get_gizmo_if_visible), + # so the hide flag must be set first; that gates whether the matrix is written. if props.is_editing: self.pen_gizmo.hide = True self.validate_gizmo.hide = self.is_gizmo_hidden_by_modal(self.validate_gizmo) - self.validate_gizmo.matrix_basis = icon_matrix_base - + self.set_icon_gizmo_position( + "validate_gizmo", mw=mw, x=self.ICON_VALIDATE_X, y=icon_y, z=icon_z, billboard_rot=billboard_rot + ) self.cancel_gizmo.hide = self.is_gizmo_hidden_by_modal(self.cancel_gizmo) - local_pos_cancel = Vector((self.ICON_VALIDATE_X + self.ICON_CANCEL_X, icon_y, icon_z)) - world_pos_cancel = mw @ local_pos_cancel - self.cancel_gizmo.matrix_basis = Matrix.Translation(world_pos_cancel) @ billboard_rot @ Matrix.Scale(0.5, 4) - + self.set_icon_gizmo_position( + "cancel_gizmo", + mw=mw, + x=self.ICON_VALIDATE_X + self.ICON_CANCEL_X, + y=icon_y, + z=icon_z, + billboard_rot=billboard_rot, + ) if self.cycle_type_operator: self.cycle_gizmo.hide = self.is_gizmo_hidden_by_modal(self.cycle_gizmo) - local_pos_cycle = Vector((self.ICON_VALIDATE_X + self.ICON_CYCLE_X, icon_y, icon_z)) - world_pos_cycle = mw @ local_pos_cycle - self.cycle_gizmo.matrix_basis = ( - Matrix.Translation(world_pos_cycle) @ billboard_rot @ Matrix.Scale(0.30, 4) + self.set_icon_gizmo_position( + "cycle_gizmo", + mw=mw, + x=self.ICON_VALIDATE_X + self.ICON_CYCLE_X, + y=icon_y, + z=icon_z, + billboard_rot=billboard_rot, + scale=0.30, ) else: self.pen_gizmo.hide = self.is_gizmo_hidden_by_modal(self.pen_gizmo) - self.pen_gizmo.matrix_basis = icon_matrix_base + self.set_icon_gizmo_position( + "pen_gizmo", mw=mw, x=self.ICON_VALIDATE_X, y=icon_y, z=icon_z, billboard_rot=billboard_rot + ) self.validate_gizmo.hide = True self.cancel_gizmo.hide = True if self.cycle_type_operator: diff --git a/src/bonsai/bonsai/bim/module/model/stair.py b/src/bonsai/bonsai/bim/module/model/stair.py index 30137a15d6..0834263552 100644 --- a/src/bonsai/bonsai/bim/module/model/stair.py +++ b/src/bonsai/bonsai/bim/module/model/stair.py @@ -614,15 +614,14 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties" # noqa: ARG002 ) -> None: """Update stair-specific lock and tread count gizmos. Lock positioning is - handled per-frame in :py:meth:`_update_lock_gizmo_position`.""" + handled per-frame in the dimension-positioning hook.""" self.update_lock_gizmo(props) self.update_tread_lock_gizmo(props) self.update_tread_count_gizmos(props) def update_lock_gizmo(self, props: "BIMStairProperties") -> None: - """Update lock gizmo color and visibility. Positioning is handled in - :py:meth:`_update_lock_gizmo_position` (called per frame via - :py:meth:`_update_dimension_gizmo_positions`).""" + """Update lock gizmo color and visibility. Positioning is handled + per-frame by the dimension-positioning hook.""" gizmo_prefs = self.get_gizmo_prefs() if not self.update_gizmo_visibility(self.lock_gizmo, props.is_editing, gizmo_prefs.lock): return # Hidden, skip color update diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index c08f15b60c..b923aa3446 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -1617,6 +1617,7 @@ class FinishEditingWall(bpy.types.Operator, tool.Ifc.Operator): height_changed = not tool.Cad.is_x(props.height, props.snap_height, tolerance=1e-5) x_angle_changed = not tool.Cad.is_x(props.x_angle, props.snap_x_angle, tolerance=1e-5) baseline_changed = props.desired_offset_baseline != props.snap_offset_baseline + any_change = length_changed or height_changed or x_angle_changed or baseline_changed # Order matters: baseline shifts the layer-set reference line, then length # adjusts endpoints relative to that, then x_angle changes the slope (and @@ -1638,7 +1639,7 @@ class FinishEditingWall(bpy.types.Operator, tool.Ifc.Operator): if height_changed: bpy.ops.bim.change_extrusion_depth(depth=props.height) - if length_changed or height_changed or x_angle_changed or baseline_changed: + if any_change: props.mesh_dirty = False else: _restore_wall_mesh_if_dirty(obj) @@ -1816,7 +1817,6 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): # The baseline icons (EXT / CEN / INT) all share ICON_CYCLE_X — only one is # ever visible at a time so they don't overlap. ICON_ROTATE_X = 1.24 - ICON_TOGGLE_OPENINGS_X = 1.61 # Mapping from BIMWallProperties.desired_offset_baseline value to the # attribute on `self` that holds the corresponding state icon. @@ -1969,8 +1969,10 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): - Toggle-openings icon next to the pen. Lives outside edit mode because opening visibility is a viewport-display concern, not a wall-edit action. - Uses ``billboarded_at`` directly for parity with the base class's - ``update_editing_gizmos`` validate/cancel/cycle pattern.""" + Calls ``billboarded_at`` directly rather than routing through + ``set_icon_gizmo_position`` because the icon row has wall-specific + visibility/state branching (baseline-indicator selection, edit-mode + toggle for opening-visibility) that the helper does not model.""" if not hasattr(self, "rotate_gizmo"): return gizmo_prefs = self.get_gizmo_prefs() @@ -2030,7 +2032,7 @@ def _commit_active_wall_edit_if_any(context: bpy.types.Context) -> bpy.types.Obj def _commit_pending_wall_edits_for_selection(context: bpy.types.Context) -> None: # noqa: ARG001 - """Thin wall-scoped alias for :meth:`tool.Parametric.commit_pending_edits_for_selection`. + """Thin wall-scoped alias for `tool.Parametric.commit_pending_edits_for_selection`. Kept as a named helper because every multi-wall operator (split / join / merge / unjoin / extend-to-wall …) calls it at the top of ``_execute``; centralising the @@ -2198,12 +2200,12 @@ def _wall_axis_world_segment_from_geom(obj: bpy.types.Object, geom: dict) -> tup class _WallGeomCachedBillboardingMixin(gizmo.BillboardingGizmoGroupMixin): - """Adds IFC-read caching to :class:`BillboardingGizmoGroupMixin` for wall-driven + """Adds IFC-read caching to `BillboardingGizmoGroupMixin` for wall-driven gizmo groups. ``refresh()`` is Blender's "something state-relevant changed" signal — that's when we drop the cache. ``draw_prepare()`` (every redraw) reuses whatever ``_get_wall_geom_cached`` populated, so plain camera orbits don't re-hit IFC. ``_get_wall_geom_cached`` also drops entries on its own when - :meth:`tool.Parametric.get_geom_generation` advances (any ``tool.Ifc.Operator`` + `tool.Parametric.get_geom_generation` advances (any ``tool.Ifc.Operator`` commit) so external ``bpy.ops`` mutations on the same selection don't leave stale geometry behind.""" @@ -2274,7 +2276,7 @@ def _are_walls_collinear( parallel_threshold: float = 0.9994, line_tolerance: float = 0.05, ) -> bool: - """Vector wrapper around :func:`core.are_axes_collinear` — converts Vector + """Vector wrapper around `core.are_axes_collinear` — converts Vector endpoints to plain tuples at the boundary so the math stays unit-testable in ``test/core/`` without a mathutils dependency.""" return core.are_axes_collinear( @@ -2286,7 +2288,7 @@ def _are_walls_collinear( def _collinear_boundary_world(seg_a: tuple[Vector, Vector], seg_b: tuple[Vector, Vector]) -> Vector: - """Vector wrapper around :func:`core.closest_endpoint_midpoint`.""" + """Vector wrapper around `core.closest_endpoint_midpoint`.""" return Vector( core.closest_endpoint_midpoint( (tuple(seg_a[0]), tuple(seg_a[1])), @@ -2302,7 +2304,7 @@ class GizmoWallAddOpening(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMixin object's projected origin. Clicking dispatches `bim.add_opening`, which lets the existing FilledOpeningGenerator decide how the opening is applied. - Per-frame positioning via :class:`BillboardingGizmoGroupMixin` ensures the icon + Per-frame positioning via `BillboardingGizmoGroupMixin` ensures the icon keeps facing the camera as the viewport is orbited.""" bl_idname = "OBJECT_GGT_bim_wall_add_opening" @@ -2447,7 +2449,7 @@ class GizmoWallJoinIntersection(bpy.types.GizmoGroup, _WallGeomCachedBillboardin "join the corner" vs "extend this wall into the other." - **None of the above**: all icons hidden. - Per-frame positioning via :class:`BillboardingGizmoGroupMixin` ensures the icons + Per-frame positioning via `BillboardingGizmoGroupMixin` ensures the icons keep facing the camera as the viewport is orbited.""" bl_idname = "OBJECT_GGT_bim_wall_join_intersection" diff --git a/src/bonsai/bonsai/bim/parametric_lifecycle.py b/src/bonsai/bonsai/bim/parametric_lifecycle.py index dcfeb82b38..94324afa06 100644 --- a/src/bonsai/bonsai/bim/parametric_lifecycle.py +++ b/src/bonsai/bonsai/bim/parametric_lifecycle.py @@ -22,12 +22,12 @@ Two mixins fit the parametric-edit triads in ``bim/module/model/``: -:class:`FeatureModifierEditMixin` +`FeatureModifierEditMixin` Door, Window — BBIM_ pset with nested ``lining_properties`` / ``panel_properties``; Finish calls ``update__modifier_representation`` via ``ifcopenshell.api.feature``; Cancel restores via ``switch_representation``. -:class:`PathPreservingEditMixin` +`PathPreservingEditMixin` Railing, Roof — BBIM_ pset whose ``path_data`` is preserved through edit (only general kwargs are user-editable); Finish calls ``update__modifier_bmesh`` / ``update__modifier_ifc_data``; @@ -38,7 +38,7 @@ fit either mixin without optional escape hatches (Stair has a unique ``update_ifc_stair_props`` post-Finish step + a separate ``get_props_kwargs_for_ifc_export``; Wall is validation-first, snapshot-driven, no preview regen in operators). -This module sits separately from :class:`bonsai.tool.Parametric` (the registry + +This module sits separately from `bonsai.tool.Parametric` (the registry + auto-commit) because it imports ``bonsai.tool`` freely, while the registry itself must stay light — ``tool/blender.py`` consumes the registry at module load.""" @@ -201,9 +201,9 @@ class PathPreservingEditMixin(_ParametricEditMixinBase): Enable: Fetch pset data via ``tool.Model.get_modeling_bbim_pset_data`` → set - draft props → ``is_editing = True``. Subclass override - :meth:`_post_load_data` lets railing JSON-serialise ``path_data`` for - the PropertyGroup string field. + draft props → ``is_editing = True``. The subclass post-load hook + lets railing JSON-serialise ``path_data`` for the PropertyGroup + string field. Finish: Read fresh pset → keep ``path_data`` → gather ``general`` kwargs diff --git a/src/bonsai/bonsai/tool/parametric.py b/src/bonsai/bonsai/tool/parametric.py index 8a2fe75279..ce6659976c 100644 --- a/src/bonsai/bonsai/tool/parametric.py +++ b/src/bonsai/bonsai/tool/parametric.py @@ -21,7 +21,7 @@ """Registry + save-time auto-commit for parametric draft edits. Single source of truth: adding a new parametric element type is one entry in -:attr:`Parametric.EDIT_TYPES`. Every consumer — save-time auto-commit, the +`Parametric.EDIT_TYPES`. Every consumer — save-time auto-commit, the finish/cancel chains in ``tool.Blender.Modifier``, the ``PointerProperty`` attachment in ``bim/module/model/__init__.py``, and the per-type ``GizmoPreferences`` registration in ``bim/__init__.py`` — derives the @@ -45,7 +45,7 @@ implementation files it references — if a step's example code stops matching the real registration site, the step is out of date. STEP 1 — Add the registry entry (this file) - Append to :attr:`Parametric.EDIT_TYPES`:: + Append to `Parametric.EDIT_TYPES`:: ParametricObject("slab", has_non_editable_path=False), @@ -57,7 +57,7 @@ STEP 1 — Add the registry entry (this file) STEP 2 — Define the ``PropertyGroup`` (``bim/module/model/prop.py``) Class name **must** be ``BIMProperties`` — capitalisation matches - :attr:`ParametricObject.props_attr`:: + `ParametricObject.props_attr`:: class BIMSlabProperties(bpy.types.PropertyGroup): is_editing: BoolProperty(...) @@ -70,7 +70,7 @@ STEP 3 — Register the PropertyGroup class Add it to the ``classes`` tuple in ``bim/module/model/__init__.py`` (near the existing ``prop.BIMProperties`` entries). The ``bpy.types.Object.BIMSlabProperties`` attachment is automatic — - :meth:`Parametric.register_object_properties` loops the registry. + `Parametric.register_object_properties` loops the registry. STEP 4 — Implement the Enable / Finish / Cancel triad In ``bim/module/model/slab.py``, define three ``bpy.types.Operator`` @@ -81,7 +81,7 @@ STEP 4 — Implement the Enable / Finish / Cancel triad - ``CancelEditingSlab`` → ``bl_idname = "bim.cancel_editing_slab"`` **First, check if your new type fits one of the existing lifecycle - shapes** in :mod:`bonsai.bim.parametric_lifecycle`. If it does, inherit + shapes** in `bonsai.bim.parametric_lifecycle`. If it does, inherit the matching mixin and the triad collapses to ~25 lines total: - ``FeatureModifierEditMixin`` — BBIM_ pset with nested @@ -128,7 +128,7 @@ STEP 6 — Add the element-type predicate (``tool/blender.py``) return tool.Pset.get_element_pset(element, "BBIM_Slab") The method name **must** be ``is_`` to match - :attr:`ParametricObject.name` — :meth:`Parametric.find_for_element` + `ParametricObject.name` — `Parametric.find_for_element` looks it up by string. STEP 7 — OPTIONAL: typed property accessor (``tool/model.py``) @@ -153,9 +153,9 @@ STEP 8 — OPTIONAL: gizmo visibility preferences (``bim/ui.py``) slab: bpy.props.PointerProperty(type=GizmoPreferencesSlab) Do **not** add ``GizmoPreferencesSlab`` to the ``classes`` list in - ``bim/__init__.py`` — :meth:`Parametric.iter_gizmo_preference_classes` - discovers it from the registry automatically by its name - (``GizmoPreferences`` + capitalised registry token). + ``bim/__init__.py`` — the registry-driven discovery in this module finds + it by name (``GizmoPreferences`` + capitalised registry token) and + registers it automatically. STEP 9 — OPTIONAL: pure geometry helpers (``core/model.py``) Per-type math (collinearity checks, slope/displacement conversions, @@ -172,13 +172,12 @@ STEP 10 — Verify pytest test/core/ -x -q blender -b -P runpytest.py -- test/bim/ -x -q -m model - The Blender-backed lane runs the registration smoke test in - ``test/bim/test_parametric_registry.py`` — it iterates - :attr:`Parametric.EDIT_TYPES` and asserts each ``enable_op`` / - ``finish_op`` / ``cancel_op`` resolves to a registered operator, that - ``bpy.types.Object`` carries the matching ``BIMProperties`` - attribute, and that ``tool.Blender.Modifier.is_`` exists. Forget - any of the steps above and that test fails with a precise pointer at + The Blender-backed lane runs a registry smoke test that iterates the + EDIT_TYPES list and asserts each entry's enable/finish/cancel operator + resolves to a registered ``bpy.ops.bim.*``, that ``bpy.types.Object`` + carries the matching ``BIMProperties`` attribute, and that the + ``is_`` predicate exists on ``tool.Blender.Modifier``. Forget any + of the steps above and that test fails with a precise pointer at what's missing. Then manually in Blender: @@ -223,14 +222,19 @@ class ParametricObject: "wall", …) drives every derived identifier: the ``BIMProperties`` attribute on ``bpy.types.Object`` and the ``bim.enable_editing_`` / ``bim.finish_editing_`` / ``bim.cancel_editing_`` operator - ``bl_idname``s. The ``name`` is validated at construction time — - multi-word IFC types (e.g. ``IfcCurtainWall``) would silently mis-derive - through ``str.capitalize()`` and need a different approach than - appending to :data:`Parametric.EDIT_TYPES` directly. + ``bl_idname``s. The ``name`` is validated at construction time — a + multi-word IFC type would silently mis-derive through + ``str.capitalize()`` and breaks the single-token assumption. ``has_non_editable_path`` flags element types whose modifier exposes no - user-editable path (door, window, stair) — historically queried via - ``tool.Blender.Modifier.is_modifier_with_non_editable_path``.""" + user-editable path (door, window, stair). + + The paired runtime predicate ``tool.Blender.Modifier.is_(element)`` + is part of the registry contract: it MUST be **total** — accept any + IFC entity and return a boolean, never raise. The registry iterates + every predicate against the active element on save; a raising predicate + propagates upward and breaks the save path for *all* parametric types, + not just its own.""" name: str has_non_editable_path: bool = False @@ -342,12 +346,17 @@ class Parametric(bonsai.core.tool.Parametric): def run_bim_op(cls, bl_idname: str) -> None: """Invoke a ``bim.*`` operator by its ``bl_idname``. - Constraint: only use with operators that are themselves - ``tool.Ifc.Operator`` subclasses — their transaction wrap is what + Constraint enforced via ``assert``: the operator MUST be a + ``tool.Ifc.Operator`` subclass — its transaction wrap is what makes the IFC mutation undo-aware. Direct ``bpy.ops.bim.*`` invocation of a non-``Ifc.Operator`` would mutate IFC outside Bonsai's transaction system.""" - getattr(bpy.ops.bim, bl_idname.removeprefix("bim."))() + verb = bl_idname.removeprefix("bim.") + op_cls = getattr(bpy.types, f"BIM_OT_{verb}", None) + assert op_cls is not None and issubclass( + op_cls, tool.Ifc.Operator + ), f"{bl_idname!r} must be a registered tool.Ifc.Operator subclass for undo-safe IFC mutation" + getattr(bpy.ops.bim, verb)() @classmethod def commit_object_draft(cls, obj: bpy.types.Object, finish_op: str) -> bool: @@ -397,7 +406,7 @@ class Parametric(bonsai.core.tool.Parametric): def commit_pending_edits_for_selection( cls, names: Optional[tuple[str, ...]] = None ) -> tuple[int, list[bpy.types.Object]]: - """Selection-scoped variant of :meth:`commit_pending_edits`. ``names`` + """Selection-scoped variant of `commit_pending_edits`. ``names`` filters which registry entries to consider — e.g. ``("wall",)`` to commit only wall drafts among selected objects; ``None`` considers every type. @@ -439,7 +448,7 @@ class Parametric(bonsai.core.tool.Parametric): @classmethod def iter_gizmo_preference_classes(cls, ui_module) -> list[type]: """``GizmoPreferences`` classes that exist on ``ui_module`` for - every registry entry. Order matches :attr:`EDIT_TYPES`. Used by + every registry entry. Order matches `EDIT_TYPES`. Used by ``bim/__init__.py`` to inject the per-type ``GizmoPreferences`` classes at the correct point — before ``ui.GizmoPreferences``, which references them via ``PointerProperty``."""