Wall topology mutations (merge / join / extend-to-wall / unjoin /
fillet) applied to a Bonsai array child are silently overwritten by
the next ``regenerate_array``; merge also orphans a GUID listed in
the parent's ``BBIM_Array.Data``. Add a central
``tool.Blender.Modifier.any_selected_is_array_child`` predicate and
gate the five wall topology gizmo groups plus the six bound operators
behind it. Operator gating is defence in depth against keymap / F3
invocation paths that bypass the gizmo.
The base ``_wall_gizmo_poll_gate`` keeps its loose two-check shape
(viewport gizmos + no preview). A new
``_wall_topology_gizmo_poll_gate`` wraps it with the array-child
filter and is what the topology gizmos use. Host-opening gizmos
deliberately stay on the loose gate: openings authored on a child
are preserved through ``regenerate_array`` and track with the
replicated instance.
A forward-compat AST guard walks wall.py for ``GizmoGroup`` subclasses
and asserts each routes its poll through the tighter gate or the
central predicate, with an allow-list for the parametric-edit and
preview-owner exceptions. New wall topology gizmos inherit the
contract by construction.
Generated with the assistance of an AI coding tool.
Selecting a Bonsai-parametric IfcDoor now shows the swing arc(s)
without entering edit mode. A new viewport decorator polls on the
active object, reads the door's BBIM_Door pset, and draws the same
arcs the parametric door swing gizmo would draw — matching the
hinge / panel-width / x-mirror contract minus the is_editing gate.
A forward-compat test walks every door operation type and cross-
checks the readonly decorator's arc selection against the gizmo's
swing-arc config table, so future enum additions fail in both
surfaces simultaneously.
Also disables the inherited 8-pass dark halo on GizmoArc: an open
curve has no enclosed silhouette, so the offset passes read as
ghost arcs rather than a uniform outline. The arc's own cross-
section thickness keeps it legible without the halo.
Generated with the assistance of an AI coding tool.
The MEP bend feature's IfcSweptDiskSolid representation produces
geometrically correct output but fails to round-trip through the
OpenCascade geometry kernel (upstream issue #8106) — the body is
dropped on the next file load. Until upstream is fixed, MEPAddBend
captures the bend centerline in world space before the segments are
extended (otherwise the post-extension axes no longer reach the
original intersection and arc reconstruction is wrong), then after
the fitting is placed it hand-meshes the bend body and swaps the
type's swept-disk representation for an IfcTessellatedFaceSet via
tool.Geometry.export_mesh_to_tessellation + tool.Model.
replace_object_ifc_representation.
The centerline includes the straight start_length / end_length legs
in addition to the arc so the bend covers the full segment-to-
segment span. Sweep uses parallel-transport framing — each ring's
(right, up) basis is rotated by the minimum rotation that maps the
previous tangent to the current one, eliminating the twist a fixed
world-axis reference produces when the tangent crosses the
reference. Cross-section orientation seeds from the source segment's
matrix_world local +X / +Y so asymmetric IfcRectangleProfileDef
ducts land with XDim / YDim on the same axes the segment expects;
parallel transport then preserves that alignment around the arc.
Centerline radius is radius + profile_dim[lateral_axis] to match
MEPAddBend's ref_point_radius — without this offset, the bend legs
fall short of the extended segments by profile_dim * tan(angle/2).
Face winding is left to the caller to correct via
bmesh.ops.recalc_face_normals on the closed bend tube.
Two FIXME(#8106) markers (capture site + helper call site) so both
can be dropped once upstream lands a swept-disk round-trip fix.
Generated with the assistance of an AI coding tool.
IfcTriangulatedFaceSet/IfcPolygonalFaceSet were introduced in
Fix#7992: IFC4 and do not exist in IFC2X3. Previously, requesting an
IfcTessellatedFaceSet representation in an IFC2X3 file silently
fell back to a faceted brep after unassigning material sets.
Add a guard in the update_representation operator (user-facing
error) and in the add_representation API (ValueError) so the
unsupported request is caught instead of failing silently.
Generated with the assistance of an AI coding tool.
GizmoWallEdition gains a fourth cursor-anchored icon that
spawns a perpendicular branch wall from the cursor's
orthogonal projection on the source wall axis. Click forms
a T-junction; shift+click forms an L-corner with the source
wall trimmed at the projection, keeping its longer portion.
The branch inherits the source's spatial container and
centerline baseline so its authored axis matches the source's
alignment rather than the type's default.
Also includes a floor-plane preview quad for the new gizmo,
a floor-Z cross line on the split preview for top-down
visibility, a small bump to QUAD_ALPHA for clearer preview
fills, and a stacking-offset helper that centralises the
cursor-row screen-up step across three call sites.
Generated with the assistance of an AI coding tool.
The MEP one-shot operators (join, unjoin variants, terminal removal,
path-select, obstruction add/remove) had no viewport surface. This
commit adds GizmoMEPActions — the icon-action gizmo group that
surfaces them as billboarded icons around selected MEP elements.
Three anchor regions: a horizontal row above the bbox top
(selection-cardinality icons), per-port endpoints for the three-state
lock / unjoin icons (open lock for PORT_FREE, closed for
PORT_TERMINAL, unjoin for PORT_JOINED — resolved per-frame from
port_connection_state), and the predicted join location
(compute_mep_join_location, shared with the bend preview) for the
join / unjoin_pair pair. Unjoin icons render at full
DEFAULT_BILLBOARD_SCALE with warning-red hover; endpoint lock icons
shrink so the lock row stays subordinate to the row icons. The
group hides itself entirely while a bend preview is active.
MEPAddObstruction grew a position enum (CURSOR / START / END) and a
mode enum (ADD / REMOVE / TOGGLE) so the gizmo can target a specific
port without touching the cursor and dispatch ADD or REMOVE based on
the click target — the lock_open icons drive ADD with position
pinned, the lock_closed icons drive bim.mep_remove_terminal_fitting.
Without the new fields the gizmo wiring (op_props.position = ...)
crashed at setup() with AttributeError on the obstruction operator.
validate_bend_preconditions extracts the type-match and profile-kind
checks MEPAddBend enforces so EnableBendPreview surfaces the
rejection immediately — the user no longer tunes a preview only to
learn at commit time that the segments use an unsupported profile
(e.g. IfcArbitraryClosedProfileDef).
Generated with the assistance of an AI coding tool.
Four discrete one-shot operators driven by the MEP segment's port
state. mep_unjoin_at_port deletes the IfcFlowFitting bridging a
segment's named port to a second element when the port is in the
JOINED state. mep_remove_terminal_fitting deletes the terminal
fitting at a port (closed-lock state) and dispatches by fitting
type — OBSTRUCTION fittings go through MEPGenerator.remove_obstruction
so the segment absorbs the freed length, other terminal fittings go
through the standard delete path. mep_unjoin_pair finds the single
fitting bridging two selected MEP segments and deletes it.
select_mep_path_members walks the connected MEP network from the
active element via IfcRelConnectsPorts and replaces the selection
with every reachable member. Foundation for the MEP Actions gizmo
group which surfaces these operators as icon affordances around
selected segments.
Generated with the assistance of an AI coding tool.
The extend icon used a pure screen-space billboard that always
pointed +X across the screen — the arrow ran horizontally
regardless of the pipe / duct's orientation. The new
billboarded_along_axis helper rotates the gizmo about the camera-
forward axis so its local +X aligns with the segment's local +Z
projected onto the screen, keeping the icon camera-facing but
visually following the extrusion direction. The flip-mirror branch
now reads from cursor-vs-current-end along the segment axis (not
screen-X), so the arrow points away from the current endpoint
regardless of viewport orientation. The split icon stacks
perpendicular to the rotated extend arrow in screen space so the
two don't overlap.
The decorator's green preview line no longer clamps the cursor
projection to min_projected_length — it follows the raw projection
so the line stays visible when the cursor crosses behind the
segment origin (the user still sees where they're pointing even
though the operator floors the actual commit).
Generated with the assistance of an AI coding tool.
The bend preview gizmo group (commit 2) populated a Scene draft but
the user saw nothing in the viewport until they hit finish — they
had to commit blindly. This commit ports the BendPreviewDecorator
(centerline arc + two leg projections on valid geometry, warning-red
axes on invalid in-segment intersections) and the interactive
GizmoBendPreview group (three dimension widgets for start_length /
end_length / radius plus validate / cancel icons). The bend axis
math lives in a pure compute_bend_preview_polylines helper, fed
into both the gizmo group's per-frame positioning and the GPU
decorator's draw path. MEPSegmentExtendPreviewDecorator lands at
the same time because it shares the decorator install / uninstall
plumbing — renders the extend-to-cursor preview line for the
GizmoPipeSegmentEdition / GizmoDuctSegmentEdition extend icons
when hovered, clamping the projected endpoint to the operator's
minimum so the preview matches where the commit lands. The
MEPJoinSegments dispatcher routes two selected MEP segments to
mep_add_transition (parallel) or enable_bend_preview (non-parallel)
— the F3 search entry point that makes the bend preview testable
before the gizmo-icon dispatch lands.
11 new tests in test_mep_bend_preview.py cover the geometry helper
truth table (parallel rejection, right-angle happy path, near-
collinear rejection, in-segment invalid_axes), the
_intersection_past_near parametrized boundary, registration probes
for the lifecycle operators / join dispatcher / gizmo group /
decorator, and the FinishBendPreview RuntimeError catch contract.
6 extend-preview-line tests (deferred from commit 3) join the
existing 35 in test_mep_segment_edition.py.
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.
Pipe and duct segments had no parametric-edit affordance — the only
length edit path was a property panel value with no live preview.
This commit ports the per-segment parametric edit triad
(enable / finish / cancel) plus a cursor-anchored extend operator
and a cursor-projected split operator into one gizmo group per
segment type. The two PropertyGroups (BIMPipeSegmentProperties,
BIMDuctSegmentProperties) host the draft length plus snap fields
so cancel / no-op-finish restore the segment to its exact pre-edit
visual state including a non-identity pre-edit scale. Length
commits are written through DumbProfileJoiner.set_depth and
auto-dispatch bim.regenerate_distribution_element so adjacent
fittings track the port move. The split operator preserves
downstream port connectivity and runs through tool.Ifc.run for
single-step undo. The two segment types are now first-class
entries in tool.Parametric.EDIT_TYPES, which resolves the FIXME
on auto-commit-on-save dispatch.
35 unit tests cover predicate truth tables, segment_world_length
geometry, preview-via-scale / restore-scale helpers, gizmo class
wiring, lifecycle operator registration, dimension matrix_position
rotation respect, and lifecycle drift-handling. The 6 extend-
preview-line decorator tests stay deferred until the bend preview
decorator commit lands MEPSegmentExtendPreviewDecorator.
Generated with the assistance of an AI coding tool.
Parametric gizmos (wall/door/window/stair/roof/array/MEP) recompute
matrix_basis every frame from obj.matrix_world. While Blender's
transform modal (G/R/S and the Bonsai macro overrides) drags the
matrix, the gizmos slide off-cursor and fight the transform overlay.
Detect via context.window.modal_operators (Blender 4.2+) — the
collection of running modal operators. Gate poll() (forward-compat)
and draw_prepare() (production path: gizmo.hide=True preserves the
GizmoGroup across the drag instead of destroying it). Cover the
Bonsai macro override for G key (and Shift/Alt/Ctrl+Shift+D) by
matching the BIM_OT_* macro idnames that surface in modal_operators.
Forward-compat test walks every parametric-edit module for GizmoGroup
subclasses and asserts poll returns False with the detector mocked,
so new gizmo groups inherit the hide automatically.
Generated with the assistance of an AI coding tool.
Every preview operator (commit + cancel for both bend and wall
fillet) was inlining the same 3-4 line cleanup: set is_active to
False, zero every *_id IntProperty. The new clear_preview_state
helper in preview_base.py introspects bl_rna and applies that
contract generically — adopters become a single call. Two new tests
pin the contract: every *_id IntProperty zeroes, non-id fields stay.
Generated with the assistance of an AI coding tool.
Extract the pending_opening_recut tracking, three operators (apply /
dismiss / select), Project-panel banner, and the sibling
multi-instance warning banner (its backend helpers already landed
on this branch) from commit a85ed6032 on gizmos-8088.
All tool.* dependencies (Geometry.reimport_element_representations,
Blender.set_objects_selection, Array.*) and IfcImporter.gross_elements
are already on this branch -- no other diffs from a85ed6032 are
pulled.
The source's narrow except-tuple paraphrase comments are trimmed
to keep only the durable "don't swallow programmer errors" note,
per CLAUDE.md s4a.
Tests: 5 bim-lane tests in test/bim/module/project/
test_pending_opening_cuts.py covering apply happy-path + missing
entity, dismiss, select happy-path + cancellation.
Generated with the assistance of an AI coding tool.
MEPAddBend exists on the main flow but commits bend geometry with
hardcoded defaults (start_length=0.1, end_length=0.1, radius=0.2)
with no opportunity to tune before commit. The new scene-level
BIMBendPreviewProperties hosts a draft (start_segment_id,
end_segment_id, start_length, end_length, radius); EnableBendPreview
populates it from the two selected MEP segments after asserting they
are non-parallel, FinishBendPreview dispatches MEPAddBend with the
tuned values and clears the draft, CancelBendPreview discards it.
Scene-level placement follows CLAUDE.md 2.9: a bend creates a new
fitting entity between two segments, so neither segment alone owns
the draft. Foundation for the upcoming bend preview gizmo group and
decorator.
Generated with the assistance of an AI coding tool.
When an operator mutated IFC then raised mid-execute the user was left
staring at a raw traceback with the IFC graph captured by the active
transaction but the Blender side stale. Blender does not push an undo
step for a raised operator (the same gap that the CANCELLED-modal arm
patches via bpy.ops.ed.undo_push), so the WARNING the framework can
emit is only honest if it pushes that undo step too. The framework
now detects partial state via ifc_file.transaction.operations,
pushes a Recover undo step, then reports a WARNING naming Ctrl+Z so
the recovery path is discoverable. The bespoke try/except wrapper in
UnjoinWallPathConnection becomes redundant and is retired in the
same change.
Generated with the assistance of an AI coding tool.
Three files flagged by black --check on the lint-formatting job:
* bim/module/geometry/operator.py — single-arg `.update(...)` rejoined
onto one line under the 120-char budget.
* test/bim/module/model/test_wall_gizmos.py — same join on a
_make_path_rel call.
* test/modal/test_modal.py — pre-existing baseline noise picked up
via the upstream merge: PEP-8 blank-line separators between top-
level functions, `0.68+` → `0.68 +`, double quotes, trailing
whitespace stripped.
No behavioural change; pure whitespace.
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.
In plan view world-Z collapses to zero on screen, so every wall-edit
icon anchored on the floor — the projected 3D cursor, wall endpoints,
wall-to-wall corners, IfcRelConnectsPathElements connection points —
projects onto the click target it represents. The result on a typical
extend / split / unjoin action: the icon sits on top of the cursor
crosshair (or the corner the user wants to click), defeating precise
positioning.
Add shared ``gizmo.top_down_clearance(context, billboard_rot)`` to
bim/module/drawing/gizmos.py: returns a screen-up Vector in top-down
view (cosine cone around world Z, matching ``is_view_top_down``) and a
zero Vector elsewhere, so call sites apply it unconditionally before
``billboarded_at``. Default distance 0.4 m aligns with the inter-icon
stack spacing already used by GizmoWallJoinIntersection so single
icons and stack bases land at consistent screen-up positions when
multiple groups render around the same wall endpoint.
Apply at the seven wall-edit anchor sites:
* GizmoWallEdition cursor stack (top-down branch only — non-top-down
already stacks along world-Z at structural points clear of the
cursor).
* GizmoWallExtendVertically (single icon at wall origin endpoint,
active-object Z elevation).
* GizmoWallJoinIntersection corner stack base + merge midpoint.
* GizmoWallUnjoinSingle link-toggle pool (one icon per IFC path
connection, previously sitting exactly on the connection point).
* GizmoWallFilletReedit pen icon at fillet corner.
* GizmoWallFilletToggleOpenings.
The clearance is a pure visual offset — bound operators still read
the world-space anchor (cursor / endpoint / connection point) at
execute time, so the action's target is unaffected.
Also tighten GizmoWallUnjoinSingle: gate poll on ``props.is_editing``
so the link-toggle icons only surface during the wall edit lifecycle
(matching every other edit-row icon), and downsize them via a new
``ICON_SCALE = 0.35`` constant since 16 of them at default scale
cluttered the viewport on path-heavy walls.
ruff + black clean. Wall gizmos test lane 14/14 pass.
Generated with the assistance of an AI coding tool.
Some distributions (e.g. Fedora) ship only a shared RocksDB that exports
RocksDB::rocksdb-shared rather than RocksDB::rocksdb. The CMake target
selection now falls back to the shared target when the static one is absent.
Newer RocksDB also changed DB::Open and DB::OpenForReadOnly to take
std::unique_ptr<DB>* instead of DB**. IfcFile.cpp uses SFINAE tag dispatch
to build against both old and new APIs without version detection.
CGAL 6.x deleted operator< from Point_d, so std::map<Point_d, ...>
no longer compiles. Adds a custom lexicographic comparator and updates
the three affected maps in snap_halfspaces and snap_halfspaces_2.
bim/handler.py was importing two feature-module internals
(wall_offset_gizmos.clear_caches, preview_base.discard_pending_previews)
to drain load-transient parametric state alongside the existing
tool.Parametric.heal_stale_edit_flags() call inside
_apply_save_file_invariants. Each new parametric drain added one
top-level import and one inline call — every load_post drain leaked
into handler.py's namespace.
Hide all three drains behind tool.Parametric.on_load_post(scene),
sited adjacent to heal_stale_edit_flags. The two feature-module
imports become late imports inside on_load_post — same pattern as
refresh_post_commit's existing `import bonsai.bim.handler` — which
sidesteps the tool.parametric -> bim.module.model.preview_base ->
bonsai.tool registration-time cycle.
The forward-compat AST contract that pinned "every module-scope
GenerationKeyedCache + clear_caches MUST be drained on load_post"
follows the call site to its new home — the test now walks
tool.Parametric.on_load_post instead of _apply_save_file_invariants.
No behaviour change. 45/45 affected bim tests pass
(test_handler_forward_compat, test_preview_base,
test_wall_offset_gizmos, test_parametric_registry).
ruff + black clean on all touched files.
Generated with the assistance of an AI coding tool.
Three feature-specific decorators previously lived in
bim/module/model/decorator.py despite owning state only their
home module reads:
* ArrayPreviewDecorator + ArraySelectionHighlightDecorator +
draw_array_layer_children_bbox -> array.py (read array
edit-state props and walk BBIM_Array psets)
* WallGizmoPreviewDecorator + draw_wall_partner_bbox -> wall.py
(dereference wall.py-private classes and helpers via lazy
imports)
decorator.py keeps cross-cutting infrastructure
(BoundingBoxDecorator, SlabDirectionDecorator, WallAxisDecorator,
WallFilletPreviewDecorator, PolylineDecorator, ProductDecorator)
and the shared bbox primitives (bbox_world_edges,
draw_polyline_segments, _BBOX_EDGES, _stroke_lines_alpha,
_fill_quads_alpha) that several feature files now import.
handler.py and gizmos.py update their import paths; the
wall-feature lazy imports inside WallGizmoPreviewDecorator
methods collapse to direct references now that the decorator
lives in wall.py.
No behaviour change. Wall lane 37/37, array lane 15/15, wall
forward-compat 6/6, parametric-registry 8/8 still pass.
Generated with the assistance of an AI coding tool.
Three concerns sharing the same architectural theme (collapse inline
bbox / edit-state lookups, drop overrides that re-do base-class work):
== Bbox helpers and array operator DRY ==
* tool/blender.py: add a "dimensions" tuple key to both
get_object_bounding_box and get_object_world_bounding_box return
dicts. The (max - min) per-axis extent — which callers previously
computed via local helpers — is now a key alongside min_x / max_x
/ min_point / max_point / center. Distinct from Blender's built-in
obj.dimensions (which folds object-level scale): the local variant
is the intrinsic mesh bbox extent; the world variant is the
matrix_world-applied AABB.
* bim/module/model/array.py: drop the local _bbox_dims helper; the
two callers now read tool.Blender.get_object_bounding_box["dimensions"]
directly.
* Rename _parent_geometry_changed -> _array_children_need_rebuild.
The old name suggested "did the parent change just now", implying
the function was a parent-edit-finish trigger. It actually runs
only inside the array-edit-finish path as a drift safety net (the
upstream-deliberate design — see commit 83d97d7e9 "Fix #7616. Make
regenerate array an operator instead of an array preference" —
means the array doesn't auto-regen when its parent geometry edits
finish). New name matches the call-site phrasing
``if X: _wipe_array_children(layers)`` and clarifies that this is
a children-state check, not a parent-edit trigger.
* Extract _resolve_array_edit_props(context) — returns the active
object's array props during an active edit lifecycle, or None.
Collapses the obj-active-then-is-editing prologue (3 lines + return)
to one resolver call across 4 sites: ToggleArrayMethod.execute,
AdjustArrayCount.execute, RemoveArrayLayerFromEdit._execute and
.poll. Each call site shrinks from 7 lines to 3.
* Migrate two inline bbox reads inside GizmoArrayEdition to the new
dict keys: get_axis_world_face_center collapses the manual
xs/ys/zs min/max + center math to bbox["center"] + bbox["max_x"] /
["max_y"] / ["max_z"]; get_element_height collapses
``max(corner[2] for corner in obj.bound_box)`` to
tool.Blender.get_object_bounding_box(obj)["max_z"].
The _BBOX_EQUALITY_EPS = 1e-5 tolerance stays inline as a single-
consumer constant — no other call site needs tolerance-equality on
dimension tuples, so extracting it to a shared util would be
speculative abstraction.
== Drop dead code ==
* GizmoArrayEdition.update_editing_gizmos override + its
_has_other_parametric_type helper: redundant with
hide_pen_button = True at line 1024. The base class already hides
the pen in every idle case (when hide_pen_button is truthy) AND in
every editing case (unconditionally). The override's conditional
hide-when-parametric only re-hid a pen that was already hidden in
both branches. Removes the only remaining path that could re-show
the array's pen icon; array-edit entry is now uniformly via the
per-layer ARRAY icons (which is the documented preferred
affordance, see the hide_pen_button comment).
* _wall_fillet_preview_active in wall.py: defined but never called.
_wall_fillet_props (the sibling thin-wrapper around
preview_base.get_preview_props) is heavily used; the
is_preview_active wrapper was added speculatively and never picked
up a consumer.
Generated with the assistance of an AI coding tool.
Bundled bug fixes + the forward-compat AST guard that prevents the
underlying class of bug from coming back.
* bim/module/model/wall.py: FinishEditingWall._execute early-returns
CANCELLED when props.is_editing is False. Without this guard, a
failed enable (e.g. on a wall without IfcMaterialLayerSetUsage)
leaves is_editing False but a press on finish still walked the
sub-ops below, which dereferenced layer-set-dependent state and
crashed.
* tool/model.py: Model.offset_wall now guards against
ifcopenshell.util.element.get_material returning None before
calling .is_a("IfcMaterialLayerSetUsage"). Fixes the pre-existing
test/bim/module/model/test_wall_header_refresh.py crash that has
been the only failing test in the wall lane since this branch
started.
* bim/handler.py: _apply_save_file_invariants drains
wall_offset_gizmos.clear_caches() on load_post. The module-scope
GenerationKeyedCache instance survives the .blend reload; without
the drain the cache may serve entries whose bpy_struct references
point into the freed bpy.data of the previous file.
* test/bim/test_handler_forward_compat.py: AST-walk test that
enumerates every bim/module/model/*.py source declaring both a
module-scope GenerationKeyedCache assignment AND a top-level
clear_caches function, and asserts each module appears as a
<module>.clear_caches() call in _apply_save_file_invariants. Pins
the contract: any future module-scope geom cache that exposes
clear_caches must wire into the load_post drain.
* test/bim/feature/model.feature + test/bim/test_feature.py: wall
edit-lifecycle scenarios switch from "add cube + assign as
IfcWallType" to "load the demo construction library + add an
occurrence of the WAL100 wall type", so the parametric edit runs
against a real LAYER2 wall with IfcMaterialLayerSetUsage rather
than a vanilla-mesh promotion that lacks one. The demo-library
step also picks the schema-matching library file (IFC2X3 /
IFC4 / IFC4X3) so the appended types remain valid across schemas.
Door saved-height assertion updates from 2.5 → 2500 to reflect
that BBIM_Door pset stores project units (METRIC_MM in the
empty-project fixture).
Generated with the assistance of an AI coding tool.
Fixes builds with newer GCC/libstdc++ that no longer provide <cstdint>,
<cstring>, <cfloat>, <memory>, <algorithm> etc. transitively. Also
disambiguates visit<> calls in taxonomy.h with the full namespace and
casts the character value in IfcCharacterDecoder to uint32_t to silence
ambiguous overload warnings.
bpy.ops.bim.new_project(preset='demo') crashed in
refresh_bim_tool_headers: the post-commit hook fired for every
nested bpy.ops.bim.append_library_element during template
loading, and the operator context Blender hands to
programmatically-invoked nested operators is stripped of the
view-layer attributes the refresh reads.
Two changes resolve it.
Gate the header refresh in tool.Parametric.refresh_post_commit
on operator.bl_idname being one of the EDIT_TYPES finish_op
idnames. Only validate-gizmo commits (bim.finish_editing_<name>)
now trigger the refresh; demo-loader and other non-edit
operators skip it. Querying the registry directly is the
canonical signal — string-prefix matching would silently drift
if ParametricObject.finish_op changes derivation.
Harden tool.Blender.get_active_object so its view_layer fallback
also uses getattr; the 150+ callers routed through it now
tolerate stripped contexts. _resolve_bim_tool_context applies
the same defensive pattern to mode / workspace.
Tests:
- test_handler_restricted_context covers get_active_object's
defensive path and the BimTool-family whitelist (excludes
annotation, spatial, structural).
- test_handler_forward_compat AST-pins that the gate consults
EDIT_TYPES (not a string prefix).
- test_wall_header_refresh rewritten — three tests cover the
gated-by-registry contract: counter bumps for every commit,
finish_op operators refresh headers, others don't.
Hotkey-driven in-place edits (S_E / C_E) no longer trigger the
refresh — they were caught by the pre-refactor "every commit"
design. Left out of scope; the new skip-non-finish test pins
this as intentional.
Generated with the assistance of an AI coding tool.
Two related cleanups bundled because each was too small on its own.
== Drop dead Geometry.has_material_styles duplicate ==
Two parallel has_material_styles implementations existed on HEAD:
* Geometry.has_material_styles (tool/geometry.py:853, added by
3483683cb "Add tool.Geometry helpers for body representation +
placement"): checks each material via tool.Material.get_style
for an IfcSurfaceStyle. This is the implementation gizmos-8088
uses — its core/root.py:58 calls geometry.has_material_styles.
* Root.has_material_styles (tool/root.py:75, added by e76455913
"Route _has_material_styles through tool.Root.has_material_styles"):
checks each material for a HasRepresentation inverse. Added to
fix the test/core/test_root.py::TestCopyClass::test_AAAAAAAAAAAA
failure by routing the check through a Prophecy-mockable seam.
HEAD's core/root.py:59 calls root.has_material_styles. The Geometry
version became orphaned by that migration — zero callers historically
(git log -S "Geometry.has_material_styles" returns nothing). The
Root placement is the right architectural home: has_material_styles
pairs with assign_body_styles in the copy_class flow as "is there
material-defined styling? if not, apply body styling" — both
decisions live on the same interface, called in sequence from the
same caller.
The semantic delta (HasRepresentation vs IfcSurfaceStyle) is a close
approximation in real IFC files where HasRepresentation almost always
indicates a styled material; if precision becomes necessary, the
Root impl can be tightened independently of this cleanup.
Drop the Geometry method + its abstract declaration in core/tool.py.
== Sanitation sweep per CLAUDE.md §4a ==
Eight rot-prone references in code we authored on this branch get
their first-draft mistakes cleaned up. The §4a rule (no sibling
symbol names, no test paths, no motivation history in docstrings)
got added during this branch, so older commits sometimes named their
siblings in prose; this is a focused cleanup of the worst offenders.
* bim/module/model/wall.py:201 — _CommitWallDraftsFirstMixin
docstring carried motivation history ("...that every multi-wall
operator … used to repeat at the top of _execute"). Rewrite to
describe only the current contract.
* bim/module/model/wall.py:1910 — cycle_type_operator comment named
two sibling methods. Rephrase to describe what happens at the slot.
* bim/module/model/wall.py:2025 — _active_instances ClassVar comment
named WallGizmoPreviewDecorator. Rephrase to "the wall-gizmo
preview decorator" (role, not class).
* bim/module/drawing/gizmos.py:3402 — GizmoFillet hit_uses_bbox
comment named GizmoWallJoinIntersection. Rephrase to "the wall-join
gizmo group".
* bim/module/drawing/gizmos.py:3887 — GizmoCountLabel docstring had
a :meth:`set_count` cross-reference. Drop — reader sees the method
next to the class.
* bim/module/model/host_add_opening_gizmo.py:201 — poll-exclusion
comment named GizmoWallEdition + GizmoRoofEdition. Rephrase to
describe why we skip ("walls and parametric roofs both render
their own toggle in the pen row").
* bim/module/void/operator.py:45 — preserve_placement comment named
FilledOpeningGenerator.generate. Rephrase to "the filling-opening
generator gates its snap-to-wall-axis block on this flag".
* bim/parametric_lifecycle.py:64 — module docstring named the test
file path (test/bim/test_parametric_registry.py). Rewrite to
"enforced by the registry contract tests".
Sweep otherwise clean: no third-party software names in this-branch-
authored comments (upstream Revit / Tekla / ArchiCAD references are
legitimate external-constraint workarounds, §4a-allowed). No
PR/issue numbers we authored except the FIXME(PR5) in
tool/parametric.py:150, deliberately preserved until PR6's MEP slice
resolves it.
Generated with the assistance of an AI coding tool.
The 7 multi-wall operators (UnjoinWalls, UnjoinWallPathConnection,
ExtendWallsToUnderside, ExtendWallsToWall, SplitWall, MergeWall,
JoinWallsIntersection) each opened their _execute with an identical
prologue:
_commit_pending_wall_edits_for_selection(context)
# ... operator-specific logic
— flushing any in-progress wall parametric drafts so the operator
acts on committed IFC state rather than the draft preview box.
Extract that prologue into _CommitWallDraftsFirstMixin: its _execute
calls the commit helper, then delegates to a subclass-supplied
_perform. Subclasses inherit the mixin first in their bases tuple so
the mixin's _execute resolves first via the MRO. The IFC transaction
opened by tool.Ifc.Operator.execute still wraps both the commit and
the perform.
Behaviour-equivalent — same call, same order, same selection scope.
Architectural cleanup only: a future multi-wall operator can no
longer forget the commit step. The named helper
_commit_pending_wall_edits_for_selection stays as the single
encapsulation of the names=("wall",) filter; its docstring loses
the stale "every multi-wall operator calls it at the top of
_execute" sentence and now just describes the filter contract.
Matches gizmos-8088's _CommitWallDraftsFirstMixin pattern.
Generated with the assistance of an AI coding tool.
The regular bim.add_opening click on the host-add-opening gizmo
(wall + door/window co-selected) routes through
FilledOpeningGenerator.generate, which snaps the filling to the
wall's reference-line axis, optionally rotates 180° when the
filling sits on the opposite side, and re-applies an rl1 / rl2
Z-elevation default. That is the right default for "drag a fresh
door onto a wall and let the model place it for me", but defeats
the workflow where the user has already positioned the filling
precisely (e.g. snapped to a window in an adjacent wall, copy-
pasted at an exact Z, aligned to a reference object).
Holding SHIFT while clicking the gizmo now opts into a
"preserve placement" mode: the filling stays at its current
matrix_world and the opening is created at the filling's existing
position. The opening / filling rels and representation work are
unchanged — only the snap-to-axis branch is skipped, so the IFC
graph is identical to the regular click; only the spatial
position of the filling differs (user-chosen vs auto-snapped).
Implementation:
* bim/module/void/operator.py: AddOpening gains a hidden
preserve_placement BoolProperty + an invoke() that sets it from
event.shift. The call into FilledOpeningGenerator.generate
forwards the flag. bl_description documents the SHIFT modifier
so it surfaces in F3 search / hover tooltip.
* bim/module/model/opening.py: FilledOpeningGenerator.generate
accepts preserve_placement (default False — backwards-compatible
with the other caller, tool.Model.add_filled_opening). The
voided_obj.data-gated snap block (raycast + axis projection +
rl-Z default + filling_obj.matrix_world write) skips entirely
when the flag is True. The opening's matrix_world reads from
filling_obj.matrix_world below the gate, so the opening lands
at the filling's preserved position automatically.
Generated with the assistance of an AI coding tool.
The door / window / stair edit-row's type-cycle icon advanced one
type per click (CycleDoorType / CycleWindowType / CycleStairType
bound to cycle_type_operator). DoorType has 8 IFC variants,
WindowType 9, StairType 3 — so cycling past the target was the norm.
Threshold rule for cycle-vs-menu: cycle is appropriate for exactly 2
values (advance-one-per-click stays predictable). Three or more
values warrants a popup menu. Door / window / stair all qualify;
roof (RoofGenerationMethod has 2 values) keeps cycle. Wall has no
type cycle. Array is unaffected.
Swap to the popup-menu pattern (PickTypeMixin already on HEAD at
bim/parametric_lifecycle.py:442): clicking the icon opens a menu
listing all type_literal values; selecting one applies it in a
single undo step. The hamburger icon (VIEW3D_GT_menu) is wired into
BaseParametricGizmoGroup.setup_editing_gizmos whenever
pick_type_operator is set (mutually exclusive with
cycle_type_operator). Matches gizmos-8088's pattern exactly.
Per-feature shape:
* door.py: PickDoorType replaces CycleDoorType.
GizmoDoorEdition.cycle_type_operator → pick_type_operator.
* window.py: PickWindowType replaces CycleWindowType. Same swap.
* stair.py: PickStairType replaces CycleStairType (no
tool.Ifc.Operator inheritance — stair-type changes
BIMStairProperties only, no IFC mutation). Same swap.
* bim/module/model/__init__.py: registration entries renamed
Cycle* → Pick*.
* bim/module/drawing/gizmos.py: drop the
CycleTypeMixin / PickTypeMixin / TypeAccessorBase shim re-export —
its own docstring already noted "PR5 cleanup drops these" and the
three callers (door / window / stair Cycle*Type) it served are
gone. Roof's CycleTypeMixin import was already direct from
bim.parametric_lifecycle. Also update GizmoMenu docstring to
reflect the 2-vs-3+ threshold.
Generated with the assistance of an AI coding tool.
When entering parametric edit on a door or window that fills a
wall opening, four dimension gizmos now measure the distances
from the wall edges to the filling's jambs and from the wall's
base/top to the sill/header. Dragging any gizmo translates the
filling along the wall's local axis; 180°-flipped fillings and
slanted LAYER2 walls round-trip correctly. The has_host_wall
predicate hides all four when the filling → opening → wall
chain cannot be resolved.
Generated with the assistance of an AI coding tool.
Four concerns that together make the cursor-anchored gizmos
(extend_x_gizmo, extend_z_gizmo, split_gizmo on GizmoWallEdition)
fully functional and visually informative without entering parametric
edit mode first:
* Drop the props.is_editing gate in _update_cursor_gizmos. The three
bound operators (bim.extend_wall_to_cursor,
bim.extend_wall_height_to_cursor, bim.split_wall_at_cursor) already
poll on wall-selected and commit any pending wall edit before
acting, so single-click without entering edit mode is now the
canonical flow. Matches gizmos-8088's always-on behaviour.
* Register GizmoWallEdition instances in a per-region weakref map
(_active_instances) populated at setup_element_specific_gizmos
time. The WallGizmoPreviewDecorator dereferences this map to read
live is_highlight state off the cursor icons. Without the
registration its _cursor_icon_hovered always returned False and
the hover-gated GPU previews silently never drew. Mirrors the
same pattern already in place on GizmoWallJoinIntersection.
* Add post-operator resync to all three cursor operators
(_maybe_resync_wall_props_from_ifc for the single-wall split /
extend-height paths, _resync_walls_after_mutation for the
selection-wide extend-X path). Without this, props.length /
props.height stayed stale after the operator ran, so the
orientation flips _apply_wall_extend_flips computes from
cursor_local vs wall dimensions kept using the pre-extend values
until the next selection change. Matches gizmos-8088's pattern.
* Hover-gated GPU previews per icon:
- extend-X: filled Z=0 floor quads spanning the wall's offset to
offset+thickness Y band, visible from plan view without side-
view clutter. Grow case (cursor beyond either endpoint): one
green decorator_color_selected quad over the extension. Shrink
case (cursor inside extent): green quad for the portion that
REMAINS + red decorator_color_error quad for the portion the
operator REMOVES.
- extend-Z: vertical lines at the cursor's projected X in the
wall's y=0 reference-line plane. Grow case (cursor above wall
top): one green segment from z=height to z=cursor.z. Shrink
case: green from z=0 to z=cursor.z (REMAINS) + red from
z=cursor.z to z=height (REMOVES).
- split: one red vertical line at the cursor's projected X from
base to wall top — the cut plane.
Quads use QUAD_ALPHA=0.25 so the underlying wall body stays
visible.
* New module-level _fill_quads_alpha helper next to
_stroke_lines_alpha, plus a per-decorator _fill convenience method
and a _wall_floor_quad corner builder.
Modal-active gizmo hiding (is_gizmo_hidden_by_modal) is preserved.
Generated with the assistance of an AI coding tool.
Add openings — GizmoWallAddOpening only fired when a wall was active +
co-selected with a non-host; slabs and roofs got no in-viewport handle.
GizmoHostAddOpening covers all three host types via is_supported_host,
dispatching walls to the axis-projection anchor and slabs/roofs to a
world-Z anchor lifted just above the host's top face (predictable
height regardless of the void's vertical position).
Show openings on hosts with their own parametric-edit toolbar —
GizmoRoofEdition gains an idle-row toggle_openings_gizmo parallel to
the wall's, parked at the cancel-slot X next to the pen. Visible only
when the host carries HasOpenings and the edit triad is idle. Roof
overrides get_element_height to return the mesh's world-AABB top in
object-local Z, so the WHOLE pen-row anchors visibly above sloped or
stepped roof bodies. The wall's idle-row toggle now also hides when
HasOpenings is empty.
Show openings on hosts WITHOUT a parametric-edit toolbar —
GizmoHostToggleOpenings scoped strictly to the fallback case: a single
host selected, HasOpenings non-empty, NOT a path-connectable wall, NOT
a parametric roof. Covers slabs today plus any foreign-authored IfcRoof
without BBIM_Roof. Anchored at object origin XY + world-AABB top Z.
When slab parametric-edit eventually lands, the slab predicate joins
the exclusion list and this gizmo's poll narrows automatically.
Operator move — ToggleWallOpenings was already host-agnostic; renamed
to ToggleHostOpenings in opening.py (bl_idname bim.toggle_host_openings).
Three callers (the wall idle-row binding, GizmoWallFilletToggleOpenings,
and workspace.py's hotkey_A_O for Alt+O) now route through the renamed
operator. The Alt+O binding is surfaced in the operator's
bl_description so it appears in F3 search and hover tooltips.
DRY refactors —
* GizmoWallAddOpening deleted (subsumed by GizmoHostAddOpening)
* tool.Blender.get_object_world_bounding_box added as the world-AABB
sibling of the existing local helper; 3 inline call sites in
tool/misc.py (set_object_origin_to_bottom, scale_object_to_height)
and gizmos.py adopt it (2 other sites in drawing/operator.py and
project/operator.py inherently need raw transformed corners for
per-corner plane / NDC tests — not AABB candidates)
* BaseParametricGizmoGroup gains setup_pen_row_toggle_openings_icon +
update_pen_row_toggle_openings_icon; wall + roof + any future host
gizmo wire up the idle-row toggle with two one-line calls
* _resolve_active_host shared poll prologue between the two host
gizmos (gate + selection count + active-in-selected + entity lookup
+ supported-host check)
* HasOpenings non-empty checks at 3 sites route through
tool.Geometry.has_openings
* hotkey_A_O body collapsed to bpy.ops.bim.toggle_host_openings()
The forward-compat AST guard pinning "must accept fillet-corner walls"
retargets from GizmoWallAddOpening.poll to is_supported_host.
Generated with the assistance of an AI coding tool.
The extend-X / extend-Z / split icons share the cursor's projected X
on the wall axis, separated only by world Z (floor / cursor / wall
top). World Z collapses to a single screen point in plan view, so
every icon piled onto extend-X's hit target and only the topmost was
clickable.
Two refinements ported from gizmos-8088:
* When ``tool.Blender.is_view_top_down(context)`` reports the camera
is near plan-view, swap world-Z stacking for screen-up stacking:
anchor all icons at the floor world position and offset each by
``index * CURSOR_STACK_OFFSET`` along ``tool.Blender.get_screen_up_world(context)``.
Each icon lands in its own screen-space slot regardless of view
rotation.
* In the same top-down branch, drop ``extend_z_gizmo`` entirely. A
vertical-intent gizmo has no readable cue when looking down +Z —
clicking it would mutate the wall in a direction the user can't
see change.
* Bonus: split's local Z now goes through
``core.extrusion_depth_from_vertical_height(props.height, props.x_angle)``
so the icon lands on the slanted top edge of sloped walls (x_angle
!= 0) instead of the vertical-height target the wall isn't at.
All three helpers (``is_view_top_down``, ``get_screen_up_world``,
``extrusion_depth_from_vertical_height``) already on HEAD from PR2/PR3.
Non-top views unchanged — same world-Z stacking + cascading bumps as
before.
Generated with the assistance of an AI coding tool.
Hover-gated viewport preview lines that show where a wall-join /
extend / split operator would land before the user clicks. Four
preview paths, each gated on a specific icon's ``is_highlight`` state:
* **Join intersection** — two LAYER2 walls selected in the ``intersect``
state (non-joined, non-collinear, non-parallel). Draws four lines:
each wall's axis at both base and top Z, extending from the wall's
nearer endpoint to the projected XY intersection. The pair of lines
per wall communicates the full plane the join welds at, not just
the floor edge.
* **Cursor extend** — single LAYER2 wall, hover on ``extend_x_gizmo``.
One line from the wall's nearer X endpoint to the cursor's projected
X on the wall axis.
* **Cursor extend-Z** — hover on ``extend_z_gizmo``. Vertical line at
the cursor's projected X from wall base to cursor Z (the new total
height).
* **Cursor split** — hover on ``split_gizmo``. Vertical line at the
cursor's projected X from wall base to wall top — the cut plane.
Warning-red colour matches the icon's destructive-action signal.
Hover colour rules for the join preview:
* **Join or Fillet hover** → all four lines highlight in
``decorator_color_selected``. Both icons commit a symmetric corner
meet, so every line is part of the operation.
* **Extend-to-Wall hover** → only the non-active wall's two lines
(base + top) highlight. The default-direction extend operator
moves the non-active wall into the active one's axis; only that
wall's preview should signal motion.
* No hover → all four lines in ``decorations_colour``.
Three coordinated changes:
* ``bim/module/model/wall.py`` gains the ``_classify_wall_join_state``
wrapper over ``core.classify_wall_join_state`` (feeds the
``_are_walls_joined`` flag the core helper expects) AND a
``_active_instances`` per-region weakref ClassVar on
``GizmoWallJoinIntersection`` populated in ``setup()``. Without the
weakref registration, the decorator's ``_lookup_active_instance``
call returns None every frame and the hover gates silently
evaluate False — the symptom would be preview lines that never
switch colour. Both pieces ported from gizmos-8088.
* ``bim/module/model/decorator.py`` gains
``WallGizmoPreviewDecorator`` (~280 LOC across the four preview
paths + shared helpers ``_stroke`` /
``_active_layer2_wall_for_gizmo_preview`` /
``_join_group_hover_state`` / ``_extended_wall_index``). All
cross-file dependencies (``core.classify_wall_join_state``,
``core.wall_join_preview_lines``, ``_stroke_lines_alpha``,
``_cursor_icon_hovered``, ``_lookup_active_instance``,
``tool.Parametric.is_path_connectable_wall``,
``_wall_axis_world_segment_from_geom``) already on HEAD.
* ``bim/handler.py`` wires ``WallGizmoPreviewDecorator.install()`` /
``.uninstall()`` alongside the other always-on preview decorators.
The decorator self-polls every frame; cost is one selection-count
check + one ``is_highlight`` read when no eligible state is active.
Verified: headless smoke green, ruff + black clean. Live testing
confirms the four preview paths fire correctly when hovering each
icon.
Generated with the assistance of an AI coding tool.
GizmoWallJoinIntersection used to place its icons at state-specific
world points: join at floor Z, extend-to-wall at the active wall's
top Z, fillet stacked screen-up above join. Same XY at different Z
collapses to a single screen pixel in plan / top view, so two icons
became one hit target — invisible from above.
* position_gizmos now always-stacks along screen-up at a wall-top
anchor in both the joined (unjoin + fillet) and the intersecting
(extend + join + fillet) states. Order bottom-up is
extend / L / fillet. Collinear-merge keeps its single boundary
icon (no stack needed).
* New _stack_anchor_z picks the active wall's top Z (or the taller
of the two on mid-selection-transition frames). New _stack_at
lays a tuple of icons along screen-up at the resolved anchor.
* Glyph swap: join_icon -> VIEW3D_GT_wall_corner (L), extend_to_wall_icon
-> VIEW3D_GT_wall_tee (T). Both classes already existed in
bim/module/drawing/gizmos.py from an earlier commit; only the
setup() bl_idname strings changed. The previous arrow-merge /
arrow-extend pair read as the same direction once stacked.
Forward-compat AST contracts in test_wall_gizmos_forward_compat.py
pin the new invariants: the L and T bl_idnames must appear in
setup(), and position_gizmos must route through _stack_at so a
regression that reintroduces a direct billboarded_at write for any
state-specific icon fails CI before it flattens the stack again.
Also folds in a one-line typo fix in core/spatial.py:
assign_container's per-element can_contain check iterated `e` but
predicate-tested `root_element` (the outer for-loop variable), so
every element in the comprehension was tested against the same
container/element pair. Switch the argument to `e`.
Generated with the assistance of an AI coding tool.
Bring in 13 commits from upstream v0.8.0 (tip f158ae737):
- Add regenerate_wall_to_underside operator + has_underside_connection
Model interface (closes#7943)
- Extend/regenerate walls to multiple undersides
- Fix duplicate booleans in extend_walls_to_underside
- Fix extend_walls_to_underside ridge artifact
- Regenerate connected walls when recalculating a slab
- Fix validate_type corruption; remove debug prints
- Lazy BVH tree construction in SnapObj + early-terminate solid raycasts
in non-xray mode + optimize 2D projection in ray_cast_by_proximity_2d
- Fix crash in update_bim_tool_props when selected type isn't a valid
ifc_class
- Fix assign_container in spatial.py (#8079)
- Fix sign of temporary offset restore in sweep_along_curve
Auto-merge resolved all overlap files cleanly:
- bim/handler.py: work branch's update_bim_tool_props refactor and
upstream's try/except hardening converged on identical try/except
around props.ifc_class assignment (no net change).
- bim/module/model/__init__.py: upstream's wall.RegenerateWallToUnderside
entry and work branch's roof gizmo entries occupy disjoint sections.
- bim/module/model/wall.py: upstream's RegenerateWallToUnderside operator
and work branch's GizmoWallEdition/IconSlot refactors occupy disjoint
sections.
- core/tool.py: upstream's four new Model stubs and work branch's
Root.has_material_styles stub occupy different classes.
Partly generated with the assistance of an AI coding tool.
Completes the PR4/PR5 cleanup the FIXME at tool/blender.py
flagged: every is_<type> / Array.<helper> shim on
tool.Blender.Modifier delegated one-for-one to tool.Parametric /
tool.Array. Callers now reach the canonical home directly, and the
shim block — seven is_<type> classmethods plus the inner class Array
— comes out.
Renames (no semantic change):
* tool.Blender.Modifier.is_<door|railing|roof|stair|wall|window>
→ tool.Parametric.is_<x>
13 sites across tool/loader.py, bim/import_ifc.py,
bim/module/geometry/{data,operator}.py, bim/module/model/{door,
railing,roof,stair,ui,wall,window}.py.
* tool.Blender.Modifier.Array.<helper> → tool.Array.<helper>
4 sites across tool/root.py, bim/import_ifc.py,
bim/module/geometry/operator.py.
* test_parametric_registry.py: the two getattr probes that hunt
predicates by name now look on tool.Parametric. Docstring + the
test function name (test_every_entry_has_modifier_predicate →
test_every_entry_has_parametric_predicate) follow the move.
Kept on tool.Blender.Modifier (non-shim, no equivalent on
tool.Parametric): try_applying_edit_mode,
try_canceling_editing_modifier_parameters_or_path,
is_eligible_for_<x>_modifier (×5), is_array_child, is_slab.
Verified: 109 model-lane tests + 8 parametric-registry tests pass
(the one pre-existing failure in test_wall_header_refresh.py is
unrelated — it patches handler.update_bim_tool_props which has been
renamed). git grep for tool\.Blender\.Modifier\.(is_<type>|Array\.)
returns empty. black + ruff clean on every touched file.
Generated with the assistance of an AI coding tool.
Ports roof parametric edit gizmo group from gizmos-8088 and folds in
three roof-mesh bug fixes surfaced during live testing.
Port:
* ``CycleRoofGenerationMethod`` operator (bim.cycle_roof_generation_method)
cycles props.generation_method between "HEIGHT" and "ANGLE". Shift+click
cycles in reverse via the ``CycleTypeMixin`` contract.
* ``GizmoRoofEdition`` gizmo group: 3 dimension gizmos for height
(visible in HEIGHT mode) / slope angle with tan/atan2 rise round-trip
+ degree formatter (ANGLE mode) / roof_thickness. All three handles
anchor at the object's local origin and separate visually via their
declared axes (height/slope +Z, thickness -Z) — height + slope are
mutually exclusive via ``visibility_condition`` so they never paint
at the same time. Anchoring at the origin sidesteps the first-click
default-identity-matrix symptom that footprint-derived anchoring
would have hit on a stale ``RoofData`` cache.
* Lifecycle factory swap: explicit ``EnableEditingRoof / CancelEditingRoof
/ FinishEditingRoof`` classes replaced by ``tool.Parametric.build_edit_lifecycle("roof", _RoofEditMixin, ...)``.
Same bl_idnames out, no external caller changes.
* Registration: ``CycleRoofGenerationMethod`` + ``GizmoRoofEdition``
added to ``bim/module/model/__init__.py`` classes tuple.
* Tests: ``test_roof_gizmos.py`` covering slope round-trip, visibility
gates, cycle operator metadata, and origin-anchored positioning.
Bug fixes:
* ``generate_hipped_roof_bmesh`` flipped the bottom slab face's normal
at low slope angles. The kernel's outward-inference becomes
ambiguous on near-flat geometry once ``remove_doubles`` and
internal-face deletion run, and the early ``recalc_face_normals``
pass at line 389 ran BEFORE the topology was final. A second pass
on the final closed mesh fixes the eave plane (now reliably points
down regardless of slope).
* ``bpypolyskel.polygonize`` can emit a face whose vertex list
contains the same index twice on certain footprint/slope
combinations (a straight-skeleton ridge collapse). ``bm.faces.new``
rejects those with ``found the same (BMVert) used multiple times``,
aborting the whole rebuild. Filter the degenerate faces out so the
rest of the roof renders.
* ``_RoofEditMixin._restore_viewport_after_cancel`` now rebuilds the
bmesh from the just-restored draft via ``update_roof_modifier_bmesh``.
The hook was abstract on ``PathPreservingEditMixin`` and raised
``NotImplementedError`` on cancel-after-edit, leaving the user
stranded.
Also folds in a parallel ``tool/loader.py`` swap from
``tool.Blender.Modifier.is_railing`` to ``tool.Parametric.is_railing``
(consistent with the rest of the loader using ``tool.Parametric.*``).
Verified: headless smoke green, test_parametric_registry.py 8/8,
test_roof_gizmos.py 15/15. ruff + black clean on the touched files.
Generated with the assistance of an AI coding tool.
Three wall-gizmo fixes:
* GizmoWallFilletPreview crashed on every draw_prepare after the
DRY-colors refactor moved decoration lookups onto
self.get_decoration_colors() — that method lives on
BillboardingGizmoGroupMixin / BaseParametricGizmoGroup, but
GizmoWallFilletPreview inherited only from bpy.types.GizmoGroup.
setup() AttributeError'd silently, leaving radius_dim and friends
unset. Add the mixin to the bases; rename _position_gizmos to
position_gizmos so the mixin's refresh/draw_prepare dispatch lands
correctly and drop the now-redundant overrides.
* GizmoWallAddOpening's poll gated on the strict is_wall predicate,
which rejects fillet-corner walls (no LAYER2 usage by IFC spec).
Switch to is_path_connectable_wall on both the active and the
partner-exclusion checks so the add-opening icon surfaces over
curved corners — matching every other wall-state gizmo's host gate.
* Show / hide openings was only available on LAYER2 walls because
GizmoWallEdition's parametric edit pipeline (which carries the
toggle) refuses fillet bodies. Add GizmoWallFilletToggleOpenings,
a dedicated single-icon group that polls on is_fillet_corner_wall
and reuses bim.toggle_wall_openings — the body stays untouched.
Forward-compat AST guards in test_wall_gizmos_forward_compat.py pin
both invariants: every wall GizmoGroup that calls
self.get_decoration_colors() must inherit a mixin that provides it,
and GizmoWallAddOpening.poll must keep using the looser predicate.
Generated with the assistance of an AI coding tool.