When HDF5 is found via its CMake config file, the code previously hardcoded
the hdf5_cpp-static target. On distributions that ship only shared HDF5
(e.g. Fedora rawhide where the config file was added in a newer package),
this caused a link failure. Now checks for hdf5_cpp-static, hdf5_cpp-shared,
and hdf5::hdf5_cpp-shared in order, falling back to module-mode discovery.
props.ifc_class is an EnumProperty whose items list only the element/space
types present in the model. Assigning element_type.is_a() crashed with
`enum "<class>" not found` when the selected element's type wasn't a member
(e.g. a raw IfcTypeProduct, or a stale item list mid-rebuild), aborting the
post-commit refresh.
Wrap the assignment in the same try/except TypeError guard already used for
the sibling relating_type_id assignments (added in 233cc344fa).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When validate_type selected a preferred_item from remaining_items
(e.g. the sole IfcBooleanResult in a representation), it left that
item in the list. The subsequent Items filter removed every item,
leaving Items=[] and causing guess_type to return
"MappedRepresentation" — silently corrupting the representation.
Also removes temporary debug print statements added during
investigation of the wall-to-slab extension workflow.
Generated with the assistance of an AI coding tool.
Re-running the operator on the same wall/slab pair created
additional IfcPolygonalFaceSet booleans each time. Now each
wall's existing booleans are removed before re-clipping, and
previously connected slabs are merged with the new selection
so no earlier clips are silently discarded.
Generated with the assistance of an AI coding tool.
When Shift+G is pressed on a LAYER3 element, any LAYER2 walls
connected via IfcRelConnectsElements(TOP) are now re-clipped
to the slab's updated geometry after recalculate_slab runs.
Generated with the assistance of an AI coding tool.
extend_walls_to_underside now accepts multiple slab/roof
objects in a single operation — all selected non-LAYER2 IFC
elements are treated as clip targets, all LAYER2 elements as
walls. Placement sync is done once upfront; each wall is then
clipped against every selected slab before reloading.
Also adds bim.regenerate_wall_to_underside (Shift+G): after
moving a slab, re-clips connected walls using the existing
IfcRelConnectsElements(TOP) relationship. Old booleans are
removed via remove_representation_item before re-clipping.
Generated with the assistance of an AI coding tool.
When extend_walls_to_underside is applied to a wall and the
roof/slab is later moved, pressing Shift+G now re-clips the
wall to the slab's new position.
The IFC relationship created by connect_wall_to_slab
(IfcRelConnectsElements, Description="TOP") is used to look
up which slabs a wall is clipped to. On regeneration, the
existing manual booleans (IfcPolygonalFaceSet operands) are
cleanly removed via remove_representation_item, then
clip_wall_to_slab is re-applied for each connected slab.
Shift+G on a LAYER2 wall that has a TOP connection now calls
bim.regenerate_wall_to_underside; walls without a connection
continue to call bim.recalculate_wall as before.
Generated with the assistance of an AI coding tool.
When the operator was called twice on the same wall for a
ridge roof, the two IfcPolygonalFaceSet clip solids shared
an exact ridge edge (kissing-solid). OCCT produced spurious
extra vertices at the coincident boundary.
Fix by building the clip solid from a rectangle on the slope
plane that extends slightly past the face edge (1 project
unit margin) rather than the exact face footprint. Adjacent
slope solids now volumetrically overlap at the ridge instead
of sharing a boundary face, which OCCT handles correctly.
Generated with the assistance of an AI coding tool.
ifc.get_object(element) can return None for IFC elements that aren't loaded as Blender objects (e.g., decomposed sub-elements).
The loop now skips those instead of passing None into collector.assign().
Cheers!
The temporary-offset workaround (#7408, commit bd57cc8735) subtracts the
directrix centroid (`mean`) from the curve points before building the
sweep near the origin, then must add it back to restore the original
location. The restore negated the sign — `Move(-mean)` instead of
`Move(+mean)` — placing the swept solid at -mean (mirrored through the
origin) rather than its true position.
Only triggers for polyline directrixes (`is_polyhedron()`) whose centroid
is more than 100 m from the origin (`mean.norm() > 1e2`), so models
centered near the origin are unaffected. Models that keep absolute site
coordinates (e.g. many Revit/ODA IFC exports) render affected swept
solids — reinforcing bars, pipes — at a mirrored phantom location far
from the rest of the model.
Three test files covering PR4's new surfaces — preview registry,
wall-gizmo poll behaviour, fillet operator registration. Every test
walks the live registry or class hierarchy instead of hard-coding
preview keys, operator names, or helper function names, so adding a
new preview / wall gizmo group / fillet operator exercises the same
invariants without test edits.
test_preview_base.py (6 tests):
* RegistryContract: every PREVIEW_CANCEL_OPS entry resolves to a
callable cancel operator on bpy.ops.bim.
* GetPreviewPropsTolerance: get_preview_props returns None for
contexts without a scene (regression guard for the SimpleNamespace
bug fixed in commit ee63137c6).
* ActivationCycle (registry-driven loop): any_preview_active toggles
with each registered preview's is_active flag;
discard_pending_previews clears every active flag across every
registered preview.
* SaveOnDiscardWired: locates the bim.save_project operator
dynamically and verifies its execute path references the discard
helper by its actual __name__.
test_wall_gizmo_poll_gate.py (4 tests):
* WallGizmoGroupsHideDuringPreview: walks the wall module for
bpy.types.GizmoGroup subclasses (skips preview-owner exceptions
whose bl_idname contains 'preview'), mocks any_preview_active to
True, and asserts every discovered gizmo's poll returns False.
* BaseParametricGizmoPollHidesDuringPreview: mirrors the test for
the cross-feature parametric framework base class.
test_fillet_operators.py (3 tests):
* FilletOperatorsRegistered: at-least-four-ops + every-discovered-op-
is-callable. Catches accidental deregistration.
* EnableRejectsIneligibleSelection: poll returns False without a
selection so the operator is greyed-out in menus.
State-clearing tests via bpy.ops.bim.cancel_wall_fillet_preview() are
deliberately omitted — the operator early-returns when context.screen
is unattached and prior tests in the model lane can leave the screen
in that state, making the dispatch path inherently flaky. Live testing
covers the behaviour.
Net: 13 tests pass cleanly in both single-file and full model lane.
Generated with the assistance of an AI coding tool.
deepdiff 9.1.0 added cachebox<6,>=5.2 as a direct runtime dep.
cachebox 5.2.3 only publishes macOS x86_64 wheels for macosx_10_12+,
incompatible with the macos py311 build's --platform macosx_10_10_x86_64.
The daily build's linux-wheel safeguard fires when the resulting
cachebox-*-manylinux_*.whl leaks into the macOS / windows wheels folder
(builds run on ubuntu-latest and cross-build via pip download --platform).
Pin deepdiff to <9.1 (resolves to 9.0.0, no cachebox transitive dep) as
the minimal hotfix. Long-term cleanup: bump the macos py311 platform tag
from 10_10 to 10_13 (matching py312/py313) and re-flag this line with the
standard \$(PYPI_PLATFORM) --only-binary=:all: pattern used by brickschema
and python-socketio.
Partly generated with the assistance of an AI coding tool.
Save-path:
* SaveProject._execute (project/operator.py) now calls
preview_base.discard_pending_previews(context.scene) right after
tool.Parametric.commit_pending_edits(). Previews are session-
transient — discard rather than commit. Sibling gizmo polls gate
on each preview's is_active flag; a stuck flag persisted through
the save would silently hide them on reload. Mirrors the pattern
already in gizmos-8088.
Preview-active gate hardening:
* preview_base.get_preview_props tolerates contexts without a
``scene`` attribute. Pre-existing tests use SimpleNamespace mocks
for the context; the previous getattr(context.scene, ...) raised
AttributeError before the inner default kicked in.
Test update:
* test_wall_header_refresh.test_geom_generation_invalidates_wall_geom_cache
patches tool.Wall.read_geometry instead of the now-deleted local
wall._read_wall_geometry (commit 7e5e7b8d6 migrated the call site).
Generated with the assistance of an AI coding tool.
When override_mode_set_edit encounters an unsupported profile (Couldn't
import profile), deselect the object so Tab continues to cycle cleanly.
Also restores the selection that existed before entering aggregate mode
when finally tabbing out, via save/restore_previous_selection().
Following the pattern from 586f9be077, deselect the active object after
exiting item mode so Tab continues to cycle cleanly. Also deselects
parametric LAYER1/LAYER2 items that cannot be edited directly, avoiding
the need to manually deselect before Tab-cycling out of aggregate mode.
Two cohesive cleanups in one commit.
A. Migrate wall.py to PR3-absorbed tool methods (fixes bug 4: pen icon
missing on fillet corner walls):
PR3 shipped tool.Wall.read_geometry + tool.Wall.validate_for_parametric_edit
but wall.py kept local duplicates predating that work. The local
_read_wall_geometry guards on tool.Blender.Modifier.is_wall (LAYER2-only)
while the tool method guards on tool.Parametric.is_path_connectable_wall
(LAYER2 OR fillet corner). Consequence: _get_wall_geom_cached → local
_read_wall_geometry returned None for every fillet corner →
GizmoWallFilletReedit.position_gizmos hit `if geom is None: hide` →
pen icon was unreachable for every fillet corner the user created.
Three _read_wall_geometry callers migrated to tool.Wall.read_geometry
(_read_wall_state_into_props, _get_wall_geom_cached,
GizmoWallJoinIntersection.position_gizmos). Two
_validate_wall_for_parametric_edit callers migrated to
tool.Wall.validate_for_parametric_edit (_maybe_resync_wall_props_from_ifc,
EnableEditingWall._execute). Local helpers deleted; docstring references
updated.
B. Drop over-restrictive gizmo gates (fixes bug 1: join icons missing
when walls intersect away from endpoints):
GizmoWallJoinIntersection.position_gizmos no longer hides itself when
the projected intersection lands further than MAX_DISTANCE_TO_ENDPOINT_
FACTOR (0.75 wall lengths) from any endpoint. The remaining
PARALLEL_DOT_THRESHOLD (cos 2°) gate via project_axis_intersection
returns None for near-parallel walls and is the only correctness bound;
distance from endpoints is a UI concern, not a geometric one.
GizmoWallFilletReedit.poll drops the has_a / has_b ConnectedFrom +
ConnectedTo guard — the IsFilletCorner pset is the authoritative signal.
EnableWallFilletPreviewFromCorner.execute already separately validates
both neighbour connections and reports a user-facing error if either
side is disconnected.
Generated with the assistance of an AI coding tool.
Three live-session regressions surfaced after the fillet feature
landed.
Sister gizmos competed with the active preview:
* preview_base.any_preview_active(context): new helper iterates the
PREVIEW_CANCEL_OPS registry and returns True if any preview is open.
Future previews registered there automatically gate sister gizmos.
* BaseParametricGizmoGroup.poll (gizmos.py): short-circuits on
any_preview_active so every parametric gizmo (door/window/stair/
roof/railing/wall edition) hides during ANY preview.
* The 4 wall gizmo groups with explicit polls (GizmoWallAddOpening,
GizmoWallExtendVertically, GizmoWallJoinIntersection,
GizmoWallUnjoinSingle) + GizmoWallFilletReedit gain the same gate.
DRY: extract _wall_gizmo_poll_gate(context):
* 5 wall gizmo polls each duplicated the 2 pre-flight checks
(viewport-gizmos enabled + no preview active). The helper centralises
them — each poll becomes a single short-circuit line followed by its
per-feature selection inspection.
ESC cancels the active preview:
* try_cancel_active_preview already existed in preview_base since PR3
but had no caller. Hooked into OverrideEscape.execute (geometry/
operator.py) as a new elif branch — same keymap that already cancels
pen gizmo edit mode + item mode + edit mode + aggregate mode. Order
in the branch chain matters: try preview cancel before falling back
to try_canceling_editing_modifier_parameters_or_path so the in-
flight preview wins over a stale modifier-edit cancel attempt.
Generated with the assistance of an AI coding tool.
In BIM_PT_type_attributes and BIM_PT_object_attributes (when
the active object is a type), attribute value buttons now use
"type.<Attr>" as the selector key so the operator finds
matching occurrences via their relating type rather than the
occurrence's own (often unset) attributes.
Generated with the assistance of an AI coding tool.
After selecting objects in the same container, copy a `location="Name"`
filter query to the clipboard and report it — consistent with the same
behaviour in SelectSimilarType, SelectSimilarAggregate, SelectIfcClass,
and SelectSimilarMaterial.
Generated with the assistance of an AI coding tool.
When a `+`-separated filter group returns no results, `FacetTransformer.facet_list`
was skipping the reset of `has_additive_facet_in_current_list` because the reset
was inside the `if self.elements:` guard. The stale flag caused the next group's
`add_default_elements()` to bail out early, leaving its element set empty and
silently dropping every subsequent group from the result.
Move the flag reset outside the guard so it always fires regardless of whether
the group produced any results.
In the Placement panel, show Location and Rotation X/Y/Z
each on their own row beneath a header label. When the IFC
file uses imperial units, display a read-only feet-and-inches
label alongside each Location input field.
Generated with the assistance of an AI coding tool.
Display general and calculated stair parameters (Width,
Height, Tread Run, Tread Rise, Length, etc.) formatted
to the IFC file's configured length unit rather than
raw numeric values.
Generated with the assistance of an AI coding tool.
End-to-end fillet flow on top of the helpers + recreate_wall hook
(landed in the previous commit). Users select two LAYER2 walls, click
the fillet entry icon, drag the live radius widget, and validate to
replace the corner with a curved LAYER2 corner wall (banana body).
Operators (5):
* EnableWallFilletPreview: 2-wall selection → validates LAYER2 +
straight axis + zero-slope + intersect-or-joined state → seeds the
preview props with a default radius computed from the shorter
available leg.
* FinishWallFilletPreview: dispatches CreateWallFillet with the tuned
radius; clears preview state on FINISHED, preserves it on failure so
the user can re-tune without re-selecting.
* CancelWallFilletPreview: clears preview state, no IFC mutation.
* EnableWallFilletPreviewFromCorner: pen-icon re-edit on an existing
fillet corner — pre-fills the preview from the corner's BBIM_Wall
pset + walks the inverse graph to recover wall A and wall B.
* CreateWallFillet: deletes any prior corner + A↔B path connection,
shortens A and B to the tangent points, instantiates a corner wall
from A's type, unassigns the swept-layer material/type (the explicit
banana body MUST own its geometry), assigns the dominant material,
rebuilds the body, sets a straight 2-point chord axis, stores
BBIM_Wall.IsFilletCorner+FilletRadius, reconnects A and B to the
corner with NOTDEFINED on the corner's side.
Gizmo groups (2 new + entry icon on existing):
* GizmoWallFilletPreview: visible while a preview is active. Bundles
a radius_dim widget at the arc apex, a trim_dim widget along wall A
expressing the same DOF via the leg setback distance
(trim = |radius| * tan(sweep/2)), and validate / cancel icons
anchored above the apex in screen-up.
* GizmoWallFilletReedit: pen-icon entry on an existing fillet corner
wall (single-selection, BBIM_Wall.IsFilletCorner set, both neighbour
connections present). Mutually exclusive with an active preview.
* GizmoWallJoinIntersection now stacks a fillet entry icon
(VIEW3D_GT_fillet → bim.enable_wall_fillet_preview) above the
existing join/unjoin icon in the joined and intersect state branches.
Property + decorator infrastructure:
* prop.py: BIMWallFilletPreviewProperties (Scene-level draft) +
BIMPreviewProperties umbrella with only the wall_fillet pointer.
The umbrella is the seam preview_base.py (landed in PR3) already
reads via getattr(scene, "BIMPreviewProperties", None).
* decorator.py: _stroke_lines_alpha helper + WallFilletPreviewDecorator.
Polls is_active; renders leg projections + arc + arc-center
construction lines from tool.Wall.compute_wall_fillet_geometry.
* __init__.py: registers operators + gizmo groups + property groups +
wires Scene.BIMPreviewProperties.
* handler.py: WallFilletPreviewDecorator.install/uninstall in
_install_decorators — always installed, self-polls on is_active.
Drive-by: extract gizmo.get_screen_up(billboard_rot) helper —
the local +Y of a billboard rotation is the camera's screen-up world
direction. Replaces 4 inline `billboard_rot @ Vector((0.0, 1.0, 0.0))`
sites added across the fillet feature's gizmo groups.
Generated with the assistance of an AI coding tool.
DecorationsHandler now caches dissolved edges (mesh-keyed), world-space
draw payload, and GPUBatch objects with per-object epoch invalidation —
moving one wall doesn't wipe 50 opening caches. Object-mode dissolve
removes triangulation noise; 2-pass depth-test split dims occluded lines
instead of hiding them. Edit-mode behavior unchanged.
Also: disable viewport shadows for IfcFeatureElementSubtraction objects,
and wire DecorationsHandler.uninstall() into the model module's
unregister() so the new persistent handlers don't leak on addon disable.
Generated with the assistance of an AI coding tool.
EditAssignedMaterial propagated layer set usage attributes
to all selected objects but skipped this loop for profile
set usage. Add the same loop so CardinalPoint and
ReferenceExtent are copied to each selected object's
IfcMaterialProfileSetUsage on save.
Generated with the assistance of an AI coding tool.
When the user enters `-0' - 10"`, Python parses feet as -0.0.
The check `feet < 0` is False for negative zero, so the sign was
silently dropped. Use math.copysign to detect it correctly.
Generated with the assistance of an AI coding tool.
Eleven module-level helpers in wall.py that the upcoming wall-fillet
operators + gizmo groups depend on. Each is self-contained or
references only helpers earlier in the file; the operators and
gizmos themselves land in follow-up commits.
* _wall_fillet_props / _wall_fillet_preview_active /
_wall_fillet_preview_walls: thin read-side accessors over the
BIMPreviewProperties.wall_fillet pointer (added with the
operators commit). Safe today: get_preview_props returns None
until the pointer is attached.
* _walls_have_zero_slope_for_fillet: validates that input walls
are vertical (x_angle ~ 0); slanted-extrusion fillets require
swept-along-curve geometry the banana profile builder doesn't
support.
* _build_curved_corner_body_representation: builds the banana
(annular sector) IfcExtrudedAreaSolid as a polyline-tessellated
IfcIndexedPolyCurve.
* _apply_fillet_corner_geometry: positions the corner wall at
tangent_a and rebuilds its body. Shared by the creation operator
and the regenerate path.
* _resolve_two_walls: pulls (active, other) from a 2-wall
selection, validates both as LAYER2 + straight-axis + not-already-
a-fillet-corner.
* _pick_dominant_wall_material: returns the thickest layer's
material from an element's IfcMaterialLayerSet / Usage.
* regenerate_fillet_corner_wall: re-runs the geometry build from
BBIM_Wall.FilletRadius + current neighbour layer parameters.
Called by tool.Model.recreate_wall when the IsFilletCorner pset
is set; the FIXME(PR4) placeholder in recreate_wall is dropped.
* _wall_fillet_gizmo_x_matrix: 4x4 placement matrix with local +X
aligned to a world-space direction; used by the fillet preview
gizmo group.
Centralises the IsFilletCorner pset read as
tool.Parametric.is_fillet_corner_wall — replaces 3 inline
get_pset(element, "BBIM_Wall", "IsFilletCorner") sites
(tool.Model.recreate_wall, tool.Model.recalculate_walls,
tool.Parametric.is_path_connectable_wall) plus the new
_resolve_two_walls call.
Generated with the assistance of an AI coding tool.
The framework's parametric-edit icon row currently binds an array
icon to bim.add_array_from_feature_edit, but the supporting per-
feature add-array flow and gizmo positioning haven't fully landed.
Showing the icon today lets the user click it and trigger a half-
wired flow.
Force the icon hidden inside the props.is_editing branch of
BaseParametricGizmoGroup.update_editing_gizmos. The else-branch
(not editing) already hides it, so this just mirrors that behavior
during edit mode. Drop this gate when array integration completes
to re-enable the icon position + visibility plumbing.
Generated with the assistance of an AI coding tool.
The extend-X / extend-Z icons in GizmoWallEdition's cursor row are
billboarded toward the camera; without orientation polish they
always point in the same screen-space direction regardless of which
wall endpoint the click will move (or whether the cursor sits above
or below the wall top). New helper mirrors the icon's local-X (extend-X)
or local-Y (extend-Z) axis so each arrow points toward the end it
will move:
* Extend-X: walk wall midpoint to figure out which endpoint stays
fixed (cursor past midpoint → ATSTART stays; cursor before midpoint
→ ATEND stays). Project the fixed endpoint into screen-space and
flip the arrow when the gizmo's anchor sits on the same side.
* Extend-Z: flip when the cursor is below the wall top (within
EXTEND_FLIP_EPSILON tolerance).
Called once per resolved cursor gizmo from
``GizmoWallEdition._update_cursor_gizmos``, after the gizmo's
``matrix_basis`` is set by ``gizmo.billboarded_at``. Reuses
``gizmo.should_flip_extend_arrow`` + ``EXTEND_FLIP_MIRROR_X/Y`` +
``EXTEND_FLIP_EPSILON`` already on tool.
Generated with the assistance of an AI coding tool.
GizmoWallJoinIntersection's unjoin only fires when exactly two walls
are selected and surfaces one icon at their shared corner — useless
when the wall has 3+ joins and the user wants to disconnect just one.
* UnjoinWallPathConnection: surgical counterpart to UnjoinWalls.
Disconnects the active wall from a single partner wall identified
by IFC GlobalId (invariant under Blender-object renames + file
save/reload + undo). Walks both inverse arrays of the active wall
for the specific IfcRelConnectsPathElements joining the pair —
matches DumbWallJoiner.split's pattern and avoids disconnect_path's
direction-sensitivity. Resyncs both walls' draft props after the
recreate_wall pass.
* GizmoWallUnjoinSingle: activates on exactly-one selected
LAYER2 wall. Preallocates a pool of 16 unjoin icons (Blender forbids
gizmo allocation outside setup(); ATSTART + ATEND + ATPATH rels are
rarely more than a handful). Per-frame, iterates _iter_path_connections,
positions one billboarded icon at each join via
tool.Wall.path_connection_location_world, and hides the rest. Each
visible icon's bound operator carries the partner GlobalId, so a
click removes only that one rel.
* model/__init__.py: register both classes alphabetically.
Mutually exclusive with GizmoWallJoinIntersection via poll() — that
group requires len(selected) == 2; this one requires 1.
Generated with the assistance of an AI coding tool.
The single-wall unjoin gizmo needs to enumerate every
IfcRelConnectsPathElements a wall participates in, regardless of which
side of the rel the wall was authored on, and place an icon at each
join's physical location. Two helpers carry that work:
_path_connection_location_world wraps core.compute_path_connection_location
at the Vector boundary. _iter_path_connections walks ConnectedTo +
ConnectedFrom, normalises orientation to (other, self_ct, other_ct),
and filters non-wall partners + None refs so per-frame gizmo positioning
survives malformed IFC.
Generated with the assistance of an AI coding tool.
After a one-shot wall IFC mutation (unjoin / split / merge / extend /
join-at-corner …) the always-visible gizmos on the OTHER side of the
join can be left reading stale ``BIMWallProperties`` — the IFC
geometry moved but the draft props that drive the gizmo handles still
point at the pre-mutation numbers, so a subsequent edit-mode enter
shows the wall at its old length / position.
* New ``_maybe_resync_wall_props_from_ifc(obj)``: re-primes a single
wall's draft props from current IFC, with guards for non-walls,
non-parametric walls, and walls in an active draft session (the
draft is then the source of truth, not IFC). Must run from an
operator ``_execute`` — ID writes from gizmo refresh raise.
* New ``_resync_walls_after_mutation(objs)``: iterates the above
across a selection.
* Six existing mutation operators gain a resync call after their
``core.*`` / ``DumbWallJoiner`` mutation completes:
UnjoinWalls, ExtendWallsToUnderside, ExtendWallsToWall, SplitWall,
MergeWall, JoinWallsIntersection. MergeWall resyncs only the
surviving wall — the active wall is the deletion target.
Generated with the assistance of an AI coding tool.
Bundle of bugs surfaced when exercising the new gizmo framework
end-to-end in a live Blender session after the
bim/module/drawing/gizmos.py refactor + TypeAccessor/CycleType/PickType
mixins landed.
Register / annotation resolution
* parametric_lifecycle.py: hoist `entity_instance` import out of
TYPE_CHECKING so typing.get_type_hints resolves the
Callable[[entity_instance], bool] annotation at operator registration
(CycleDoorType, CycleWindowType, CycleStairType failed with NameError).
Clarify the INTERFACE return contract on the picker entry-point so
readers see why the gizmo step stays off the undo stack.
Framework callable contracts
* model/wall.py, door.py, window.py, stair.py: migrate `props_getter`
and `element_checker` from bl_idname strings to bound classmethods
on tool.Model / tool.Parametric. BaseParametricGizmoGroup.get_props
expects a callable; the string form raised TypeError on first
gizmo poll.
* model/door.py, model/stair.py: drop the dead `prop_path=` operator
kwarg from create_arc_gizmo / create_icon_gizmo call sites. The
framework helper blindly setattrs every kwarg onto the operator's
OperatorProperties, but ToggleDoorSwing / ToggleStairProperty don't
declare prop_path — the setattr raised mid-setup_element_specific_gizmos,
so self.gizmo_door_type / self.lock_gizmo never got assigned and
every subsequent draw_prepare tornadoed AttributeError. Nothing
reads op.prop_path anywhere; the kwarg was dead data.
Dispatcher operators
* model/array.py: add EnableEditingParametric (the framework pen-icon
dispatcher that routes to a per-feature edit operator by bl_idname
string) and AddArrayFromFeatureEdit (binds the framework's array
icon to bim.add_array on the current parametric draft).
* model/__init__.py: register both new operators.
Per-frame robustness
* drawing/gizmos.py: guard BaseParametricGizmoGroup.draw_prepare with
is_setup_complete() — matches the existing guard in refresh() and
in BaseSchematicGizmoGroup.draw_prepare(). Defense-in-depth: when
any subclass's setup raises mid-way, draw_prepare now no-ops cleanly
instead of per-frame AttributeError-tornadoing on whatever attribute
the failed setup phase was meant to populate.
* model/decorator.py: guard ProfileDecorator.__call__ against
context.active_object is None. The decorator is a per-frame
viewport draw handler; deselecting or deleting the active object
while it's installed crashed on obj.mode access. Treat None the
same as "no longer in edit mode" — uninstall + fire the exit
callback if present.
* geometry/data.py: ViewportData.load() populates `data` before
flipping `is_loaded`, so a raise from cls.mode() no longer leaves
the class flag-set but data-empty for subsequent reads.
Generated with the assistance of an AI coding tool.
Three concerns bundled into one cohesive refactor of gizmos.py
(splitting them surgically requires intermediate commits with
duplicate same-named classes that Python can't parse):
1. Framework primitives — StaticTrisGizmoMixin + TexturedQuadGizmoMixin
replace the older TrisGizmoMixin. New module-level helpers:
_get_static_tris_shader / _get_static_tris_batch / clear_static_
tris_cache for cached GPU batch reuse, _draw_outline_and_body for
the shared outline-then-body render path, draw_tris_with_outline
as the public wrapper. billboarded_at(world_pos, billboard_rot,
scale) is the canonical billboard-matrix helper; should_flip_extend_
arrow encapsulates the view-aware mirror decision for extend
gizmos; get_warning_color_from_prefs reads the user's warning
color.
2. Config classes — BaseValueGizmoConfig (shared visibility + dimension-
text contract), CountGizmoConfig (array N indicator),
DimensionGizmoConfig (length / height / depth labels), IconActionConfig
(icon-only gizmos that invoke an operator on click). DimensionRenderer
draws the actual numeric label using BLF.
3. Icon classes — each rewritten on StaticTrisGizmoMixin so they share
the cached GPU batch + outline-then-body render path:
GizmoLockOpen / GizmoLockClosed (replacing the single-state
GizmoLock), GizmoArc, GizmoFillet, GizmoWallCornerIcon,
GizmoWallTeeIcon, GizmoPen / GizmoValidate / GizmoCancel (the
parametric-edit triad), GizmoPlus / GizmoMinus / GizmoTrash,
GizmoArrayParent / GizmoArrayAll / GizmoArrayLayerIndicator (array
context indicators with a small digit-rendering helper for the "xN"
count label), GizmoMerge / GizmoSplit / GizmoUnjoin (wall-join
icons), and GizmoMenu (textured-quad icon-action menu trigger).
The legacy TrisGizmoMixin, GizmoLock, and DimensionDrawConfig are
removed; downstream callers in subsequent PR4 commits swap to the
new mixin and config classes when their feature operators land.
CycleTypeMixin / PickTypeMixin / TypeAccessorBase live in
bim.parametric_lifecycle (previous commit). The three mixins are
re-exported from gizmos.py here so feature-module access via
``gizmo.<MixinName>`` keeps working until PR5 cleanup drops the
re-exports.
bim/module/drawing/__init__.py is updated in the same commit to
register the 11 new gizmo classes (GizmoLockOpen / GizmoLockClosed /
GizmoFillet / GizmoWallCornerIcon / GizmoWallTeeIcon / GizmoTrash /
GizmoArrayParent / GizmoArrayAll / GizmoArrayLayerIndicator /
GizmoUnjoin / GizmoMenu) — without that, the new classes exist in
gizmos.py but aren't usable as bpy gizmo types.
Generated with the assistance of an AI coding tool.