Adds a viewport overlay that traces the connected element path from
the selected wall or MEP element. Walls follow IfcRelConnectsPathElements
and draw each connected wall's reference axis with endpoint dots; MEP
elements follow IfcRelConnectsPorts and draw each segment's axis plus
a port-to-port spider for each fitting.
The new BIMModelProperties.show_paths toggle (Element Paths in the
Bonsai Decorators group of Blender's viewport overlay popover) drives
install / uninstall of both decorators on flip and on file load,
mirroring the show_slab_direction wiring. The popover row also
surfaces the pre-existing BIMSystemProperties.should_draw_decorations
toggle (System Decorations) so both connectivity overlays sit
together.
Dot colors split free endpoints (decorator_color_special, blue by
default) from junction nodes (decorator_color_selected, green by
default) so dangling chain tips read apart from interior joins. Walls
classify endpoints by IFC topology: rels expose RelatingConnectionType /
RelatedConnectionType and ATPATH dots use tool.Wall.path_connection_location_world
for the canonical T-meets join. MEP keeps the geometric classifier
because port positions coincide exactly across fitting + segment.
Generated with the assistance of an AI coding tool.
Edit-mode click-select rejected verts inside the clip volume
because clip_bb stayed at the first-arm view; the depsgraph and
PRE_VIEW handlers updated clip_planes but skipped the
view3d.clip_border call that refreshes clip_bb. Schedule a full
re-arm at transform-commit, IFC reload, and view drift.
The empty's wireframe was clipped by its own planes when rotated
at non-trivial scale because the 1e-6 absolute margin can't
absorb float-precision drift that scales with the box's world
half-extent. Add a 1e-5 relative expand.
Generated with the assistance of an AI coding tool.
Per IFC4+, IfcContext is the abstract supertype of IfcProject and
IfcProjectLibrary; library-only files legitimately contain only
IfcProjectLibrary as their root context. Bonsai assumed an IfcProject
was always present at three crash sites: the parent-library enum
(reported in #8183), RefreshLibrary's tree view, and AddProjectLibrary.
Introduce tool.Project.get_root_context() that prefers IfcProject and
falls back to IfcProjectLibrary, and route the three sites through it.
get_parent_library() now returns None for a root IfcProjectLibrary;
get_project_hierarchy() and the EditProjectLibrary parent-swap branch
handle that. AddProjectLibrary creates the nested sub-library via
IfcRelNests when the root is an IfcProjectLibrary, matching the
existing convention for library-under-library nesting.
For the separate "Open IFC Project" path, abort with a friendly error
pointing users to Project Setup -> Project Library -> Select Library
File instead of letting set_units() crash deep in the importer.
Closes#8183.
Partly generated with the assistance of an AI coding tool.
IfcQuantity* carries an optional Formula (IfcLabel) documenting how a
quantity was derived. Export it alongside each quantity so it survives
in the Quantities column.
The per-quantity entry shape grows from [name, value] to
[name, value, formula], which stays backward compatible for positional
consumers reading index 0/1. Formula is read with a schema-safe getattr
(it does not exist on IfcPhysicalComplexQuantity, nor in IFC2X3) and is
coalesced to "" when absent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
A clip box hides everything outside a user-controllable oriented
bounding box, with cross-section caps drawn where IFC product
geometry intersects the planes. The box is hosted on a Blender
empty (CUBE display); its matrix_world is the single source of
truth — G/R/S edits the empty and the viewport clip planes track.
State persists through IFC save/load via a project-level pset
(IfcProject.BBIM_ClipBoxes) so the boxes survive without binding
to any IfcRoot entity (avoids the IFC scale-lock / strip).
UI: BIM_PT_clip_box under the Sandbox tab. Prominent Enable
Clipping + Show Caps toggles at top, then Add, then a UIList with
per-row duplicate / remove icons. Scene-level enabled / show_caps
so the "hide everything outside" intent applies file-wide;
enabled is intentionally not persisted to the pset so reopening
an IFC never silently hides geometry. Adding a clip box arms
clipping so the user immediately sees the cut.
Default spawn at the 3D cursor with scale 10 (a 20 m cube) so the
volume covers a typical building storey or two rather than the
meaningless 2 m unit cube.
Modal-aware: depsgraph + draw-handler paths gate per-frame side
effects on tool.Blender.is_transform_modal_active so dragging
G/R/S on the box only writes the pset once on commit, not per
frame. Shift+D / Alt+D / Ctrl+Shift+D on a clip box gets adopted
as a first-class entry via the collection-to-list sync.
Cap eligibility is gated on IfcElement (walls, slabs, doors, …)
so spatial structure (IfcSpace, IfcBuildingStorey, IfcSite) and
annotations / grids never sprout solid fills at clip boundaries.
Cap rebuild is debounced behind a 1 s quiet window so external
gizmo drags (and any other burst of non-Bonsai depsgraph updates)
collapse to one rebuild on release. Bonsai's own G/R/S keeps the
snappy on-release feel via a modal-end fast-path. The relevance
filter compares a per-Object matrix hash against a baseline so a
plain selection click — which Blender quirkily flags as a
transform update — doesn't churn the cache or flash the caps off.
Edit mode short-circuits both the rebuild scheduler and the draw
handler entirely.
Caps use the evaluated mesh (modifier stack applied) and a
session/matrix/clip-box-hash cache so a typical scene only
re-bisects meshes whose geometry actually changed.
Performance: every per-frame poller (refresh, depsgraph handlers,
draw handlers) short-circuits on the cheapest available check
first — cap_cache emptiness for the post-view draw handler,
scene_props.enabled for the rest — so a session with clipping
disabled pays only one boolean read per tick.
Known v1 limitations documented in tests / docstrings: hollow
profiles cap as solid discs (single-ring tessellation only),
non-watertight inputs may produce degenerate caps, quad-view
untested, Cycles / EEVEE render not supported (GPU-overlay only).
Generated with the assistance of an AI coding tool.
Bisects a BMesh against a set of planes (clear_outer per plane),
then fills the cut edges as cap faces tagged via a BMesh int layer
so the tag survives subsequent bisects. Cut edges are grouped into
connected components before filling so a hollow profile's outer +
inner loops produce two separate cap faces instead of a single
welded outer face that hides the hole. Pre-welds T-junctions
introduced by IFC Boolean meshes so the cut closes into a fillable
loop.
Callers are responsible for input mesh quality. Non-watertight
inputs (terrain, single-shell surfaces) may produce degenerate
cap faces — that's an accepted user-supplied data limitation
which can be revisited if real-world feedback shows it matters.
Used by the clip-box feature to compute cross-section caps per IFC
product mesh.
Generated with the assistance of an AI coding tool.
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.
The transform-modal active check (Bonsai keymap macros + Blender's
TRANSFORM_OT_* family) was a module-local helper in drawing/gizmos.py
used by per-gizmo poll callbacks. It needs to be shared with other
features that gate per-frame side effects on whether a drag is in
progress (clip box plane re-arming, future modal-aware decorators).
Move BONSAI_TRANSFORM_MACROS and the gate into tool.Blender as
is_transform_modal_active classmethod; widen its window scan to all
WM windows for callers without a window-bound context (depsgraph
callbacks). Leave a thin module-local alias in drawing/gizmos.py so
AST scans and existing call sites stay decoupled from the helper's
home module.
Also add generic Blender helpers needed by the clip-box feature
(reusable by any future feature):
- iter_view3d_regions: yield (area, region, region_3d) for every
WINDOW region in every 3D viewport — for clip-plane / draw-handler
fanout.
- get_or_create_collection: idempotent named-collection lookup +
link to a scene.
- is_in_edit_mode: True iff the active object is in any EDIT_*
mode — for features that need to suspend per-tick work during
vert/edge/face manipulation.
- serialize_matrix / deserialize_matrix / hash_matrix: round-trip a
4x4 matrix as a 16-float CSV string for IFC pset persistence + a
matching hash for cache keys.
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.
serialise_cost_quantities built the "Quantities" JSON string by manual
concatenation, inserting quantity.Name and the related element's Name
without any escaping. A name containing a double quote, backslash or
newline produced invalid JSON, breaking any downstream parser (e.g. a
Typst json.decode consumer reporting "failed to parse JSON"). It also
crashed with a TypeError when a name was None (str += None).
Build a Python list and serialise it with json.dumps instead, keeping
the exact same [[name, value], ...] output shape, the element-name
prefix and the unsupported-type behaviour. None names are coalesced to
"" and quantity values are defensively coerced to float.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wraps three over-length lines black wanted on the merge-filter +
fillet-lock commit (wall.py's ``either_is_fillet`` chain rewraps the
right-hand ``or`` operand; test_disconnect_elements.py patch-stacks
break each ``patch(`` onto its own continuation line).
Adds per-test docstrings to test_wall_props_resync_on_dim_change.py
and test_wall_split_filled_opening.py so the contract each pins is
visible on grep / on test-run failure output without scrolling to
the module-level docstring. Drops a flip_object sibling-symbol
mention from the module docstring per CLAUDE.md §4a.
Generated with the assistance of an AI coding tool.
DumbWallJoiner.merge previously crashed on walls with a slab underside
clip because the ConnectedTo / ConnectedFrom migration loops assumed
every rel was an IfcRelConnectsPathElements. The slab's
IfcRelConnectsElements(TOP) rel has no RelatingConnectionType /
RelatedConnectionType and raised AttributeError mid-migration. Filter
on rel class; the slab rel dies with element2 via the trailing
delete_ifc_object cascade.
The fillet preview pen icon now also flips the corner's
BIMWallProperties.is_editing so the connection-disconnect gizmos
surface in parallel with the radius drag. CancelWallFilletPreview
clears the flag before tearing the preview state down so both UIs
hide together. GizmoWallUnjoinSingle.poll inlines the viewport +
array-child guards from the topology gate so the gizmo can show
during preview — its own is_editing check is the real gate.
Fillet-to-source-wall path connection icons render in a muted gray
(LOCKED_COLOR) instead of the active disconnect tone, and the
bim.disconnect_elements operator early-returns with an INFO report
("Fillet wall path connections can't be unjoined — delete the fillet
wall element to remove the corner.") when either side resolves to a
fillet corner. The slab clip rel kind stays disconnect-able since
its identity is separate from the fillet's chord-axis reference.
Drive-by /improve polish on adjacent wall.py code: 3 comment tightenings
dropping sibling-symbol names + a defensive ``if opening.ObjectPlacement:``
guard in the merge opening migration matching the pattern used elsewhere
in the same file.
Generated with the assistance of an AI coding tool.
bim.merge_wall now consumes the non-active selection into the active
one — matching Blender's OBJECT_OT_join (Ctrl+J) and MESH_OT_merge
"at last" convention. The wall the user clicks last absorbs the
other; users following Blender muscle-memory get the result they
expect. DumbWallJoiner.merge is already structurally asymmetric
(wall1 = survivor); only the caller in MergeWall._perform needed
flipping. Audit confirmed the previous call site was the sole
caller of DumbWallJoiner.merge in production code.
Drive-by tidies on adjacent code: collapse two over-length comprehensions
under black's 120-char budget, and switch ``any(True for _ in gen)`` to
``any(gen)`` since the iterable yields tuples that are always truthy.
Generated with the assistance of an AI coding tool.
Two bugs in DumbWallJoiner.split's filled-opening branch:
1. Side classification read filling_obj.matrix_world.translation —
flip-fragile because flip_object rotates the filler 180° + translates
so the bbox stays visually in place, moving the door origin to the
opposite bbox corner. A flipped door centred over the cut could be
classified on the wrong side. Switch to the opening's axis-projected
midpoint, which the unfilled-opening loop already uses.
2. When the void straddles the cut and the filling moves to element2,
the void copy for element1 was taken from the rebound new_opening
whose PlacementRelTo had been swapped to element2 — the new void on
element1 then sat in element2's local frame. Reorder so the copy
reads from the original opening (still hosted by element1) before
remove_feature destroys it.
Generated with the assistance of an AI coding tool.
ChangeExtrusionDepth, ChangeExtrusionXAngle, and ChangeLayerLength
mutate IFC extrusion / axis but never re-prime BIMWallProperties from
the post-mutation state. Gizmo icons that position from props.height
then sit at the pre-mutation elevation even though the wall mesh shows
the new one — visible asymmetry against the workspace header H field
which redraws live. Add the existing _resync_walls_after_mutation
call to each operator's epilogue. _maybe_resync_wall_props_from_ifc
already skips non-walls and walls in edit mode, so calling on the raw
selection list is safe.
Generated with the assistance of an AI coding tool.
DumbWallJoiner.merge cascade-deletes element2's HasOpenings via
delete_ifc_object, which previously dropped every IfcOpeningElement
(and any IfcDoor / IfcWindow filling) hosted by the discarded wall.
Re-host each void rel onto the survivor BEFORE the delete fires, and
re-apply the opening's captured world matrix via edit_object_placement
so the void doesn't drift when the two walls have different
placements — a PlacementRelTo swap alone would fail this when origins
differ along the shared axis.
Generated with the assistance of an AI coding tool.
Schematic dimensions float in billboarded viewport space; their labels
carry the value, not the bar length. Snapping the dragged tip to scene
vertices produces nonsensical value jumps when the mouse crosses
unrelated meshes. Add an opt-out flag on the parametric gizmo group
base and override it on the schematic base — every schematic subclass
inherits no-snap behaviour, and in-place parametric gizmos (door,
window, wall, stair, roof, mep) keep the existing Ctrl-toggleable
snap because the default stays True.
GizmoDimension.invoke also forces tool_settings.use_snap = False for
schematic gizmos so the header magnet visibly switches off for the
drag's duration. The existing exit path restores the user's previous
setting on release.
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.
Add a "Copy" option to bim.cad_offset. When enabled (the
default) it offsets a new copy of the selected edges as
before; when disabled it moves the existing edges to the
offset location instead. The toggle is exposed in the CAD
tool's Offset panel and the operator redo panel.
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.
Wires slabs into the parametric edit framework (tool.Parametric
.EDIT_TYPES) so the wall-slab disconnect UI gets ESC handling, red
cancel icon, mutual exclusion with other parametric edits, and
per-feature gizmo prefs — all from BaseParametricGizmoGroup — without
duplicating the lifecycle.
Adds:
- ParametricObject("slab") registry entry + tool.Parametric.is_slab
predicate (any IfcSlab).
- BIMSlabProperties with is_editing flag; PointerProperty wired by
the framework's register_object_properties.
- bim.enable_editing_slab / bim.finish_editing_slab /
bim.cancel_editing_slab operators on tool.Ifc.Operator so they
flow through tool.Parametric.run_bim_op cleanly. No IFC mutation
— slab edit is a pure UI gate; finish and cancel share the body.
- tool.Model.get_slab_props accessor.
- GizmoSlabEdition inheriting BaseParametricGizmoGroup with the
pen / validate / cancel triad. is_element_type narrows to
IfcSlab with at least one wall clipped to its underside.
The disconnect-icon group GizmoSlabUnjoinWalls polls behind
_slab_connection_gizmo_poll_gate(require_editing=True), which now
reads is_editing through tool.Model.get_slab_props.
Drops the standalone GizmoSlabConnectionAccess + the
setup_pen_cancel_icons helper added earlier in this branch — both
superseded by the framework integration.
Also folds in the wall + multi-slab gizmo polish requested live:
- Wall side: stack the per-slab unjoin icons vertically (up to 5)
so multi-slab connections each get a distinct clickable icon;
hover-highlight reveals which slab will disconnect.
- GizmoPairDisconnect activates when 2 elements with an
IfcRelConnectsElements(TOP) rel are selected, with the icon at
the wall-slab connection world anchor.
- Wall-slab anchor moved from slab clip Z to wall top +
WALL_SLAB_CONNECTION_Z_CLEARANCE so the disconnect icon perches
above the extend-vertical / slope gizmo instead of overlapping.
- Shared _resolve_active_partner_pair helper for 2-selection
gizmos; _slab_connection_gizmo_poll_gate added to
_REQUIRED_CALLEES + GizmoSlabEdition added to the AST
forward-compat allowlist.
Build note: wall.py's DisconnectElements._perform imports
bonsai.core.connection.disconnect_rel — that core module is being
added in a parallel-session commit. Until that lands the addon
import will fail.
Generated with the assistance of an AI coding tool.
Single generic dispatcher replaces UnjoinWallPathConnection +
DisconnectWallSlab. Takes two GlobalIds, looks up every supported
rel between them via tool.Connection.find_rels, dispatches the right
cleanup by rel kind:
- path (IfcRelConnectsPathElements): remove_connection on every rel
in both orientations + recreate both walls + resync drafts.
- element-top (IfcRelConnectsElements with Description=="TOP"):
disconnect_element + regenerate_wall_to_underside on the wall side
via orient_element_top to recover which input is wall vs slab.
- element (other IfcRelConnectsElements): plain disconnect_element.
tool.Connection lands as a new tool module with two helpers:
- find_rels(a, b): every supported rel between two elements, walking
both ConnectedTo + ConnectedFrom (catches both authoring
orientations and dedups by id).
- find_rel(a, b): first-match convenience.
- orient_element_top(rel, a, b): recovers (wall, slab) from a TOP
rel regardless of which input came first.
Updates GizmoWallUnjoinSingle to target bim.disconnect_elements with
both element_a_guid + element_b_guid pre-filled per icon. Adds the
single registration in tool/__init__.py and the classes-tuple entry
in bim/module/model/__init__.py. Drops the two retired classes.
Tests cover both cleanup branches (path + element-top), missing
endpoints, no-rel-found, and registration smoke.
Generated with the assistance of an AI coding tool.
Counterpart to UnjoinWallPathConnection on the wall-slab side: takes
a wall + slab GlobalId pair, locates the IfcRelConnectsElements(TOP)
between them via tool.Wall.find_wall_slab_rel, removes it via
ifcopenshell.api.geometry.disconnect_element, then re-runs
core.regenerate_wall_to_underside so the wall re-clips against any
remaining connected slabs (the disconnected slab is excluded
naturally because the helper walks tool.Model.get_connected_slab_objs
which filters by the rel set).
Defensive reports replace silent CANCELLED on three error paths the
UI can hit when the gizmo dispatches against stale state: unknown
GlobalIds, wall entity without a Blender object, no rel found
between the resolved pair.
Tests cover all four control flows (happy path + three error paths)
plus a registration smoke that catches a forgotten classes-tuple
update.
A follow-up commit will retrofit this + UnjoinWallPathConnection +
the MEP port disconnects through a unified bim.disconnect_elements
dispatcher with a small connection-type registry; that lands as a
separate single-concern commit so the typed operator can be
reviewed first.
Generated with the assistance of an AI coding tool.
Four classmethods enable the new wall-slab connection gizmo work:
- iter_wall_slab_connections(wall): yields (slab, rel) tuples for
every IfcRelConnectsElements(TOP) on wall.ConnectedFrom — the rel
kind extend_walls_to_underside creates.
- iter_slab_wall_connections(slab): mirror, walks slab.ConnectedTo
so a slab-side gizmo can enumerate every wall clipped to its
underside.
- find_wall_slab_rel(wall, slab): locates the specific rel between
a wall + slab pair so a disconnect operator knows what to remove.
- wall_slab_connection_location_world(wall_obj, slab_obj): returns
the world-space icon anchor — wall axis midpoint X/Y lifted to
the slab's mesh-bbox underside Z. Approximate (uses slab bbox vs
reconstructing the slab's clip plane) but adequate for icon
placement on a wall whose top meets the slab; returns None when
the wall has no IFC Axis representation.
Tests (11) pin the rel-shape contract (class + Description=="TOP",
non-TOP and non-IfcRelConnectsElements rels skipped, None relating
defensively skipped) plus the icon-anchor math (axis-mid lifted to
slab-bbox bottom; None for axisless walls).
Generated with the assistance of an AI coding tool.
update_railing skipped the bmesh rebuild for WALL_MOUNTED_HANDRAIL
railings because the only mesh source available at the time mutated
IFC. The viewport-only preview helper that lands with the parametric
gizmo work (generate_wall_mounted_handrail_preview) sidesteps IFC
entirely, so the WALL_MOUNTED_HANDRAIL branch can join the
FRAMELESS_PANEL path and trigger update_railing_modifier_bmesh on
every property write. Gizmo drag now repaints the viewport in real
time instead of waiting for Finish Editing.
Generated with the assistance of an AI coding tool.
Switches the IfcRailingType terminal-type selector from cycle-on-click
to a popup menu of all terminal-type literals — 5+ values trip the
§2.8 menu-pick threshold. Updates classes registration; removes
EditRailingTerminalType in favour of PickRailingTerminalType which
inherits PickTypeMixin.
Adapts the cherry-pick from db016d881 to post-PR5 framework state:
- Imports CycleTypeMixin / PickTypeMixin / PathPreservingEditMixin
from bim.parametric_lifecycle (PR5 moved them off gizmos.py).
- Routes is_railing through tool.Parametric (predicates moved off
tool.Blender.Modifier between PR3-PR5).
Skips the parametric_lifecycle.py framework refactor the source
commit shipped — HEAD has the more-evolved post-PR5 framework that
already covers it.
Adds the _FakePropsBase + make_lifecycle_obj test helpers to
test/bim/conftest.py so the new test_railing_lifecycle.py can
exercise the edit triad without a real bpy.types.Object. Brings the
test_railing_schematic.py marker in line with the rest of the model
lane.
Generated with the assistance of an AI coding tool.
IFC2X3 representations have no HasShapeAspects inverse; opening the
Geometry & Materials subpanel on an IFC2X3 object raised AttributeError
and left the items list empty. Wrap the access with a getattr default
so pre-IFC4 schemas return an empty iterable, and pin the contract with
an AST forward-compat guard that scans bim/, tool/, and core/ for any
future direct .HasShapeAspects access.
Closes#8157
Generated with the assistance of an AI coding tool.
Port gizmos-8088's railing gizmo block to v0.8.0:
- _RailingEditMixin (PathPreservingEditMixin specialisation) +
EnableEditingRailing / CancelEditingRailing / FinishEditingRailing
edit triad
- CycleRailingType (2-value type cycler) + ToggleRailingUseManualSupports
one-shot + EditRailingTerminalType
- FlipRailingPathOrder + EnableEditingRailingPath /
CancelEditingRailingPath / FinishEditingRailingPath path-edit
operators (mutually exclusive with the schematic frame)
- GizmoRailingSchematic (BaseSchematicGizmoGroup specialisation) —
axonometric schematic frame with per-attribute dimension gizmos
for FRAMELESS_PANEL + WALL_MOUNTED_HANDRAIL railing types;
hover-on-attr highlights the schematic edges tagged with the
matching feature
Tests: test_railing_lifecycle.py (280 LOC) +
test_railing_schematic.py (272 LOC).
Drops the per-feature GizmoPreferences{Door,Window,Stair,Wall,Roof,
Railing} PropertyGroups that the source commit added to bim/ui.py
— that finer-grained per-attribute toggle model was deliberately
collapsed to flat per-feature bools in the PR5b prefs sweep, and
GizmoRailingSchematic gates on the flat ``prefs.gizmos.railing``
bool via ``gizmo_pref_name`` so no functionality is lost.
Generated with the assistance of an AI coding tool.