Three test files covering PR4's new surfaces — preview registry,
wall-gizmo poll behaviour, fillet operator registration. Every test
walks the live registry or class hierarchy instead of hard-coding
preview keys, operator names, or helper function names, so adding a
new preview / wall gizmo group / fillet operator exercises the same
invariants without test edits.
test_preview_base.py (6 tests):
* RegistryContract: every PREVIEW_CANCEL_OPS entry resolves to a
callable cancel operator on bpy.ops.bim.
* GetPreviewPropsTolerance: get_preview_props returns None for
contexts without a scene (regression guard for the SimpleNamespace
bug fixed in commit ee63137c6).
* ActivationCycle (registry-driven loop): any_preview_active toggles
with each registered preview's is_active flag;
discard_pending_previews clears every active flag across every
registered preview.
* SaveOnDiscardWired: locates the bim.save_project operator
dynamically and verifies its execute path references the discard
helper by its actual __name__.
test_wall_gizmo_poll_gate.py (4 tests):
* WallGizmoGroupsHideDuringPreview: walks the wall module for
bpy.types.GizmoGroup subclasses (skips preview-owner exceptions
whose bl_idname contains 'preview'), mocks any_preview_active to
True, and asserts every discovered gizmo's poll returns False.
* BaseParametricGizmoPollHidesDuringPreview: mirrors the test for
the cross-feature parametric framework base class.
test_fillet_operators.py (3 tests):
* FilletOperatorsRegistered: at-least-four-ops + every-discovered-op-
is-callable. Catches accidental deregistration.
* EnableRejectsIneligibleSelection: poll returns False without a
selection so the operator is greyed-out in menus.
State-clearing tests via bpy.ops.bim.cancel_wall_fillet_preview() are
deliberately omitted — the operator early-returns when context.screen
is unattached and prior tests in the model lane can leave the screen
in that state, making the dispatch path inherently flaky. Live testing
covers the behaviour.
Net: 13 tests pass cleanly in both single-file and full model lane.
Generated with the assistance of an AI coding tool.
Save-path:
* SaveProject._execute (project/operator.py) now calls
preview_base.discard_pending_previews(context.scene) right after
tool.Parametric.commit_pending_edits(). Previews are session-
transient — discard rather than commit. Sibling gizmo polls gate
on each preview's is_active flag; a stuck flag persisted through
the save would silently hide them on reload. Mirrors the pattern
already in gizmos-8088.
Preview-active gate hardening:
* preview_base.get_preview_props tolerates contexts without a
``scene`` attribute. Pre-existing tests use SimpleNamespace mocks
for the context; the previous getattr(context.scene, ...) raised
AttributeError before the inner default kicked in.
Test update:
* test_wall_header_refresh.test_geom_generation_invalidates_wall_geom_cache
patches tool.Wall.read_geometry instead of the now-deleted local
wall._read_wall_geometry (commit 7e5e7b8d6 migrated the call site).
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.
The single-wall unjoin gizmo needs to enumerate every
IfcRelConnectsPathElements a wall participates in, regardless of which
side of the rel the wall was authored on, and place an icon at each
join's physical location. Two helpers carry that work:
_path_connection_location_world wraps core.compute_path_connection_location
at the Vector boundary. _iter_path_connections walks ConnectedTo +
ConnectedFrom, normalises orientation to (other, self_ct, other_ct),
and filters non-wall partners + None refs so per-frame gizmo positioning
survives malformed IFC.
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.
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.
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.
test_parametric_lifecycle.py covers the door/window/railing/roof
state-transition contracts (enable/finish/cancel; no-op on
non-matching elements; draft preserved on finish-time failure)
that the registry smoke test never exercised.
test_parametric_registry.py gains a check that every is_<name>
predicate stays total (never raises on a non-matching IFC entity)
— a raising predicate would break the save path for unrelated
types. Also rewrites the gizmo-prefs check to read __annotations__
instead of hasattr, which depended on Blender registration timing.
Generated with the assistance of an AI coding tool.
Aligns with the test/bim/ convention: heavy imports go inside test
functions so the autouse _require_real_bpy fixture skips cleanly
when bpy is mocked, rather than module-level imports failing at
collection time and erroring out the whole file.
Generated with the assistance of an AI coding tool.
When two GizmoDimension hit regions overlap (a short dimension
nested inside a longer one along the same axis), the larger one
used to win because hit boxes are scaled by world-space length —
the long box fully contains the short one, leaving the short
gizmo unreachable. The larger gizmo stays clickable at its
exposed ends, so smaller-wins is the right UX default.
Sets self.select_bias = -self._dimension_length inside
GizmoDimension.set_dimension_length. The smaller gizmo writes a
less-negative depth value in the GPU select buffer and wins the
tie-break. select_bias is unused elsewhere in the codebase, so
icon and arrow gizmos keep bias=0 and are unaffected (icons
correctly still win against dimensions, since 0 > -length).
Adds test/bim/module/drawing/test_dimension_gizmo_priority.py
with 5 cases: direct ordering, monotonicity across length ranges,
abs() handling for signed dimensions, and NaN/Inf safety.
Generated with the assistance of an AI coding tool.
set_icon_gizmo_position computed
``mw @ (Translation @ billboard_rot @ Scale)`` — the object's world
matrix was applied AFTER the billboard rotation, so any non-trivial
object rotation (e.g. a wall rotated in plan, a stair rotated to
match a corridor) carried over into the icon's transform and tilted
it edge-on to the camera instead of facing it.
Switch to ``billboarded_at(world_pos, billboard_rot, scale)`` where
``world_pos = mw @ local_pos``: translate to world space first, then
apply the billboard rotation independently of the object's rotation.
This matches the manual pattern the base class's
``update_editing_gizmos`` already uses for validate/cancel/cycle for
exactly this reason.
Drops the now-stale workaround docstring on
``GizmoWallEdition._update_icon_row_extras`` that documented why it
bypassed ``set_icon_gizmo_position`` — the helper does the right
thing now.
Adds ``test/bim/module/model/test_stair_gizmos.py`` as the regression
guard: parametrised over six rotation angles, asserts that the rotation
part of the resulting matrix equals ``billboard_rot`` (no contribution
from ``mw``'s rotation) and that the translation lands at
``world_pos``. Also exercises ``set_icon_gizmo_position`` end-to-end via
a stub gizmo to catch the exact shape of the previously-broken call
site.
Generated with the assistance of an AI coding tool.
Walls gain in-viewport parametric editing matching the door/window/stair
UX: drag handles for length, height, slope (x-angle), layer baseline
cycle, plus cursor-anchored quality-of-life operators (split at cursor,
extend to cursor, extend height, rotate 90, toggle openings) and
two-object state-machine gizmos (unjoin / merge / join-corner /
extend-to-wall / extend-vertically / add-opening).
Wall enters tool.Parametric.EDIT_TYPES, so save-time auto-commit,
GizmoPreferencesWall registration, and the in-progress-edit predicates
all light up automatically through the registry plumbing landed two
commits back.
The three-layer commit model (drag -> BIMWallProperties -> bmesh
preview -> Finish -> single ifc.run) means dragging a handle through
hundreds of intermediate values produces zero extra IFC entities. A
no-op enable->finish round-trip is byte-identical. The snapshot diff
in FinishEditingWall skips unchanged params.
_commit_active_wall_edit_if_any ensures cursor-anchored operators see
committed geometry, not the draft preview box.
Also lands the `prompt_auto_commit_parametric_edits` BoolProperty on
BIM_ADDON_preferences (consumed by the auto-commit dialog landed in
the framework commit) and refactors
`draw_{door,window,stair}_gizmo_parameters` into a shared
`_draw_parametric_gizmo_parameters` helper that the new
`draw_wall_gizmo_parameters` reuses. This commit and the framework
commit are stacked - the framework commit references the BoolProperty
defined here, so they must land together.
Tests cover pure math (core/test_model.py), DimensionGizmoConfig text
formatter, GizmoWallExtendVertically.poll() preconditions, and the
refresh_post_commit cache-invalidation regression. BDD scenarios in
model.feature cover the edit triad, auto-commit on save, and the
two-object gizmos. Documentation added to creating_walls.rst.
Generated with the assistance of an AI coding tool.
Establish a single source of truth for parametric element types (door,
window, stair, railing, roof). tool.Parametric.EDIT_TYPES drives:
- BIM<Name>Properties PointerProperty attachment via the registry
- GizmoPreferences<Name> class registration in bim/__init__.py
- save-time auto-commit of pending draft edits
- the refresh_post_commit epilogue called from IfcStore after every IFC
mutation, which fixes the stale-header bug where in-place hotkey
mutations (S_E / C_E) left BIMModelProperties and the gizmo cache
pointing at obsolete values.
Refactors door/window/railing/roof onto shared mixins from
bim/parametric_lifecycle.py (FeatureModifierEditMixin and
PathPreservingEditMixin); stair gets the lock-gizmo refactor and
frame-cache integration. Behavior preserved.
Adds BaseParametricGizmoGroup._prime_frame_caches so the parametric
gizmos stop re-deriving preferences, view direction, and billboard
rotation per frame; reorders poll() to short-circuit on the cheapest
predicate first. Adds the icon library + BillboardingGizmoGroupMixin
that the wall feature in the next commit will consume.
Generated with the assistance of an AI coding tool.
When a project has a ifc file associated, selecting non-ifc objects and duplicating them with SHIFT + D now correctly both duplicate them, keep the new objects selected and starts the transform modal. IFC objects behaviour is unaffected.
Parse ifcmerge JSON output and display a per-conflict breakdown in the
panel when merge fails. Ctrl+click on the Merge button previews
conflicts without committing. Add SelectConflictEntity operator to
select and frame the conflicting object in the 3D viewport.
Generated with the assistance of an AI coding tool.
Move all business logic into bonsai core and tool. Performance fixes to
minimise file IO, various minor bug fixes and tests.
Generated with the assistance of an AI coding tool.
Fix#7034
bpy.ops.bim.extend_to_underside doesn't exist - the correct operator
name is bim.extend_walls_to_underside. The AttributeError killed the
entire panel draw, hiding mirror, align, aggregation, and QTO buttons.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add tests for all four covering generation operators: flooring/ceiling
from walls and flooring/ceiling from cursor. Previously only flooring
from walls was tested.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Walk up the full hierarchy via get_parent() in can_nest() and
can_aggregate() to reject assignments that would create a cycle.
Also reject self-assignment.
Fix#7248
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests using custom first/last tread runs were not setting
custom_tread_lock=False, so the custom values were silently ignored
since 8f7cf76d9 introduced the lock gate in the calculation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix#7712 - global alignment controls now affects all literals
* Fix#7760 - goodbye 3x3 box alignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Since we added more data to debug info in fcf5614 Fatal Error itself started to fail and was never displayed due some props being inaccessible during load, should be fixed now.
Possible error that were fixed:
```
File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 328, in <module>
print(format_debug_info(get_debug_info()))
~~~~~~~~~~~~~~^^
File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 117, in get_debug_info
if bpy.data.is_saved:
^^^^^^^^^^^^^^^^^
AttributeError: '_RestrictData' object has no attribute 'is_saved'
Traceback (most recent call last):
File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 366, in draw
info = get_debug_info()
File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 152, in get_debug_info
bim_props = tool.Blender.get_bim_props()
^^^^
NameError: name 'tool' is not defined. Did you mean: 'bool'?
Traceback (most recent call last):
File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 366, in draw
info = get_debug_info()
File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 141, in get_debug_info
import bonsai.tool as tool
File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\__init__.py", line 355, in <module>
print(format_debug_info(get_debug_info()))
~~~~~~~~~~~~~~^^
File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\__init__.py", line 141, in get_debug_info
import bonsai.tool as tool
File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\tool\__init__.py", line 23, in <module>
from bonsai.tool.attribute import Attribute
File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\tool\attribute.py", line 31, in <module>
import bonsai.bim.helper as helper
File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\bim\__init__.py", line 28, in <module>
from . import handler, operator, prop, ui
File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\bim\handler.py", line 36, in <module>
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\bim\module\aggregate\__init__.py", line 21, in <module>
from . import operator, prop, ui
File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\bim\module\aggregate\operator.py", line 32, in <module>
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
^^^^^^^^
AttributeError: partially initialized module 'bonsai.tool' from '\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\tool\__init__.py' has no attribute 'Ifc' (most likely due to a circular import)
```
Blender doesn't have it's own quick favorites manager and working with them can be not very flexible - you can add them in context menu and remove them from Quick Favorites menu. But you can't reorder them, you can't rename them and you can't even add a new button to favorites if it's not added by some addon in the UI.
Have been stumbling upon this for awhile and decided to create an experimental manager UI for this. Things it can do:
- help user create a button with any operator in Blender and properties they prefer to then save it Quick Favorites. Which seems can be very useful in Bonsai, since you can create separate buttons for all kinds of selectors expressions, class assignment or other operators.
- it can import quick favorites from user's actual current quick favorites, so they can just modify them a bit, reorder, rename and then add them again.
- Since quick favorites are not exposed to Python API in Blender, we're using a very hacky way to retrieve them from Blender and don't provide our own buttons for adding and removing quick favorites, as it may be dangerous and even more hacky in implementation. So the workflow for user is to either generate some buttons and add them to quick favorites using Manager or to import it's own quick favorites, then change them how they like, then remove quick favorites using usual quick favorites menu and then add new button one by one.
Small demo - https://files.catbox.moe/vyffp6.mp4