mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-29 00:03:17 +00:00
82465a64a59a2ac666449afa334097c194db4d3d
3666 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
82465a64a5 |
Brighten and dash opening occlusion outline
The opening preview's outline used a single-batch two-pass scheme that dimmed the occluded back pass via alpha=0.25. The visible front pass also inherited the source decorator color's modest alpha, so the outline read as subtle on both sides. Replace with a CAD hidden-line convention: solid full-alpha front pass on the visible side, world-space dashed back pass on the occluded side. Both passes use POLYLINE_UNIFORM_COLOR so depth and line-weight paths match. The dashed batch is built once per object epoch by a new pure helper tool.Blender.build_dashed_line_segments (pre-segments edges into world- space dash chunks), then cached via the existing batch-cache mechanism under "<uid>_dashed". The solid front pass is rendered at a slightly wider line width than the dashed back pass so its halo overpowers Blender's WIRE-display overlay bias at outline pixels — without the asymmetry the wire's anti-z-fight forward bias makes the LESS_EQUAL comparison narrowly fail and the dashed pass wins on visible edges too. Generated with the assistance of an AI coding tool. |
||
|
|
6bde619fe6 |
Migrate MEPConnectElements args from object names to IFC GUIDs
MEPConnectElements took obj1_name/obj2_name (Blender object names), which break when objects are renamed or replicated by array duplication. Switch to obj1_guid/obj2_guid resolved via ifc_file.by_guid, with by_guid RuntimeError surfaced as an operator error rather than a stack trace. DrawPolylineProfile (the sole in-tree caller) updates to pass GlobalIds. Generated with the assistance of an AI coding tool. |
||
|
|
ba5321fdfa |
Add MEP bend tessellation helper tests
Pins the geometry contracts the hand-meshed bend body relies on while IfcSweptDiskSolid round-trip is broken upstream (#8106): - profile cross-section sampling: circle returns 16 evenly-spaced points starting at (radius, 0); rectangle returns the four canonical corners; anything else returns None so the rep swap is skipped rather than meshed against the wrong section - parallel-transport framing keeps the cross-section continuous around L-shaped corners — pinned via start / end ring planes - initial_basis override seeds the first ring with the source segment's local +X / +Y axes, fixing the asymmetric-rectangle twist the world-Z seed produces Generated with the assistance of an AI coding tool. |
||
|
|
49ddc97918 |
Add MEP port operator dispatch tests
Pins which IFC mutation each port operator commits and which inputs each refuses with CANCELLED: - MEPUnjoinAtPort removes the fitting + reconnects the two free ports; refuses if the named port is free or terminal - MEPRemoveTerminalFitting deletes the terminal element + leaves the segment's port free; refuses on bridged fittings - SelectMEPPathMembers walks IfcRelConnectsPorts in both directions from the active segment and selects every fitting / segment reachable through the port graph Boundary mocks for tool.Ifc, tool.System and MEPGenerator stand in for the IFC fixture; tests assert against the recorded ifcopenshell.api.* calls. Generated with the assistance of an AI coding tool. |
||
|
|
39bcd9db63 |
Add GizmoMEPActions wiring contract tests
Pins two regressions the live MEP gizmo group can hit: - per-icon setup() must write `position` (and `mode` on open-lock icons) onto every target_set_operator result; the test stands in for the AttributeError on bim.mep_add_obstruction that surfaced when a field was dropped from the operator declaration - each visibility_condition lambda must stay total against None / non-IFC inputs, since a single raising predicate silently disables every sibling icon in the group Generated with the assistance of an AI coding tool. |
||
|
|
9346f45bba |
Fix decorator face-tri overlay artifacts
ProfileDecorator.draw_faces (used by the roof path-edit overlay) and SystemDecorator.draw_faces called bmesh.ops.triangulate on the live bmesh — both mutated the input and produced ear-clip fans that rendered as visible streaks across n-gon roof faces at alpha 0.1. The opening DecorationsHandler edit-mode branch had a separate bug: it computed triangles from obj.data.calc_loop_triangles() while iterating the edit-mode bmesh, so any topology added mid-edit desynced the indices. Centralise the correct draw path on tool.Blender.draw_bmesh_face_tris (wraps bm.calc_loop_triangles, non-mutating, beauty triangulator) and route all three call-sites through it. A forward-compat AST guard walks every *Decorator / DecorationsHandler class under bim/module/ and pins the no-bmesh.ops.triangulate rule against future regressions. Generated with the assistance of an AI coding tool. |
||
|
|
b22687891b |
Warn on shared-rep parametric edits
A user clicking the pen icon on a typed-product occurrence whose body representation is mapped from its type would silently mutate every sibling occurrence's geometry. Add a confirmation dialog at the pen-icon dispatcher (the single chokepoint every feature routes through) showing the sibling count, with a session-scoped suppress checkbox. The check is read-only: tool.Model.get_sibling_occurrence_count wraps tool.Geometry.get_elements_by_representation against the resolved body rep and subtracts self + type. A forward-compat AST guard pins the dispatcher monopoly so any future feature that binds pen_gizmo directly to a feature-specific enable op fails the test before merge. Generated with the assistance of an AI coding tool. |
||
|
|
784f0b1fe2 |
Add bend re-edit gizmo
Once a bend was created, the only way to retune start_length / end_length / radius was to delete and recreate from scratch. EnableBendPreviewFromBend re-opens the preview on an existing parametric bend: it walks the bend's ports to resolve the two connected segments, reads start / end length and radius from the bend type's BBIM_Fitting pset, and sets editing_bend_id on the preview props. MEPAddBend then deletes the old bend + its port connections (single undo step) before the recreate path runs, so finish replaces the bend in place and cancel discards the edit without touching the original. GizmoMEPActions surfaces a pen icon on single bend-fitting selections via the new _active_is_bend_fitting predicate; the icon dispatches the new operator. Mirror of the wall fillet re-edit flow (EnableWallFilletPreviewFromCorner + editing_corner_id in CreateWallFillet). Test coverage: registration probe for the new operator, an attached editing_bend_id field probe on the preview umbrella, and a parametrized truth-table for the _is_bend_fitting predicate (IfcFlowFitting with BEND PredefinedType, with other PredefinedType, with no type, IfcFlowSegment, IfcWall, None). Generated with the assistance of an AI coding tool. |
||
|
|
e0ceda6856 |
Hide wall topology gizmos on array children
Wall topology mutations (merge / join / extend-to-wall / unjoin / fillet) applied to a Bonsai array child are silently overwritten by the next ``regenerate_array``; merge also orphans a GUID listed in the parent's ``BBIM_Array.Data``. Add a central ``tool.Blender.Modifier.any_selected_is_array_child`` predicate and gate the five wall topology gizmo groups plus the six bound operators behind it. Operator gating is defence in depth against keymap / F3 invocation paths that bypass the gizmo. The base ``_wall_gizmo_poll_gate`` keeps its loose two-check shape (viewport gizmos + no preview). A new ``_wall_topology_gizmo_poll_gate`` wraps it with the array-child filter and is what the topology gizmos use. Host-opening gizmos deliberately stay on the loose gate: openings authored on a child are preserved through ``regenerate_array`` and track with the replicated instance. A forward-compat AST guard walks wall.py for ``GizmoGroup`` subclasses and asserts each routes its poll through the tighter gate or the central predicate, with an allow-list for the parametric-edit and preview-owner exceptions. New wall topology gizmos inherit the contract by construction. Generated with the assistance of an AI coding tool. |
||
|
|
17951427fe |
Add readonly door swing arc preview
Selecting a Bonsai-parametric IfcDoor now shows the swing arc(s) without entering edit mode. A new viewport decorator polls on the active object, reads the door's BBIM_Door pset, and draws the same arcs the parametric door swing gizmo would draw — matching the hinge / panel-width / x-mirror contract minus the is_editing gate. A forward-compat test walks every door operation type and cross- checks the readonly decorator's arc selection against the gizmo's swing-arc config table, so future enum additions fail in both surfaces simultaneously. Also disables the inherited 8-pass dark halo on GizmoArc: an open curve has no enclosed silhouette, so the offset passes read as ghost arcs rather than a uniform outline. The arc's own cross- section thickness keeps it legible without the halo. Generated with the assistance of an AI coding tool. |
||
|
|
734f4df84e |
Add MEP bend preview + bend tessellation fallback
The MEP bend feature's IfcSweptDiskSolid representation produces geometrically correct output but fails to round-trip through the OpenCascade geometry kernel (upstream issue #8106) — the body is dropped on the next file load. Until upstream is fixed, MEPAddBend captures the bend centerline in world space before the segments are extended (otherwise the post-extension axes no longer reach the original intersection and arc reconstruction is wrong), then after the fitting is placed it hand-meshes the bend body and swaps the type's swept-disk representation for an IfcTessellatedFaceSet via tool.Geometry.export_mesh_to_tessellation + tool.Model. replace_object_ifc_representation. The centerline includes the straight start_length / end_length legs in addition to the arc so the bend covers the full segment-to- segment span. Sweep uses parallel-transport framing — each ring's (right, up) basis is rotated by the minimum rotation that maps the previous tangent to the current one, eliminating the twist a fixed world-axis reference produces when the tangent crosses the reference. Cross-section orientation seeds from the source segment's matrix_world local +X / +Y so asymmetric IfcRectangleProfileDef ducts land with XDim / YDim on the same axes the segment expects; parallel transport then preserves that alignment around the arc. Centerline radius is radius + profile_dim[lateral_axis] to match MEPAddBend's ref_point_radius — without this offset, the bend legs fall short of the extended segments by profile_dim * tan(angle/2). Face winding is left to the caller to correct via bmesh.ops.recalc_face_normals on the closed bend tube. Two FIXME(#8106) markers (capture site + helper call site) so both can be dropped once upstream lands a swept-disk round-trip fix. Generated with the assistance of an AI coding tool. |
||
|
|
192bf00d31 |
Add cursor-bound perpendicular wall gizmo
GizmoWallEdition gains a fourth cursor-anchored icon that spawns a perpendicular branch wall from the cursor's orthogonal projection on the source wall axis. Click forms a T-junction; shift+click forms an L-corner with the source wall trimmed at the projection, keeping its longer portion. The branch inherits the source's spatial container and centerline baseline so its authored axis matches the source's alignment rather than the type's default. Also includes a floor-plane preview quad for the new gizmo, a floor-Z cross line on the split preview for top-down visibility, a small bump to QUAD_ALPHA for clearer preview fills, and a stacking-offset helper that centralises the cursor-row screen-up step across three call sites. Generated with the assistance of an AI coding tool. |
||
|
|
ad672d0edb |
Add GizmoMEPActions + bend precondition + obstruction modes
The MEP one-shot operators (join, unjoin variants, terminal removal, path-select, obstruction add/remove) had no viewport surface. This commit adds GizmoMEPActions — the icon-action gizmo group that surfaces them as billboarded icons around selected MEP elements. Three anchor regions: a horizontal row above the bbox top (selection-cardinality icons), per-port endpoints for the three-state lock / unjoin icons (open lock for PORT_FREE, closed for PORT_TERMINAL, unjoin for PORT_JOINED — resolved per-frame from port_connection_state), and the predicted join location (compute_mep_join_location, shared with the bend preview) for the join / unjoin_pair pair. Unjoin icons render at full DEFAULT_BILLBOARD_SCALE with warning-red hover; endpoint lock icons shrink so the lock row stays subordinate to the row icons. The group hides itself entirely while a bend preview is active. MEPAddObstruction grew a position enum (CURSOR / START / END) and a mode enum (ADD / REMOVE / TOGGLE) so the gizmo can target a specific port without touching the cursor and dispatch ADD or REMOVE based on the click target — the lock_open icons drive ADD with position pinned, the lock_closed icons drive bim.mep_remove_terminal_fitting. Without the new fields the gizmo wiring (op_props.position = ...) crashed at setup() with AttributeError on the obstruction operator. validate_bend_preconditions extracts the type-match and profile-kind checks MEPAddBend enforces so EnableBendPreview surfaces the rejection immediately — the user no longer tunes a preview only to learn at commit time that the segments use an unsupported profile (e.g. IfcArbitraryClosedProfileDef). Generated with the assistance of an AI coding tool. |
||
|
|
0df1f0cf49 |
Add MEP unjoin / terminal-remove / path-select operators
Four discrete one-shot operators driven by the MEP segment's port state. mep_unjoin_at_port deletes the IfcFlowFitting bridging a segment's named port to a second element when the port is in the JOINED state. mep_remove_terminal_fitting deletes the terminal fitting at a port (closed-lock state) and dispatches by fitting type — OBSTRUCTION fittings go through MEPGenerator.remove_obstruction so the segment absorbs the freed length, other terminal fittings go through the standard delete path. mep_unjoin_pair finds the single fitting bridging two selected MEP segments and deletes it. select_mep_path_members walks the connected MEP network from the active element via IfcRelConnectsPorts and replaces the selection with every reachable member. Foundation for the MEP Actions gizmo group which surfaces these operators as icon affordances around selected segments. Generated with the assistance of an AI coding tool. |
||
|
|
d7dd8ecf57 |
Align extend gizmo arrow with segment axis
The extend icon used a pure screen-space billboard that always pointed +X across the screen — the arrow ran horizontally regardless of the pipe / duct's orientation. The new billboarded_along_axis helper rotates the gizmo about the camera- forward axis so its local +X aligns with the segment's local +Z projected onto the screen, keeping the icon camera-facing but visually following the extrusion direction. The flip-mirror branch now reads from cursor-vs-current-end along the segment axis (not screen-X), so the arrow points away from the current endpoint regardless of viewport orientation. The split icon stacks perpendicular to the rotated extend arrow in screen space so the two don't overlap. The decorator's green preview line no longer clamps the cursor projection to min_projected_length — it follows the raw projection so the line stays visible when the cursor crosses behind the segment origin (the user still sees where they're pointing even though the operator floors the actual commit). Generated with the assistance of an AI coding tool. |
||
|
|
becbcfdfe7 |
Add MEP bend preview decorator + join dispatcher
The bend preview gizmo group (commit 2) populated a Scene draft but the user saw nothing in the viewport until they hit finish — they had to commit blindly. This commit ports the BendPreviewDecorator (centerline arc + two leg projections on valid geometry, warning-red axes on invalid in-segment intersections) and the interactive GizmoBendPreview group (three dimension widgets for start_length / end_length / radius plus validate / cancel icons). The bend axis math lives in a pure compute_bend_preview_polylines helper, fed into both the gizmo group's per-frame positioning and the GPU decorator's draw path. MEPSegmentExtendPreviewDecorator lands at the same time because it shares the decorator install / uninstall plumbing — renders the extend-to-cursor preview line for the GizmoPipeSegmentEdition / GizmoDuctSegmentEdition extend icons when hovered, clamping the projected endpoint to the operator's minimum so the preview matches where the commit lands. The MEPJoinSegments dispatcher routes two selected MEP segments to mep_add_transition (parallel) or enable_bend_preview (non-parallel) — the F3 search entry point that makes the bend preview testable before the gizmo-icon dispatch lands. 11 new tests in test_mep_bend_preview.py cover the geometry helper truth table (parallel rejection, right-angle happy path, near- collinear rejection, in-segment invalid_axes), the _intersection_past_near parametrized boundary, registration probes for the lifecycle operators / join dispatcher / gizmo group / decorator, and the FinishBendPreview RuntimeError catch contract. 6 extend-preview-line tests (deferred from commit 3) join the existing 35 in test_mep_segment_edition.py. Generated with the assistance of an AI coding tool. |
||
|
|
5b79cefee2 |
Fix #8138: door/window container assignment no-op
Spatial.get_root_element walks aggregate / nest / filled-void / voided-element chains and core.assign_container assigns the container to whatever the walk returns. For an IfcDoor the filled-void hop redirects to the IfcOpeningElement, then voided-element to the host wall, so a user who selects a door and runs bim.assign_container ends up targeting the wall — and silently no-ops on the door if the wall is already in the target storey. Per IFC4 / IFC4.3 (IfcDoor, IfcWindow): the spatial containment of a filling is defined independently of the filling relationship. Major exporters (Revit, ArchiCAD, Tekla, Allplan) emit independent ContainedInStructure on doors / windows accordingly. Drop the filled-void / voided-element hops from the walk; aggregate and nest remain — those are true sub-part relationships where the parent legitimately owns the container. New TestGetRootElement in test/tool pins the new contract (filling resolves to itself) plus the retained aggregate / nest / loose-element paths so a future PR that re-adds either hop is caught. Two new TestAssignContainer cases in test/core pin filling-to-self through the core layer and per-element can_contain filtering. Generated with the assistance of an AI coding tool. |
||
|
|
3346a59284 |
Add MEP pipe / duct segment edit gizmos
Pipe and duct segments had no parametric-edit affordance — the only length edit path was a property panel value with no live preview. This commit ports the per-segment parametric edit triad (enable / finish / cancel) plus a cursor-anchored extend operator and a cursor-projected split operator into one gizmo group per segment type. The two PropertyGroups (BIMPipeSegmentProperties, BIMDuctSegmentProperties) host the draft length plus snap fields so cancel / no-op-finish restore the segment to its exact pre-edit visual state including a non-identity pre-edit scale. Length commits are written through DumbProfileJoiner.set_depth and auto-dispatch bim.regenerate_distribution_element so adjacent fittings track the port move. The split operator preserves downstream port connectivity and runs through tool.Ifc.run for single-step undo. The two segment types are now first-class entries in tool.Parametric.EDIT_TYPES, which resolves the FIXME on auto-commit-on-save dispatch. 35 unit tests cover predicate truth tables, segment_world_length geometry, preview-via-scale / restore-scale helpers, gizmo class wiring, lifecycle operator registration, dimension matrix_position rotation respect, and lifecycle drift-handling. The 6 extend- preview-line decorator tests stay deferred until the bend preview decorator commit lands MEPSegmentExtendPreviewDecorator. Generated with the assistance of an AI coding tool. |
||
|
|
0bf8e9283f |
Hide parametric gizmos during transform modal
Parametric gizmos (wall/door/window/stair/roof/array/MEP) recompute matrix_basis every frame from obj.matrix_world. While Blender's transform modal (G/R/S and the Bonsai macro overrides) drags the matrix, the gizmos slide off-cursor and fight the transform overlay. Detect via context.window.modal_operators (Blender 4.2+) — the collection of running modal operators. Gate poll() (forward-compat) and draw_prepare() (production path: gizmo.hide=True preserves the GizmoGroup across the drag instead of destroying it). Cover the Bonsai macro override for G key (and Shift/Alt/Ctrl+Shift+D) by matching the BIM_OT_* macro idnames that surface in modal_operators. Forward-compat test walks every parametric-edit module for GizmoGroup subclasses and asserts poll returns False with the detector mocked, so new gizmo groups inherit the hide automatically. Generated with the assistance of an AI coding tool. |
||
|
|
db7591a867 |
Add clear_preview_state helper + DRY preview cleanup
Every preview operator (commit + cancel for both bend and wall fillet) was inlining the same 3-4 line cleanup: set is_active to False, zero every *_id IntProperty. The new clear_preview_state helper in preview_base.py introspects bl_rna and applies that contract generically — adopters become a single call. Two new tests pin the contract: every *_id IntProperty zeroes, non-id fields stay. Generated with the assistance of an AI coding tool. |
||
|
|
7b9af9f533 |
Backport pending-opening-cuts banner from gh8088
Extract the pending_opening_recut tracking, three operators (apply / dismiss / select), Project-panel banner, and the sibling multi-instance warning banner (its backend helpers already landed on this branch) from commit a85ed6032 on gizmos-8088. All tool.* dependencies (Geometry.reimport_element_representations, Blender.set_objects_selection, Array.*) and IfcImporter.gross_elements are already on this branch -- no other diffs from a85ed6032 are pulled. The source's narrow except-tuple paraphrase comments are trimmed to keep only the durable "don't swallow programmer errors" note, per CLAUDE.md s4a. Tests: 5 bim-lane tests in test/bim/module/project/ test_pending_opening_cuts.py covering apply happy-path + missing entity, dismiss, select happy-path + cancellation. Generated with the assistance of an AI coding tool. |
||
|
|
704a2d36be |
Add MEP bend preview Scene properties + lifecycle
MEPAddBend exists on the main flow but commits bend geometry with hardcoded defaults (start_length=0.1, end_length=0.1, radius=0.2) with no opportunity to tune before commit. The new scene-level BIMBendPreviewProperties hosts a draft (start_segment_id, end_segment_id, start_length, end_length, radius); EnableBendPreview populates it from the two selected MEP segments after asserting they are non-parallel, FinishBendPreview dispatches MEPAddBend with the tuned values and clears the draft, CancelBendPreview discards it. Scene-level placement follows CLAUDE.md 2.9: a bend creates a new fitting entity between two segments, so neither segment alone owns the draft. Foundation for the upcoming bend preview gizmo group and decorator. Generated with the assistance of an AI coding tool. |
||
|
|
516696cd73 |
Add partial-state rollback on execute_ifc_operator
When an operator mutated IFC then raised mid-execute the user was left staring at a raw traceback with the IFC graph captured by the active transaction but the Blender side stale. Blender does not push an undo step for a raised operator (the same gap that the CANCELLED-modal arm patches via bpy.ops.ed.undo_push), so the WARNING the framework can emit is only honest if it pushes that undo step too. The framework now detects partial state via ifc_file.transaction.operations, pushes a Recover undo step, then reports a WARNING naming Ctrl+Z so the recovery path is discoverable. The bespoke try/except wrapper in UnjoinWallPathConnection becomes redundant and is retired in the same change. Generated with the assistance of an AI coding tool. |
||
|
|
a139adaa2c |
Apply black formatting to satisfy lint-formatting CI
Three files flagged by black --check on the lint-formatting job: * bim/module/geometry/operator.py — single-arg `.update(...)` rejoined onto one line under the 120-char budget. * test/bim/module/model/test_wall_gizmos.py — same join on a _make_path_rel call. * test/modal/test_modal.py — pre-existing baseline noise picked up via the upstream merge: PEP-8 blank-line separators between top- level functions, `0.68+` → `0.68 +`, double quotes, trailing whitespace stripped. No behavioural change; pure whitespace. Generated with the assistance of an AI coding tool. |
||
|
|
a4a806147d | Merge remote-tracking branch 'ifcopenshell/v0.8.0' into bonsai/parametric-framework-features-pt2 | ||
|
|
4962e3256d |
Promote idle-row icons into the slot system
The toggle_openings icon lived outside the IconSlot layout — each host (wall, roof) declared an ad-hoc setup_pen_row_toggle_openings_icon + update_pen_row_toggle_openings_icon pair, and GizmoArrayEdition queried a hardcoded _FEATURE_IDLE_MAX_X dict to position past it. On an arrayed wall the dict was shadowed: find_for_element returns "array" before "wall" in EDIT_TYPES order, the wall reservation was never consulted, and the first per-layer ARRAY icon (local X=0.37) landed 13cm from the wall's toggle_openings (X=0.50) — visually on top of each other. Promote idle-row icons into the slot system instead of patching the dict: * IconSlot gains an Optional visible_when predicate for state-driven visibility (toggle_openings only when the host carries openings). * BaseParametricGizmoGroup gains idle_slots: ClassVar[tuple[IconSlot]] + _idle_slot_x_positions() + _idle_row_right_edge() helpers; the setup + idle-branch positioning loops mirror the existing feature_slots path. * Wall and roof declare toggle_openings as an idle_slot and drop their ad-hoc setup/update calls. * GizmoArrayEdition's _resolve_feature_idle_max_x walks BaseParametricGizmoGroup.REGISTRY and takes the max _idle_row_right_edge() across peers whose poll passes — no more hardcoded dict, no more find_for_element-order shadowing. * setup_pen_row_toggle_openings_icon + update_pen_row_toggle_openings_icon helpers deleted from drawing/gizmos.py. * 3 forward-compat AST guards pin the new contract. Also bundles an unrelated array-test fix: TestUsingArrays in test/tool/test_model.py was asserting against bpy.context.selected_objects which is a fragile signal after remove_array / apply_array. A new _array_objects() helper filters bpy.data.objects via the BIM_Array pset's IfcActuator type instead. Layout on an arrayed wall after the fix: pen X = 0.00 toggle X = 0.50 (idle_slot 0) array[0] X = 0.87 (one ICON_ARRAY_GAP past idle row) array[1] X = 1.27 All separated by the standard inter-icon spacing. Generated with the assistance of an AI coding tool. |
||
|
|
06d99feeea | Add no headless test for Bonsai Snap Target. | ||
|
|
f584a50fbb |
Clear wall-edit gizmos off click targets in plan view
In plan view world-Z collapses to zero on screen, so every wall-edit icon anchored on the floor — the projected 3D cursor, wall endpoints, wall-to-wall corners, IfcRelConnectsPathElements connection points — projects onto the click target it represents. The result on a typical extend / split / unjoin action: the icon sits on top of the cursor crosshair (or the corner the user wants to click), defeating precise positioning. Add shared ``gizmo.top_down_clearance(context, billboard_rot)`` to bim/module/drawing/gizmos.py: returns a screen-up Vector in top-down view (cosine cone around world Z, matching ``is_view_top_down``) and a zero Vector elsewhere, so call sites apply it unconditionally before ``billboarded_at``. Default distance 0.4 m aligns with the inter-icon stack spacing already used by GizmoWallJoinIntersection so single icons and stack bases land at consistent screen-up positions when multiple groups render around the same wall endpoint. Apply at the seven wall-edit anchor sites: * GizmoWallEdition cursor stack (top-down branch only — non-top-down already stacks along world-Z at structural points clear of the cursor). * GizmoWallExtendVertically (single icon at wall origin endpoint, active-object Z elevation). * GizmoWallJoinIntersection corner stack base + merge midpoint. * GizmoWallUnjoinSingle link-toggle pool (one icon per IFC path connection, previously sitting exactly on the connection point). * GizmoWallFilletReedit pen icon at fillet corner. * GizmoWallFilletToggleOpenings. The clearance is a pure visual offset — bound operators still read the world-space anchor (cursor / endpoint / connection point) at execute time, so the action's target is unaffected. Also tighten GizmoWallUnjoinSingle: gate poll on ``props.is_editing`` so the link-toggle icons only surface during the wall edit lifecycle (matching every other edit-row icon), and downsize them via a new ``ICON_SCALE = 0.35`` constant since 16 of them at default scale cluttered the viewport on path-heavy walls. ruff + black clean. Wall gizmos test lane 14/14 pass. Generated with the assistance of an AI coding tool. |
||
|
|
8faf9ff43d |
Consolidate load_post parametric drains
bim/handler.py was importing two feature-module internals (wall_offset_gizmos.clear_caches, preview_base.discard_pending_previews) to drain load-transient parametric state alongside the existing tool.Parametric.heal_stale_edit_flags() call inside _apply_save_file_invariants. Each new parametric drain added one top-level import and one inline call — every load_post drain leaked into handler.py's namespace. Hide all three drains behind tool.Parametric.on_load_post(scene), sited adjacent to heal_stale_edit_flags. The two feature-module imports become late imports inside on_load_post — same pattern as refresh_post_commit's existing `import bonsai.bim.handler` — which sidesteps the tool.parametric -> bim.module.model.preview_base -> bonsai.tool registration-time cycle. The forward-compat AST contract that pinned "every module-scope GenerationKeyedCache + clear_caches MUST be drained on load_post" follows the call site to its new home — the test now walks tool.Parametric.on_load_post instead of _apply_save_file_invariants. No behaviour change. 45/45 affected bim tests pass (test_handler_forward_compat, test_preview_base, test_wall_offset_gizmos, test_parametric_registry). ruff + black clean on all touched files. Generated with the assistance of an AI coding tool. |
||
|
|
87bca20df7 |
Relocate feature decorators to their owning modules
Three feature-specific decorators previously lived in bim/module/model/decorator.py despite owning state only their home module reads: * ArrayPreviewDecorator + ArraySelectionHighlightDecorator + draw_array_layer_children_bbox -> array.py (read array edit-state props and walk BBIM_Array psets) * WallGizmoPreviewDecorator + draw_wall_partner_bbox -> wall.py (dereference wall.py-private classes and helpers via lazy imports) decorator.py keeps cross-cutting infrastructure (BoundingBoxDecorator, SlabDirectionDecorator, WallAxisDecorator, WallFilletPreviewDecorator, PolylineDecorator, ProductDecorator) and the shared bbox primitives (bbox_world_edges, draw_polyline_segments, _BBOX_EDGES, _stroke_lines_alpha, _fill_quads_alpha) that several feature files now import. handler.py and gizmos.py update their import paths; the wall-feature lazy imports inside WallGizmoPreviewDecorator methods collapse to direct references now that the decorator lives in wall.py. No behaviour change. Wall lane 37/37, array lane 15/15, wall forward-compat 6/6, parametric-registry 8/8 still pass. Generated with the assistance of an AI coding tool. |
||
|
|
a30546f1f2 |
Bbox dimensions key, DRY array operators, drop dead code
Three concerns sharing the same architectural theme (collapse inline
bbox / edit-state lookups, drop overrides that re-do base-class work):
== Bbox helpers and array operator DRY ==
* tool/blender.py: add a "dimensions" tuple key to both
get_object_bounding_box and get_object_world_bounding_box return
dicts. The (max - min) per-axis extent — which callers previously
computed via local helpers — is now a key alongside min_x / max_x
/ min_point / max_point / center. Distinct from Blender's built-in
obj.dimensions (which folds object-level scale): the local variant
is the intrinsic mesh bbox extent; the world variant is the
matrix_world-applied AABB.
* bim/module/model/array.py: drop the local _bbox_dims helper; the
two callers now read tool.Blender.get_object_bounding_box["dimensions"]
directly.
* Rename _parent_geometry_changed -> _array_children_need_rebuild.
The old name suggested "did the parent change just now", implying
the function was a parent-edit-finish trigger. It actually runs
only inside the array-edit-finish path as a drift safety net (the
upstream-deliberate design — see commit
|
||
|
|
fbe6fe5384 |
Fix wall edit lifecycle + drain wall_offset_gizmos cache on load
Bundled bug fixes + the forward-compat AST guard that prevents the
underlying class of bug from coming back.
* bim/module/model/wall.py: FinishEditingWall._execute early-returns
CANCELLED when props.is_editing is False. Without this guard, a
failed enable (e.g. on a wall without IfcMaterialLayerSetUsage)
leaves is_editing False but a press on finish still walked the
sub-ops below, which dereferenced layer-set-dependent state and
crashed.
* tool/model.py: Model.offset_wall now guards against
ifcopenshell.util.element.get_material returning None before
calling .is_a("IfcMaterialLayerSetUsage"). Fixes the pre-existing
test/bim/module/model/test_wall_header_refresh.py crash that has
been the only failing test in the wall lane since this branch
started.
* bim/handler.py: _apply_save_file_invariants drains
wall_offset_gizmos.clear_caches() on load_post. The module-scope
GenerationKeyedCache instance survives the .blend reload; without
the drain the cache may serve entries whose bpy_struct references
point into the freed bpy.data of the previous file.
* test/bim/test_handler_forward_compat.py: AST-walk test that
enumerates every bim/module/model/*.py source declaring both a
module-scope GenerationKeyedCache assignment AND a top-level
clear_caches function, and asserts each module appears as a
<module>.clear_caches() call in _apply_save_file_invariants. Pins
the contract: any future module-scope geom cache that exposes
clear_caches must wire into the load_post drain.
* test/bim/feature/model.feature + test/bim/test_feature.py: wall
edit-lifecycle scenarios switch from "add cube + assign as
IfcWallType" to "load the demo construction library + add an
occurrence of the WAL100 wall type", so the parametric edit runs
against a real LAYER2 wall with IfcMaterialLayerSetUsage rather
than a vanilla-mesh promotion that lacks one. The demo-library
step also picks the schema-matching library file (IFC2X3 /
IFC4 / IFC4X3) so the appended types remain valid across schemas.
Door saved-height assertion updates from 2.5 → 2500 to reflect
that BBIM_Door pset stores project units (METRIC_MM in the
empty-project fixture).
Generated with the assistance of an AI coding tool.
|
||
|
|
25651a1507 |
Fix demo preset crash + scope header refresh
bpy.ops.bim.new_project(preset='demo') crashed in refresh_bim_tool_headers: the post-commit hook fired for every nested bpy.ops.bim.append_library_element during template loading, and the operator context Blender hands to programmatically-invoked nested operators is stripped of the view-layer attributes the refresh reads. Two changes resolve it. Gate the header refresh in tool.Parametric.refresh_post_commit on operator.bl_idname being one of the EDIT_TYPES finish_op idnames. Only validate-gizmo commits (bim.finish_editing_<name>) now trigger the refresh; demo-loader and other non-edit operators skip it. Querying the registry directly is the canonical signal — string-prefix matching would silently drift if ParametricObject.finish_op changes derivation. Harden tool.Blender.get_active_object so its view_layer fallback also uses getattr; the 150+ callers routed through it now tolerate stripped contexts. _resolve_bim_tool_context applies the same defensive pattern to mode / workspace. Tests: - test_handler_restricted_context covers get_active_object's defensive path and the BimTool-family whitelist (excludes annotation, spatial, structural). - test_handler_forward_compat AST-pins that the gate consults EDIT_TYPES (not a string prefix). - test_wall_header_refresh rewritten — three tests cover the gated-by-registry contract: counter bumps for every commit, finish_op operators refresh headers, others don't. Hotkey-driven in-place edits (S_E / C_E) no longer trigger the refresh — they were caught by the pre-refactor "every commit" design. Left out of scope; the new skip-non-finish test pins this as intentional. Generated with the assistance of an AI coding tool. |
||
|
|
94faaa3160 |
Drop dead Geometry.has_material_styles + sanitation sweep
Two related cleanups bundled because each was too small on its own. == Drop dead Geometry.has_material_styles duplicate == Two parallel has_material_styles implementations existed on HEAD: * Geometry.has_material_styles (tool/geometry.py:853, added by |
||
|
|
0e922074b9 |
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.
|
||
|
|
90ea256cc3 |
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. |
||
|
|
387bd51b4a |
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. |
||
|
|
de4c394b50 |
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. |
||
|
|
ab64b652ff |
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.
|
||
|
|
99bb1e30ad |
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. |
||
|
|
1707c36bd8 |
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. |
||
|
|
44f5ee028f |
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. |
||
|
|
ed7b2fc233 |
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. |
||
|
|
2c18155d98 |
Merge ifcopenshell/v0.8.0 into parametric-framework-pt2
Bring in 13 commits from upstream v0.8.0 (tip
|
||
|
|
1e8c0b86a0 |
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.
|
||
|
|
ac11044261 |
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.
|
||
|
|
ab9152e32d |
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. |
||
|
|
18dc7abb06 |
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. |
||
|
|
b7549f2476 |
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. |
||
|
|
ba6cfe9c24 |
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. |