Commit Graph

19059 Commits

Author SHA1 Message Date
Thomas Krijnen 1f2b20fd86 Fix --convert-back-units on transformation object #8137 2026-06-04 22:15:35 +02:00
Thomas Krijnen 94fab271cd Check for empty result after BOPAlgo_MakerVolume and reset manifoldness state #8140 2026-06-04 21:45:27 +02:00
Thomas Krijnen 8583d0963f Make faceset duplicate loop detection respect inner/outer #8140 2026-06-04 21:45:27 +02:00
Thomas Krijnen 77a2284f8a Re-sew non-manifold operands; interior loop re-orientations affect edge identity #8140 2026-06-04 21:45:26 +02:00
Thomas Krijnen 4520a72152 Sane error messages for unsupported items in geometry libs #8106 2026-06-04 21:45:26 +02:00
Gorgious56 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.
2026-06-04 10:42:58 +02:00
Gorgious56 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
  3483683cb "Add tool.Geometry helpers for body representation +
  placement"): checks each material via tool.Material.get_style
  for an IfcSurfaceStyle. This is the implementation gizmos-8088
  uses — its core/root.py:58 calls geometry.has_material_styles.

* Root.has_material_styles (tool/root.py:75, added by e76455913
  "Route _has_material_styles through tool.Root.has_material_styles"):
  checks each material for a HasRepresentation inverse. Added to
  fix the test/core/test_root.py::TestCopyClass::test_AAAAAAAAAAAA
  failure by routing the check through a Prophecy-mockable seam.

HEAD's core/root.py:59 calls root.has_material_styles. The Geometry
version became orphaned by that migration — zero callers historically
(git log -S "Geometry.has_material_styles" returns nothing). The
Root placement is the right architectural home: has_material_styles
pairs with assign_body_styles in the copy_class flow as "is there
material-defined styling? if not, apply body styling" — both
decisions live on the same interface, called in sequence from the
same caller.

The semantic delta (HasRepresentation vs IfcSurfaceStyle) is a close
approximation in real IFC files where HasRepresentation almost always
indicates a styled material; if precision becomes necessary, the
Root impl can be tightened independently of this cleanup.

Drop the Geometry method + its abstract declaration in core/tool.py.

== Sanitation sweep per CLAUDE.md §4a ==

Eight rot-prone references in code we authored on this branch get
their first-draft mistakes cleaned up. The §4a rule (no sibling
symbol names, no test paths, no motivation history in docstrings)
got added during this branch, so older commits sometimes named their
siblings in prose; this is a focused cleanup of the worst offenders.

* bim/module/model/wall.py:201 — _CommitWallDraftsFirstMixin
  docstring carried motivation history ("...that every multi-wall
  operator … used to repeat at the top of _execute"). Rewrite to
  describe only the current contract.

* bim/module/model/wall.py:1910 — cycle_type_operator comment named
  two sibling methods. Rephrase to describe what happens at the slot.

* bim/module/model/wall.py:2025 — _active_instances ClassVar comment
  named WallGizmoPreviewDecorator. Rephrase to "the wall-gizmo
  preview decorator" (role, not class).

* bim/module/drawing/gizmos.py:3402 — GizmoFillet hit_uses_bbox
  comment named GizmoWallJoinIntersection. Rephrase to "the wall-join
  gizmo group".

* bim/module/drawing/gizmos.py:3887 — GizmoCountLabel docstring had
  a :meth:`set_count` cross-reference. Drop — reader sees the method
  next to the class.

* bim/module/model/host_add_opening_gizmo.py:201 — poll-exclusion
  comment named GizmoWallEdition + GizmoRoofEdition. Rephrase to
  describe why we skip ("walls and parametric roofs both render
  their own toggle in the pen row").

* bim/module/void/operator.py:45 — preserve_placement comment named
  FilledOpeningGenerator.generate. Rephrase to "the filling-opening
  generator gates its snap-to-wall-axis block on this flag".

* bim/parametric_lifecycle.py:64 — module docstring named the test
  file path (test/bim/test_parametric_registry.py). Rewrite to
  "enforced by the registry contract tests".

Sweep otherwise clean: no third-party software names in this-branch-
authored comments (upstream Revit / Tekla / ArchiCAD references are
legitimate external-constraint workarounds, §4a-allowed). No
PR/issue numbers we authored except the FIXME(PR5) in
tool/parametric.py:150, deliberately preserved until PR6's MEP slice
resolves it.

Generated with the assistance of an AI coding tool.
2026-06-04 09:08:47 +02:00
Gorgious56 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.
2026-06-03 17:15:45 +02:00
Gorgious56 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.
2026-06-03 16:44:38 +02:00
Gorgious56 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.
2026-06-03 16:07:13 +02:00
Gorgious56 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.
2026-06-03 15:34:48 +02:00
Gorgious56 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.
2026-06-03 13:57:39 +02:00
Gorgious56 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.
2026-06-03 12:39:17 +02:00
Gorgious56 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.
2026-06-02 19:22:38 +02:00
Gorgious56 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.
2026-06-02 17:30:13 +02:00
Gorgious56 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.
2026-06-02 15:35:31 +02:00
Gorgious56 2c18155d98 Merge ifcopenshell/v0.8.0 into parametric-framework-pt2
Bring in 13 commits from upstream v0.8.0 (tip f158ae737):

- Add regenerate_wall_to_underside operator + has_underside_connection
  Model interface (closes #7943)
- Extend/regenerate walls to multiple undersides
- Fix duplicate booleans in extend_walls_to_underside
- Fix extend_walls_to_underside ridge artifact
- Regenerate connected walls when recalculating a slab
- Fix validate_type corruption; remove debug prints
- Lazy BVH tree construction in SnapObj + early-terminate solid raycasts
  in non-xray mode + optimize 2D projection in ray_cast_by_proximity_2d
- Fix crash in update_bim_tool_props when selected type isn't a valid
  ifc_class
- Fix assign_container in spatial.py (#8079)
- Fix sign of temporary offset restore in sweep_along_curve

Auto-merge resolved all overlap files cleanly:
- bim/handler.py: work branch's update_bim_tool_props refactor and
  upstream's try/except hardening converged on identical try/except
  around props.ifc_class assignment (no net change).
- bim/module/model/__init__.py: upstream's wall.RegenerateWallToUnderside
  entry and work branch's roof gizmo entries occupy disjoint sections.
- bim/module/model/wall.py: upstream's RegenerateWallToUnderside operator
  and work branch's GizmoWallEdition/IconSlot refactors occupy disjoint
  sections.
- core/tool.py: upstream's four new Model stubs and work branch's
  Root.has_material_styles stub occupy different classes.

Partly generated with the assistance of an AI coding tool.
2026-06-02 13:31:53 +02:00
Gorgious56 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.
2026-06-02 12:59:40 +02:00
Gorgious56 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.
2026-06-02 12:20:52 +02:00
Gorgious56 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.
2026-06-02 11:39:12 +02:00
Gorgious56 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.
2026-06-02 10:57:16 +02:00
Gorgious56 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.
2026-06-02 10:37:01 +02:00
Gorgious56 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.
2026-06-02 10:15:09 +02:00
Ryan Schultz f158ae7377 Fix crash in update_bim_tool_props when selected type isn't a valid ifc_class
props.ifc_class is an EnumProperty whose items list only the element/space
types present in the model. Assigning element_type.is_a() crashed with
`enum "<class>" not found` when the selected element's type wasn't a member
(e.g. a raw IfcTypeProduct, or a stale item list mid-rebuild), aborting the
post-commit refresh.

Wrap the assignment in the same try/except TypeError guard already used for
the sibling relating_type_id assignments (added in 233cc344fa).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 08:44:21 +02:00
Gorgious56 b154cadf3b 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.
2026-06-02 08:38:01 +02:00
Bruno Perdigão 5dde402f8b Optimize 2D projection in ray_cast_by_proximity_2d 2026-06-01 22:27:58 -03:00
Bruno Perdigão 1daee04d9c Early-terminate solid raycasts in non-xray mode 2026-06-01 22:18:13 -03:00
Bruno Perdigão 0d7c378db5 Lazy BVH tree construction in SnapObj 2026-06-01 20:47:27 -03:00
Gorgious56 1ba9341201 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.
2026-06-01 18:32:54 +02:00
Gorgious56 f0aec7b38e 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.
2026-06-01 16:40:18 +02:00
Gorgious56 4cf34b69d2 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.
2026-06-01 16:19:42 +02:00
Gorgious56 44723e83b6 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.
2026-06-01 14:51:31 +02:00
Gorgious56 2272c35e9b 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.
2026-06-01 14:49:33 +02:00
falken10vdl 1c128a2d6a Add has_underside_connection method to Model class and update wall regeneration logic 2026-06-01 07:44:49 -05:00
Ryan Schultz 36372627db 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.
2026-06-01 07:44:49 -05:00
Ryan Schultz a9b6f02f02 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.
2026-06-01 07:44:49 -05:00
Ryan Schultz 9e7d97e298 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.
2026-06-01 07:44:49 -05:00
Ryan Schultz 187b8e7167 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.
2026-06-01 07:44:49 -05:00
Ryan Schultz 08e33fe572 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.
2026-06-01 07:44:49 -05:00
Ryan Schultz e142b9d7b4 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.
2026-06-01 07:44:49 -05:00
Gorgious56 de29d12b00 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.
2026-06-01 14:42:57 +02:00
Gorgious56 9440bafc32 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 c250b2c1a because no array gizmo existed; the icon's plumbing
comes back online now that GizmoArrayEdition is registered.

Verified by test/bim/test_parametric_registry.py: all 8 tests pass -
enable/finish/cancel ops resolve, PropertyGroup attached, is_array
predicate present, predicate is total on non-matching elements.

Generated with the assistance of an AI coding tool.
2026-06-01 14:39:58 +02:00
falken10vdl 431cf435ef 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!
2026-06-01 07:02:28 -05:00
Tiago Azevedo a433f56337 Fix sign of temporary offset restore in sweep_along_curve
The temporary-offset workaround (#7408, commit bd57cc8735) subtracts the
directrix centroid (`mean`) from the curve points before building the
sweep near the origin, then must add it back to restore the original
location. The restore negated the sign — `Move(-mean)` instead of
`Move(+mean)` — placing the swept solid at -mean (mirrored through the
origin) rather than its true position.

Only triggers for polyline directrixes (`is_polyhedron()`) whose centroid
is more than 100 m from the origin (`mean.norm() > 1e2`), so models
centered near the origin are unaffected. Models that keep absolute site
coordinates (e.g. many Revit/ODA IFC exports) render affected swept
solids — reinforcing bars, pipes — at a mirrored phantom location far
from the rest of the model.
2026-06-01 11:42:39 +02:00
Gorgious56 0d3543fa31 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 70845e4dd
duplicated the same logic. The only caller in wall.py already uses
the tool method (line 3687 area), so the local helper has been
dead code since the migration in 7e5e7b8d6 routed _get_wall_geom_cached
to tool.Wall.read_geometry. Drop it.

Generated with the assistance of an AI coding tool.
2026-06-01 10:48:09 +02:00
Gorgious56 e764559133 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.
2026-06-01 10:47:57 +02:00
Gorgious56 a3f92eb427 Merge pull request #8133 from Gorgious56/bonsai/parametric-framework-features
Bonsai/parametric framework features
2026-06-01 09:20:51 +02:00
Gorgious56 453e6dc1cc 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 ee63137c6).
* ActivationCycle (registry-driven loop): any_preview_active toggles
  with each registered preview's is_active flag;
  discard_pending_previews clears every active flag across every
  registered preview.
* SaveOnDiscardWired: locates the bim.save_project operator
  dynamically and verifies its execute path references the discard
  helper by its actual __name__.

test_wall_gizmo_poll_gate.py (4 tests):
* WallGizmoGroupsHideDuringPreview: walks the wall module for
  bpy.types.GizmoGroup subclasses (skips preview-owner exceptions
  whose bl_idname contains 'preview'), mocks any_preview_active to
  True, and asserts every discovered gizmo's poll returns False.
* BaseParametricGizmoPollHidesDuringPreview: mirrors the test for
  the cross-feature parametric framework base class.

test_fillet_operators.py (3 tests):
* FilletOperatorsRegistered: at-least-four-ops + every-discovered-op-
  is-callable. Catches accidental deregistration.
* EnableRejectsIneligibleSelection: poll returns False without a
  selection so the operator is greyed-out in menus.

State-clearing tests via bpy.ops.bim.cancel_wall_fillet_preview() are
deliberately omitted — the operator early-returns when context.screen
is unattached and prior tests in the model lane can leave the screen
in that state, making the dispatch path inherently flaky. Live testing
covers the behaviour.

Net: 13 tests pass cleanly in both single-file and full model lane.

Generated with the assistance of an AI coding tool.
2026-06-01 08:35:26 +02:00
Bruno Perdigão 728026d3f0 Remove debug print 2026-05-31 22:30:56 -03:00
Bruno Perdigão 3f270df11e Add more no headless test for snap 2026-05-31 22:26:16 -03:00