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>
Introduce bim.cross_select, a Rhino/CAD-style box selection operator, and route
every Bonsai workspace tool's selection through it. Dragging the box left-to-right
selects only fully-enclosed elements (window); right-to-left selects anything the
box touches (crossing). Works in Object and Edit Mesh modes; plain click, Shift
(add) and Ctrl (subtract) behave as expected, and clicking empty space deselects.
Selection logic is ported from the GPL-3.0 Blender-Cross-Select add-on
(RARA, CYX, Witty.Ming, Shuimeng), reduced to box-only mode and adapted to read
its colors and line width from Bonsai's add-on preferences.
A new "Cross Select" add-on preference (CrossSelectPreferences) toggles the
behavior. Because a WorkSpaceTool's bl_keymap is baked at import, the shared
selection keymap is split into cross-select vs native variants and applied via
apply_cross_select_preference(), which rebuilds each tool's keymap and
re-registers all selection tools in place. Toggling the preference re-applies
live via a deferred timer, and the saved preference is applied on startup. When
disabled, tools fall back to Blender's native view3d.select_box / view3d.select.
- tool/blender.py: split native/cross-select keymaps, pref-driven
get_default_selection_keypmap, apply_cross_select_preference re-registration
- bim/module/model/workspace.py: bim.cross_select operator and geometry/draw helpers
- bim/ui.py: CrossSelectPreferences group, preferences UI, live-toggle callback
- bim/__init__.py: register preference group, apply saved preference on startup
- bim/module/model/__init__.py: register CrossSelect operator
Generated with the assistance of an AI coding tool.
When override_mode_set_edit encounters an unsupported profile (Couldn't
import profile), deselect the object so Tab continues to cycle cleanly.
Also restores the selection that existed before entering aggregate mode
when finally tabbing out, via save/restore_previous_selection().
Following the pattern from 586f9be077, deselect the active object after
exiting item mode so Tab continues to cycle cleanly. Also deselects
parametric LAYER1/LAYER2 items that cannot be edited directly, avoiding
the need to manually deselect before Tab-cycling out of aggregate mode.
In BIM_PT_type_attributes and BIM_PT_object_attributes (when
the active object is a type), attribute value buttons now use
"type.<Attr>" as the selector key so the operator finds
matching occurrences via their relating type rather than the
occurrence's own (often unset) attributes.
Generated with the assistance of an AI coding tool.
After selecting objects in the same container, copy a `location="Name"`
filter query to the clipboard and report it — consistent with the same
behaviour in SelectSimilarType, SelectSimilarAggregate, SelectIfcClass,
and SelectSimilarMaterial.
Generated with the assistance of an AI coding tool.
When a `+`-separated filter group returns no results, `FacetTransformer.facet_list`
was skipping the reset of `has_additive_facet_in_current_list` because the reset
was inside the `if self.elements:` guard. The stale flag caused the next group's
`add_default_elements()` to bail out early, leaving its element set empty and
silently dropping every subsequent group from the result.
Move the flag reset outside the guard so it always fires regardless of whether
the group produced any results.
In the Placement panel, show Location and Rotation X/Y/Z
each on their own row beneath a header label. When the IFC
file uses imperial units, display a read-only feet-and-inches
label alongside each Location input field.
Generated with the assistance of an AI coding tool.
Display general and calculated stair parameters (Width,
Height, Tread Run, Tread Rise, Length, etc.) formatted
to the IFC file's configured length unit rather than
raw numeric values.
Generated with the assistance of an AI coding tool.
DecorationsHandler now caches dissolved edges (mesh-keyed), world-space
draw payload, and GPUBatch objects with per-object epoch invalidation —
moving one wall doesn't wipe 50 opening caches. Object-mode dissolve
removes triangulation noise; 2-pass depth-test split dims occluded lines
instead of hiding them. Edit-mode behavior unchanged.
Also: disable viewport shadows for IfcFeatureElementSubtraction objects,
and wire DecorationsHandler.uninstall() into the model module's
unregister() so the new persistent handlers don't leak on addon disable.
Generated with the assistance of an AI coding tool.
EditAssignedMaterial propagated layer set usage attributes
to all selected objects but skipped this loop for profile
set usage. Add the same loop so CardinalPoint and
ReferenceExtent are copied to each selected object's
IfcMaterialProfileSetUsage on save.
Generated with the assistance of an AI coding tool.
When the user enters `-0' - 10"`, Python parses feet as -0.0.
The check `feet < 0` is False for negative zero, so the sign was
silently dropped. Use math.copysign to detect it correctly.
Generated with the assistance of an AI coding tool.
Two paired test files for the framework infrastructure landed
earlier in this PR.
test_decorator_cache.py (11 tests):
* The 4-hook invalidation list (depsgraph_update_post + undo_post +
redo_post + load_post) is symmetrically managed by
install_decorator_cache_handlers / uninstall_decorator_cache_handlers.
A future edit that drops a hook from one side without the other
would land as a Blender segfault when a cached bpy.types.Object
ref outlives its underlying ID block — the regression must surface
as a test failure first.
* install is idempotent (calling twice doesn't double-register).
* uninstall when not installed doesn't raise.
* The bump handler accepts Blender's variadic args.
* The depsgraph predicate gates correctly: bumps on Object geometry
or transform updates, silently skips on Material / NodeTree / Image
updates (which would otherwise rebuild every cache on every node
edit).
* TokenCache.get_or_compute short-circuits on key+token match and
recomputes when the token bumps.
test_undo_resync_parametric_drafts.py (3 tests):
* UNDO_REGENERATORS keys must all be in tool.Parametric.EDIT_TYPES.
A typo would silently no-op on Ctrl+Z, restoring the desync the
helper is meant to prevent.
* The dispatcher skips objects with no active parametric edit
(undo_post fires for every undo, most of which touch zero drafts).
* The dispatcher silently skips parametric types that have no
UNDO_REGENERATORS entry (door / window / array are IFC-derived
with no draft preview mesh — they don't need a regenerator).
Mocks use spec=bpy.types.Depsgraph / spec=bpy.types.DepsgraphUpdate
/ spec=tool.parametric.ParametricObject so typos in mocked-attribute
access fail loudly (CLAUDE.md test discipline).
Generated with the assistance of an AI coding tool.
Splitting a wall through a door orphaned the door (door.FillsVoids
became empty). The fill rel was being reassigned by setting its
RelatedBuildingElement slot — schema-wise that's the filling slot, not
the wall slot — so when remove_feature deleted the old opening it
also cascade-removed the rel. Transferring via RelatingOpeningElement
keeps the rel pointing at the new opening so the door stays
associated. Pre-existing bug from 5a6476a57, surfaced by ef144dce2.
Generated with the assistance of an AI coding tool.
DumbWallJoiner.split assigned openings by projecting the opening's
centre-point onto the wall axis, so any opening whose footprint
straddled the cut was silently dropped from whichever wall its centre
missed. Now the full axis-projected extent (via ifcopenshell.geom.
create_shape) drives the assignment; for filled openings whose void
straddles the cut, a pure-void copy is added back to the neighbour
wall so its body is also cut.
Generated with the assistance of an AI coding tool.
Five inline copies of the same defensive pattern lived across
``tool/parametric.py``, ``bim/parametric_lifecycle.py``,
``bim/module/model/preview_base.py`` (twice), and as a near-twin
in ``tool/blender.py:update_all_viewports`` itself.
``tool.Blender.update_all_viewports`` already covered the
``tag_redraw`` job but used an ``assert context.screen`` that would
raise during background-mode operators or early-load_post calls
where ``screen`` legitimately is None. Relax to a defensive
``getattr(context, "screen", None)`` + silent return so the helper
fits every caller's needs, then collapse the 4 inline copies to
single calls.
Net -9 LOC. The helper now describes its contract ("silent no-op
when no screen attached") rather than naming specific callers, so
moving a caller doesn't rot the docstring.
Generated with the assistance of an AI coding tool.
Ctrl+Z / Ctrl+Shift+Z on an in-progress parametric draft (wall /
stair / roof) used to leave the preview mesh frozen in its
pre-undo shape — the IFC mutation rolls back but the bmesh built
from draft props doesn't repaint.
Add a registry of per-type regenerator functions
(``UNDO_REGENERATORS``) that re-build each type's preview mesh
from its current props. The dispatcher
``resync_parametric_drafts_after_undo`` walks all objects, skips
any without an active parametric edit, looks up the regenerator
by feature name, and calls it. Tagged 3D viewports for redraw.
Types without an entry (door / window / railing / etc.) are
intentionally absent — they're IFC-derived, so the undo's
representation rollback + next-frame refresh already repaints
correctly without a draft-side regenerator.
Undo/redo wiring is self-installed by
``bonsai.bim.parametric_lifecycle``: a ``@persistent``
``_resync_on_undo`` callback dispatches into the registry, and
``install_parametric_lifecycle_handlers()`` /
``uninstall_parametric_lifecycle_handlers()`` append/remove it
from ``bpy.app.handlers.undo_post`` and ``redo_post``.
``bim/__init__.py``'s ``register()`` calls the install function
*after* the central ``handler.undo_post`` / ``redo_post`` appends
so the regenerators see restored IFC state — ``bpy.app.handlers``
fire in append order. ``handler.py`` itself stays ignorant of the
parametric subsystem. The lazy function-local imports in each
regenerator break the addon-load cycle —
``bonsai.bim.parametric_lifecycle`` loads before
``bim/module/model/*``.
Generated with the assistance of an AI coding tool.
Three changes to the shared Enable/Finish/Cancel mixins:
1. Always-on drift triad on ParametricEditMixinBase. The base now
provides ``_handle_drift_on_enable`` / ``_handle_drift_on_finish``
/ ``_handle_drift_on_cancel`` classmethods, called from the
per-mixin ``_enable_one`` / ``_finish_one`` / ``_cancel_one``.
Pre-edit Blender-side translations commit to IFC on Enable
(apply_scale=False — only translation/rotation, not the user's
accidental scale), in-edit drag commits on Finish (apply_scale=True),
and Cancel restores the committed IFC placement via
``restore_or_rebaseline_placement``. Prevents the
"uncommitted drag disappears on Finish" and "preview snaps back
on Cancel" UX bugs.
2. ``_ParametricEditMixinBase`` renamed to ``ParametricEditMixinBase``
(public). Per-feature mixins that need to subclass directly
(e.g., when neither FeatureModifier nor PathPreserving fits)
can do so without reaching into a private name.
3. ``_update_modifier_bmesh`` (PathPreserving) renamed to
``_restore_viewport_after_cancel``. The old name was inaccurate
for subclasses that load a different IFC representation on
Cancel rather than rebuilding a bmesh preview from props.
Plus two polish changes:
* ``_mark_type_thumbnail_dirty`` helper on the base centralises the
``ifcopenshell.util.element.get_type`` + thumbnail-mark pattern
that both mixins repeated inline.
* ``FeatureModifierEditMixin._cancel_one`` and
``PathPreservingEditMixin._cancel_one`` wrap the restore in
``try/finally`` so ``props.is_editing = False`` flips even on
partial restore failure. Without this, a Cancel that raised
mid-restore would leave the user locked out of the edit lifecycle.
* ``PathPreservingEditMixin._finish_one`` / ``_cancel_one`` skip the
pset commit + viewport rebuild when the draft equals the stored
pset (no-op Enable→Finish round-trip should not pollute the
representation list or burn an undo entry).
``FeatureModifierEditMixin._finish_one`` now routes the pset commit
through ``tool.Pset.write_bbim_data`` instead of inlining the
``createIfcText(json.dumps(...))`` + ``ifcopenshell.api.pset.edit_pset``
dance. Two test assertions updated to match.
Generated with the assistance of an AI coding tool.
Three concerns folded into ``load_post`` argue for separation:
1. Save-file invariants every load must re-establish (msgbus
subscription, owner-settings, thumbnail cache, draft-flag healing,
blend-warning flag, H5 lock probe).
2. User-preference-driven UI setup (toolbar, workspace, viewport
shading, panel hijack, snap defaults).
3. Viewport overlay sync (every decorator's install/uninstall).
Pull each into its own function (``_apply_save_file_invariants`` /
``_apply_user_preferences`` / ``_install_viewport_overlays``). The
``load_post`` callback becomes a 3-line orchestrator. Each phase
is independently call-able from tests and from PR4 features that
need to re-trigger one phase without the others.
Two new hooks land with the decompose:
* ``tool.Parametric.heal_stale_edit_flags()`` + ``discard_pending_previews(scene)``
fire in ``_apply_save_file_invariants``. The first clears
object-level ``BIM<Name>Properties.is_editing`` flags that lost
their backing IFC element across a load; the second clears
scene-level ``BIMPreviewProperties.<x>.is_active`` so saved
preview state never resurfaces with no UI to interact with it.
* ``install_decorator_cache_handlers`` / ``uninstall_decorator_cache_handlers``
wrap the decorator install/install pass in
``_install_viewport_overlays``. The bump handlers append to
``depsgraph_update_post`` + ``undo_post`` + ``redo_post`` +
``load_post`` so the previous commit's ``TokenCache`` in
``tool.System.get_decoration_data`` finally invalidates on
structural scene changes.
Generated with the assistance of an AI coding tool.
Shared helpers for Bonsai's Scene-level parametric preview flows.
Two PR4 features will consume this — MEP bend preview and wall
fillet preview — both following the same shape:
Enable<X>Preview — populates draft on Scene.BIMPreviewProperties.<x>
Gizmo<X>Preview — polls on is_active, surfaces tunable widgets
<X>PreviewDecorator — GPU lines while is_active is True
Finish<X>Preview — bpy.ops.bim.<verb>(...) with draft kwargs
Cancel<X>Preview — pure state reset
The module hosts the cross-cutting accessors (``get_preview_props``,
``is_preview_active``), lazy-closure factories for gizmo dimension
callbacks (``make_props_callback`` / ``make_dim_getter`` /
``make_dim_setter`` — defensive against missing scene / freed RNA
struct on file open / undo), the Enable-time IFC-placement sync
(``sync_uncommitted_moves``), and the Esc + load_post discard
machinery (``PREVIEW_CANCEL_OPS`` registry, ``try_cancel_active_preview``,
``discard_pending_previews``).
Ships standalone — the consumer features land in PR4 (preview
PropertyGroups, Enable/Finish/Cancel operators, gizmo groups,
decorators, Esc keymap binding). All accessors are defensive
against missing PropertyGroups / operators on v0.8.0 — calling
``discard_pending_previews(scene)`` from the next commit's
load_post hook is a no-op until PR4 attaches BIMPreviewProperties.
Generated with the assistance of an AI coding tool.
System decoration draws on every viewport refresh — the
``_build_decoration_data`` body walks every distribution element,
resolves connected ports, builds the vert/edge arrays for the GPU
batch. A bare call per frame burns time on an unchanged scene.
Add a single-entry cache keyed on ``(decorator_cache_token,
id(decorated_elements_set))``. Reads short-circuit when neither
component moved:
* ``decorator_cache_token`` from ``bim.decorator_cache`` invalidates
on depsgraph / undo / redo / load via the bump handler.
* ``id(decorated_elements_set)`` invalidates when
``SystemDecorationData.load()`` reassigns the set (e.g. when the
user changes the set of decorated systems via the panel).
The handler that bumps the token is installed in the next commit
(bim/handler.py decompose). Until then the token stays at 0, so
the cache only hits when ``id()`` also matches — degraded behaviour
during the bisect window but not incorrect.
Generated with the assistance of an AI coding tool.
New helper module for POST_VIEW decorators. Exports:
* ``get_decorator_cache_token()`` — global int counter consumers
include in their cache key so the value invalidates on structural
scene changes.
* ``_bump_decorator_cache_token()`` — ``@bpy.app.handlers.persistent``
callback that increments the token. Gates on the depsgraph payload
so animation playback / driver evaluation doesn't churn the token.
* ``install_decorator_cache_handlers`` / ``uninstall_…`` — idempotent
append / remove against depsgraph_update_post + undo_post + redo_post
+ load_post. Called once from ``bim.register`` / ``unregister``.
* ``TokenCache[T]`` — single-entry memoiser keyed on ``(caller_key,
token)``. Cached ``bpy.types.Object`` references can't outlive the
underlying ID blocks because any depsgraph / undo / load bumps the
token and forces a recompute.
This commit ships the module standalone. The next commits in this
PR wire it: tool/system.py adds the cache wrap on get_decoration_data
and bim/handler.py installs the bump callbacks. Until both land,
the module is intentionally dead code — keeps the diff narrow and
the commit history bisectable.
Generated with the assistance of an AI coding tool.
Five code paths in slim PR2 referenced symbols that don't exist in
v0.8.0's bim layer, raising at first call. Plus three type
annotations that ty flagged as unresolved.
1. tool/system.py:get_decoration_data — drop the cache layer that
keyed on a token from a bim/decorator_cache.py module. The cache
is dead-or-broken in slim: the depsgraph bump handler that would
invalidate the token lives in PR3's bim/handler.py decompose, so
the token stays at 0 forever. Either the cache never hits
(decorated_elements rebuilt → new id() per call) or returns
stale data (list reused). Revert to direct
`_build_decoration_data()` calls. PR3 reintroduces the cache
atomically: decorator_cache module + handler install + cache
wrap + tests. Keeps `_build_decoration_data` extraction
(cleaner than v0.8.0's monolithic version regardless of cache).
2. tool/spatial.py — add `get_host_element` + `get_host_wall`.
The interface stubs in `core/tool.py:1037-1038` were declared
but never implemented. `tool/duplicate.py:99` (object duplication
with fills) and `tool/model.py:1260` (array per-child opening
mirror) call these and would raise AttributeError.
3. tool/model.py:recreate_wall — drop the fillet-corner branch
that function-locally imports `regenerate_fillet_corner_wall`
from `bim/module/model/wall`. The function lands with PR4; fall
through to the straight-extrusion path preserves v0.8.0
behaviour for fillet walls until then. Tag FIXME(PR4).
4. tool/model.py — drop `get_pipe_segment_props` /
`get_duct_segment_props` accessors. Their return types reference
`BIMPipeSegmentProperties` / `BIMDuctSegmentProperties` which
land with PR4's prop.py; calling either accessor on v0.8.0 would
AttributeError on `obj.BIM<X>SegmentProperties`. Zero callers in
slim — PR4 reintroduces both accessors together with the
PropertyGroups they wrap. Also drops the matching TYPE_CHECKING
imports.
5. tool/blender.py:557 — `Mapping[type[ViewportDecorator], bool]`
needs the qualified `Blender.ViewportDecorator` because the
annotation is on a method INSIDE the same nested class; the
bare name doesn't resolve at type-check time.
6. core/tool.py Surveyor — drop the `obj: "bpy.types.Object"` /
`z: float` / `-> float` / `-> None` annotations on
`get_z_rotation` / `set_z_rotation`. The `@interface` decorator
wraps each method as `classmethod(abstractmethod(...))` at
import time, but ty doesn't track the wrap and flags every
call site as `missing-argument` plus the `pass` body as
`empty-body` against the declared return type, plus the
`bpy.types.Object` forward-ref as `unresolved-reference`.
Reverting to v0.8.0's untyped style (matching the sibling
`get_absolute_matrix(cls, obj)` stub) clears six ty errors at
the cost of zero runtime semantics — the abstract stubs only
serve as registry markers, concrete `tool.Surveyor.*` carries
the real signatures.
Generated with the assistance of an AI coding tool.
Surfaces any regression in:
* the modules dict in bim/__init__.py (added a folder, forgot the entry)
* PointerProperty wiring on bpy.types.{Scene,Object,...}
* registry-driven GizmoPreferences<Name> auto-registration in
tool.Parametric.iter_gizmo_preference_classes
* bpy.app.handlers append/remove balance
* every register()/unregister() across the 45+ feature modules
as a single PASSED/FAILED test instead of the silent "addon failed to
enable" users encounter in a fresh Blender. Paired with the existing
test_parametric_registry.py contract tests, this catches both the
registry-shape regressions (operators/PropertyGroups/predicates) and
the registration-mechanics regressions (PointerProperty types not
registered before their owners).
Generated with the assistance of an AI coding tool.
Three corrective fixes folded into one commit. All surface as
addon-load / save-time exceptions on v0.8.0's bim layer because
PR2's tool.Parametric refactor over-committed to the PR4 contract.
1. iter_gizmo_preference_classes — the previous implementation
returned only the shared GizmoPreferencesFeature class. v0.8.0's
bim/ui.py declares PointerProperty fields ('door', 'window', ...)
on GizmoPreferences that point at per-feature
GizmoPreferences<Name> classes; those must be registered BEFORE
GizmoPreferences itself. The shared-class-only return broke
addon registration with:
'door' PointerProperty could not register (see previous error)
Restore the v0.8.0 per-feature lookup (iterate EDIT_TYPES, look
up each GizmoPreferences<Capitalize(name)> on ui_module) and
keep the shared-class lookup as forward-compat. Tag FIXME(PR5).
2. EDIT_TYPES — drop the array / pipe_segment / duct_segment
entries from the registry. Their bim.finish_editing_<name>
operators land with PR4. Registering them in PR2's EDIT_TYPES
without the operators makes auto-commit-on-save dispatch a
non-existent finish_op for any object whose
BIM<Name>Properties.is_editing flag is True, raising:
RuntimeError: 'bim.finish_editing_array' must be a registered
tool.Ifc.Operator subclass for undo-safe IFC mutation
PR4 re-adds the three entries together with their operators.
Tag FIXME(PR4).
3. tool.Blender.Modifier shim block — upgrade the prose comment to
a formal FIXME(PR5) marker so the PR5 cleanup sweep finds it via
grep alongside every other tagged shim site.
Generated with the assistance of an AI coding tool.
Fixes addon-load ImportError that surfaces when tool/geometry.py
and tool/model.py (extracted in C8 / C9) reference symbols that
don't exist on v0.8.0:
* bim/ifc.py: get_cache_or_detect_lock — IfcStore.get_cache
variant that tracks the multi-instance-cache-locked-by-other-
process flag, sets it on PermissionError, clears it (along with
the dismiss flag) on subsequent success. Used by
tool.Geometry.* to gate IFC cache reads without crashing when
another Blender instance holds the cache lock.
* tool/cad.py: WELD_TOLERANCE constant + paired CAD helpers
(auto-detect-curves vertex precision, polyline normal helpers,
etc.) used by tool.Model.* + by the parametric model operators
that land in PR4.
Both modules had zero upstream commits since the gizmos-8088 fork
point — safe bulk extraction. PR4 has no caller-line work for
either file (the additions are pure additions, no existing API
removed); the v0.8.0 callers of get_cache_or_detect_lock and
WELD_TOLERANCE are the PR2-scope files that needed them.
Generated with the assistance of an AI coding tool.
The previous commit moved is_<type> predicates off tool.Blender.Modifier
onto tool.Parametric, and earlier C4 moved the Array helper bag off
tool.Blender.Modifier.Array onto tool.Array. PR4 will migrate every
caller; this commit keeps the OLD entry points alive as thin delegates
so PR2 ships without breaking ~30 caller sites that still spell the
old API in v0.8.0:
* tool.Blender.Modifier.is_door / is_railing / is_roof / is_stair /
is_wall / is_window — delegate to tool.Parametric.is_<type>.
* tool.Blender.Modifier.Array.bake_children_transform / constrain_
children_to_parent / get_all_children_objects / get_all_objects /
get_children_objects / get_modifiers_data / remove_constraints /
set_children_lock_state — delegate to tool.Array.<same name>.
These shims are removed in PR5's cleanup commit once PR4 has rewritten
the call sites in bim/import_ifc.py, bim/module/geometry/operator.py,
bim/module/geometry/data.py, bim/module/model/array.py + the per-feature
operators (door, wall, window, railing, roof, stair, ui).
Generated with the assistance of an AI coding tool.
tool.Parametric becomes the central registry for Bonsai's parametric
features (wall, slab, door, window, railing, roof, stair, plus
mep-segment variants). Each feature registers a ParametricObject spec
declaring its enable/finish/cancel op names, props accessor, regen
callback, and is_element_type predicate.
Public surface:
* tool.Parametric.WALL / SLAB / DOOR / WINDOW / RAILING / ROOF /
STAIR / PIPE_SEGMENT / DUCT_SEGMENT — typed accessors per feature.
* tool.Parametric.is_wall / is_door / is_window / is_railing /
is_roof / is_stair — element-type predicates that move off
tool.Blender.Modifier into the parametric registry. The next
commit adds backward-compat shims on tool.Blender.Modifier so
v0.8.0 callers keep working.
* tool.Parametric.is_object_editing(obj) — returns the registered
feature an object is currently editing, or None.
* tool.Parametric.run_bim_op(op_name) — invoke a parametric op by
bl_idname.
* tool.Parametric.heal_stale_edit_flags — clear is_editing flags
on file load so a saved-mid-edit project doesn't leave gizmos
poll-locked.
* supports_build_edit_lifecycle field on ParametricObject — declares
whether the feature implements the build/edit/cancel triad.
The previous bare `print(f"Bonsai: commit of {obj.name!r} via
{finish_op} failed: {e}")` exception-handler is replaced with
logger.warning(..., exc_info=True). Same channel (Bonsai configures
logging to the Blender console at WARNING level), strictly more
information (full traceback), correct idiom for an error-path
message. A second logger.warning is added for parametric predicate
failures, also exception-handler scope.
Generated with the assistance of an AI coding tool.
tool.Model gains:
* get_pipe_segment_props / get_duct_segment_props — typed prop accessors
for the MEP-segment edit lifecycle.
* resolve_active_props_for_edit — picks the right BIM*Properties to
drive a parametric edit triad based on the active object's IFC class.
* mirror_parent_void_fillings_to_children — when an array parent has
hosted fillings (door/window in a wall), replicate the same fill
rels onto each array child. Uses tool.Array.get_parametric_propagation_
targets so the propagation stays within the array family (the old
get_all_element_occurrences over-propagated to standalone occurrences
of the same type, which silently mutated unrelated arrays).
* unshare_opening_representation — fork a shared IfcShapeRepresentation
so editing one opening doesn't mutate its array sibling.
* duplicate_ifc_objects gains a post-condition select-restore on the
array parent so callers don't get a deselected parent for N>=2 arrays.
sync_object_ifc_position is kept as a thin delegate to
tool.Geometry.commit_placement_if_moved (the new home, added in C8) so
the 6 v0.8.0 callers in mep / product / system don't AttributeError;
PR4 migrates each caller and removes the delegate.
tool.Pset gains:
* upsert_pset — get-or-add-or-edit in one call.
* write_bbim_data — JSON-encode + write BBIM_* metadata in one call.
tool.Slab is new — slab-specific reads (active extrusion, axis
direction) used by the slab gizmos, pure-IFC, no PropertyGroup mutation.
Generated with the assistance of an AI coding tool.
Adds:
* get_body_representation(element) — DRY of the repeated
ifcopenshell.util.representation.get_representation(element, "Model",
"Body", "MODEL_VIEW") call across slab / wall / opening / stair /
roof / door / window / mep. One central place to read the body rep;
every caller stops re-spelling the four magic strings.
* has_axis_representation(element) — predicate for elements with a
GRAPH_VIEW Axis representation. Used by the wall/MEP path decorators
to skip elements without an unambiguous 1D path.
* has_material_styles(element) — predicate for whether the element
carries IfcStyledItem material assignments.
* restore_placement_from_ifc(obj, element) — snap obj.matrix_world back
to element's committed IFC placement + rebaseline the drift checksum.
* restore_or_rebaseline_placement(obj, element) — Cancel-flow helper:
restores if ObjectPlacement exists, just rebaselines the checksum if
not.
* detach_representation(product) — remove the active representation
from a product without deleting the entity (used by parametric
rebuilds that wipe + re-add).
commit_placement_if_moved docstring expanded with a "drop-in scope"
note so callers don't redundantly wrap it in an is_moved check that
the helper already does.
Switches the duplicate-aware helper calls (formerly tool.Root.*) to
tool.Duplicate.* now that the service exists (C6).
Generated with the assistance of an AI coding tool.
Adds:
* ViewportDecorator base class — install/uninstall/draw lifecycle for
3D viewport gpu overlays, with handler-rollback-on-failure so a
partial install can't leave dangling draw handlers.
* sync_all classmethod — drive each listed ViewportDecorator subclass
to its desired install state in one call.
* is_view_top_down + top_down_factor — viewport-camera orientation
predicates used by gizmo billboarding and decorator layout.
* get_screen_up_world — screen-up vector in world space for gizmo
text orientation.
* are_viewport_gizmos_enabled — central gate for the global
draw_gizmos_in_3d_viewport pref, replacing duplicated prefs reads.
* DecoratorColors NamedTuple + get_decorator_colors — single source
for the colour palette every viewport decorator binds.
Preserves Ryan Schultz's add_layout_hotkey_operator polish (719309571,
2026-05-25): the row-position move + separator(factor=1) between the
modifier and key icons stay intact in this extraction.
Generated with the assistance of an AI coding tool.
Extract the duplicate-aware relationship-walk + restoration logic
(get_decomposition_relationships, get_connection_relationships,
get_port_connection_relationships, recreate_decompositions,
recreate_connections, recreate_port_connections, consume_warnings)
out of tool.Root into its own service.
tool.Root's responsibility is identity and addressing of IFC roots;
the duplicate-aware bookkeeping of "before duplication, what relations
did this graph have, and how do I restore them on the new copies?"
deserves its own home. The split was already declared on core/tool.py
(C2); this commit lands the concrete tool.Duplicate implementation.
tool.Root keeps its own copies of the methods on v0.8.0's tool/root.py
during this PR so callers in bim/module/spatial/operator.py keep
working at runtime; the Root cleanup lands in PR4 alongside the
caller updates.
Generated with the assistance of an AI coding tool.
Adds:
* direction_from_port_pair(port_a, port_b) — derive the connect_port
direction kwarg from each port's FlowDirection (NOTDEFINED for
non-canonical pairs). Centralises a pattern that callers were
inlining inconsistently.
* tool.System.walk_connected_mep_elements — BFS over connected MEP
flow elements via IfcRelConnectsPorts.
* tool.System.get_port_world_position — port placement → world-space
Vector, used by the MEP path decorator.
* tool.System._build_decoration_data — cached decoration metadata
for the MEP system-path overlay.
Plus a get_port_relating_element return-type tightening (Union with
None) and a partial-init cycle workaround on bim.module.system.data
imports (now function-local — top-level import triggered the cycle
through tool.Ifc.Operator).
Generated with the assistance of an AI coding tool.
Top-level array-domain service extracted out of tool.Blender.Modifier.Array.
Owns the BBIM_Array pset graph navigation (constrain_children_to_parent,
remove_constraints, get_modifiers_data, get_children_objects,
get_all_children_objects, get_child_layer_index, bake_children_transform),
plus the Blender-side CHILD_OF constraint lifecycle that ties each child
replica to its parent's transform.
Array's own module gives the parent/child semantics a clean home — array
behaviour was previously scattered between tool.Blender.Modifier and ad-hoc
helpers in bim/module/model/array.py. The relocation eliminates the inline
duplication and gives Bonsai callers a single import surface.
Generated with the assistance of an AI coding tool.
Bpy-permitted wall reads — get_axis_local_extent, get_length_and_height,
get_x_angle, get_path_connection_location, walk_connected_walls — used
by gizmo lambdas that need wall dimensions and join topology without
the side effect of loading the wall's draft BIMWallProperties (the
loader mutates PropertyGroup state and would clobber the wall's own
gizmo state when both the wall and a hosted filling are selected).
All reads go through ifcopenshell.util.representation / .util.element
so the IFC graph stays the source of truth. tool.Wall consumes
core.model's PARALLEL_DOT_THRESHOLD + collinearity helpers (no inline
magic numbers).
Generated with the assistance of an AI coding tool.
Declares the bpy-free contract for tool services landing in subsequent
commits — tool.Wall, tool.Array, tool.System, tool.Duplicate (extracted
from tool.Root), tool.Parametric, plus minor additions on existing
interfaces (tool.Spatial.get_host_element / get_host_wall,
tool.Geometry.has_axis_representation / has_material_styles,
tool.Surveyor.get_z_rotation / set_z_rotation).
The @interface declarations are empty-bodied; concrete implementations
land in the per-service tool/* commits below. Keeping the contract in
core lets core/* helpers and tests reference the surface without
importing the concrete tool modules.
Moves get_decomposition_relationships + recreate_decompositions off
tool.Root onto the new tool.Duplicate (extraction of duplicate-aware
behaviour into its own service).
Generated with the assistance of an AI coding tool.
core/model.py gains:
* Three calibrated dot-product / distance thresholds — PARALLEL_DOT_THRESHOLD
(~2° from parallel, cos(2°) ≈ 0.9994), COLLINEAR_LINE_TOLERANCE (50mm
perpendicular distance for two parallel wall axes to share a line),
BASELINE_OFFSET_TOLERANCE — replacing inline magic numbers that the
wall-join classifier, fillet-state machine, and gizmo preview decorator
all read from.
* Pure wall-join geometry helpers (project_axis_intersection,
are_axes_collinear, classify_wall_join_state, wall_join_preview_lines,
resolve_extend_walls_target, extrusion_depth_from_vertical_height,
length_and_height_from_extrusion). They take primitive tuples + floats,
no bpy, no ifcopenshell — testable in the core lane.
core/product.py is new — pure-Python aggregate-walk helpers (resolve_host_
of_product, collect_decomposed_products) that downstream tool/spatial and
tool/aggregate consumers can call without importing ifcopenshell at module
load.
Generated with the assistance of an AI coding tool.
add_railing_representation now factors into two parts:
* compute_wall_mounted_handrail_geometry returns a pure-geometry
WallMountedHandrailGeometry dataclass (handrail polyline + support
list + terminal caps), no IFC mutation.
* add_railing_representation wraps that dataclass into an
IfcShapeRepresentation as before.
Downstream consumers that want the same math without round-tripping
through an IFC file (Blender gizmo previews, viewport drafts) now
drive compute_X directly. Future add_X_representation work in the
geometry API is encouraged to follow the same shape — a sibling
compute_X function + thin IFC wrapper.
The railing_type parameter is dropped from the signature — only
WALL_MOUNTED_HANDRAIL was ever supported, so the kwarg was dead.
The Bonsai railing-modifier caller is updated in the same commit
to stop passing it; without that update Bonsai's
finish_editing_railing_path raises TypeError on the first edit.
RailingSupport and WallMountedHandrailGeometry use @dataclass(slots=True)
— they're constructed N-per-cap during arc sampling, so the per-instance
overhead matters.
Public symbols (RailingSupport, TERMINAL_TYPE,
WallMountedHandrailGeometry, compute_wall_mounted_handrail_geometry,
add_railing_representation) re-exported from ifcopenshell.api.geometry.
New test/api/geometry/test_add_railing_representation.py covers the
compute/wrap contract.
Generated with the assistance of an AI coding tool.
Drops the module-local ``mm()`` helper in favour of the centralised
``ifcopenshell.util.unit.mm_to_m`` (added earlier in this PR). The
``as mm`` import alias preserves the existing call sites' readability.
Generated with the assistance of an AI coding tool.
Drops the module-local ``mm()`` helper in favour of the centralised
``ifcopenshell.util.unit.mm_to_m`` (added earlier in this PR). The
``as mm`` import alias preserves the existing call sites' readability.
Generated with the assistance of an AI coding tool.
ShapeBuilder gains module-level NP_X / NP_Y / NP_Z / NP_XY / NP_XZ /
NP_YZ / NP_YX axis-index constants. Downstream geometry builders had
been redefining local copies for indexing np.ndarray vectors of shape
(3,) or (N, 3); centralising removes the duplication.
mep_transition_length and mep_transition_calculate verbose default
flipped from True to False. The prints are diagnostic-only output;
True-by-default spammed the console on every transition computation,
which fires per-fitting on IFC load.
Generated with the assistance of an AI coding tool.
Centralises the millimetre-to-metre conversion shortcut that
add_door_representation and add_window_representation each defined
locally. Subsequent commits in this PR switch both call sites to
import this from util.unit, removing the duplicate definitions.
Generated with the assistance of an AI coding tool.
Use add_layout_hotkey_operator for draw_regen_operations so the Regen
button shows text and shortcut icons in the sidebar like all other
panel buttons. Add a separator between modifier and key icons for
readability.