Adds geometry primitives the viewport clip-box feature needs:
- obb_world_clip_planes / obb_clip_planes_from_matrix: derive the 6
inward clip planes of an oriented bounding box (or unit cube under
a matrix_world) in RegionView3D.clip_planes form. expand / expand_rel
margins let callers visualising the box with overlapping geometry
(an empty CUBE display sharing edges with the planes) keep the box's
own wireframe inside the clip volume.
- point_is_inside_clip_planes / corners_might_cross_clip_planes: cheap
reject tests for the per-mesh capping pass to skip the expensive
bisect when an object's AABB is fully outside the box.
- newell_normal / plane_basis: robust planar-ring normal for thin
near-degenerate cap rings where a two-edge cross product is unstable.
- tessellate_ring_planar: triangulate [outer, *inners] 3D rings in the
outer ring's best-fit plane, with a shapely constrained-Delaunay
fallback for the known failure mode of mathutils.tessellate_polygon
on complex concave polygons-with-holes.
Tests cover unit-box, translated, rotated, and scaled cases for the
OBB-from-matrix builder + the rejection helpers.
Generated with the assistance of an AI coding tool.
The transform-modal active check (Bonsai keymap macros + Blender's
TRANSFORM_OT_* family) was a module-local helper in drawing/gizmos.py
used by per-gizmo poll callbacks. It needs to be shared with other
features that gate per-frame side effects on whether a drag is in
progress (clip box plane re-arming, future modal-aware decorators).
Move BONSAI_TRANSFORM_MACROS and the gate into tool.Blender as
is_transform_modal_active classmethod; widen its window scan to all
WM windows for callers without a window-bound context (depsgraph
callbacks). Leave a thin module-local alias in drawing/gizmos.py so
AST scans and existing call sites stay decoupled from the helper's
home module.
Also add generic Blender helpers needed by the clip-box feature
(reusable by any future feature):
- iter_view3d_regions: yield (area, region, region_3d) for every
WINDOW region in every 3D viewport — for clip-plane / draw-handler
fanout.
- get_or_create_collection: idempotent named-collection lookup +
link to a scene.
- is_in_edit_mode: True iff the active object is in any EDIT_*
mode — for features that need to suspend per-tick work during
vert/edge/face manipulation.
- serialize_matrix / deserialize_matrix / hash_matrix: round-trip a
4x4 matrix as a 16-float CSV string for IFC pset persistence + a
matching hash for cache keys.
Generated with the assistance of an AI coding tool.
serialise_cost_quantities built the "Quantities" JSON string by manual
concatenation, inserting quantity.Name and the related element's Name
without any escaping. A name containing a double quote, backslash or
newline produced invalid JSON, breaking any downstream parser (e.g. a
Typst json.decode consumer reporting "failed to parse JSON"). It also
crashed with a TypeError when a name was None (str += None).
Build a Python list and serialise it with json.dumps instead, keeping
the exact same [[name, value], ...] output shape, the element-name
prefix and the unsupported-type behaviour. None names are coalesced to
"" and quantity values are defensively coerced to float.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wraps three over-length lines black wanted on the merge-filter +
fillet-lock commit (wall.py's ``either_is_fillet`` chain rewraps the
right-hand ``or`` operand; test_disconnect_elements.py patch-stacks
break each ``patch(`` onto its own continuation line).
Adds per-test docstrings to test_wall_props_resync_on_dim_change.py
and test_wall_split_filled_opening.py so the contract each pins is
visible on grep / on test-run failure output without scrolling to
the module-level docstring. Drops a flip_object sibling-symbol
mention from the module docstring per CLAUDE.md §4a.
Generated with the assistance of an AI coding tool.
DumbWallJoiner.merge previously crashed on walls with a slab underside
clip because the ConnectedTo / ConnectedFrom migration loops assumed
every rel was an IfcRelConnectsPathElements. The slab's
IfcRelConnectsElements(TOP) rel has no RelatingConnectionType /
RelatedConnectionType and raised AttributeError mid-migration. Filter
on rel class; the slab rel dies with element2 via the trailing
delete_ifc_object cascade.
The fillet preview pen icon now also flips the corner's
BIMWallProperties.is_editing so the connection-disconnect gizmos
surface in parallel with the radius drag. CancelWallFilletPreview
clears the flag before tearing the preview state down so both UIs
hide together. GizmoWallUnjoinSingle.poll inlines the viewport +
array-child guards from the topology gate so the gizmo can show
during preview — its own is_editing check is the real gate.
Fillet-to-source-wall path connection icons render in a muted gray
(LOCKED_COLOR) instead of the active disconnect tone, and the
bim.disconnect_elements operator early-returns with an INFO report
("Fillet wall path connections can't be unjoined — delete the fillet
wall element to remove the corner.") when either side resolves to a
fillet corner. The slab clip rel kind stays disconnect-able since
its identity is separate from the fillet's chord-axis reference.
Drive-by /improve polish on adjacent wall.py code: 3 comment tightenings
dropping sibling-symbol names + a defensive ``if opening.ObjectPlacement:``
guard in the merge opening migration matching the pattern used elsewhere
in the same file.
Generated with the assistance of an AI coding tool.
bim.merge_wall now consumes the non-active selection into the active
one — matching Blender's OBJECT_OT_join (Ctrl+J) and MESH_OT_merge
"at last" convention. The wall the user clicks last absorbs the
other; users following Blender muscle-memory get the result they
expect. DumbWallJoiner.merge is already structurally asymmetric
(wall1 = survivor); only the caller in MergeWall._perform needed
flipping. Audit confirmed the previous call site was the sole
caller of DumbWallJoiner.merge in production code.
Drive-by tidies on adjacent code: collapse two over-length comprehensions
under black's 120-char budget, and switch ``any(True for _ in gen)`` to
``any(gen)`` since the iterable yields tuples that are always truthy.
Generated with the assistance of an AI coding tool.
Two bugs in DumbWallJoiner.split's filled-opening branch:
1. Side classification read filling_obj.matrix_world.translation —
flip-fragile because flip_object rotates the filler 180° + translates
so the bbox stays visually in place, moving the door origin to the
opposite bbox corner. A flipped door centred over the cut could be
classified on the wrong side. Switch to the opening's axis-projected
midpoint, which the unfilled-opening loop already uses.
2. When the void straddles the cut and the filling moves to element2,
the void copy for element1 was taken from the rebound new_opening
whose PlacementRelTo had been swapped to element2 — the new void on
element1 then sat in element2's local frame. Reorder so the copy
reads from the original opening (still hosted by element1) before
remove_feature destroys it.
Generated with the assistance of an AI coding tool.
ChangeExtrusionDepth, ChangeExtrusionXAngle, and ChangeLayerLength
mutate IFC extrusion / axis but never re-prime BIMWallProperties from
the post-mutation state. Gizmo icons that position from props.height
then sit at the pre-mutation elevation even though the wall mesh shows
the new one — visible asymmetry against the workspace header H field
which redraws live. Add the existing _resync_walls_after_mutation
call to each operator's epilogue. _maybe_resync_wall_props_from_ifc
already skips non-walls and walls in edit mode, so calling on the raw
selection list is safe.
Generated with the assistance of an AI coding tool.
DumbWallJoiner.merge cascade-deletes element2's HasOpenings via
delete_ifc_object, which previously dropped every IfcOpeningElement
(and any IfcDoor / IfcWindow filling) hosted by the discarded wall.
Re-host each void rel onto the survivor BEFORE the delete fires, and
re-apply the opening's captured world matrix via edit_object_placement
so the void doesn't drift when the two walls have different
placements — a PlacementRelTo swap alone would fail this when origins
differ along the shared axis.
Generated with the assistance of an AI coding tool.
Schematic dimensions float in billboarded viewport space; their labels
carry the value, not the bar length. Snapping the dragged tip to scene
vertices produces nonsensical value jumps when the mouse crosses
unrelated meshes. Add an opt-out flag on the parametric gizmo group
base and override it on the schematic base — every schematic subclass
inherits no-snap behaviour, and in-place parametric gizmos (door,
window, wall, stair, roof, mep) keep the existing Ctrl-toggleable
snap because the default stays True.
GizmoDimension.invoke also forces tool_settings.use_snap = False for
schematic gizmos so the header magnet visibly switches off for the
drag's duration. The existing exit path restores the user's previous
setting on release.
Generated with the assistance of an AI coding tool.
Wall body rebuild + slab underside re-clip are now unified behind
tool.Model.regenerate_wall and called from split / merge / extend
operators. Fillet corner walls accept extend-to-underside (poll +
operator partition switched to is_path_connectable_wall) and surface
the wall-unjoin gizmo without the parametric-edit gate, since
fillets cannot enter that lifecycle. DumbWallJoiner.split strips the
duplicate's inherited slab-trim booleans up front so wall2 lands at
the cut point. regenerate_fillet_corner_wall re-clips after the body
rewrite so a prior extend-to-slab survives neighbour recalcs.
Drive-by bug sweep: tuple typo in hotkey_S_G's IfcSpace check,
defensive .get() in draw_regen_operations for partial AuthoringData
loads, and a try/except in get_active_representation matching the
existing convention for stale mesh ifc_definition_ids after a
representation rebuild.
Tests cover the regenerate_wall branching, the get_active_representation
stale-id contract, and the GizmoWallExtendVertically fillet acceptance.
Generated with the assistance of an AI coding tool.
Add a "Copy" option to bim.cad_offset. When enabled (the
default) it offsets a new copy of the selected edges as
before; when disabled it moves the existing edges to the
offset location instead. The toggle is exposed in the CAD
tool's Offset panel and the operator redo panel.
Generated with the assistance of an AI coding tool.
Deleting a slab that was connected to a wall via IfcRelConnectsElements(TOP)
left the wall holding orphan IfcBooleanResult items + a stale BBIM_Boolean
pset. The disconnect operator already runs the right cleanup; element delete
just never invoked it.
Extract the per-kind cleanup into core.connection.disconnect_rel so the
operator (bim.disconnect_elements) and a new cascade in
tool.Geometry.delete_ifc_object share one dispatch table. Adding a future
rel kind to tool.Connection.find_rels now flows into both call sites
automatically; an AST forward-compat guard enforces coverage.
Other adjustments:
- regenerate_wall_to_underside zero-slab branch now removes stale clip
booleans instead of silently skipping, so disconnecting the last TOP
slab also reverts the wall correctly.
- duplicate_ifc_objects (Shift+D) calls strip_underside_booleans on copied
walls so the duplicate doesn't carry over the source's slab trim, then
reloads the body representation when something was stripped so the
viewport reflects the change without waiting on Shift+G.
- batch_being_deleted_ids threads through OverrideDelete so the cascade
can suppress partner-side regenerate when both endpoints are queued for
deletion in the same batch.
This file was generated with the assistance of an AI coding tool.
Wires slabs into the parametric edit framework (tool.Parametric
.EDIT_TYPES) so the wall-slab disconnect UI gets ESC handling, red
cancel icon, mutual exclusion with other parametric edits, and
per-feature gizmo prefs — all from BaseParametricGizmoGroup — without
duplicating the lifecycle.
Adds:
- ParametricObject("slab") registry entry + tool.Parametric.is_slab
predicate (any IfcSlab).
- BIMSlabProperties with is_editing flag; PointerProperty wired by
the framework's register_object_properties.
- bim.enable_editing_slab / bim.finish_editing_slab /
bim.cancel_editing_slab operators on tool.Ifc.Operator so they
flow through tool.Parametric.run_bim_op cleanly. No IFC mutation
— slab edit is a pure UI gate; finish and cancel share the body.
- tool.Model.get_slab_props accessor.
- GizmoSlabEdition inheriting BaseParametricGizmoGroup with the
pen / validate / cancel triad. is_element_type narrows to
IfcSlab with at least one wall clipped to its underside.
The disconnect-icon group GizmoSlabUnjoinWalls polls behind
_slab_connection_gizmo_poll_gate(require_editing=True), which now
reads is_editing through tool.Model.get_slab_props.
Drops the standalone GizmoSlabConnectionAccess + the
setup_pen_cancel_icons helper added earlier in this branch — both
superseded by the framework integration.
Also folds in the wall + multi-slab gizmo polish requested live:
- Wall side: stack the per-slab unjoin icons vertically (up to 5)
so multi-slab connections each get a distinct clickable icon;
hover-highlight reveals which slab will disconnect.
- GizmoPairDisconnect activates when 2 elements with an
IfcRelConnectsElements(TOP) rel are selected, with the icon at
the wall-slab connection world anchor.
- Wall-slab anchor moved from slab clip Z to wall top +
WALL_SLAB_CONNECTION_Z_CLEARANCE so the disconnect icon perches
above the extend-vertical / slope gizmo instead of overlapping.
- Shared _resolve_active_partner_pair helper for 2-selection
gizmos; _slab_connection_gizmo_poll_gate added to
_REQUIRED_CALLEES + GizmoSlabEdition added to the AST
forward-compat allowlist.
Build note: wall.py's DisconnectElements._perform imports
bonsai.core.connection.disconnect_rel — that core module is being
added in a parallel-session commit. Until that lands the addon
import will fail.
Generated with the assistance of an AI coding tool.
Single generic dispatcher replaces UnjoinWallPathConnection +
DisconnectWallSlab. Takes two GlobalIds, looks up every supported
rel between them via tool.Connection.find_rels, dispatches the right
cleanup by rel kind:
- path (IfcRelConnectsPathElements): remove_connection on every rel
in both orientations + recreate both walls + resync drafts.
- element-top (IfcRelConnectsElements with Description=="TOP"):
disconnect_element + regenerate_wall_to_underside on the wall side
via orient_element_top to recover which input is wall vs slab.
- element (other IfcRelConnectsElements): plain disconnect_element.
tool.Connection lands as a new tool module with two helpers:
- find_rels(a, b): every supported rel between two elements, walking
both ConnectedTo + ConnectedFrom (catches both authoring
orientations and dedups by id).
- find_rel(a, b): first-match convenience.
- orient_element_top(rel, a, b): recovers (wall, slab) from a TOP
rel regardless of which input came first.
Updates GizmoWallUnjoinSingle to target bim.disconnect_elements with
both element_a_guid + element_b_guid pre-filled per icon. Adds the
single registration in tool/__init__.py and the classes-tuple entry
in bim/module/model/__init__.py. Drops the two retired classes.
Tests cover both cleanup branches (path + element-top), missing
endpoints, no-rel-found, and registration smoke.
Generated with the assistance of an AI coding tool.
Counterpart to UnjoinWallPathConnection on the wall-slab side: takes
a wall + slab GlobalId pair, locates the IfcRelConnectsElements(TOP)
between them via tool.Wall.find_wall_slab_rel, removes it via
ifcopenshell.api.geometry.disconnect_element, then re-runs
core.regenerate_wall_to_underside so the wall re-clips against any
remaining connected slabs (the disconnected slab is excluded
naturally because the helper walks tool.Model.get_connected_slab_objs
which filters by the rel set).
Defensive reports replace silent CANCELLED on three error paths the
UI can hit when the gizmo dispatches against stale state: unknown
GlobalIds, wall entity without a Blender object, no rel found
between the resolved pair.
Tests cover all four control flows (happy path + three error paths)
plus a registration smoke that catches a forgotten classes-tuple
update.
A follow-up commit will retrofit this + UnjoinWallPathConnection +
the MEP port disconnects through a unified bim.disconnect_elements
dispatcher with a small connection-type registry; that lands as a
separate single-concern commit so the typed operator can be
reviewed first.
Generated with the assistance of an AI coding tool.
Four classmethods enable the new wall-slab connection gizmo work:
- iter_wall_slab_connections(wall): yields (slab, rel) tuples for
every IfcRelConnectsElements(TOP) on wall.ConnectedFrom — the rel
kind extend_walls_to_underside creates.
- iter_slab_wall_connections(slab): mirror, walks slab.ConnectedTo
so a slab-side gizmo can enumerate every wall clipped to its
underside.
- find_wall_slab_rel(wall, slab): locates the specific rel between
a wall + slab pair so a disconnect operator knows what to remove.
- wall_slab_connection_location_world(wall_obj, slab_obj): returns
the world-space icon anchor — wall axis midpoint X/Y lifted to
the slab's mesh-bbox underside Z. Approximate (uses slab bbox vs
reconstructing the slab's clip plane) but adequate for icon
placement on a wall whose top meets the slab; returns None when
the wall has no IFC Axis representation.
Tests (11) pin the rel-shape contract (class + Description=="TOP",
non-TOP and non-IfcRelConnectsElements rels skipped, None relating
defensively skipped) plus the icon-anchor math (axis-mid lifted to
slab-bbox bottom; None for axisless walls).
Generated with the assistance of an AI coding tool.
update_railing skipped the bmesh rebuild for WALL_MOUNTED_HANDRAIL
railings because the only mesh source available at the time mutated
IFC. The viewport-only preview helper that lands with the parametric
gizmo work (generate_wall_mounted_handrail_preview) sidesteps IFC
entirely, so the WALL_MOUNTED_HANDRAIL branch can join the
FRAMELESS_PANEL path and trigger update_railing_modifier_bmesh on
every property write. Gizmo drag now repaints the viewport in real
time instead of waiting for Finish Editing.
Generated with the assistance of an AI coding tool.
Switches the IfcRailingType terminal-type selector from cycle-on-click
to a popup menu of all terminal-type literals — 5+ values trip the
§2.8 menu-pick threshold. Updates classes registration; removes
EditRailingTerminalType in favour of PickRailingTerminalType which
inherits PickTypeMixin.
Adapts the cherry-pick from db016d881 to post-PR5 framework state:
- Imports CycleTypeMixin / PickTypeMixin / PathPreservingEditMixin
from bim.parametric_lifecycle (PR5 moved them off gizmos.py).
- Routes is_railing through tool.Parametric (predicates moved off
tool.Blender.Modifier between PR3-PR5).
Skips the parametric_lifecycle.py framework refactor the source
commit shipped — HEAD has the more-evolved post-PR5 framework that
already covers it.
Adds the _FakePropsBase + make_lifecycle_obj test helpers to
test/bim/conftest.py so the new test_railing_lifecycle.py can
exercise the edit triad without a real bpy.types.Object. Brings the
test_railing_schematic.py marker in line with the rest of the model
lane.
Generated with the assistance of an AI coding tool.
IFC2X3 representations have no HasShapeAspects inverse; opening the
Geometry & Materials subpanel on an IFC2X3 object raised AttributeError
and left the items list empty. Wrap the access with a getattr default
so pre-IFC4 schemas return an empty iterable, and pin the contract with
an AST forward-compat guard that scans bim/, tool/, and core/ for any
future direct .HasShapeAspects access.
Closes#8157
Generated with the assistance of an AI coding tool.
Port gizmos-8088's railing gizmo block to v0.8.0:
- _RailingEditMixin (PathPreservingEditMixin specialisation) +
EnableEditingRailing / CancelEditingRailing / FinishEditingRailing
edit triad
- CycleRailingType (2-value type cycler) + ToggleRailingUseManualSupports
one-shot + EditRailingTerminalType
- FlipRailingPathOrder + EnableEditingRailingPath /
CancelEditingRailingPath / FinishEditingRailingPath path-edit
operators (mutually exclusive with the schematic frame)
- GizmoRailingSchematic (BaseSchematicGizmoGroup specialisation) —
axonometric schematic frame with per-attribute dimension gizmos
for FRAMELESS_PANEL + WALL_MOUNTED_HANDRAIL railing types;
hover-on-attr highlights the schematic edges tagged with the
matching feature
Tests: test_railing_lifecycle.py (280 LOC) +
test_railing_schematic.py (272 LOC).
Drops the per-feature GizmoPreferences{Door,Window,Stair,Wall,Roof,
Railing} PropertyGroups that the source commit added to bim/ui.py
— that finer-grained per-attribute toggle model was deliberately
collapsed to flat per-feature bools in the PR5b prefs sweep, and
GizmoRailingSchematic gates on the flat ``prefs.gizmos.railing``
bool via ``gizmo_pref_name`` so no functionality is lost.
Generated with the assistance of an AI coding tool.
The check `len(bytedata) == n * 2` was wrong: float64 is 8 bytes per
element, not 2. Legacy float64 checksums fell through to the float32
reader and produced a (2n,)-shaped array, breaking is_moved() and
is_camera_moved() with `ValueError: operands could not be broadcast`
on .blend files saved by Blender <5.0.
Adds a parametrized regression test covering both n=3 (translation)
and n=9 (rotation) for both dtypes.
Generated with the assistance of an AI coding tool.
GizmoWallEdition.position_gizmos used props.anchor_x / props.length
for the in-range check (split icon visibility) and perpendicular
gizmo placement. Those props mirror IFC and are re-primed by
_maybe_resync_wall_props_from_ifc — any operator path that skips
the re-sync leaves the perpendicular gizmo clamped to the previous
wall extent, so the icon parks at the old wall end instead of the
cursor's orthogonal projection. Visible after a wall mutation as
the perpendicular icon landing way off the cursor in top-down view.
Switch to the mesh bbox along local X. recreate_wall rebuilds the
mesh to match the current IFC body on every wall mutation, so
bound_box is authoritative without an explicit props sync.
Generated with the assistance of an AI coding tool.
MEP elements imported as tessellation / brep (no IfcExtrudedAreaSolid
or IfcSweptDiskSolid in their body representation) can't be
parametrically edited — the gizmos offer affordances the geometry
kernel has no path to honour. tool.System.has_parametric_body
inspects the Model/Body/MODEL_VIEW representation and returns True
only when at least one item resolves to one of the two
profile-sweep primitives.
The gate is wired into:
- GizmoMEPActions.is_eligible_object (the action icon group)
- _active_is_flow_segment / _active_is_bend_fitting visibility
predicates the icon row consults per-icon
- GizmoPipeSegmentEdition / GizmoDuctSegmentEdition is_element_type
tool.Parametric.is_pipe_segment / is_duct_segment stay IFC-class-only
so their truth-table contract test keeps reading a single concern.
Generated with the assistance of an AI coding tool.
Pure-math parallelism check (value ≡ 0 mod π within VTX_PRECISION)
that lived as a module-private helper in mep.py belongs next to
tool.Cad.is_x — same comparator family, no MEP-specific knowledge.
Other features with rotation-difference checks (wall fillet, roof
slope, railing terminus) now have a sanctioned spelling.
Generated with the assistance of an AI coding tool.
Four standalone test files pinning contracts the production code
already honours:
- test_mep_actions_cache.py: GizmoMEPActions visibility-predicate
cache evicts on selection or generation change.
- test_mep_bend_preview_cache.py: bend decorator polyline cache
re-uses within a generation and rebuilds on generation bump.
- test_mep_distribution_fit_smoke.py: bim.fit_flow_segments
round-trips a 3-segment polyline without raising.
- test_preview_cancel_ops_forward_compat.py: AST scan ensures every
preview Enable* operator has a paired Cancel* operator with the
matching prop reset.
Generated with the assistance of an AI coding tool.
Two small refactors:
- apply_transform_modal_draw_gate(group, context) replaces the
three-line _is_transform_modal_active + _hide_all_non_modal_gizmos
pair that BillboardingGizmoGroupMixin, BaseParametricGizmoGroup
and BaseSchematicGizmoGroup all repeat in draw_prepare.
- decorator.py renames _stroke_lines_alpha to a public-scope
draw_polyline_segments and drops the no-longer-private companion
docstring reference; the function is now usable by sibling
decorators that draw polyline overlays.
Plus a few one-liner tweaks in tool/model.py and opening.py
following the helper rename.
Generated with the assistance of an AI coding tool.
Two hot paths the gizmo polls fire every viewport event memoise
their result against tool.Parametric.get_geom_generation():
- tool.Blender.Modifier.any_selected_array_child caches the
per-selection scan against the selection identity-set + the
IFC generation token so a stable selection during a drag
doesn't re-walk every selected object's BBIM_Array pset every
frame.
- bim/module/model/wall.py grows a pair-predicate + connection
cache that the wall topology gizmos hit; both keyed on
(pair_uids, predicate_kind, generation) so a wall split or
axis edit invalidates correctly via the generation bump.
Behavioural contract is unchanged — stale entries are evicted
on generation bump; cache miss returns the same value the
un-cached path returned.
Generated with the assistance of an AI coding tool.
bim/module/model/conftest.py exposes the autouse _require_real_bpy
skip-guard, four make_* factories (obj / element / context /
ifc_file), and a patched_tool context-manager factory that wires
the half-dozen tool.* boundary patches every gizmo + decorator
test was repeating.
Existing test files in the directory drop their local copies of
_require_real_bpy and adopt the patched_tool / make_* fixtures
where the call site simplifies — test_mep_port_operators.py is
the biggest beneficiary (−89 LOC).
No production behaviour change.
Generated with the assistance of an AI coding tool.
Three concerns bundled by file boundary (all in mep.py):
- Extract bend preview operators + GizmoBendPreview into a focused
mep_bend_preview.py module; preview_base.py grows the shared helper
set both bend and other previews now consume; classes tuple in
model/__init__.py updated to register the new module.
- Surface ERROR reports on five silent CANCELLED returns in
MEPUnjoinAtPort / MEPRemoveTerminalFitting / MEPUnjoinPair so a
degenerate IFC file ("fitting has no Blender object", "connected
port leads nowhere") shows up in the popup instead of looking like
a no-op.
- DRY: _resolve_active_mep_segment + _require_port_state factor the
segment-id-or-active-object resolve + port-state guard out of every
port operator's prologue; _wire_anchored_icon_targets pulls the
GizmoMEPActions setup() body into an exercise-without-MRO helper so
the wiring-contract tests can hit it without instantiating the
GizmoGroup.
Drops the now-unused preview_base import that the extraction left
behind.
Generated with the assistance of an AI coding tool.
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.
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.
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.
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.
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.