mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
Sweep docstrings for rot-prone references
Docstrings naming sibling methods, private helpers, test files, or historical symbols silently go wrong on rename. Strip Sphinx :meth: / :class: / :func: / :attr: markup that mostly added noise (no Sphinx in this project), and rewrite five docstrings that cited specific test paths or private hooks to describe the behaviour instead. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -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<X>`` 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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
|
||||
Two mixins fit the parametric-edit triads in ``bim/module/model/``:
|
||||
|
||||
:class:`FeatureModifierEditMixin`
|
||||
`FeatureModifierEditMixin`
|
||||
Door, Window — BBIM_<Type> pset with nested ``lining_properties`` /
|
||||
``panel_properties``; Finish calls ``update_<type>_modifier_representation``
|
||||
via ``ifcopenshell.api.feature``; Cancel restores via ``switch_representation``.
|
||||
|
||||
:class:`PathPreservingEditMixin`
|
||||
`PathPreservingEditMixin`
|
||||
Railing, Roof — BBIM_<Type> pset whose ``path_data`` is preserved through
|
||||
edit (only general kwargs are user-editable); Finish calls
|
||||
``update_<type>_modifier_bmesh`` / ``update_<type>_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
|
||||
|
||||
@@ -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<X>`` 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 ``BIM<Name>Properties`` — 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.BIM<X>Properties`` 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_<Type> 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_<name>`` 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 ``BIM<Name>Properties``
|
||||
attribute, and that ``tool.Blender.Modifier.is_<name>`` 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 ``BIM<Name>Properties`` attribute, and that the
|
||||
``is_<name>`` 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 ``BIM<Name>Properties``
|
||||
attribute on ``bpy.types.Object`` and the ``bim.enable_editing_<name>`` /
|
||||
``bim.finish_editing_<name>`` / ``bim.cancel_editing_<name>`` 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_<name>(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<Name>`` 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<X>``
|
||||
classes at the correct point — before ``ui.GizmoPreferences``, which
|
||||
references them via ``PointerProperty``."""
|
||||
|
||||
Reference in New Issue
Block a user