Add tool.Array.is_array_child helper. Port decorator and MEP
action gizmos (lock, pen, join) hide on array children — writes
on children get wiped by the next regen, and the port topology
is inherited from the parent.
Introduce tool.Array.select_only_parent and wire it into both
bim.regenerate_array and bim.finish_editing_array so post-regen
state converges on parent-only-selected + active. Grow and shrink
paths otherwise diverge (grow left new children selected alongside
the parent; shrink left only the parent).
Relates to #8088.
Generated with the assistance of an AI coding tool.
Replace N sequential duplicate_ifc_objects([parent]) calls in
_regenerate_array_body with one duplicate_ifc_object_n_times call
per layer, batching the fixed per-call overhead (snapshot gather,
UI refresh, decorator reload).
Guard batch_host_recut drain against dead StructRNA refs and prune
orphan array-child GUIDs at regen so outliner-delete of a
Bonsai-managed child cannot crash subsequent regenerate_array.
Recalculate walls after recreate_connections so Shift+D of
connected walls produces correct junction geometry without a
manual regen step.
Relates to #8088.
Generated with the assistance of an AI coding tool.
is_drawing_active() required an open VIEW_3D area purely as a poll()
gate for bim.create_drawing, even though SVG generation is
ifcopenshell.geom-based with no viewport dependency; skip that check
when bpy.app.background is true, since a viewport is neither
obtainable nor meaningful there. Interactive behaviour is unchanged.
Generated with the assistance of an AI coding tool.
Store the selector query used at Link IFC time on the Link
PropertyGroup, restore it from the sidecar cache JSON on host
IFC reopen, and forward it through LoadLink and ReloadLink so
subsequent reloads replay the original filter instead of loading
every element. ReloadLink now opens a small dialog pre-populated
with the current query, allowing the user to edit it in place
without unlink-and-relink.
Also swap TestCalculateLinkMatrix off NamedTemporaryFile(delete=True)
which held an exclusive Windows handle and blocked the
code-under-test from reopening the sidecar path.
Closes#8219
Generated with the assistance of an AI coding tool.
Add two helpers to tool.Blender that 17+ existing viewport decorators
re-implement byte-identically:
- ViewportDecorator.draw_batch(shader_type, content_pos, color, indices=None)
collapses the validate + batch_for_shader + uniform_float + draw cycle
every shader-driven decorator needs.
- Blender.transparent_color(color, alpha=0.1) is the RGBA-alpha-override
helper duplicated across nest, project, aggregate, model, system module
scopes plus six nested-def copies inside draw methods.
Pure additions on the tool/ layer with direct unit tests covering the
default-alpha, explicit-alpha, non-mutation, new-list-instance, and
validation-guard branches.
Generated with the assistance of an AI coding tool.
Refs gh#8088. Array regen + multi-opening drops fan out N+1 wall recuts
per operator (one per child filling deletion + the final mirror recut),
making CSG opening-subtraction O(N^2) for a linear UX action.
Introduces tool.Geometry.batch_host_recut() — a context manager that
coalesces switch_representation and bpy.ops.bim.update_representation
calls per voided element within one operator transaction. The drain
re-reads the active representation so the recut reflects current IFC.
Wraps 7 entry points (regenerate_array, RegenerateArray, RemoveArray,
AddOpening, RecalculateFill, CloneOpening, regenerate_from_type) and
rewires 7 leaf call sites in opening.py, void/operator.py, and
mirror_parent_void_fillings_to_children.
An AST forward-compat guard pins the rewire contract: no direct
switch_representation or bpy.ops.bim.update_representation in the
three target files outside the helper definitions.
A 16-child array regen now recuts the wall once instead of 17 times.
The CSG cost per recut is unchanged; only the count is reduced.
21 new tests across three lanes (helper unit, entry-point coalescing,
AST guard) — all green.
Generated with the assistance of an AI coding tool.
Schema-illegal IfcDoor->IfcWallType pairings parse cleanly but propagate
into operators that fan out by type and eventually crash the wrapper.
Block the pairing at its source: API guard in ifcopenshell.api.type.
assign_type, per-object partition in BIM_OT_assign_type + DuplicateType,
new tool.Type.is_relating_type_compatible helper, AST forward-compat
guard. Files in the wild are still loaded unchanged.
Generated with the assistance of an AI coding tool.
The "mep-pair-fitting" kind added in the previous commit carries an
IfcFlowFitting (the entity whose deletion disconnects the pair), not a
relationship entity, in the dispatch slot — but the slot was named ``rel``
across the function signature and every call site. Rename to ``subject``
so the parameter name reflects the uniform intent: "the entity whose
teardown effects the disconnect", regardless of whether that's a rel or
a fitting.
Sweep covers:
- core.connection.disconnect_rel signature + body
- tool.Connection.find_rels / find_rels_for_element / find_rel docstrings
- The cascade-on-delete call site in tool.Geometry.delete_ifc_object
- DisconnectElements operator in bim.module.model.wall
- All affected test kwargs and AST forward-compat docstring
- Error message: "Unknown rel kind" -> "Unknown kind"
No behaviour change.
Generated with the assistance of an AI coding tool.
Three user-facing fixes for the MEP-system gizmo surface:
1. MEP pair-disconnect no longer crashes Blender. The
MEPSystemPathDecorator cached entity_instance references in
_cached_walk; deleting a bridging fitting via the gizmo left a freed
SWIG handle in the list, and the next _build_geometry pass segfaulted
on .is_a. The cache now stores STEP integer ids and re-resolves via
ifc_file.by_id on each draw, plus folds tool.Parametric.get_geom_generation
into the cache key — ifcopenshell.api mutations invalidate before the
next frame regardless of how the deletion was routed.
2. Bend re-edit pen icon stays reachable. The bend creation path
tessellates the swept-disk body (upstream geometry-kernel workaround),
so tool.System.has_parametric_body correctly returns False for a
freshly-committed bend. _active_is_bend_fitting and
GizmoMEPActions.is_eligible_object now fall back to the type's
BBIM_Fitting pset — the same source bim.enable_bend_preview_from_bend
reads parameters from — keeping the pen icon eligible.
3. MEP pair / per-port unjoin icons unified through bim.disconnect_elements.
The MEP gizmo group's three unjoin icons (pair, start, end) now share
the wall-disconnect surface: same VIEW3D_GT_wall_link_toggle icon, same
bim.disconnect_elements operator. tool.Connection.find_rels learned a
new "mep-pair-fitting" kind that returns the bridging fitting as the
disconnect target; core.connection.disconnect_rel grew the matching
dispatch arm. The old MEPUnjoinAtPort and MEPUnjoinPair operators are
removed.
Also registered wall.GizmoPairDisconnect (previously declared but never
in the classes tuple, so dead code) for the wall+slab pair-disconnect
surface, and extracted MEP port-topology helpers (find_bridging_fitting,
is_disconnectable_fitting, neighbours_at_ports) onto tool.System so the
canonical walk has a single home.
Generated with the assistance of an AI coding tool.
Add source-based clip box presets — a dropdown menu next to the Add
Clip Box button lets the user pre-size a clip box to the bounding box
of a chosen IFC source: a spatial element, IFC class, type, material,
profile, drawing camera frustum, status, system, group, or zone. The
picker dialog uses prop_with_search so files with hundreds of materials
or types remain browsable.
Add interactive face resize handles — six near-invisible click-target
quads render on the active clip box when its empty is the active
object. Dragging a face grows or shrinks the box one-sided on that
axis; the opposite face stays fixed. Ctrl+Click on a face aligns the
viewport to look at that face, following Blender's numpad-view
convention applied to the box's local axes so rotated boxes align
orthogonally to the screen. The gizmos honour negative-scale empties
so the visible cube and the clickable handles stay aligned.
Add settings and info menus — a gear-icon menu next to the Enable
Clipping / Show Caps toggles exposes per-file preferences (cap only
IFC products, show face handles); an info-icon menu adjacent documents
the gizmo gestures. A quick-access toggle row also appears in the
viewport Overlay popover, greyed out when no clip box exists, and
orphaned clip-box list entries now expose an X button so users can
recover from external host-empty deletions.
Plumbing: cap rebuild fires synchronously on gizmo release and
clip-box selection change, so the cross-section overlay re-forms
without waiting for the depsgraph debounce; cap eligibility honours
the "Only IFC Products" toggle. Includes 121 tests covering source
resolution, drag math, face visibility, gizmo registration, and the
view-alignment up-axis convention.
Generated with the assistance of an AI coding tool.
Adds a viewport clip box feature to Bonsai: a user-controllable oriented bounding box that hides everything outside its 6 faces and draws filled cross-section caps where IFC product geometry intersects the planes.
Quality and coordination > Sandbox > Clip Box
Adds geometry primitives the viewport clip-box feature needs:
- obb_world_clip_planes / obb_clip_planes_from_matrix: derive the 6
inward clip planes of an oriented bounding box (or unit cube under
a matrix_world) in RegionView3D.clip_planes form. expand / expand_rel
margins let callers visualising the box with overlapping geometry
(an empty CUBE display sharing edges with the planes) keep the box's
own wireframe inside the clip volume.
- point_is_inside_clip_planes / corners_might_cross_clip_planes: cheap
reject tests for the per-mesh capping pass to skip the expensive
bisect when an object's AABB is fully outside the box.
- newell_normal / plane_basis: robust planar-ring normal for thin
near-degenerate cap rings where a two-edge cross product is unstable.
- tessellate_ring_planar: triangulate [outer, *inners] 3D rings in the
outer ring's best-fit plane, with a shapely constrained-Delaunay
fallback for the known failure mode of mathutils.tessellate_polygon
on complex concave polygons-with-holes.
Tests cover unit-box, translated, rotated, and scaled cases for the
OBB-from-matrix builder + the rejection helpers.
Generated with the assistance of an AI coding tool.
* Tolerate stale array child/parent GUIDs
A real-world IFC project (an arrayed door whose host got deleted
externally) crashed Bonsai's project load with "Instance with
GlobalId not found" inside setup_arrays.
tool.Blender.get_object_from_guid declared Optional return but let
RuntimeError propagate; callers iterating BBIM_Array child lists then
crashed instead of skipping. Honour the documented contract by
returning None on miss, matching the convention used by every other
by_guid lookup helper in tool/array.py, tool/ifc.py, tool/geometry.py.
Sweep the four user-action sites that resolve array child/parent
GUIDs without a guard - they shared the same bug class but were
reachable from different operators (regenerate_array, RegenerateArray
clear, duplicate_ifc_objects, process_arrays). An already-missing
entity is the desired terminal state for each, so the fix is
try/except RuntimeError: continue/skip.
setup_arrays now also collects each parent with at least one stale
child GUID into IfcImporter.broken_arrays, surfaced via a new Project
panel banner mirroring the existing pending_opening_recut UX. The
banner reports the count and offers "Select Elements" to navigate to
the affected array parents and a Dismiss button.
constrain_children_to_parent was being called once per layer inside
setup_arrays' for loop even though it always iterates all layers
internally - lifted out of the loop (pre-existing N x perf bug
that the stale-GUID print exposed).
Regression tests:
- test_returns_none_when_guid_not_in_file pins the get_object_from_guid
Optional contract.
- test_remove_array_tolerates_stale_child_guid injects a fake child
GUID into BBIM_Array.Data and asserts bim.remove_array completes
cleanly.
Generated with the assistance of an AI coding tool.
* Black: wrap long bl_description in dismiss_pending_array_repair
Generated with the assistance of an AI coding tool.
Wall body rebuild + slab underside re-clip are now unified behind
tool.Model.regenerate_wall and called from split / merge / extend
operators. Fillet corner walls accept extend-to-underside (poll +
operator partition switched to is_path_connectable_wall) and surface
the wall-unjoin gizmo without the parametric-edit gate, since
fillets cannot enter that lifecycle. DumbWallJoiner.split strips the
duplicate's inherited slab-trim booleans up front so wall2 lands at
the cut point. regenerate_fillet_corner_wall re-clips after the body
rewrite so a prior extend-to-slab survives neighbour recalcs.
Drive-by bug sweep: tuple typo in hotkey_S_G's IfcSpace check,
defensive .get() in draw_regen_operations for partial AuthoringData
loads, and a try/except in get_active_representation matching the
existing convention for stale mesh ifc_definition_ids after a
representation rebuild.
Tests cover the regenerate_wall branching, the get_active_representation
stale-id contract, and the GizmoWallExtendVertically fillet acceptance.
Generated with the assistance of an AI coding tool.
Deleting a slab that was connected to a wall via IfcRelConnectsElements(TOP)
left the wall holding orphan IfcBooleanResult items + a stale BBIM_Boolean
pset. The disconnect operator already runs the right cleanup; element delete
just never invoked it.
Extract the per-kind cleanup into core.connection.disconnect_rel so the
operator (bim.disconnect_elements) and a new cascade in
tool.Geometry.delete_ifc_object share one dispatch table. Adding a future
rel kind to tool.Connection.find_rels now flows into both call sites
automatically; an AST forward-compat guard enforces coverage.
Other adjustments:
- regenerate_wall_to_underside zero-slab branch now removes stale clip
booleans instead of silently skipping, so disconnecting the last TOP
slab also reverts the wall correctly.
- duplicate_ifc_objects (Shift+D) calls strip_underside_booleans on copied
walls so the duplicate doesn't carry over the source's slab trim, then
reloads the body representation when something was stripped so the
viewport reflects the change without waiting on Shift+G.
- batch_being_deleted_ids threads through OverrideDelete so the cascade
can suppress partner-side regenerate when both endpoints are queued for
deletion in the same batch.
This file was generated with the assistance of an AI coding tool.
The check `len(bytedata) == n * 2` was wrong: float64 is 8 bytes per
element, not 2. Legacy float64 checksums fell through to the float32
reader and produced a (2n,)-shaped array, breaking is_moved() and
is_camera_moved() with `ValueError: operands could not be broadcast`
on .blend files saved by Blender <5.0.
Adds a parametrized regression test covering both n=3 (translation)
and n=9 (rotation) for both dtypes.
Generated with the assistance of an AI coding tool.
Two hot paths the gizmo polls fire every viewport event memoise
their result against tool.Parametric.get_geom_generation():
- tool.Blender.Modifier.any_selected_array_child caches the
per-selection scan against the selection identity-set + the
IFC generation token so a stable selection during a drag
doesn't re-walk every selected object's BBIM_Array pset every
frame.
- bim/module/model/wall.py grows a pair-predicate + connection
cache that the wall topology gizmos hit; both keyed on
(pair_uids, predicate_kind, generation) so a wall split or
axis edit invalidates correctly via the generation bump.
Behavioural contract is unchanged — stale entries are evicted
on generation bump; cache miss returns the same value the
un-cached path returned.
Generated with the assistance of an AI coding tool.
The opening preview's outline used a single-batch two-pass scheme that
dimmed the occluded back pass via alpha=0.25. The visible front pass also
inherited the source decorator color's modest alpha, so the outline read
as subtle on both sides.
Replace with a CAD hidden-line convention: solid full-alpha front pass on
the visible side, world-space dashed back pass on the occluded side. Both
passes use POLYLINE_UNIFORM_COLOR so depth and line-weight paths match.
The dashed batch is built once per object epoch by a new pure helper
tool.Blender.build_dashed_line_segments (pre-segments edges into world-
space dash chunks), then cached via the existing batch-cache mechanism
under "<uid>_dashed".
The solid front pass is rendered at a slightly wider line width than the
dashed back pass so its halo overpowers Blender's WIRE-display overlay
bias at outline pixels — without the asymmetry the wire's anti-z-fight
forward bias makes the LESS_EQUAL comparison narrowly fail and the
dashed pass wins on visible edges too.
Generated with the assistance of an AI coding tool.
A user clicking the pen icon on a typed-product occurrence whose body
representation is mapped from its type would silently mutate every
sibling occurrence's geometry. Add a confirmation dialog at the pen-icon
dispatcher (the single chokepoint every feature routes through) showing
the sibling count, with a session-scoped suppress checkbox.
The check is read-only: tool.Model.get_sibling_occurrence_count wraps
tool.Geometry.get_elements_by_representation against the resolved body
rep and subtracts self + type. A forward-compat AST guard pins the
dispatcher monopoly so any future feature that binds pen_gizmo directly
to a feature-specific enable op fails the test before merge.
Generated with the assistance of an AI coding tool.
Spatial.get_root_element walks aggregate / nest / filled-void /
voided-element chains and core.assign_container assigns the container
to whatever the walk returns. For an IfcDoor the filled-void hop
redirects to the IfcOpeningElement, then voided-element to the host
wall, so a user who selects a door and runs bim.assign_container ends
up targeting the wall — and silently no-ops on the door if the wall is
already in the target storey.
Per IFC4 / IFC4.3 (IfcDoor, IfcWindow): the spatial containment of a
filling is defined independently of the filling relationship. Major
exporters (Revit, ArchiCAD, Tekla, Allplan) emit independent
ContainedInStructure on doors / windows accordingly. Drop the
filled-void / voided-element hops from the walk; aggregate and nest
remain — those are true sub-part relationships where the parent
legitimately owns the container.
New TestGetRootElement in test/tool pins the new contract (filling
resolves to itself) plus the retained aggregate / nest / loose-element
paths so a future PR that re-adds either hop is caught. Two new
TestAssignContainer cases in test/core pin filling-to-self through the
core layer and per-element can_contain filtering.
Generated with the assistance of an AI coding tool.
The toggle_openings icon lived outside the IconSlot layout — each
host (wall, roof) declared an ad-hoc setup_pen_row_toggle_openings_icon
+ update_pen_row_toggle_openings_icon pair, and GizmoArrayEdition
queried a hardcoded _FEATURE_IDLE_MAX_X dict to position past it.
On an arrayed wall the dict was shadowed: find_for_element returns
"array" before "wall" in EDIT_TYPES order, the wall reservation was
never consulted, and the first per-layer ARRAY icon (local X=0.37)
landed 13cm from the wall's toggle_openings (X=0.50) — visually on
top of each other.
Promote idle-row icons into the slot system instead of patching the
dict:
* IconSlot gains an Optional visible_when predicate for state-driven
visibility (toggle_openings only when the host carries openings).
* BaseParametricGizmoGroup gains idle_slots: ClassVar[tuple[IconSlot]]
+ _idle_slot_x_positions() + _idle_row_right_edge() helpers; the
setup + idle-branch positioning loops mirror the existing
feature_slots path.
* Wall and roof declare toggle_openings as an idle_slot and drop
their ad-hoc setup/update calls.
* GizmoArrayEdition's _resolve_feature_idle_max_x walks
BaseParametricGizmoGroup.REGISTRY and takes the max
_idle_row_right_edge() across peers whose poll passes — no more
hardcoded dict, no more find_for_element-order shadowing.
* setup_pen_row_toggle_openings_icon + update_pen_row_toggle_openings_icon
helpers deleted from drawing/gizmos.py.
* 3 forward-compat AST guards pin the new contract.
Also bundles an unrelated array-test fix: TestUsingArrays in
test/tool/test_model.py was asserting against bpy.context.selected_objects
which is a fragile signal after remove_array / apply_array. A new
_array_objects() helper filters bpy.data.objects via the BIM_Array
pset's IfcActuator type instead.
Layout on an arrayed wall after the fix:
pen X = 0.00
toggle X = 0.50 (idle_slot 0)
array[0] X = 0.87 (one ICON_ARRAY_GAP past idle row)
array[1] X = 1.27
All separated by the standard inter-icon spacing.
Generated with the assistance of an AI coding tool.
Two bugs in BIM_PT_array:
1. The "is this layer in edit mode" predicate compared a BoolProperty
against an int (props.is_editing == i). Python evaluates False == 0
as True, so layer 0 always rendered the per-layer edit form even
when no edit was active — clicking validate/cancel then dispatched
against a phantom edit state. Switched to
props.editing_item_index == i, which defaults to -1 and matches
exactly one layer when an edit is active.
2. The panel's CHECKMARK and CANCEL buttons called bim.edit_array /
bim.disable_editing_array, a parallel lifecycle that only cleared
editing_item_index. Entering edit mode via the viewport gizmo
(bim.enable_editing_array, the triad enter) sets is_editing=True
and hides array children; the legacy panel exit unwound neither —
so committing or cancelling from the panel left is_editing=True
with children hidden, and the viewport gizmo thought the edit was
still in progress. Re-bound both panel buttons to the canonical
triad operators (bim.finish_editing_array /
bim.cancel_editing_array), which _ArrayEditMixin already owns and
which the viewport gizmo group already uses. Panel and gizmo now
share one exit path.
The three now-unreachable operators are deleted with their
registration entries: EditArray (bim.edit_array), DisableEditingArray
(bim.disable_editing_array), and EnableEditingArrayItem
(bim.enable_editing_array_item, never called from any UI). The two
test/tool/test_model.py sites that drove bim.edit_array as a commit
step are switched to bim.finish_editing_array.
External scripts or user keymaps bound to bim.edit_array /
bim.disable_editing_array will need to update — the replacements are
bim.finish_editing_array and bim.cancel_editing_array, both taking no
parameters (the layer is read from props.editing_item_index).
Partly generated with the assistance of an AI coding tool.
The check `len(bytedata) == n * 2` was wrong: float64 is 8 bytes per
element, not 2. Legacy float64 checksums fell through to the float32
reader and produced a (2n,)-shaped array, breaking is_moved() and
is_camera_moved() with `ValueError: operands could not be broadcast`
on .blend files saved by Blender <5.0.
Adds a parametrized regression test covering both n=3 (translation)
and n=9 (rotation) for both dtypes.
Generated with the assistance of an AI coding tool.
Two hot paths the gizmo polls fire every viewport event memoise
their result against tool.Parametric.get_geom_generation():
- tool.Blender.Modifier.any_selected_array_child caches the
per-selection scan against the selection identity-set + the
IFC generation token so a stable selection during a drag
doesn't re-walk every selected object's BBIM_Array pset every
frame.
- bim/module/model/wall.py grows a pair-predicate + connection
cache that the wall topology gizmos hit; both keyed on
(pair_uids, predicate_kind, generation) so a wall split or
axis edit invalidates correctly via the generation bump.
Behavioural contract is unchanged — stale entries are evicted
on generation bump; cache miss returns the same value the
un-cached path returned.
Generated with the assistance of an AI coding tool.
The opening preview's outline used a single-batch two-pass scheme that
dimmed the occluded back pass via alpha=0.25. The visible front pass also
inherited the source decorator color's modest alpha, so the outline read
as subtle on both sides.
Replace with a CAD hidden-line convention: solid full-alpha front pass on
the visible side, world-space dashed back pass on the occluded side. Both
passes use POLYLINE_UNIFORM_COLOR so depth and line-weight paths match.
The dashed batch is built once per object epoch by a new pure helper
tool.Blender.build_dashed_line_segments (pre-segments edges into world-
space dash chunks), then cached via the existing batch-cache mechanism
under "<uid>_dashed".
The solid front pass is rendered at a slightly wider line width than the
dashed back pass so its halo overpowers Blender's WIRE-display overlay
bias at outline pixels — without the asymmetry the wire's anti-z-fight
forward bias makes the LESS_EQUAL comparison narrowly fail and the
dashed pass wins on visible edges too.
Generated with the assistance of an AI coding tool.
A user clicking the pen icon on a typed-product occurrence whose body
representation is mapped from its type would silently mutate every
sibling occurrence's geometry. Add a confirmation dialog at the pen-icon
dispatcher (the single chokepoint every feature routes through) showing
the sibling count, with a session-scoped suppress checkbox.
The check is read-only: tool.Model.get_sibling_occurrence_count wraps
tool.Geometry.get_elements_by_representation against the resolved body
rep and subtracts self + type. A forward-compat AST guard pins the
dispatcher monopoly so any future feature that binds pen_gizmo directly
to a feature-specific enable op fails the test before merge.
Generated with the assistance of an AI coding tool.
Spatial.get_root_element walks aggregate / nest / filled-void /
voided-element chains and core.assign_container assigns the container
to whatever the walk returns. For an IfcDoor the filled-void hop
redirects to the IfcOpeningElement, then voided-element to the host
wall, so a user who selects a door and runs bim.assign_container ends
up targeting the wall — and silently no-ops on the door if the wall is
already in the target storey.
Per IFC4 / IFC4.3 (IfcDoor, IfcWindow): the spatial containment of a
filling is defined independently of the filling relationship. Major
exporters (Revit, ArchiCAD, Tekla, Allplan) emit independent
ContainedInStructure on doors / windows accordingly. Drop the
filled-void / voided-element hops from the walk; aggregate and nest
remain — those are true sub-part relationships where the parent
legitimately owns the container.
New TestGetRootElement in test/tool pins the new contract (filling
resolves to itself) plus the retained aggregate / nest / loose-element
paths so a future PR that re-adds either hop is caught. Two new
TestAssignContainer cases in test/core pin filling-to-self through the
core layer and per-element can_contain filtering.
Generated with the assistance of an AI coding tool.
The toggle_openings icon lived outside the IconSlot layout — each
host (wall, roof) declared an ad-hoc setup_pen_row_toggle_openings_icon
+ update_pen_row_toggle_openings_icon pair, and GizmoArrayEdition
queried a hardcoded _FEATURE_IDLE_MAX_X dict to position past it.
On an arrayed wall the dict was shadowed: find_for_element returns
"array" before "wall" in EDIT_TYPES order, the wall reservation was
never consulted, and the first per-layer ARRAY icon (local X=0.37)
landed 13cm from the wall's toggle_openings (X=0.50) — visually on
top of each other.
Promote idle-row icons into the slot system instead of patching the
dict:
* IconSlot gains an Optional visible_when predicate for state-driven
visibility (toggle_openings only when the host carries openings).
* BaseParametricGizmoGroup gains idle_slots: ClassVar[tuple[IconSlot]]
+ _idle_slot_x_positions() + _idle_row_right_edge() helpers; the
setup + idle-branch positioning loops mirror the existing
feature_slots path.
* Wall and roof declare toggle_openings as an idle_slot and drop
their ad-hoc setup/update calls.
* GizmoArrayEdition's _resolve_feature_idle_max_x walks
BaseParametricGizmoGroup.REGISTRY and takes the max
_idle_row_right_edge() across peers whose poll passes — no more
hardcoded dict, no more find_for_element-order shadowing.
* setup_pen_row_toggle_openings_icon + update_pen_row_toggle_openings_icon
helpers deleted from drawing/gizmos.py.
* 3 forward-compat AST guards pin the new contract.
Also bundles an unrelated array-test fix: TestUsingArrays in
test/tool/test_model.py was asserting against bpy.context.selected_objects
which is a fragile signal after remove_array / apply_array. A new
_array_objects() helper filters bpy.data.objects via the BIM_Array
pset's IfcActuator type instead.
Layout on an arrayed wall after the fix:
pen X = 0.00
toggle X = 0.50 (idle_slot 0)
array[0] X = 0.87 (one ICON_ARRAY_GAP past idle row)
array[1] X = 1.27
All separated by the standard inter-icon spacing.
Generated with the assistance of an AI coding tool.
Two bugs in BIM_PT_array:
1. The "is this layer in edit mode" predicate compared a BoolProperty
against an int (props.is_editing == i). Python evaluates False == 0
as True, so layer 0 always rendered the per-layer edit form even
when no edit was active — clicking validate/cancel then dispatched
against a phantom edit state. Switched to
props.editing_item_index == i, which defaults to -1 and matches
exactly one layer when an edit is active.
2. The panel's CHECKMARK and CANCEL buttons called bim.edit_array /
bim.disable_editing_array, a parallel lifecycle that only cleared
editing_item_index. Entering edit mode via the viewport gizmo
(bim.enable_editing_array, the triad enter) sets is_editing=True
and hides array children; the legacy panel exit unwound neither —
so committing or cancelling from the panel left is_editing=True
with children hidden, and the viewport gizmo thought the edit was
still in progress. Re-bound both panel buttons to the canonical
triad operators (bim.finish_editing_array /
bim.cancel_editing_array), which _ArrayEditMixin already owns and
which the viewport gizmo group already uses. Panel and gizmo now
share one exit path.
The three now-unreachable operators are deleted with their
registration entries: EditArray (bim.edit_array), DisableEditingArray
(bim.disable_editing_array), and EnableEditingArrayItem
(bim.enable_editing_array_item, never called from any UI). The two
test/tool/test_model.py sites that drove bim.edit_array as a commit
step are switched to bim.finish_editing_array.
External scripts or user keymaps bound to bim.edit_array /
bim.disable_editing_array will need to update — the replacements are
bim.finish_editing_array and bim.cancel_editing_array, both taking no
parameters (the layer is read from props.editing_item_index).
Partly generated with the assistance of an AI coding tool.
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.
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