mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
a3533bfa49f54a5cd892671bca907d6166f25dfd
21023 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a3533bfa49 |
Adopt _CommitWallDraftsFirstMixin on 7 wall operators
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.
|
||
|
|
e0aa39068d |
Shift-click add-opening preserves filling placement
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. |
||
|
|
81bacb5899 |
Use menu pick gizmo for door / window / stair type
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. |
||
|
|
56097694bf |
Add host-wall offset gizmos for door/window edit
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. |
||
|
|
d0334a72ab |
Show wall cursor gizmos outside edit mode + axis previews
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.
|
||
|
|
046917f75d |
Generalise opening gizmos + DRY toolbar plumbing
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. |
||
|
|
c398deba71 |
Stack cursor-anchored wall gizmos along screen-up in top view
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. |
||
|
|
5ce7d6f834 |
Port WallGizmoPreviewDecorator from gizmos-8088
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. |
||
|
|
497acc3d19 |
Stack wall-join trio along screen-up + L/T glyphs
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. |
||
|
|
ebb3b1ed10 | Optimize 2D projection in ray_cast_by_proximity_2d | ||
|
|
1597b30997 | Early-terminate solid raycasts in non-xray mode | ||
|
|
777eda3b09 | Lazy BVH tree construction in SnapObj | ||
|
|
0ceb61f0c0 | Add has_underside_connection method to Model class and update wall regeneration logic | ||
|
|
f5966f20a8 |
Fix validate_type corruption; remove debug prints
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. |
||
|
|
5f8688862f |
Fix duplicate booleans in extend_walls_to_underside
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. |
||
|
|
f3e4852f3f |
Regenerate connected walls when recalculating a slab
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. |
||
|
|
8ec946d189 |
Add extend/regenerate walls to multiple undersides
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. |
||
|
|
b10c9cd902 |
Closes #7943: Add regenerate_wall_to_underside operator
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. |
||
|
|
97cd08ee92 |
Fix extend_walls_to_underside ridge artifact
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. |
||
|
|
a002e1e56d |
Fix assign_container in spatial.py (#8079)
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! |
||
|
|
c559ee0015 |
Fix sign of temporary offset restore in sweep_along_curve
The temporary-offset workaround (#7408, commit
|
||
|
|
d71856d884 |
Migrate Modifier shim callers + drop the shim block
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.
|
||
|
|
0c8b6e93c6 |
Add GizmoRoofEdition + fix low-slope normals + cancel restore
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.
|
||
|
|
e1ab5047b0 |
Fix fillet preview crash + surface openings on fillet walls
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. |
||
|
|
28491b290a |
Split update_bim_tool_props commit vs selection
tool.Parametric.refresh_post_commit was calling update_bim_tool_props after every IFC mutation. The function does two things — refresh read-only header values (extrusion_depth/length/x_angle) and re-target user-intent enums (ifc_class, relating_type_id) from the active object. Doing both on the commit path crashed on IfcAnnotation actives (the type isn't in the bim_tool ifc_class enum) and silently overwrote the user's "what to build next" choice on every other element. Split the function: update_bim_tool_props remains selection-driven and does both halves; new refresh_bim_tool_headers is header-only and is what refresh_post_commit now calls. Behaviour on selection change is preserved. Also ports the upstream PR #8136 try/except guard onto the props.ifc_class write for the selection-driven path. Adds test_handler_forward_compat.py to pin both contracts via AST. Generated with the assistance of an AI coding tool. |
||
|
|
645054aa6a |
Wire array panel buttons to triad lifecycle
Two bugs in BIM_PT_array: 1. The "is this layer in edit mode" predicate compared a BoolProperty against an int (props.is_editing == i). Python evaluates False == 0 as True, so layer 0 always rendered the per-layer edit form even when no edit was active — clicking validate/cancel then dispatched against a phantom edit state. Switched to props.editing_item_index == i, which defaults to -1 and matches exactly one layer when an edit is active. 2. The panel's CHECKMARK and CANCEL buttons called bim.edit_array / bim.disable_editing_array, a parallel lifecycle that only cleared editing_item_index. Entering edit mode via the viewport gizmo (bim.enable_editing_array, the triad enter) sets is_editing=True and hides array children; the legacy panel exit unwound neither — so committing or cancelling from the panel left is_editing=True with children hidden, and the viewport gizmo thought the edit was still in progress. Re-bound both panel buttons to the canonical triad operators (bim.finish_editing_array / bim.cancel_editing_array), which _ArrayEditMixin already owns and which the viewport gizmo group already uses. Panel and gizmo now share one exit path. The three now-unreachable operators are deleted with their registration entries: EditArray (bim.edit_array), DisableEditingArray (bim.disable_editing_array), and EnableEditingArrayItem (bim.enable_editing_array_item, never called from any UI). The two test/tool/test_model.py sites that drove bim.edit_array as a commit step are switched to bim.finish_editing_array. External scripts or user keymaps bound to bim.edit_array / bim.disable_editing_array will need to update — the replacements are bim.finish_editing_array and bim.cancel_editing_array, both taking no parameters (the layer is read from props.editing_item_index). Partly generated with the assistance of an AI coding tool. |
||
|
|
95ad96c25e |
Fix door swing arcs + declarative SwingArcConfig
The recent per-gizmo-prefs cleanup left ``update_swing_gizmos`` with a stale ``prefs`` reference that raised NameError mid-refresh, so the flip arc's ``matrix_basis`` was never reassigned and the gizmo drifted to the world origin. SINGLE_SWING_RIGHT also lacked an X-mirror on the primary arc, so the swing extended past the door's right edge instead of sweeping back over the panel. Five related fixes / additions: * Drop the leftover ``prefs.decorations_colour[:3]`` per-frame colour override (the setup-time ``decorator_color_special`` is the durable contract — there's no reason to overwrite it every refresh). * Add X-mirror to RIGHT-hinged single-panel transforms so the arc sweeps back over the door rather than past the right edge. * Treat DOUBLE_DOOR_SINGLE_SWING as a two-panel layout: 4 arcs total (left + right panels, each with its own Y-mirrored flip) scaled to ``overall_width / 2``. * Hide all swing arcs for SLIDING_TO_LEFT / SLIDING_TO_RIGHT / DOUBLE_DOOR_SLIDING — sliding doors don't swing. A slide-direction indicator is deferred to a separate change. * Pin ``select_bias = -1000.0`` on every arc gizmo so the big quarter-arc hit shapes don't steal clicks from the smaller dimension and edit gizmos drawn on top. Architectural cleanup driven by the same diff: the imperative 4-create + 50-line update block is replaced by a declarative ``swing_arc_props`` list of ``SwingArcConfig`` entries (mirrors the existing ``dimension_gizmo_props`` pattern). Setup iterates the list and creates one (main, flip) pair per entry under ``gizmo_swing_arc_<name>`` / ``gizmo_swing_arc_<name>_flip``; update iterates the same list and positions each pair via the lambdas. Adding a hypothetical multi-panel variant becomes a config entry rather than two more attribute names plus a transform branch. ``ToggleDoorSwing`` gets a ``description`` classmethod that returns user-facing wording per ``flip_geometry`` branch so the tooltip on hover stops reading like operator internals. ``test/bim/module/model/test_door_gizmos.py`` (new) pins the per-door-type contract: 11 cases covering LEFT / RIGHT hinge positions, DOUBLE_SWING parity with SINGLE_SWING, DOUBLE_DOOR 4-arc layout, the sliding-types hide invariant, ``is_editing=False`` hide invariant, flip-arc matrix re-assignment, and world-matrix pre-multiplication. Verified: ``pytest test/bim/module/model/test_door_gizmos.py`` 11/11 green; combined wall + stair + door gizmo lanes 37/37 green; ruff + black clean on the three touched files. Generated with the assistance of an AI coding tool. |
||
|
|
a2d600b9af |
Add IconSlot placeholders + stair xN tread label
Add a clickable "xN" badge to GizmoStairEdition's edit row, mirroring the array's popup-input UX: click opens a number dialog (no more shift+click-into-modal). Text-only — no 2x2 grid glyph. Structural changes that enable this cleanly: * IconSlot.placeholder=True: slots reserve an X position in the row without auto-creating a gizmo. Subclasses resolve the reserved X via _slot_x_positions()[name] to place their own dynamic gizmos. Drops the brittle "remember to add extra_gap_before" workaround that would silently rot on slot reorders. * Array bug fix: the count badge collided with the "-" icon because the slot manager placed count_minus at the cycle position (X=0.87) where ICON_NUMBER_X also lives. Migrating the badge to a placeholder slot lets the manager allocate the X naturally and the "-" no longer overlaps. ICON_NUMBER_X constant removed. * IntegerInputDialogMixin in parametric_lifecycle.py: extracts the popup-dialog plumbing shared between InputArrayCount and the new InputStairTreads. Subclasses declare an IntProperty + attr_name + props_getter; the mixin owns invoke/execute. _resolve_props helper factors the common obj/props/requires_editing prologue. Tests: BIM_GT_count_label registration; IconSlot placeholder contract (no gizmo_idname required; gizmo_attrs() returns empty); the stair edit-row slot layout reserves the label position between tread_lock and plus at one ICON_ARRAY_GAP each; visibility propagates from props.is_editing. Partly generated with the assistance of an AI coding tool. |
||
|
|
fbfbe93550 |
Drop per-gizmo preferences + fix dynamic-wall face normals + DRY colors
Three related cleanups in one pass: * **Per-gizmo preferences removed.** The ``visibility_pref`` field on IconSlot, the ``prefs.gizmos.<feature>.<icon>`` PropertyGroups, and the dispatcher that surfaced them in the addon preferences UI are all gone. ``update_gizmo_visibility`` loses its ``pref_enabled`` parameter — visibility is now driven purely by editing state and modal gating. bim/ui.py drops ~257 lines of dead PropertyGroup definitions; bim/__init__.py and tool/parametric.py shed their matching wiring; door / wall slot declarations stop referencing the now-nonexistent prefs. * **Dynamic-wall face normals fixed.** ``regenerate_wall_mesh_from_props`` in wall.py now calls ``bmesh.ops.recalc_face_normals`` before writing the mesh. Without it, walls regenerated from the parametric edit draft could ship with inward-facing normals on some faces, which rendered as visual holes under any backface-cull or normal-aware shading. ``test/bim/module/model/test_wall_preview_mesh.py`` pins the invariant (every face's normal points away from the wall centre). * **Color constants DRY.** ``COLOR_RED`` / ``COLOR_GREEN`` / ``COLOR_BLUE`` / ``COLOR_NEUTRAL`` now live at module scope in gizmos.py; the BaseParametricGizmoGroup class attributes alias the same tuples so ``self.COLOR_GREEN`` keeps working. IconSlot declarations in stair.py (plus / minus) and array.py (count_minus / count_plus / delete) now reference the named constants instead of duplicating the RGB tuples inline. Verified: headless smoke green at 1267 BIM_OT_ classes, test_parametric_registry.py 8/8, wall lane 31/31 (includes the new preview-mesh test). ruff + black clean on the touched files. Generated with the assistance of an AI coding tool. |
||
|
|
782f25bd31 |
Highlight partner wall on link-toggle hover
Hovering a wall-junction link-toggle icon today only swaps the icon shape — the user doesn't see which wall the click will disconnect from until after they click. ATPATH (T-junction) configurations especially make the partner ambiguous when multiple connections sit close together. On hover, paint a wireframe bbox around the partner wall using the same shader, constants and color the array module already established for its layer-children highlight (POLYLINE_UNIFORM_COLOR, decorator_color_special, line width 1.8, alpha 0.8). The line-width / alpha constants in decorator.py are renamed from _ARRAY_LAYER_BBOX_LINE_* to _BBOX_HIGHLIGHT_LINE_* and shared between draw_array_layer_children_bbox and the new draw_wall_partner_bbox so the two highlights stay in lockstep. The trigger lives in a new GizmoWallLinkToggle subclass in wall.py which keeps the base gizmos.GizmoLinkToggle generic (per the generic-naming convention for shared widgets). The subclass's draw() calls super().draw(context) then on self.is_highlight outlines its partner_obj via the shared decorator helper. Same trigger pattern as GizmoArrayLayerIndicator. Blender's Gizmo API exposes target_set_operator but no symmetric getter, so the partner reference can't be read back from the bound operator handle. Instead GizmoWallUnjoinSingle.position_gizmos mirrors the resolved partner_obj onto each visible icon every frame next to the existing other_wall_guid write — the icon's draw() reads from its own __slots__-declared attribute. A forward-compat AST test pins the contract: GizmoWallLinkToggle.draw must reference is_highlight and call draw_wall_partner_bbox. Catches the regression where someone tidies the draw() override into super() or replaces the shared helper with an ad-hoc draw call. Generated with the assistance of an AI coding tool. |
||
|
|
f2868c2631 |
Replace hardcoded icon-X constants with IconSlot layout manager
The parametric edit toolbar row used to assign each feature icon its own ICON_<NAME>_X constant, with a separate FEATURE_ICON_MAX_X override each subclass had to bump whenever a new icon was added. Forgetting the bump silently collided icons — wall's rotate icon and the array button both landed at X=1.24 in edit mode. The new IconSlot dataclass + feature_slots tuple replace the constants-and-override pattern with order-driven positioning: the layout manager assigns each slot an X from its tuple index plus a uniform ICON_ARRAY_GAP. Adding an icon is now a one-line append; the "forget to bump" failure mode is structurally impossible. Slot capabilities cover every existing icon-row shape: * Single icon (wall rotate, array delete). * N-variant slots — N gizmos at the same X with one visible per frame via a subclass picker (stair tread-lock open/closed, wall baseline exterior/center/interior). Pair becomes the N=2 case; triplet the N=3 case. Variant idnames can be authored either as a tuple of explicit names or as a string prefix that auto-suffixes _<variant>. * Visibility prefs gate slot rendering without reflowing the row — hidden slots still consume their X position. * Extra per-slot gap before for visual separation (array's delete trails the routine controls by an extra 0.2 m). * Operator props forwarded to target_set_operator so adjusters (+/-, increment) and generic toggles (property_name=...) work. When the cycle slot is unused, feature slots collapse into the cycle position so the row stays tight — that's how wall's baseline triplet sits at X=0.87 without a gap before it. Three subclasses migrate to the new system: * wall.py — rotate icon + baseline triplet variants. Drops ICON_ROTATE_X, _BASELINE_GIZMO_ATTRS, the manual triplet creation loop, and the matching positioning block in _update_icon_row_extras (it now just picks variant visibility). * stair.py — tread_lock pair (open/closed) + plus + minus. _update_editing_icon_positions reads slot X via _slot_x_positions instead of three hardcoded constants. Also fixes the standalone total_length_lock gizmo, which was broken since PR4 split VIEW3D_GT_lock into open/closed pair (caller wasn't updated). * array.py — count_minus + count_plus + method + delete (with extra_gap_before=0.20 to separate the destructive action). Drops the manual edit-row positioning loop entirely; the base loop handles it. GizmoArrayChild now inherits BillboardingGizmoGroupMixin and uses the shared setup_icon_gizmo helper, dropping its duplicated _make_icon wrapper. Two helpers added on BillboardingGizmoGroupMixin to fold the duplicated prefs/color preamble that appeared at the top of six wall gizmo setups plus the array-child setup: * get_decoration_colors() — (decorations_colour, decorator_color_selected), the active-state pair. * get_unselected_decoration_colors() — (decorator_color_unselected, decorator_color_selected) for gizmos surfaced on already-selected geometry that should not pull focus. Verified: headless smoke green at 1267 BIM_OT_ classes, test_parametric_registry.py 8/8 pass, wall lane 29/29 pass, model lane unchanged at 135 pass + 7 pre-existing v0.8.0 failures (no regressions). ruff + black clean. Generated with the assistance of an AI coding tool. |
||
|
|
a48f326ab2 |
Add link-toggle hover gizmo for wall junctions
The previous single-wall unjoin gizmo used a bracket-pair icon (VIEW3D_GT_unjoin) that reads as "unjoin" only after you know what it is, with no clear "linked" inverse — closing the brackets to suggest the connected state collapses to a hollow square that doesn't read as a link at all. Add GizmoLinkToggle (VIEW3D_GT_link_toggle): two filled dots joined by a horizontal connector in the default state. On hover the two halves shear vertically apart — left dot+stub slip down as a unit, right dot+stub slip up — with a horizontal gap at the centre, signalling that a click will sever the underlying connection. The glyph lives next to the generic icon classes (GizmoLockOpen/Closed, GizmoArc) so any path / link / pair-of-connected-items context can reuse it; it isn't wall-specific despite the first caller. The class keeps its own per-state GPUBatch cache so the shape swap on hover doesn't allocate per frame. The hit-shape is sourced from the broken form (the larger bbox of the two states) so the cursor doesn't lose hover at the offset dots' outer edges and flicker between states. GizmoWallUnjoinSingle.setup() now requests VIEW3D_GT_link_toggle. The operator binding (bim.unjoin_wall_path_connection), the POOL_SIZE, and the per-frame partner-GUID write are unchanged. Generated with the assistance of an AI coding tool. |
||
|
|
49fe0756fa |
Fix spurious X/Y rotation on fillet corner wall
When the two source walls were placed at different elevations, the fillet corner wall ended up with sub-degree X and Y Euler rotations even though both source walls had only a Z rotation. Cause: _apply_fillet_corner_geometry derived the corner's local X axis from `chord = tangent_b - tangent_a` (a 3D vector). With walls at different Z, `chord.z` was non-zero, so `x_dir = chord.normalized()` inherited that Z component. The Z axis was already hardcoded to world Z, so x_dir and z_dir were no longer orthogonal — the resulting matrix_world was non-orthonormal, and Blender's Euler decomposition surfaced the skew as the visible X/Y rotation drift. Project the chord to the XY plane before normalising so x_dir is strictly XY-aligned and orthogonal to z_dir. The corner wall is now placed at wall A's elevation with a pure Z rotation, which matches the user's expectation when both inputs are Z-aligned regardless of their relative elevation. Generated with the assistance of an AI coding tool. |
||
|
|
1534003e51 |
Fix fillet partner missing from wall unjoin gizmo
GizmoWallUnjoinSingle.poll accepts fillet-corner walls via the looser tool.Parametric.is_path_connectable_wall predicate (fillet corners have no LAYER2 usage by IFC spec, but they still participate in IfcRelConnectsPathElements). The partner filter inside _iter_path_connections used the stricter tool.Blender.Modifier.is_wall (LAYER2-only), so adjacent LAYER2 walls silently dropped their fillet-corner partners from the connection list — the unjoin icon appeared when the fillet wall itself was selected but not on either of its LAYER2 neighbours. Switch the partner filter to is_path_connectable_wall so host and partner predicates match. Add a regression test for the fillet case and an AST forward-compat guard pinning the predicate symbol so a future "tidy the imports" can't silently re-introduce the asymmetry. Generated with the assistance of an AI coding tool. |
||
|
|
ea487fb17c |
Add array parametric edit lifecycle + GizmoArrayEdition / Child
Ports the array parametric-edit lifecycle, gizmo group, child guard,
per-layer ARRAY entry icons, and the array bbox decorators
(preview + selection highlight + layer-children) from gizmos-8088.
Restores the array_gizmo icon's positioning + visibility in the
framework's parametric edit row.
Registry (tool/parametric.py):
* EDIT_TYPES adds ParametricObject("array", supports_build_edit_lifecycle=True).
_ArrayEditMixin in array.py feeds build_edit_lifecycle which auto-
generates EnableEditingArray / FinishEditingArray / CancelEditingArray
with the conventional bl_idnames the gizmo references.
tool/blender.py:
* Adds is_array predicate wrapper around tool.Parametric.is_array.
The registry contract test test_every_entry_has_modifier_predicate
enforces every EDIT_TYPES entry has a matching is_<name> wrapper on
tool.Blender.Modifier.
array.py (+1130 LOC port from gizmos-8088):
* _ArrayEditMixin(ParametricEditMixinBase) drives the auto-generated
enable / finish / cancel lifecycle.
* GizmoArrayEdition: validate + cancel + count display + +/- adjusters
+ method toggle + delete button + per-layer ARRAY entry icons
(preallocated pool of MAX_LAYER_GIZMOS=8).
* GizmoArrayChild: child-array gizmo for the array-replica case.
* EditArrayFromChild: resolves the spawning layer via
tool.Array.get_child_layer_index so clicking a child's array gizmo
opens the layer that produced that child rather than always layer 0
(the gizmos-8088 source itself hardcoded item=0; HEAD has the helper
to do it right).
* New operators: EnableEditingArrayItem, ArrayParentGizmoClick,
ArrayGizmoClick, ToggleArrayMethod, RemoveArrayLayerFromEdit,
InputArrayCount, AdjustArrayCount.
prop.py: BIMArrayProperties gets per_child_opening BoolProperty
(when the array parent fills a host, give each child its own
opening + filling pair).
Bug fix: guard update_relating_array_from_object against the
cleanup-time None set. _finish_one writes relating_array_object = None
to clear the source-array reference; that fired the update callback,
which dispatched bpy.ops.bim.enable_editing_array(item=self.is_editing).
With is_editing just flipped to False, the bool coerced to 0 and
re-opened layer-0 edit immediately after every validate. The guard
short-circuits on None; item is also fixed to 0 (the bool-as-layer-
index was always meaningless for the legitimate user-pick path).
decorator.py (+312 LOC, all ports from gizmos-8088):
* bbox_world_edges / draw_polyline_segments / _BBOX_EDGES - shared
geometry helpers usable across array decorators.
* draw_array_layer_children_bbox - green wireframe bbox per child of
one array layer, drawn inline from a gizmo's draw() so the highlight
tracks the hover cursor without POST_VIEW lag.
* ArrayPreviewDecorator - faint cyan ghost bboxes at each future
array instance during the edit lifecycle (offset math mirrors
Model.regenerate_array, gated on props.is_editing).
* ArraySelectionHighlightDecorator - bounding-box overlay surfacing
the array family of the selected object. Child selected -> parent
in special color + siblings in unselected color; parent selected
(idle) -> all children in unselected color. TokenCache-backed.
handler.py: imports + uninstall/install the 2 always-on decorators in
_install_viewport_overlays. Both self-poll, so installation has no
cost when no array is selected / in edit mode.
Registration (bim/module/model/__init__.py):
* Adds the 3 lifecycle classes generated by build_edit_lifecycle
(CancelEditingArray, EnableEditingArray, FinishEditingArray) -
they exist as module-level names but are only visible to Blender's
operator registry when included in the classes tuple.
* Adds the 8 new operators + 2 new gizmo groups in alphabetical order.
gizmos.py: restores the array_gizmo icon position + visibility block
in BaseParametricGizmoGroup.update_editing_gizmos. Was force-hidden
in
|
||
|
|
e0ad34ffd4 |
Drop duplicate _path_connection_location_world in wall.py
PR3 shipped tool.Wall.path_connection_location_world; the local _path_connection_location_world added in PR4 commit |
||
|
|
12f9e377c8 |
Route _has_material_styles through tool.Root.has_material_styles
Pre-existing architectural smell on v0.8.0: core/root.py.copy_class
called a module-level _has_material_styles helper that did
ifcopenshell.util.element.get_materials() directly, bypassing the
Prophecy mock seam that every other branch in copy_class flowed
through. Symptom: test/core/test_root.py::TestCopyClass::
test_AAAAAAAAAAAA passed mock strings into copy_class, the helper
called .is_a() on the string, AttributeError.
Move the check to tool.Root.has_material_styles (paired with
assign_body_styles — they're called in sequence as "is there a
material style? if not, assign body style"). core/root.py now
calls root.has_material_styles(new) like every other dependency,
fixing the test failure and dropping the ifcopenshell.util.element
import that was the only consumer of the ifcopenshell import at
module load in core/root.py.
* core/tool.py: add abstract has_material_styles to Root interface.
* tool/root.py: add concrete classmethod near assign_body_styles.
* core/root.py: replace _has_material_styles helper call site with
root.has_material_styles; drop the local helper and its import.
* test/core/test_root.py: add the new mock expectation
root.has_material_styles("element").will_return(False) before the
existing assign_body_styles expectation.
Generated with the assistance of an AI coding tool.
|
||
|
|
669e5c2aed |
Add behaviour-contract tests for PR4 surfaces
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
|
||
|
|
3f5273744d |
Discard previews on IFC save + harden preview-active gate
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
|
||
|
|
d6f55b0bf0 |
Drop wall.py local read_geometry + validate dupes + relax gates
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. |
||
|
|
bf6fd52786 |
Hide sister gizmos during preview + ESC cancels + DRY wall polls
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. |
||
|
|
9f748fa4a9 |
Add wall-fillet feature: operators, gizmos, decorator
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. |
||
|
|
eeede522d9 |
Add wall-fillet helper functions + recreate_wall hook
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. |
||
|
|
d955763f63 |
Gate parametric-edit array gizmo until integration completes
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. |
||
|
|
93c51c1a39 |
Add cursor-aware extend-arrow flip on wall edit gizmos
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. |
||
|
|
deaf090a50 |
Add single-wall unjoin operator + gizmo group
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. |
||
|
|
db94877d8b |
Add wall path-connection inverse-walk helpers
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. |
||
|
|
8e93fde930 |
Add wall draft-resync helper + wire 6 mutation operators
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. |
||
|
|
f28b901a49 |
Fix parametric framework live-session regressions
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. |
||
|
|
1b272c039d |
Refactor bim/module/drawing/gizmos — framework + icon infra
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. |