When grabbing an array child, the selection now expands to include
the array parent and all sibling children before the move operator
runs. Mirrors existing behavior for aggregates and nests.
Generated with the assistance of an AI coding tool.
Ran the new sync_stub.py against a real local build: adds
context.delete_same_facet_edge_pairs (present on the compiled wrapper,
missing from the stub) and drops the module-level logger_or_root
(present in the stub, no longer exists on the wrapper at all).
Nothing else changes - no license header rewrite, no docstring loss,
none of the 14 hand-curated named-parameter constructor/function
signatures touched, unlike the wholesale regeneration this replaces.
Generated with the assistance of an AI coding tool.
generate_stub.py (this branch's earlier commit) regenerates
ifcopenshell_wrapper.pyi wholesale from the compiled wrapper: it
reliably fixes real drift, but it also discards everything that isn't
mechanically recoverable from the wrapper alone - the license header,
docstrings, and hand-curated named-parameter signatures for
SWIG-overloaded constructors/functions (SWIG itself always emits
generic `*args` for those, so a regenerator can't tell a deliberate
curation from real drift and just overwrites it).
sync_stub.py takes the smaller-blast-radius approach: it only adds
top-level symbols/class members that are genuinely missing, and only
removes ones that are genuinely gone, cross-checking against
validate_stub.py's own full canonicalisation (via the newly-exposed
get_names_tree()) so it never mistakes a property()/staticmethod()-
wrapped member for something absent just because its own narrower
parser skips that form. Anything that exists on both sides under the
same name but with a different signature - exactly where curation
lives - is left untouched and reported for a human to review instead
of guessed at.
Verified against a real local build: applying it to the current
ifcopenshell_wrapper.pyi produces a small, targeted diff (add one
missing method, drop one stale function) with the license header,
docstrings, and all 14 curated constructor/function signatures
preserved byte-for-byte, versus generate_stub.py's ~1000-line
wholesale rewrite for the same underlying fix.
Generated with the assistance of an AI coding tool.
When a STEP instance has fewer attribute tokens than its schema declares
(commonly from corrupted/malformed syntax), parse_context::construct()
sized the in-memory attribute storage to the smaller token count instead
of the schema's attribute count. This left the storage's last N attribute
slots simply nonexistent rather than blank, so any later read of one of
those trailing attributes by index threw an uncaught IfcParse::IfcException
("Index N is out of range for storage of size N") that terminated the
whole process (SIGABRT) instead of being handled as a parse warning.
Fix: when the schema declaration is known, size the storage to the
schema's attribute count. Indices beyond the number of tokens found are
left at their existing default-constructed blank value (the storage
constructor already blank-initializes every slot), so a truncated
instance now degrades to blank values for its missing trailing
attributes, matching the parser's existing "expected N attribute values,
found M" warning intent instead of crashing.
Reproduced with the fuzzing script attached to #5679: single-byte
mutations of a minimal IFC4 file that corrupt the IFCPROJECT instance's
token stream reliably aborted IfcConvert with this exact exception before
the fix, and now parse with a logged syntax error and exit code 0.
Fixes#5679
Generated with the assistance of an AI coding tool.
set_shape_transparency() called AIS_InteractiveContext.SetTransparency(),
whose argument count is inconsistent across pythonocc-core versions
(reported as a TypeError in #1037). Set transparency directly on the AIS
object instead, the same stable pattern already used elsewhere in this
file (display_shape() calls ais.SetTransparency() directly, never through
the Context), then call Context.UpdateCurrentViewer() to refresh.
app.py's viewer used a "SetSelectionPriority(counter)"/"SelectionPriority()"
pair as an ad hoc unique key to map a displayed AIS object back to its IFC
product. On modern pythonocc-core this crashed with AttributeError because
.GetObject() (needed to unwrap the old handle-based API) no longer exists
on AIS objects (#1098, PR #1113 partially patched one of the two call
sites but left the one in HandleSelection unguarded).
Live pythonocc-core 7.9.3 testing showed the GetObject() guard alone is
not sufficient: SetSelectionPriority/SelectionPriority themselves have
been removed from AIS_InteractiveObject entirely in modern OCCT (only
AIS_Trihedron keeps a same-named but unrelated method for datum parts),
so gating the .GetObject() call with the existing USE_OCCT_HANDLE flag
would still crash the first time a shape is selected. Verified live that
AIS objects retain correct __eq__/__hash__ (matching the underlying OCCT
instance) across separate SWIG wrapper instances, so ais_to_product is
now keyed directly by the AIS object itself, removing the dependency on
the removed OCCT API and the GetObject()/handle distinction altogether.
Verified live against pythonocc-core 7.9.3 (conda-forge) using real
AIS_Shape objects obtained from ifcopenshell.geom.occ_utils.display_shape()
and a real IFC file: reproduced both the original TypeError (#1037) and
AttributeError (#1098), confirmed both fixes resolve them, and confirmed
the ais_to_product dict lookup round trips correctly through a real
Context.Select()/SelectedInteractive() call. Could not exercise the full
Qt-embedded viewer.finished()/HandleSelection() flow end to end because
this pythonocc-core build segfaults natively when creating a second GL
context inside a Qt widget on this macOS host, a pre-existing environment
issue unrelated to this diff (reproduces identically with unpatched code,
before any touched line executes).
AI-generated, reviewed and tested by Petru Conduraru.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
create_shape() returns a Python-owned Element (SWIG_POINTER_OWN in the
boost::variant out typemap). Its .geometry property calls Element::geometry(),
which returns a reference into the element's boost::shared_ptr<Representation>
_geometry member. SWIG wraps that reference as a non-owning pointer, so the
returned Triangulation/BRep/Serialization proxy does not keep the element alive.
When a caller keeps only .geometry (e.g. create_shape(s, e).geometry) and drops
the parent element, Python garbage-collects the element, destroying its
shared_ptr and freeing the underlying representation. Subsequent reads of
verts/faces then return freed memory: empty or implausible float/int garbage,
non-deterministically depending on GC and allocator timing. This is silent data
corruption, not a crash, and has bitten users since 2020.
Fix: in the TriangulationElement/SerializedElement/BRepElement pythoncode, wrap
the geometry getter so the returned geometry stores a backreference to its
owning element (result._parent = self). This makes the parent's lifetime at
least as long as the geometry's, automatically and transparently, so no caller
has to remember to hold the element. This is aothms's suggested backreference,
applied generically in the binding rather than left as a workaround.
Reproduced deterministically (washBasin fixture): before, verts len 0 vs 133500
across repeated GC-pressure runs; after, 133500 every run for all three element
types. test_create_shape passes; no regressions.
Note: tree.select_ray()'s ray_intersection_result (2024 follow-up in #1124) is a
separate ownership mechanism (std::vector element reference + std::array member
pointer) and is left as follow-up scope.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
recalculate_cut() and recalculate_fill() each ran is_intersecting_camera(),
which builds a bmesh and scans every vertex. When a redraw recalculated both
(camera moved, cache miss, or the object selected) that was two full
intersection tests per object per frame for the same answer.
Compute it once in decorate() and pass it to both, and skip the test
entirely when neither recalculation is needed. Never more tests than before,
identical result since the camera can't move within a frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
is_camera_moved() runs eval()/numpy over the camera matrix and, as a side
effect, refreshes the stored checksum the first time it returns True. It was
called up to twice per object inside decorate(), so on a frame where the
camera actually moved the first call updated the checksum and every later
call - the fill check on the same object, and both checks on all remaining
objects - then saw an already-current checksum and returned False. Only the
first object's cut got recalculated; its fill and every other element stayed
stale until something else invalidated the cache.
Evaluate it once at the top of __call__ and reuse the flag. This halves the
per-object eval overhead on the common path (viewport navigation with the
camera object stationary) and, when the camera does move, correctly
recalculates the cut and fill for every intersecting element instead of just
the first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deleting a type used to strip its occurrences: any that displayed the
type's mapped representation lost their geometry, and inherited material
and presentation styles were dropped too.
The no-SHIFT "Delete Type" path now bakes each occurrence's geometry,
styles, and inherited material onto the occurrence before the type is
removed:
- Refactor UnassignType's unmap logic into a reusable
UnassignType.unassign_and_unmap(), and extend it to re-attach styled
items (copy_deep only follows forward refs, so IfcStyledItem is lost)
and bake down any inherited (non-owned) material.
- Add RemoveType._detach_type_material_set(): unhook the type's
IfcMaterialLayerSet/ProfileSet association cascade-free before deletion,
so remove_product's aggressive unassign_material never fires and the
occurrences' layer/profile-set usages survive intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a trash button in BIM_PT_type_attributes that deletes the relating
type via bim.remove_type. SHIFT+Click also deletes every occurrence of
the type in the project, behind a confirmation dialog showing the count.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aothms asked for the toposort dependency ordering used by the dedup
walk to try igraph's C-backed topological_sorting() first, since it
should shave off additional time on top of the non-recursive
get_info fix. Falls back to the pure python toposort package with a
warning if igraph is not installed.
Generated with the assistance of an AI coding tool.
The 2020 profiling in issue #1043 found the Optimise recipe's dedup
loop spent almost all of its time in entity_instance.get_info(recursive=True):
because the topological sort already guarantees every referenced entity
is folded before the entity that references it, recomputing each
already-folded subtree's canonical value from scratch for every parent
that points to it is wasted work. Confirmed this is still exactly the
bottleneck in the current codebase, unchanged since 2020 (get_info's
recursive path still walks the whole subtree on every call).
Applied aothms's suggested fix from the issue thread: canonicalize each
entity with a non-recursive get_info, and for referenced entities substitute
the already-computed identity of their folded replacement (looked up in
instance_mapping) instead of re-expanding the subtree. Also limited the
toposort dependency graph to direct references (max_levels=1), since a
topological sort only needs direct edges, not the full transitive closure
traverse() was computing for every entity.
Benchmarked before and after on real IFC test fixtures and a larger
synthetic file with heavily shared geometry (thousands of walls sharing
a handful of profile/point subtrees, mirroring the sharing pattern
described in the issue):
- test/input/geometrygym_great_court_roof.ifc (56989 entities): 9.9s -> 1.7s
- test/input/acad2010_objects.ifc (16296 entities): 3.7s -> 0.4s
- synthetic 120083-entity fixture with heavy geometry sharing: 19.2s -> 3.4s
Verified correctness by comparing the full canonical (recursive get_info)
multiset of the optimized output between the old and new implementation on
all three fixtures: identical results, same fold counts.
Added test_Optimise.py covering the core scenario from the issue: entities
built from separate, value-identical non-rooted subtrees fold to a shared
instance, while entities with distinct values do not.
Generated with the assistance of an AI coding tool.
IfcSurfaceCurveSweptAreaSolid regressed in 0.8 for geometry far from the
origin (for example parapets on a georeferenced building), which went
missing or glitched.
The kernel offsets the directrix toward the origin when it is far away
(mean.norm() > 1e2), storing the offset copy in a local curve variable and
setting applied_temporary_offset so the finished solid is translated back by
+mean. But the wire was still built from scs->curve, the un-offset original,
so the offset never took effect and the result was translated by +mean from
its correct location. Build the wire from curve instead. When no offset is
applied curve aliases scs->curve, so near-origin geometry is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
references_to_resolve is never set while parsing header
entities, so binding a reference to it was UB, caught by
UBSan on any file with a header.
Generated with the assistance of an AI coding tool.
Two related bugs in read_from_stream's reference-resolution
loop, both reachable from malformed input:
- has_attribute_value<IfcBaseClass*> only checks the stored
slot's type, not that it's non-null (e.g. an explicit $
value), so the following get_attribute_value() call could
return null and inst->declaration() crashed on it.
- byid_[ref] default-inserts (and returns) a null pointer
when the owning instance id isn't present, which was then
dereferenced unconditionally via ->data().
Added regression tests using the two minimized crash inputs
that found these.
Generated with the assistance of an AI coding tool.
`values` dictionary was missing and variables were never collected to it, so `FormulaEvaluator(values)` was always resulting in missing variable error.
The fixture declared FILE_SCHEMA(('IFC2X3')) but used
IFCPROPERTYSETDEFINITIONSET(...), a defined type that only exists in
IFC4+ (confirmed absent from the generated Ifc2x3-schema.cpp/
Ifc2x3-definitions.h, present in the IFC4 equivalents). The file's own
FILE_NAME record ('Column_4x3.ifc') suggests it was originally
exported as IFC4X3 and the schema tag was later miscopied to IFC2X3.
Traced with an instrumented parser build: on encountering the
unrecognized keyword, declaration_by_name() correctly throws
"Entity with name 'IFCPROPERTYSETDEFINITIONSET' not found in schema
'IFC2X3'", caught by the existing IfcException handler in
in_memory_file_storage::load(). The parser then falls back to parsing
the trailing (#136,#138) as a plain nested SET rather than the typed
value, so RelatingPropertyDefinition ends up as a bare tuple instead
of an IfcPropertySetDefinitionSet-wrapped value with .is_a(). This is
correct, expected behavior for content that doesn't match its
declared schema - not a parser bug. Fixing the header to IFC4 (which
does declare the type) resolves test_stream, test_file, and test_rocks
in test_streaming_rocksdb_and_simpletyperefs.py.
Generated with the assistance of an AI coding tool.
src/ifcgeom/kernels/opencascade/boolean_utils.cpp, OpenCascadeKernel.cpp,
and boolean_result.cpp logged diagnostics (including the "Processed
fully in 2D" family of messages) through the global Logger::Root()
singleton. IfcConvert's main() constructs its own Logger and wires it
to --log-file via SetOutput(), then threads that instance through
Converter/kernel constructors as logger_ (see AbstractKernel). Since
Logger::Root() is never itself configured with an output stream, every
Notice/Warning/Message call through it was silently dropped instead of
reaching the log file - Logger::Message's log1_/log2_ null checks just
no-op.
This made src/ifcopenshell-python/test/test_wall_opening.py fail: it
asserts on specific log messages that the underlying boolean-op code
was still emitting correctly, just to nowhere. The geometry itself was
never wrong.
Add a Logger*, defaulting to null, to boolean_settings (with a log()
accessor falling back to Logger::Root() for the few remaining
call sites with no injected logger available), thread it through
eliminate_narrow_operands and boolean_subtraction_2d_using_builder,
and have OpenCascadeKernel/boolean_result.cpp populate it from their
inherited logger_ member instead of relying on the global singleton.
Generated with the assistance of an AI coding tool.
The class-pairing validation added in 10ee5aef4f rejects any type
assignment whose class isn't in the buildingSMART implementer
agreement map. That map only covers physical product occurrence/type
pairs (IfcWallType -> IfcWall, etc); IfcTypeProcess and IfcTypeResource
subtypes such as IfcTaskType, IfcProcedureType and the resource types
have no entry, so previously-valid assignments like
IfcTaskType -> IfcTask were rejected with "allowed occurrence
classes: <none>".
These classes still follow the schema's universal Type-suffix naming
convention, so derive the pairing the same way the existing
ApplicableOccurrence fallback does: strip "Type" from the relating
type's class name and accept it only if the schema actually declares
that entity. This can only add pairings implied by the type's own
class name, so it cannot loosen the existing rejection of genuine
mismatches (e.g. IfcWallType -> IfcWindow).
Generated with the assistance of an AI coding tool.
The crease and sharp weighting seemed flipped to my sensibilities, so
now crease is heavier than sharp. I also added a commented out block
for debug colours in case someone wants to quickly use bright colours
to diagnose future problems.
Add UseEdgeClassification, RenderCreases, ValleyAngleMinDegrees,
RenderSharp, RidgeAngleMinDegrees, and RenderFlush to EPset_Drawing,
following the existing HasUnderlay/DPI/PerspectiveShiftX pattern.
The master toggle defaults off, preserving current linework output;
the three dependent controls only show in the panel once it's on.
Removes the previous dormant, transient operator-redo properties for
the ridge/valley thresholds and flush-edge toggle, which were never
persisted per-drawing or exposed in any panel, replacing them with
the persistent camera properties read in setup_serialiser().
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add svg-use-edge-classification (default off, preserving today's
linework), svg-render-crease-edges, and svg-render-sharp-edges
settings, gating the existing 5-class classification feature so it
can be disabled entirely (falling back to the pre-classification
whole-shape output) or have individual classes suppressed.
Also fixes a bug uncovered while wiring this into Bonsai: ready(),
where geometry_settings() actually gets read into the serializer,
was only ever invoked explicitly by IfcConvert's CLI driver and
isn't exposed to Python. Every Svg* setting -- including the three
from previous rounds -- silently stayed at its hardcoded constructor
default when the serializer was constructed directly through the
Python bindings, as Bonsai does. Fixed by calling ready() from
SvgSerializer's own constructor, safe since it only reads
geometry_settings() with no other side effects, and settings are
always finalized before construction in every call path.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Circular-profile IfcColumn/IfcPile elements produce a genuine
analytic cylindrical BRep face (via BRepPrimAPI_MakePrism), not a
tessellated facet. The edge classification/extraction pipeline is
edge-identity-based end to end, but a smooth surface's silhouette is
synthesized by HLR on the fly and has no corresponding pre-existing
edge to bucket, so it was silently dropped once any edge in the
product had been classified. Add a face-level pass that includes any
non-planar face directly in the outline bucket, giving HLR's
per-face OutLine reconstruction a face identity to correlate
against. Purely additive: diffing the whole test scene's output
before and after shows only the two previously-missing tangent
lines appear, nothing else changes.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Re-enable the view-relative sign flip for folds seen through an
opening (e.g. a box with a face removed), reverted in the previous
commit after it corrupted unrelated geometry. The earlier revert's
diagnosis was slightly off: bucket reassignment can't affect HLR's
own visibility computation, so the corruption was actually an
asymmetric-threshold artifact -- an unconditional flip re-tested
small, correctly-flush deviations against the much smaller valley
threshold instead of the ridge one. Gating the flip so it only
reinterprets folds that already clear their own pre-flip threshold
fixes the box case while leaving every other test object's
classification unchanged (verified against the full test scene).
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes three bugs in classify_edge_from_faces() found via real-world
testing against a dedicated stress-test scene (icosphere, Suzanne,
cylinders/cones at various orientations, a dihedral-angle sweep rig):
- The outline (silhouette) test used a bare sign comparison, so a face
at or near exactly edge-on to the camera could land on the wrong
side of zero and fall through to angle-based classification instead
of being drawn as outline. Now uses a tolerance band around zero,
matching an equivalent check already used elsewhere in this file.
- The signed deviation-from-flat formula was inverted (180 - angle
instead of angle), so small, genuinely near-flat facet angles came
out with a large computed deviation and always classified as
sharp/crease, never flush. This is why thresholds appeared to have
no effect. Also replaced the edge/wire-orientation-based convexity
sign (unreliable on real BRep topology, verified wrong against a
known fully-convex icosphere) with a simpler position-based test.
- A specific edge that was previously missing entirely (not just
misclassified) reappears correctly as a side effect of the outline
fix above; no separate change was needed for it.
A fourth issue (folds viewed through an opening, e.g. a box missing a
face, should read as crease rather than sharp) was attempted via a
back-facing sign flip, but reverted: it broke the fixes above broadly,
since "both faces back-facing" isn't a rare look-through-a-hole case
once HLR has already filtered to visible edges only. Documented in a
code comment for whoever picks this up next.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds boundary/outline/sharp/crease/flush classification of HLR
projection edges in SvgSerializer, so CSS can style silhouettes,
ridges, and valleys differently instead of drawing every edge
identically (fixes the "ugly faceted sphere" problem from #3668).
Classification happens pre-HLR on the original solid's real face
topology (three prior attempts tried to classify HLR's own output,
which carries no face topology at all and can't be correlated back
by edge identity). Each class's visible portion is then extracted via
HLRBRep_HLRToShape::VCompound(S)/OutLineVCompound(S), the same
per-shape filtering mechanism already used for per-product
segmentation, applied per class instead. Classes are tagged directly
on individual <path> elements so Bonsai's merge_linework_and_add_metadata
group-level class rewrite in operator.py never touches them.
New settings: svg-ridge-angle-min-degrees, svg-valley-angle-min-degrees,
svg-emit-flush-edges (ConversionSettings.h), wired through Bonsai's
CreateDrawing operator and exposed via its redo panel.
Refs #3668.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Reassign Class operator refused to reassign an element to a different
IFC product family unless it was an IfcElement <-> IfcElementType swap, so a
piece of geometry mistakenly hosted on IfcSite could not be turned into
IfcFurniture even though root.reassign_class handles it fine.
Loosen the guard: only block the case that actually matters - a spatial
element (IfcSpatialElement / IfcSpatialStructureElement for IFC2X3) with no
geometry, which would be a real containment-hierarchy container rather than
a stray modelled object. Everything else reassigns freely.
Closes#8664
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
When converting a wall representation to a parametric extrusion via the
Representation Utilities buttons, an element that has openings would report
"has openings - representation cannot be updated" and stop, without telling
the user there is an ALT+click path that bakes the openings into the new
representation. Point the message at that path so the error is actionable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Bonsai: place auto-generated opening boundaries at their real position #8237
auto_generate_boundaries (single-space mode) built each opening/filling boundary
from the opening's LOCAL geometry (get_vertices) but first did
mat.translation = (0, 0, 0) on its placement matrix. Because the vertices are
local, that placement translation is exactly what carries the opening to its
real location, so zeroing it collapsed every window/door boundary onto the
origin. This is why the auto path misplaced window boundaries while the
single-element path (create_element_boundary) placed them correctly, as
@MDHering observed with the two modes. Keep the full placement matrix.
Verified on the reporter's file: the opening's real placement is (0.1, 1.5, 1.0);
a vertex went from (0.6, 0, 0) under the old code to (0.7, 1.5, 1.0) with the fix,
i.e. moved by exactly the (0.1, 1.5, 1.0) that was being discarded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Remove superfluous comment from #8237 fix
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: CyrilWaechter <cyril@biminsight.ch>
It was passing `IFC4X3` directly to `schema_by_name` which is expecting
schema identifier (e.g. IFC4X3_ADD2, not IFC4X3 allowed by `IFC_SCHEMA`
- IFC4X3 is one of the IFC4X3 iterations while it was in development,
not the final one).
Noticed by tests failing:
FAILED
test/util/test_schema.py::TestGeometryClassesIntroducedAfter::test_ifc4x3_to_ifc2x3_is_superset_of_ifc4_to_ifc2x3
- RuntimeError: No schema named IFC4X3
FAILED
test/util/test_schema.py::TestGeometryClassesIntroducedAfter::test_ifc4_to_ifc4x3_is_empty
- RuntimeError: No schema named IFC4X3
Error was:
```
configure: error: could not find a working compiler, see config.log for details
```
config.log:
```
conftest.c: In function 'f':
conftest.c:12:48: error: too many arguments to function 'g'; expected 0, have 6
12 | for(i=0;i<1;i++){if(e(got,got,9,d[i].n)==0)h();g(i,d[i].src,d[i].n,got,d[i].want,9);if(d[i].n)h();}}
| ^ ~
```
Last reference to this file was dropped in 7ae685dbf, though the ref was
pointing to `/patches/opencollada/pr622.patch`, so IIUC
`patches/pr622.patch` was never used.
AST parser has changed a bit and there are some minor differences in the .py output. Updating files just to avoid seeing these diffs when rerunning rule compiler.
Example error:
```
ast.Str(s=node.attr),
^^^^^^^
AttributeError: module 'ast' has no attribute 'Str'
```
`ast.Str` was deprecated since 3.8 and was removed in 3.14, see https://docs.python.org/3/whatsnew/3.14.html#id9
Because uv was always trying to install when starting a venv in `ifcopenshell` folder, though they might be already available globally. And also they were listed twice - in pyproject and in the ci-lint.yml, now there's a single source of truth.
versionURLs in brand.html used http:// while the docs sites are
served over https://, so currentURL.includes(url) never matched and
the <select> never reflected/switched to Unstable. Fixes#8023.
Generated with the assistance of an AI coding tool.
Two TestImplementsTool failures on v0.8.0:
- test_cost.py: Cost could not be instantiated because
core.tool.Cost declared abstract get_direct_cost_item_products, which
tool.cost.Cost never implements. The method is dead (zero call sites;
get_cost_item_products(is_deep=False) already covers the 'direct'
case), so remove the abstract declaration.
- test_ifcgit.py: tool.ifcgit.IfcGit was not declared as a subclass of
its core.tool.IfcGit interface (unlike every sibling tool class), so
the isinstance check failed. Add the base class (and the
bonsai.core.tool import it needs). All 50 interface methods are
already implemented on the concrete class.
No behaviour change. Verified in headless Blender: isinstance(Cost(), core.tool.Cost) and isinstance(IfcGit(), core.tool.IfcGit) both True (were TypeError / False); repo abstract-vs-impl diff confirms all IfcGit abstracts are implemented.
This change was made with the assistance of an AI tool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
black (the version CI's psf/black@stable resolves to) flags three spots
in util/selector.py: the chained .replace() in FormatTransformer.number,
the suppress_zero_inches kwarg in format_length, and the long
`elif key in (...) and hasattr(...)` placement-key tuple in
set_element_value. Reformat all three to black's multi-line style.
Formatting only, no behavioural change (all keys preserved).
This change was made with the assistance of an AI tool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
generate_annotation built the annotation list from a set union and sorted it by
ZIndex and TEXT-ness only. Annotations that tied on that key kept set iteration
order, which follows entity hash (step id plus the process memory address), so
the order of tied annotations (for example a label and its background fill)
shuffled between Blender restarts and flipped their draw order.
Add the stable IFC step id as a final tiebreaker so the order is total and
session independent. Behavior preserving, no z-layer semantics changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
coerce_value assumed value_str was always a CLI string, but ifcmcp
passes JSON-decoded native types (int, None) straight through. Guard
the Union/Optional "none" check so it only calls .lower() on strings,
and handle native None explicitly.
IfcSpace is not a subtype of IfcElement, so quantify.run_quantify()'s
default selector silently skipped all spaces, reporting
elements_quantified: 0 with no error or warning.
Generated with the assistance of an AI coding tool.
Importing a Primavera P6 XML crashed with
`AttributeError: 'NoneType' object has no attribute 'text'` in
P62Ifc.parse_activity_xml, which read
activity.find("pr:CalendarObjectId").text unconditionally. CalendarObjectId
is optional on a P6 Activity; when omitted, the activity inherits the
project's ActivityDefaultCalendarObjectId.
Capture the project default in parse_xml and fall back to it when an
activity has no CalendarObjectId (`calendar_id or self.default_calendar_id`).
Verified on the reporter's attached file (20241021 Cronograma.xml): 3 of 14
activities lack a CalendarObjectId and reproduced the exact crash on
v0.8.0; after the fix parse_xml completes and those activities resolve to
the project default calendar "2" (a valid calendar in the file). An
activity with an explicit CalendarObjectId keeps its own value.
Fixes the P6 re-import crash reported in #5617 (that issue tracks several
Gantt items; this addresses the import AttributeError).
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
get_pset and get_psets assumed RelatingPropertyDefinition is a single property
definition and read definition.Name directly. When it is an
IfcPropertySetDefinitionSet (a defined type wrapping a list of property set
definitions) that attribute access raised AttributeError, so an element whose
psets are grouped in a set returned none of them.
Unpack IfcPropertySetDefinitionSet into its members in both loops and process
each one. Single property definitions and the psets_only and qtos_only filters
are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Several BDD scenarios hardcode absolute representation-item object names
whose trailing number is the IFC STEP line id
(f"Item/{item.is_a()}/{item.id()}"). Those ids drift when file-creation
order changes; a recent shift moved all of them by a uniform -4, so the
scenarios failed with "Item/.../NN does not exist".
The failing step (the_object_name_exists in test_feature.py) dumps the
full bpy.data.objects listing on failure, so the correct current ids are
recoverable directly from the CI log (run 29208793599, tested commit
36e21e882f, an ancestor of HEAD with only a .gitignore commit between).
Renumber to match:
IfcExtrudedAreaSolid/77->73, IfcPolygonalFaceSet/76->72,
IfcVertexPoint/69->65, IfcEdge/72->68, IfcFace/74->70.
Verified against the CI failure dump (a local build produces different
ids, so this is validated by CI's own object listing rather than a local
run). boolean.feature also hardcodes IfcHalfSpaceSolid/90 and panel text
[91] downstream of the failing assertion, which CI never reached and so
never dumped; left as-is to avoid guessing - they will print a fresh dump
next run for a follow-up if still stale.
This change was made with the assistance of an AI tool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two host-environment bugs in the build-env scripts that break on
macOS/Apple Silicon hosts, independent of target architecture:
- Dockerfile: groupadd fails outright when USER_GID collides with an
existing system group in the rockylinux9 base image (e.g. macOS
default user GID 20 "staff" collides with RHEL's GID 20 "games").
Guard with getent so useradd attaches to the existing group instead.
- ifcos_env: `sed -si` is GNU-only syntax and errors under BSD/macOS
sed. Do the UNIQUE_ID substitution via a portable temp-file + mv.
Per sboddy's review on the original PR: dropped the linux/amd64
platform-pin additions from this change. The stack already targets
Rocky9/x64 build outputs by design, and Docker Desktop on macOS has
no native container runtime regardless (it's a Linux VM either way),
so forcing the image to run under emulation doesn't produce anything
that's actually loadable into a native macOS Blender/Bonsai install.
That's a separate, harder problem worth solving via a native build
path instead (mirroring build_osx.yml), not by fighting emulation
here. These two fixes stand on their own merits on any host.
This change was made with the assistance of an AI tool.
The ccache named volume had no explicit name, so Docker Compose
namespaced it under the per-checkout project name (derived from
UNIQUE_ID), giving each checkout its own cache even though
docker/README.md already documented them as shared. Give the volume
a fixed name so all checkouts attach the same one.
Measured cache size after a full build (IfcParse+IfcGeom+IfcConvert+
wrapper, one Python version) is ~300MB, only ~5% of the previous 5G
cap. Shrink CCACHE_MAXSIZE to 2G, which comfortably covers the shared
baseline plus per-branch deltas from several diverging checkouts.
Generated with the assistance of an AI coding tool.
Three host-portability fixes to the docker/ toolchain from #8564 so it
runs on macOS as well as Linux. All three are no-ops on native amd64
Linux.
1. Dockerfile: only groupadd when the target GID is free. macOS's default
primary group `staff` is GID 20, which already exists as `games` in
rockylinux:9, so `groupadd -g 20` aborted the image build. Guard with
`getent group "${USER_GID}" || groupadd ...`; useradd -g accepts the
existing GID.
2. ifcos_env unique(): replace GNU-only `sed -si` (BSD/macOS sed errors
"illegal option -- s") with a portable `sed > tmp && mv` rewrite of the
UNIQUE_ID line. Verified against macOS BSD sed.
3. create() + compose.yaml: build with an explicit `--platform linux/amd64`
so the locally built image's platform matches the `platform:
linux/amd64` pin in compose.yaml. Without it, on arm64 the local image
is tagged linux/arm64, compose treats the platform-mismatched image as
absent and tries to pull `ifcopenshell-build-env:updated` from Docker
Hub (which does not exist -> access denied). Also add `pull_policy:
never` as a safety net so a future mismatch surfaces as a clear "image
not found" rather than a registry auth error.
Note: on Apple Silicon the amd64 build runs under emulation and a cold
full build is slow; ccache makes incremental rebuilds tolerable. A native
Linux/Intel host or CI remains the better choice for routine use, but these
fixes turn "hard broken" into "works with a caveat" on macOS.
This change was made with the assistance of an AI tool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reported: Blender segfaults when clicking Cancel on the "newer
autosave found" recovery popup shown by LoadProject at startup.
Root cause: LoadProject.execute()/invoke() triggered the recovery
popup via bpy.ops.bim.load_autosaved_recovery_popup("INVOKE_DEFAULT",
...) and returned that call's result ({'RUNNING_MODAL'}) as their own
return value, without LoadProject itself ever calling
modal_handler_add(). Blender's window manager takes a RUNNING_MODAL
return as a promise the operator registered its own modal handler;
since it hadn't, the WM's operator bookkeeping was left corrupted -
silently, since this is heap/state corruption rather than an
immediate crash. It only surfaced later, when the real modal operator
(the popup) closed and the WM reconciled its modal stack, which lines
up with the crash occurring specifically on dialog close regardless
of which button was pressed. check_autosave_recovery() now returns a
plain bool and fires the popup fire-and-forget; LoadProject reports
its own honest {"FINISHED"}.
Also hardened, as defense in depth: LoadAutosavedRecoveryPopup's
execute()/cancel() call back into bim.load_project(...), which (with
should_start_fresh_session) calls wm.read_homefile() and tears down
the window manager/screens. Doing that synchronously from inside this
popup's own execute()/cancel() - itself invoked from deep inside
Blender's modal handling for the popup's button click - risks the
same class of use-after-free as the timer bug fixed in the previous
commit. The reload is now deferred by one timer tick so it runs after
the popup's modal handling has fully unwound, and the deferred
callback closes over plain values rather than `self`, since the
operator instance may not survive past cancel()/execute() returning.
This defer-only change was tried and tested first, on the (incorrect)
assumption it was the root cause: it produced a byte-for-byte
identical crash backtrace on retest, which is what pointed at the
RUNNING_MODAL bug above as the actual cause - the defer change alone
was insufficient because the corruption happens when the popup is
first shown, not when it's closed.
Generated with the assistance of an AI coding tool.
The periodic autosave timer called reset_timer() at the end of its
own callback, which unregistered the timer that was still executing
(itself). Blender frees the timer's internal registry entry on that
manual unregister, then frees it again when the callback returns
None - a double free that corrupts the heap and can crash Blender
later, once the corrupted memory is reused.
Reschedule by returning the next interval from the callback instead,
which is the safe, documented way to repeat a bpy.app.timers
callback. External reset_timer() calls (from SaveProject,
LoadProject, AutosavePrompt) are unaffected since they run from a
separate call stack (UI events), not from inside the timer.
Found while investigating a segfault reported when cancelling the
autosave recovery popup; not itself the cause of that crash (see the
following commit), but the same reentrant-unregister pattern and a
real, independent latent bug in the periodic reminder path.
Generated with the assistance of an AI coding tool.
Dockerfile (renamed from Dockerfile_init, Dockerfile_update removed):
- Run as a non-root `builder` user matching the host UID/GID (passed as
--build-arg by create() from id -u/id -g), so build output under the
bind mount stays owned by the host user instead of root.
- Fix CCACHE_MAXSIZE: `ccache -M 5G` wrote its limit to a config file
under /ccache at image-build time, but /ccache is a volume mount
point, so that file gets shadowed by the (empty) volume the moment
the container actually runs - the cap never took effect. Set
CCACHE_MAXSIZE=5G as an image ENV instead.
- Dedupe ccache/libffi-devel, add --setopt=install_weak_deps=False
--setopt=tsflags=nodocs, add `git lfs install --system`, combine the
dnf update+install into one layer.
- Drop Dockerfile_update: it built FROM its own previous output, so
every `update` call made the image strictly larger forever (Docker
layers are append-only, `dnf clean` in a later layer can't shrink an
earlier one). `update` now just calls create(), which already runs
`dnf update -y` FROM a clean rockylinux:9 every time.
compose.yaml: pin platform: linux/amd64 so this doesn't silently run
under emulation on an ARM host.
ifcos_env:
- Split the previously-conflated stop/down into six distinct,
Compose-native lifecycle commands: up (create-or-start), down
(remove), stop, start, restart (stop+start, same container),
recreate (down+up, fresh container). Previously `stop` was aliased
to `down`, which silently removed the container instead of pausing
it.
- Implement try(): copies the built wrapper into a real Blender/Bonsai
install for manual testing, reading the target from a new
BLENDER_USER_RESOURCE .env variable and auto-detecting the built
Python version (disambiguating via PY_TGT for multi-version builds).
Deliberately kept human-only - it mutates a live Blender install, so
it shouldn't run unattended as part of an automated/AI workflow,
which should instead copy the wrapper into the repo's own
src/ifcopenshell-python/ifcopenshell/ (documented in SKILL.md).
- Fix unique(): the "has .env already got a UNIQUE_ID line" check
referenced an unset $FILE instead of $ENV_FILE, so it always
evaluated true and appended a fresh "UNIQUE_ID=dummy" line to .env
on every single `up`.
- Minor: differentiate remove()'s log message from down()'s (no longer
identical now that they're distinct operations), tidy help text
alignment and a stray double-space typo in clean().
SKILL.md: rewritten as current-state documentation (no more "fixed in
this copy" changelog framing) covering the above, plus a migration
note for anyone hitting root-owned leftovers from an older image.
Verified by actually building the image and driving every new
lifecycle command (stop/start/restart keep the same container ID;
down+up and recreate produce a new one) and try() (including the
quoted-tilde BLENDER_USER_RESOURCE edge case) against the real container.
Generated with the assistance of an AI coding tool.
First functional version, but it needs some improvements and fixes
identified as I've used it personally on one thing, and when an AI
(Claude) used it to work through the CI test errors.
I had the AI make a SKILL.md file. If the AI indicates it needs to
build the ifcopenshell binary, use this and let it rip.
The project-unit to Blender-unit mapping in format_distance only knew
FOOT/INCH/METRE/DECIMETRE/CENTIMETRE/MILLIMETRE, so creating a project
with Kilometers or Miles in the New Project Wizard crashed with
KeyError: 'KILOMETRE' (or 'MILE') as soon as the spatial tree formatted
an elevation. Add the missing Blender-supported units (kilometre, mile,
micrometre) and fall through gracefully for anything else (for example
HECTOMETRE) so unknown units use the adaptive formatting branch instead
of raising.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expose the Euler rotation of an element's placement in degrees through
get_element_value, alongside the existing x/y/z and easting/northing/
elevation keys. This makes element rotation exportable through ifccsv,
e.g. for placing oriented symbols in GIS.
Adopts the approach agreed in the review of the stale PR #6272 by
@TZwielehner: reuse util.shape_builder.np_matrix_to_euler and do the
degree conversion inside get_element_value.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Project loading set scene length_unit to f"{Prefix}METERS", but Blender's
enum only defines KILOMETERS, CENTIMETERS, MILLIMETERS and MICROMETERS.
A model with a DECIMETRE (or HECTO/DECA/etc.) length unit therefore raised
on the enum assignment and the file failed to open. Guard with the set of
supported values and fall back to ADAPTIVE display for the rest.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The quickstart ended with three empty sections whose bodies were only
"TODO" (placing occurrences, changing locations, modeling a building),
which read as a dead end on docs.bonsaibim.org. The page now ends on the
completed save-and-view flow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The guard that avoids re-assigning the same object to the same resource
tested is_a("IfclRelAssignsToResource") (stray "l"), so it never matched.
A repeat assignment therefore fell through and appended the related object
to RelatedObjects a second time. Corrected to "IfcRelAssignsToResource".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
IfcDiff defaulted to relationships=["geometry"], so a plain diff only ever
compared geometry. Attribute-only edits on an element that kept its GlobalId
(a modified or removed PredefinedType, a renamed element, etc.) were silently
missed. The CLI made this worse: --relationships did not list "attributes" or
"geometry" as valid values, so there was no documented way to enable it.
The default is now ["attributes", "geometry"], so a plain `ifcdiff old new`
reports attribute changes alongside geometry changes. The CLI help and the
IfcDiff docstring now document all valid relationship values.
Added a regression test covering a PredefinedType change detected with the
default configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FormatTransformer.round() called Decimal() directly on the input value,
which raises decimal.InvalidOperation when the value is a non-numeric
string (a text property, or a value carrying a unit suffix like "12.5 m").
In a spreadsheet export this crashed the entire operation as soon as one
element carried such a value.
Now round() catches InvalidOperation and returns the value unchanged, the
same graceful-fallback convention used by add(). Numeric rounding is
unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In the cobie24 Coordinate sheet, Floor rows use get_local_placement, whose values
are in the project length unit, but Space rows come from ifcopenshell.geom
create_shape, whose vertices are in SI metres, and the space branch never scaled
them back. So on a non metre model (for example millimetres) the Coordinate sheet
mixed units a thousandfold apart and disagreed with the Facility sheet's declared
LinearUnits.
Scale the space bounding box by the project unit scale so the whole Coordinate
sheet is consistent. A metre model is unchanged since the scale is 1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The filter_elements selector grammar had no way to comment out part of a
query, so users had to delete and retype text to temporarily toggle a
facet. Add a /* ... */ block comment terminal that is ignored by the
lexer, and tolerate a trailing "+" so that commenting out the final
operand (e.g. "IfcWall + /* IfcSlab */") parses cleanly. Comments may
span multiple lines; a /* sequence inside a quoted string is not treated
as a comment. Only the filter grammar is affected, not get_element or
format which use "/" for regex and division.
Adds a regression test and documents the syntax.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The location and parent filters both match at any depth in the spatial
hierarchy, which surprises users who want only the elements immediately
under a given container. Document that the parent query key resolves the
direct parent only (e.g. query:"parent.Name"="My Site"), add a matching
filter example, and note the immediacy on the parent value key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SHIFT+CTRL+CLICK on Activate Drawing now imports the
annotations of all selected drawings without switching
the active view or camera, then selects their cameras with
the first as active. SHIFT+CTRL+ALT+CLICK also selects the
loaded annotation objects. The drawing camera is imported
when missing so annotations land in the correct collection.
Loading is idempotent.
Generated with the assistance of an AI coding tool.
Converting to a path whose directory does not exist (or is not writable)
failed silently: the serializer's ready() check correctly returned false,
but IfcConvert deleted the temp file and returned EXIT_FAILURE without any
message, so the user saw no reason for the failure.
Log a SYS error naming the output file before returning, matching the
existing "Unable to open output file" reporting used elsewhere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Property sets contained in an IfcPropertySetDefinitionSet were exported as
an empty element in XML. The XmlSerializer already had a block to expand
such a set into its member property sets, but it was gated behind
#ifdef SCHEMAS_HAS_IfcPropertySetDefinitionSet while the schema generator
emits SCHEMA_HAS_IfcPropertySetDefinitionSet (singular). The plural spelling
is defined nowhere, so the block was dead code and a RelatingPropertyDefinition
holding a set produced nothing.
Correct the macro name so the set is expanded and its property sets are
serialized. The parse layer already reads these nested sets (they are
reachable from util.element), so this only completes the XML path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
escape_xml escaped the five XML metacharacters but passed control
characters (0x00 to 0x1F other than tab, newline and carriage return)
through unchanged. Those bytes are illegal in XML 1.0 and cannot be
represented even as numeric character references, so any IFC string
containing them produced non-well-formed XML and SVG output.
Strip those illegal control characters before escaping. Bytes belonging to
a valid UTF-8 multibyte sequence are always >= 0x80, so filtering on the low
control range leaves real text intact. This is the shared helper used by the
SVG serializer text and attribute sites (audited: all route through it) and
by the XML/Collada paths, so both reports are resolved at one place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
IfcTriangulatedFaceSet and IfcPolygonalFaceSet used CoordIndex values to
index Coordinates.CoordList directly, ignoring the optional PnIndex
attribute. When PnIndex is present it remaps point references, so a
CoordIndex value i must resolve as CoordList[PnIndex[i-1]-1] (both 1-based).
Without the indirection any model carrying a PnIndex was built from the wrong
points.
Add a resolve() helper in both mappings that applies the PnIndex indirection
when present and is a plain bounds-checked lookup otherwise, with bounds
checks at both index levels. When PnIndex is absent the behavior is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Boost.System has been header-only since Boost 1.69 and its compiled stub
library was removed in newer Boost, so listing system in the requested
find_package components makes configuration fail on Boost 1.70 and up (for
example Boost 1.90 errors with "Could not find boost_system"). Boost.System
is still pulled in transitively by thread / iostreams where it is needed, so
drop it from the explicit component list.
Verified: with this change IfcOpenShell configures and builds IfcConvert
cleanly against Homebrew Boost 1.90 and OpenCASCADE 7.9.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address maintainer request on #8368: instead of a deflection floor on top
of a fixed CircleSegments count, use one mode or the other. When
CircleSegments == 0 (the new default) the CGAL kernel derives the conic
segment count from MesherLinearDeflection, matching the deflection based
meshing OpenCascade already does and fixing #8051. When CircleSegments is
non zero it is used directly as a fixed, radius independent count.
CircleSegments is only read by the CGAL kernel; OpenCascade meshes by
deflection and never reads it, so the new default has no effect there.
Update the setting description and the ifcconvert / geometry-settings docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CGAL kernels (cgal and cgal-simple) allocate arc segments as a
fraction of the full circle via CircleSegments, ignoring the radius.
A large-radius arc that spans a small angle therefore collapsed to a
single chord, turning curved curtain-wall mullions straight while the
OpenCascade kernel (which meshes by deflection) kept them curved.
evaluate_conic now also enforces a deflection-based floor on the number
of segments, keeping the chord deviation within mesher-linear-deflection,
matching OpenCascade. Small circles are unchanged (CircleSegments floor
still dominates); only large-radius curves get denser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
IfcConvert returned a success exit code even when geometry conversion logged
errors and silently dropped elements (for example a failed TopoDS::Shell build
under layerset slicing produced valid looking output with most objects
missing), so CI and scripts could not detect a partial conversion.
Add an opt-in --fail-on-error flag that makes IfcConvert exit non-zero when any
error was logged during processing, reusing the existing MaxSeverity based
failure check already used for --validate. The default exit behaviour is
unchanged, so pipelines that tolerate individual element failures are
unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A face whose inner boundary crosses the outer boundary (or another inner
boundary) is invalid per the schema. Open Cascade silently heals or drops
such a face, so the intended hole is lost or the face is corrupted with no
diagnostic at all (the 2018 report saw a dropped face; on the current line
the face survives as wrong geometry, still silently).
After the wires are collected, if a face has inner boundaries, measure the
BRepExtrema distance between each inner wire and every earlier wire. Two
non intersecting loops have strictly positive distance, so a distance at
or below the modelling precision means the boundaries touch or cross; emit
a warning (GEO 402) naming the offending face. This is diagnostic only, no
geometry change.
The message is emitted via the kernel logger() rather than Logger::Root():
IfcConvert configures a local Logger and worker logs merge into it, while
Logger::Root() is a separate unconfigured singleton whose messages are
discarded (a latent issue affecting some existing GEO messages too).
Verified on OCC 7.9.2 with synthesized IFC4 faces: an inner triangle
crossing the outer edge, and one straddling the bottom edge, each emit one
GEO 402; a valid 4x4 hole emits none and triangulates identically (area
84.0), in both sequential and multithreaded runs. Pure inner self
intersection and full containment are distinct classes and intentionally
left untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comply with AGENTS.md: new AI-generated files must carry a top-of-file
comment indicating AI assistance.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In IFC2X3 IfcAsymmetricIShapeProfileDef is a subtype of
IfcIShapeProfileDef, so the IfcIShapeProfileDef mapping dispatched it by
inheritance. From IFC4 onwards it is a standalone subtype of
IfcParameterizedProfileDef, so nothing mapped it and the extruded solid
came out empty (GEO326, 0 verts).
Add a dedicated map_impl that builds the twelve-point asymmetric section
(independent bottom/top flange widths, thicknesses, fillet/edge radii and
flange slopes), plus a guarded BIND. Both are wrapped in
SCHEMA_IfcAsymmetricIShapeProfileDef_HAS_BottomFlangeWidth, which is only
defined where the type is standalone, so IFC2X3 keeps its existing
subtype route unchanged.
Verified on OCC 7.9.2: an IFC4 asymmetric extrusion goes from 0 verts to
a correct 72-vert solid (bottom flange wider than top); IFC2X3 output is
unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This allows a file that will be automatically picked up by Claude. It can either
be a copy of a CLAUDE.md, or a one line file pointing to a shared common file. i.e.
@~/.claude/conventions-ifcopenshell.md
Feature #5753 - Autosave for ifc files
Merging because it could be a life saver. It is hidden behind an option and is off by default.
- Provides the option have an autosave file created periodically (duration in prefs).
- Can be set to save immediately or a dialog prompt to save, but can be dismissed.
- Removes the autosave when Blender quits cleanly.
- If the autosave file exists at startup, it will prompt which file to load.
_Every_ AI had a hand in this, but I have reviewed, understood and tested it. AI Credits go to:
Cursor, Grok, Copilot, and Claude.
- autosave.py: black formatting (blank line) and ruff's
collections.abc.Callable import fix.
- project/__init__.py, tool/__init__.py: ruff import-sort fixes. The
autosave import in tool/__init__.py is deliberately kept last (must
come after tool.drawing, per its existing comment) via `# isort: skip`
rather than letting ruff move it, which would reintroduce that bug.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The recovery popup used invoke_popup, which is dismissed the instant
the mouse leaves its bounds - closing the prompt without loading
either file, and with no visible feedback that anything happened.
Switches to invoke_props_dialog, which blocks the rest of the UI and
is only dismissed by an explicit action. Since Blender always renders
both a fixed "Cancel" button and one labelled by confirm_text on that
dialog type, the prompt is reframed as a direct Yes/Cancel question
("Do you want to load the autosaved version instead?") instead of
adding separate Load Original/Load Autosave buttons on top of those.
Folds the load logic directly into the popup's execute()/cancel(), so
the now-redundant LoadAutosavedRecovery operator is removed.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously the autosaved copy was only ever overwritten, never removed,
so a deliberate quit (whether the user saved or chose "don't save")
still nagged with a recovery prompt on next startup.
Registers an atexit cleanup that removes the active IFC's autosave
file(s) on a graceful interpreter shutdown. atexit never runs on an
actual crash, so a genuine crash still leaves the recovery file in
place as before.
The cleanup reads a cached plain-string path kept up to date by
reset_timer(), rather than looking it up live via bpy.context - by
the time atexit fires, Blender's C++ side is torn down far enough
that even a read-only bpy.context.scene access aborts the process
(std::bad_optional_access) instead of raising a catchable exception.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implemented as described in #5753, with two options:
- A nag dialog with save or cancel options.
- An autosaved file.
Settings are in preference to activate the feature (default: off), the period before prompting/saving,
and choosing between the two methods.
Prevent the autosave file being added to the recent files list when the user opens the original, but selects to open the autosaved version.
black/ruff
This commit was created using AI assistance. Cursor for the initial code, then Grok and I fixing all the errors
that Cursor made. Finally Copilot did a code review.
I have reviewed and tested the code, and I understand it, and it works and does not introduce any obvious bugs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Grok
Co-authored-by: Cursor
- add_stationing_referent.py: black reformat (new drift from v0.8.0).
- update_fallback_position.py: v0.8.0's changes to this file made the
ifcopenshell.util.unit import (added in an earlier commit here) unused;
removed per ruff.
poe ty's sequence only reaches ty-ios once ty-bonsai passes, so these
never surfaced until now:
- util/alignment.py: drop the stale `include_referent=False` kwarg from
add_zero_length_segment() - that parameter was removed from the function's
signature in 45ea5eb07 but this caller in a different file was missed,
leaving a latent TypeError if this code path is ever exercised.
- ifcopenshell_wrapper.pyi: add the optional trailing `logger` parameter to
parse_ifcxml/open/construct_iterator*, matching the real SWIG signatures
in src/ifcwrap/*.i (all declare `Logger& logger = Logger::Root()`) that
the hand-maintained stub never picked up.
- ifcopenshell/__init__.py: remove a stale `ty: ignore[unknown-argument]`
comment that ty confirms is no longer suppressing anything.
- assign_cost_item_quantity.py: OPERATORS mixes 2-arg binary operators with
the 1-arg `operator.neg` (for ast.USub), but FormulaEvaluator has no
visit_UnaryOp so USub can never reach this lookup via visit_BinOp.
Suppressed at the call site rather than touching the dict, since this
looks like scaffolding for unary-minus support rather than dead code.
- Explicit submodule imports (ifcopenshell.geom / api.alignment / util.unit
/ api.aggregate / api.context / api.spatial) added where accessed but
only reachable by accident of import order.
- tool.py: drop the `-> int` annotation on the Parametric interface's
get_geom_generation stub; its `pass` body implicitly returns None, which
ty can't reconcile with the runtime @interface/@abstractmethod rewriting
it never sees statically. Matches the file's other stubs (-> None).
- railing.py: qualify the "BIMRailingProperties" string annotations as
"prop.BIMRailingProperties" on the two functions using it, since the bare
name was never imported into this module's namespace.
- product.py: suppress ty's missing-argument errors on
copy_z_rotation_to_selected's Surveyor.get_z_rotation/set_z_rotation
calls with targeted ty: ignore comments. The function is unused and its
two dependencies were never implemented on the concrete Surveyor tool;
left as-is rather than deleted or implemented.
- gizmos.py: TYPE_CHECKING-guard `import bmesh` for the string-literal
annotation in build_schematic_mesh; suppress the still-unresolved
gizmo_textures import in TexturedQuadGizmoMixin (WIP dependency, not dead
code).
- model/__init__.py: register the `decorator` submodule, which unregister()
already calls (would have raised NameError on addon disable).
- mep.py / tool/model.py: add explicit imports for bonsai.core.geometry and
bonsai.core.model, previously only reachable by accident of import order.
- Test files: add explicit ifcopenshell.api.pset / ifcopenshell.util.element
submodule imports used but not imported.
Convert bare Mock() to Mock(spec=bpy.types.Object) for Blender-object
stand-ins in TestRecalculateWallsWithNewConnections, TestMEPActionGuards,
and TestRecreateAggregateIteratesAllNew so typos on the Blender API
fail loudly instead of silently returning a MagicMock.
Replace the ad-hoc Mock() ConnectionRecord stand-in in
TestRecreateConnectionsZipsPairs with a real ConnectionRecord instance,
which pins field names at construction and catches drift if the
dataclass fields ever get renamed.
IFC entity mocks remain bare Mock() intentionally: entity_instance
attributes are schema-driven at runtime rather than defined statically
on the class, so spec= would refuse the .GlobalId / .HasFillings /
.ConnectedTo attribute writes the tests need.
Relates to #8088.
Generated with the assistance of an AI coding tool.
Rewrite tool.Array.select_only_parent as a thin call to
tool.Blender.select_and_activate_single_object; drop the ad-hoc
per-child deselect loop and the unused parent_element parameter.
Replace the tail parent_obj.select_set(True) in _regenerate_array_body
with tool.Blender.set_object_selection, which wraps select_set in the
hidden-object try/except the utility already owns.
Relates to #8088.
Generated with the assistance of an AI coding tool.
Add tool.Array.is_array_child helper. Port decorator and MEP
action gizmos (lock, pen, join) hide on array children — writes
on children get wiped by the next regen, and the port topology
is inherited from the parent.
Introduce tool.Array.select_only_parent and wire it into both
bim.regenerate_array and bim.finish_editing_array so post-regen
state converges on parent-only-selected + active. Grow and shrink
paths otherwise diverge (grow left new children selected alongside
the parent; shrink left only the parent).
Relates to #8088.
Generated with the assistance of an AI coding tool.
Sweep [0]-indexing in recreate_aggregate, recreate_connections,
and recreate_port_connections so batched N-child duplicates
recreate relationships on every new child, not just the first.
Single-source callers unaffected (loop collapses to one iteration
on 1-element lists).
Relates to #8088.
Generated with the assistance of an AI coding tool.
Replace N sequential duplicate_ifc_objects([parent]) calls in
_regenerate_array_body with one duplicate_ifc_object_n_times call
per layer, batching the fixed per-call overhead (snapshot gather,
UI refresh, decorator reload).
Guard batch_host_recut drain against dead StructRNA refs and prune
orphan array-child GUIDs at regen so outliner-delete of a
Bonsai-managed child cannot crash subsequent regenerate_array.
Recalculate walls after recreate_connections so Shift+D of
connected walls produces correct junction geometry without a
manual regen step.
Relates to #8088.
Generated with the assistance of an AI coding tool.
IfcSurfaceFeature (e.g. road markings) adheres to a host element through
IfcRelAdheresToElement, a [1:1] cardinality hierarchical relationship in the
same family as aggregation, containment and nesting since IFC4.3. The spatial
traversal never followed it, so surface features had no resolvable parent or
container: on import they landed in the Unsorted collection instead of the
host's spatial collection, and were dropped entirely in DECOMPOSITION filter
mode.
Add get_adhered_element (feature to host) to the get_parent resolver chain and
walk HasSurfaceFeatures in get_decomposition, plus a get_surface_features helper
mirroring get_parts/get_contained. With get_parent resolving adherence,
get_container now returns the host's spatial container, so tool.Collector places
surface features under the host. Also follow HasSurfaceFeatures in the Bonsai
DECOMPOSITION filter path so they load in that mode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
file.write() streamed directly onto the target path, so a crash mid-write
left a truncated file with dangling STEP references. Serialize to a temp
file in the same directory, then atomically rename it onto the target.
- New IfcUtil::path::atomic_rename_file: std::rename on POSIX, MoveFileExW
with MOVEFILE_REPLACE_EXISTING on Windows. Unlike rename_file it never
unlinks the destination first, so there is no window where it goes missing.
- Fully in C++/swig (per aothms), so the FILE_NAME header is untouched: it
comes from the model header, not the output path (verified empirically).
- Temp lives next to the target so the rename stays on one filesystem.
- Stream is closed before the rename (Windows cannot move an open file).
- On any write error the temp is removed and the original target is intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
IfcConvert --version reported 0.8.0 on a plain source build even though the
VERSION file says 0.8.6 (#8164). buildinfo.cpp already falls back to the
IFCOPENSHELL_VERSION_STRING macro and CMake already passes it as
${RELEASE_VERSION}, but RELEASE_VERSION was only read from the VERSION file
when VERSION_OVERRIDE was on. A default build (VERSION_OVERRIDE off,
ADD_COMMIT_SHA off, as the nixpkgs package builds it) fell through to the
hardcoded "0.8.0", so the fallback macro carried the stale value.
Read the VERSION file unconditionally so RELEASE_VERSION is always the real
version. VERSION_OVERRIDE still governs the branch name embedded when
ADD_COMMIT_SHA is on, and project()/CPack now also reflect the true version.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
roughnessFactor was computed as 1/specularity. An IfcSpecularExponent of
0 produced infinity, which nlohmann::json serialises as null and makes
the glTF invalid; exponents below 1 produced values above 1, which glTF
also forbids. Map exponents <= 1 to full roughness and keep 1/exponent
above that, so the factor always lands in [0, 1].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The georeferencing fix (e6dc582) iterates IfcProject.RepresentationContexts
unconditionally, but the attribute is OPTIONAL and None on projects without
contexts, crashing every extraction on such files with
TypeError: 'NoneType' object is not iterable.
Also extend the #8199 regression test to assert element placements are
copied verbatim, so extraction can never bake map coordinates into local
placements.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IfcGeometricRepresentationContext.Precision is typed as a plain IfcReal
but is interpreted in the project length unit, so the IfcLengthMeasure
traversal in convert_file_length_units never touched it. A model
converted from mm to m kept a Precision of e.g. 0.01 (fine in mm, huge
in m), which breaks downstream geometry interpretation such as
IfcConvert boolean cleanup.
Subcontexts derive Precision from their parent, so only root contexts
are scaled.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get_info_2 advertises the same signature as get_info but raised a bare
AssertionError for anything the C++ fast path does not implement --
including its own default arguments (recursive=False).
Use the fast path when recursive=True, return_type=dict and ignore=()
hold, and delegate to the pure Python get_info otherwise. As noted in
the issue, without recursion there is no meaningful performance gain to
lose by delegating.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
std::to_chars is locale-independent, so the ostringstream and imbue(locale)
are no longer needed. Build the REAL string with plain std::string operations.
Output is unchanged (verified in standalone compile: same shortest values, all
round-trip). Addresses review feedback on #8309.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
format_double formatted doubles with setprecision(max_digits10) (17 digits),
which padded clean values with noise: 0.0174532925199433 was rewritten as
0.017453292519943299 and 1.E-05 as 1.0000000000000001E-05. Every REAL in a file
changed on save, producing enormous diffs for anyone version-controlling IFC.
Use std::to_chars, which emits the shortest string that round-trips exactly
(like Python's repr), then keep the existing mantissa/exponent formatting.
Verified in a standalone compile of the exact function logic: the reporter's
values become 0.0174532925199433 and 1.E-05, 0.1 stays 0.1, and every tested
value (including a denormal) round-trips back to the identical double.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
check_aggregate_of_type used an exact type comparison (element->ob_type ==
type_obj), so a numpy array was rejected because its elements are numpy scalars
(numpy.float64) rather than direct float instances. For the numeric types,
accept subclasses: PyFloat_Check for double (numpy.float64 subclasses float) and
PyLong_Check (excluding bool) for int. The SPF REAL vs INTEGER distinction is
kept, so a float is not accepted where an int is expected and vice versa.
This replaces the earlier Python-side walk() approach, which the maintainer
preferred not to take since walk() is removed in v0.9. Verified with a runtime
red-green (built as a shared lib, called via ctypes): the old check rejects
np.array([3.0, 4.0]) and the new one accepts it, plain lists still work, an int
list is still rejected where a REAL is expected, and bool is rejected for INTEGER.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
asStringRef removes the first and last characters of a string, enumeration
or binary token to drop the delimiters, guarded only by !str.empty(). A
malformed single-character token (e.g. a bare '.' left when a fuzzer turns
'.PHYSICAL.' into '.)HYSICAL.') has length 1, so the first erase empties the
string and the second erase(str.begin()) runs on an empty string. That is
undefined behaviour: benign on a normal build, but it aborts (or throws
std::length_error from a later append) under a hardened libstdc++ with
_GLIBCXX_ASSERTIONS, which is why this file only segfaulted on the Fedora
build. Require at least two characters before stripping.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When ADD_COMMIT_SHA is off (the default for release tarballs), buildinfo.cpp
fell back to a hardcoded "0.8.0", so a 0.8.5/0.8.6 build reported 0.8.0 from
IfcConvert --version and in written file headers. Pass CMake's RELEASE_VERSION
(read from the VERSION file) to IfcParse as IFCOPENSHELL_VERSION_STRING and use
it as the fallback, mirroring how the branch/commit defines are handled. The
commit-sha build and the last-resort literal are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
generate_linework() loops over the main file plus any linked models
(added in 5db955d4), reassigning drawing_elements each iteration. The
SHAPELY fill pass builds elements_with_faces/raycast_objs from
drawing_elements, but did so once *after* that loop finished, so it
only ever saw whichever file was processed last -- silently dropping
surface fills for every other file, including the main model whenever
any link was loaded.
Accumulate elements_with_faces/raycast_objs across every file inside
the loop instead of capturing drawing_elements once after it ends.
Generated with the assistance of an AI coding tool.
The underlying bug (has_material_styles bypassing the tool layer) was
already fixed by e76455913, which added the required mock expectation
here, but left the test under its quarantine placeholder name
test_AAAAAAAAAAAA. Restore the real name now that it genuinely passes.
test_memusage_partial_open was silently skipped in CI (psutil was
never installed there). Add psutil so it actually runs, and run the
RSS measurement in a subprocess so the fixture file isn't already in
the page cache from earlier tests, which was making both deltas read
as zero.
Generated with the assistance of an AI coding tool.
mathutils only ships pre-built wheels for Python 3.13+ (verified
against PyPI's file list); on CI's Python 3.11, `pip install
mathutils` falls back to a slow/unreliable source build. Skip the
mathutils-dependent tests when the interpreter is too old instead.
These packages already have their own pytest suites (ifc5d, ifcedit,
ifcmcp, ifcquery) but were never run in CI, so regressions in them
went unnoticed. Add path triggers and test steps for all four, plus
odfpy and xlsxwriter which ifc5d's spreadsheet export tests need and
mcp which ifcmcp's server tests need.
Generated with the assistance of an AI coding tool.
ItemIsASum and Quantities are exporter columns that were missing from
MAIN_CSV_HEADER_COLUMNS, causing them to be misidentified as numeric cost
value categories on re-import. Also initialise rate_cost_schedule to None
before the search loop to avoid UnboundLocalError when no match is found.
Generated with the assistance of an AI coding tool.
is_drawing_active() required an open VIEW_3D area purely as a poll()
gate for bim.create_drawing, even though SVG generation is
ifcopenshell.geom-based with no viewport dependency; skip that check
when bpy.app.background is true, since a viewport is neither
obtainable nor meaningful there. Interactive behaviour is unchanged.
Generated with the assistance of an AI coding tool.
Linked IFC files were included in SVG output but without their
world transform, causing geometry to appear at wrong coordinates.
Falls back to no transform if the link cache is unavailable.
Generated with the assistance of an AI coding tool.
Link empty handles were missing from visible_objects, so linked
models were always hidden when activating a drawing.
Generated with the assistance of an AI coding tool.
bpy.ops.bim.reload_link(link_index=...) from a script skips invoke(),
so self.query stayed at its empty default and execute() overwrote the
link's stored query, reloading everything. Only update link.query when
the property was explicitly set (dialog or script argument), and reload
using the stored query.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Store the selector query used at Link IFC time on the Link
PropertyGroup, restore it from the sidecar cache JSON on host
IFC reopen, and forward it through LoadLink and ReloadLink so
subsequent reloads replay the original filter instead of loading
every element. ReloadLink now opens a small dialog pre-populated
with the current query, allowing the user to edit it in place
without unlink-and-relink.
Also swap TestCalculateLinkMatrix off NamedTemporaryFile(delete=True)
which held an exclusive Windows handle and blocked the
code-under-test from reopening the sidecar path.
Closes#8219
Generated with the assistance of an AI coding tool.
Fold two related cleanups from post-PR review into one commit:
Shared filling predicate — the gizmo poll and AddOpening._add_openings
both need to decide whether an IFC entity is a Bonsai-supported filling
(IfcDoor / IfcWindow, the classes the opening generator can derive
geometry from). Centralise the check in bim.module.model.opening as
is_filling_supported so a schema-broadening tomorrow only edits one
predicate. The gizmo's own predicate is renamed
is_supported_filling_or_opening to reflect its wider domain (also
accepts None for raw meshes and IfcOpeningElement for reassignment).
Aggregate-host guard — regenerate_filling_opening_body returns the
voided host Blender object so callers can recut it. Aggregates have
no mesh data; returning them made callers hit switch_representation
against a None data-block. Guard on voided_obj.data is None and
return None so callers can skip cleanly.
Adds a direct position_gizmos test asserting host-at-index-1 (filling
active) still anchors on the slab — pins the class-based dispatch's
selection-order independence.
Generated with the assistance of an AI coding tool.
Both files interleaved bpy.types imports with ifcopenshell.util
imports, which ruff's I001 rejects for standard-library / third-party
ordering. Running ruff check --fix on the two files reorders them into
the isort-canonical shape with no behaviour change.
Generated with the assistance of an AI coding tool.
EditOpenings.edit_openings unconditionally walked sibling wall sets
twice on every processed opening — once by mapped source id via
get_similar_openings_building_objs, once by filling type via
get_all_building_objects_of_similar_openings — and unioned both into
the building_objs recut set. reload_body_representation then hit every
one of those walls with a switch_representation call, even for the
show/hide toggle path where nothing about the opening changed.
Move both sibling-wall unions inside the is_edited / is_moved branch.
Pure show/hide (no shape edit, no move) now touches only the wall(s)
directly hosting the toggled openings. The edit and move paths still
refresh siblings the same as before, since a mapped-source rewrite
propagates the new shape to every sharing wall and each one needs a
recut.
Generated with the assistance of an AI coding tool.
recalculate_walls commits the wall's own placement to IFC before
recreating its geometry but did not touch its fillings. A door moved
along the wall's reference line therefore stayed cut at its old
position when the user pressed SHIFT+G on the wall, because the wall
recut ran against the still-stale opening placement in IFC.
Walk each wall's HasOpenings and, for every filling whose Blender
matrix_world differs from its committed IFC placement (tool.Ifc.is_moved),
commit the filling's placement and propagate the new matrix to the
enclosing opening via ifcopenshell.api.geometry.edit_object_placement.
The subsequent recreate_wall pass then sees the fresh opening positions
and cuts at the right spot.
Generated with the assistance of an AI coding tool.
Commit 82dd1d94d switched RecalculateFill from
bonsai.core.geometry.switch_representation to the surgical
tool.Geometry.recut_host to speed up batched host recuts. The trade-off
was intentional for that scope but dropped the implicit opening body
refresh that switch_representation used to provide: SHIFT+G on a door
whose parametric dimensions had drifted from its opening no longer
resized the opening, so the wall recut still hit a stale mapped source.
Extract a targeted single-source helper on tool.Model
(regenerate_filling_opening_body) that regenerates one filling's
mapped opening body via the existing FilledOpeningGenerator and
inverse-substitutes the new representation across every filling that
shares the mapped source. Refactor the family-wide caller
(update_simple_openings, used by the parametric-edit finish path) to
delegate to the same helper, deduped by source id so fragmented type
families where multiple mapped sources coexist all get refreshed.
Call the targeted helper at the top of RecalculateFill._recalculate_fills
for each distinct source among the selected fillings. All body-
representation lookups go through tool.Geometry.get_body_representation
rather than inlining the ("Model", "Body", "MODEL_VIEW") triple. An AST
forward-compat guard pins the call site.
Generated with the assistance of an AI coding tool.
The + gizmo previously appeared whenever a fillable host and any
non-host object were selected, so clicking it against an IfcCovering
crashed the geometry kernel when the opening generator tried to derive
a shape it couldn't build (AttributeError on 'NoneType.wrapped_data').
Tighten the gizmo poll to require the secondary selection to be a
class the operator can dispatch on: IfcDoor, IfcWindow,
IfcOpeningElement, or a non-IFC mesh. Make the poll selection-order-
independent so either click order activates it. Validate the same
class set at the operator boundary so keymap or scripted invocations
report a clear warning instead of crashing.
The narrower Door/Window support in the opening generator is a Bonsai
implementation limit, not an IFC schema restriction —
IfcRelFillsElement.RelatedBuildingElement is typed as IfcElement and
the schema permits any subtype. The tooltip and inline comment on the
validation branch note this so a future reader knows the gate is
future-work, not schema-mandated.
Rewrite the operator's bl_description to end-user-friendly wording that
drops the internal terms matrix_world and rl1/rl2.
Fixes#8215.
Generated with the assistance of an AI coding tool.
Companion to the assign_type.py fix. The same class-pairing validation
added in 10ee5aef4f also gates the Bonsai-side type assignment UI via
tool.Type.is_relating_type_compatible, which the AssignType operator
uses to filter selectable objects. For annotation types (abstract
IfcTypeProduct), get_applicable_types(IfcAnnotation) is empty, so every
annotation was skipped with "No selected object can be typed by
IfcTypeProduct."
Honor the type's ApplicableOccurrence attribute as a fallback, matching
the core API fix. occurrence.is_a() handles subtypes and returns False
for unknown tokens, so free-form text is not trusted blindly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The class-pairing validation added in 10ee5aef4f rejected every typed
annotation with "IfcTypeProduct cannot type IfcAnnotation ... (allowed
occurrence classes: <none>)".
The check derived allowed occurrence classes solely from the
buildingSMART implementer-agreement map, which has no entry for the
abstract IfcTypeProduct that Bonsai uses for annotation types (IFC4 has
no IfcAnnotationType). The intended occurrence class is declared in the
type's ApplicableOccurrence attribute (e.g. "IfcAnnotation/TEXT"), the
schema-defined mechanism for exactly this purpose.
Augment the allow-list with the ApplicableOccurrence class, but only
when its leading token resolves to a real entity in the schema so
free-form text is not trusted blindly. Genuine mismatches (e.g.
IfcWallType -> IfcWindow) are still rejected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SHIFT+CTRL+CLICK on Activate Drawing now imports the
annotations of all selected drawings without switching
the active view or camera. The drawing camera is imported
when missing so annotations are collected into the correct
drawing collection. Loading is idempotent.
Generated with the assistance of an AI coding tool.
The drawing Include filter replaced the camera-view element set
entirely, so elements outside the camera boundary were drawn.
Intersect the filter results with the camera-view set instead.
Generated with the assistance of an AI coding tool.
The georef orientation gizmo (crosshair, project-north arrow,
grid-north arrow, true-north arrow, WCS leader) is a coordinate-
system overlay: its purpose is to communicate orientation regardless
of what the model contains, so it must remain visible regardless of
whether 3D geometry occupies the gizmo's z=0 footprint.
Wrap GeoreferenceDecorator.draw_geometry's draw cycle in a
gpu.state.depth_test_set("ALWAYS") / restore pair so the overlay
draws on top of any 3D geometry between the camera and the gizmo.
Matches the precedent set by the dashed-line overlay in
bim/module/model/opening.py.
Generated with the assistance of an AI coding tool.
Three crashes that surfaced when viewport decorators ran against
selected non-IFC blender objects or top-level objects with no
aggregate parent:
- WallAxisDecorator.draw_wall_axis: tool.Ifc.get_entity(obj) returns
None for a non-IFC selection (default cube, lamp, camera). The
subsequent element.is_a("IfcWall") raised AttributeError on every
redraw. Guard with `element and element.is_a(...)`.
- _ConnectedNetworkPathDecorator flow-segment loop: same shape;
iterates entries that may be None, calls .is_a("IfcFlowSegment")
unconditionally. Same guard.
- AggregateDecorator.draw_aggregate: indexes aggregates_list[-1]
unconditionally in the else branch; raises IndexError when the
selected element has no aggregate parent. Also leaves `aggregate`
unbound across loop iterations in the `in_aggregate_mode` branch
when `index <= 0`. Define `aggregate = None` per loop iteration
and guard the [-1] indexing with `elif aggregates_list:`.
Generated with the assistance of an AI coding tool.
Migrate 17 legacy viewport decorators (ClashDecorator, SolarDecorator,
MeasureDecorator, ItemDecorator, GeoreferenceDecorator, NestDecorator,
NestModeDecorator, GridDecorator, LoadsDecorator, AggregateDecorator,
AggregateModeDecorator, PolylineDecorator, ProductDecorator,
WallAxisDecorator, SlabDirectionDecorator, FaceAreaDecorator,
BoundingBoxDecorator) from hand-rolled install/uninstall lifecycles
onto the canonical tool.Blender.ViewportDecorator base. The legacy
uninstall removed each handler from Blender but never cleared
cls.handlers, growing a stale-reference list across enable/disable
cycles. The base's uninstall clears the list correctly.
State-derived install methods (ItemDecorator, ProductDecorator,
LoadsDecorator, PolylineDecorator) keep an install override per the
base's documented contract.
Drop the now-redundant per-class draw_batch copies and the module-
or method-scope transparent_color defs in favour of the base helpers
introduced in the preceding commit. system/decorator.py and
boundary/decorator.py keep their installed-flag lifecycle (different
pattern, no leak) but consume tool.Blender.transparent_color.
Add an AST forward-compat guard pinning the contract structurally:
any class declaring handlers = [] (Assign or AnnAssign) must subclass
tool.Blender.ViewportDecorator. Add a runtime regression on
ClashDecorator's install/uninstall cycle.
Generated with the assistance of an AI coding tool.
Add two helpers to tool.Blender that 17+ existing viewport decorators
re-implement byte-identically:
- ViewportDecorator.draw_batch(shader_type, content_pos, color, indices=None)
collapses the validate + batch_for_shader + uniform_float + draw cycle
every shader-driven decorator needs.
- Blender.transparent_color(color, alpha=0.1) is the RGBA-alpha-override
helper duplicated across nest, project, aggregate, model, system module
scopes plus six nested-def copies inside draw methods.
Pure additions on the tool/ layer with direct unit tests covering the
default-alpha, explicit-alpha, non-mutation, new-list-instance, and
validation-guard branches.
Generated with the assistance of an AI coding tool.
A pending RegionView3D.update() timer registered before wm.open_mainfile()
fires during the load against freshly-allocated regions whose GPU contexts
are not yet wired, CTD-ing inside GPU_matrix_ortho_set. Cancel both the
refresh and cap-rebuild timers in a new load_pre handler, hold a
_file_loading gate from load_pre through the first on_pre_view tick (first
paint = GPU ready), and short-circuit on_depsgraph_update during the
window so its IFC-reload schedule_refresh + apply_clip_planes_direct
branches can't re-arm against unready regions.
Generated with the assistance of an AI coding tool.
Collapse two parallel hand-maintained lists in the addon-preferences
PropertyGroups into derivations from `tool.Parametric.EDIT_TYPES`:
- GizmoPreferences: the 10 `<name>: BoolProperty` annotations now
generated from the full EDIT_TYPES list.
- DefaultParameters: add `has_default_parameters` flag to
ParametricObject (set True on door/window/stair/railing/roof);
derive the 5 `<name>: PointerProperty(type=BIM<X>Properties)`
annotations and collapse the 5 hand-written `draw_expandable_panel`
blocks in `draw_default_parameters` into loops driven by the flag.
Existing `test_gizmo_preferences_field_per_registry_entry` pinned the
GizmoPreferences contract; new
`test_default_parameters_field_per_registry_entry_with_defaults`
pins the DefaultParameters contract (one-directional: flag=True
implies field present, flag=False allows absence).
Generated with the assistance of an AI coding tool.
Refs gh#8088. Array regen + multi-opening drops fan out N+1 wall recuts
per operator (one per child filling deletion + the final mirror recut),
making CSG opening-subtraction O(N^2) for a linear UX action.
Introduces tool.Geometry.batch_host_recut() — a context manager that
coalesces switch_representation and bpy.ops.bim.update_representation
calls per voided element within one operator transaction. The drain
re-reads the active representation so the recut reflects current IFC.
Wraps 7 entry points (regenerate_array, RegenerateArray, RemoveArray,
AddOpening, RecalculateFill, CloneOpening, regenerate_from_type) and
rewires 7 leaf call sites in opening.py, void/operator.py, and
mirror_parent_void_fillings_to_children.
An AST forward-compat guard pins the rewire contract: no direct
switch_representation or bpy.ops.bim.update_representation in the
three target files outside the helper definitions.
A 16-child array regen now recuts the wall once instead of 17 times.
The CSG cost per recut is unchanged; only the count is reduced.
21 new tests across three lanes (helper unit, entry-point coalescing,
AST guard) — all green.
Generated with the assistance of an AI coding tool.
Mirror the icon-click entry into wall parametric edit on the TAB key.
The dispatch in Modifier.try_applying_edit_mode had no branch for fresh
LAYER2 walls, so TAB landed in item mode instead of the parametric
draft + gizmos. Add the missing entry leg of the toggle, placed after
the generic is_object_editing branch so the finish leg still fires
when a wall is already in edit mode.
Generated with the assistance of an AI coding tool.
Merging more than two IFC models with the MergeProjects recipe leaves
duplicated IfcGeometricRepresentationContext entities behind. All elements
are kept, but the accumulated contexts cause later disciplines to appear
"not merged" in viewers.
This test merges three projects and asserts the elements are kept, a single
IfcProject remains, and the geometric contexts are reused rather than
accumulated. It currently fails on the context assertion, reproducing #7973.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EnableEditingExtrusionAxis and EnableEditingExtrusionProfile both
import a mesh from the IFC representation into obj.data as their
first real action. That mesh-import overwrites any in-memory
parametric (gizmo) draft on the object, silently discarding the
user's pending dimension edits.
Concrete reproduction: drag a wall's length gizmo (draft pending),
then click "Edit Axis" before validating the draft. The axis-edit
imports the wall axis mesh; the in-flight draft vanishes; on
cancel the wall snaps back to its pre-drag length.
Both call sites now commit the active draft via
tool.Parametric.commit_object_draft before the mesh import, gated
on tool.Parametric.is_object_editing so the guard is a no-op when
no draft is in flight.
Generated with the assistance of an AI coding tool.
Multi-instance cache-lock, "Opening Cuts Skipped", and "Arrays With
Missing Children" banners now draw in BIM_PT_tabs alongside the
existing global error / outdated-model banners, so they remain
visible regardless of the active Bonsai tab. The corresponding
blocks are removed from BIM_PT_project. Dead imports
(is_cache_locked_by_other_process, draw_multiline_text) dropped
from project/ui.py.
Generated with the assistance of an AI coding tool.
regenerate_wall_representation returns None for walls without an
IfcMaterialLayerSet (the only mode it knows how to rebuild). Feeding
None to switch_representation crashes deep in resolve_representation
on .Items. Document the None return on the API side and bail in
tool.Model.recreate_wall when it hits.
Generated with the assistance of an AI coding tool.
Schema-illegal IfcDoor->IfcWallType pairings parse cleanly but propagate
into operators that fan out by type and eventually crash the wrapper.
Block the pairing at its source: API guard in ifcopenshell.api.type.
assign_type, per-object partition in BIM_OT_assign_type + DuplicateType,
new tool.Type.is_relating_type_compatible helper, AST forward-compat
guard. Files in the wild are still loaded unchanged.
Generated with the assistance of an AI coding tool.
The "mep-pair-fitting" kind added in the previous commit carries an
IfcFlowFitting (the entity whose deletion disconnects the pair), not a
relationship entity, in the dispatch slot — but the slot was named ``rel``
across the function signature and every call site. Rename to ``subject``
so the parameter name reflects the uniform intent: "the entity whose
teardown effects the disconnect", regardless of whether that's a rel or
a fitting.
Sweep covers:
- core.connection.disconnect_rel signature + body
- tool.Connection.find_rels / find_rels_for_element / find_rel docstrings
- The cascade-on-delete call site in tool.Geometry.delete_ifc_object
- DisconnectElements operator in bim.module.model.wall
- All affected test kwargs and AST forward-compat docstring
- Error message: "Unknown rel kind" -> "Unknown kind"
No behaviour change.
Generated with the assistance of an AI coding tool.
Three user-facing fixes for the MEP-system gizmo surface:
1. MEP pair-disconnect no longer crashes Blender. The
MEPSystemPathDecorator cached entity_instance references in
_cached_walk; deleting a bridging fitting via the gizmo left a freed
SWIG handle in the list, and the next _build_geometry pass segfaulted
on .is_a. The cache now stores STEP integer ids and re-resolves via
ifc_file.by_id on each draw, plus folds tool.Parametric.get_geom_generation
into the cache key — ifcopenshell.api mutations invalidate before the
next frame regardless of how the deletion was routed.
2. Bend re-edit pen icon stays reachable. The bend creation path
tessellates the swept-disk body (upstream geometry-kernel workaround),
so tool.System.has_parametric_body correctly returns False for a
freshly-committed bend. _active_is_bend_fitting and
GizmoMEPActions.is_eligible_object now fall back to the type's
BBIM_Fitting pset — the same source bim.enable_bend_preview_from_bend
reads parameters from — keeping the pen icon eligible.
3. MEP pair / per-port unjoin icons unified through bim.disconnect_elements.
The MEP gizmo group's three unjoin icons (pair, start, end) now share
the wall-disconnect surface: same VIEW3D_GT_wall_link_toggle icon, same
bim.disconnect_elements operator. tool.Connection.find_rels learned a
new "mep-pair-fitting" kind that returns the bridging fitting as the
disconnect target; core.connection.disconnect_rel grew the matching
dispatch arm. The old MEPUnjoinAtPort and MEPUnjoinPair operators are
removed.
Also registered wall.GizmoPairDisconnect (previously declared but never
in the classes tuple, so dead code) for the wall+slab pair-disconnect
surface, and extracted MEP port-topology helpers (find_bridging_fitting,
is_disconnectable_fitting, neighbours_at_ports) onto tool.System so the
canonical walk has a single home.
Generated with the assistance of an AI coding tool.
Two new UX features in the IFC Patch panel, both backed by helpers on
bonsai.tool.Patch.
Lossy-downgrade confirmation popup. When the user picks the Migrate
recipe with a target schema older than the source's (IFC4 -> IFC2X3,
IFC4X3 -> IFC2X3), ExecuteIfcPatch.invoke shows a properties dialog
listing what's preserved vs lost: IfcIndexedPolyCurve flattened with
arcs approximated, IfcPolygonalFaceSet / IfcTriangulatedFaceSet
converted to IfcFacetedBrep, IFC4-only IfcElement subclasses (IfcLamp,
IfcPipeSegment, IfcGeographicElement, ...) demoted to
IfcBuildingElementProxy with the original class + PredefinedType
encoded into ObjectType, and PredefinedType enum values absent from
IFC2X3 dropped. The user explicitly approves before the recipe runs.
The popup is gated on tool.Patch.migration_is_lossy_downgrade() which
resolves the source schema via header-only parsing
(tool.Patch._patch_source_schema reads the first ~2KB and matches a
FILE_SCHEMA regex, then normalises via ifcopenshell.util.schema.
get_fallback_schema). Avoids a full ifcopenshell.open() on every
Execute click — multi-second saving on large files. The target schema
is looked up by argument name rather than position so it survives
recipe-parameter reordering.
Per-recipe preset menu. New BIM_MT_ifc_patch_presets + AddIfcPatchPreset
wire Blender's standard preset system into the panel. Each recipe gets
its own preset subdirectory (bonsai/ifc_patch/<RecipeName>/), so a
preset saved for ExtractElements does not pollute the Migrate preset
list. The preset operator uses Attribute.get_value_name() (single
source of truth for data_type -> storage-field mapping) to build the
preset_values list dynamically per recipe.
The recipe-change callback resets
BIM_MT_ifc_patch_presets.bl_label to the canonical title — Blender's
script.execute_preset mutates the menu's bl_label to the loaded
preset's name as a "currently-selected" indicator, and without an
explicit reset the previous recipe's preset name would falsely advertise
itself in the new recipe's menu.
tool.Patch gains get_preset_subdir, migration_is_lossy_downgrade,
_patch_source_schema as cross-cutting helpers. _SCHEMA_AGE module
constant provides the ordering used by the downgrade-detection
predicate.
Test coverage: 12 bim-lane tests under test/bim/module/patch/. The
truth table for migration_is_lossy_downgrade covers IFC4/IFC4X3 source
x downgrade/upgrade/same-schema target x Migrate/non-Migrate recipe.
The schema-sniffing tests write a real IFC4X3_ADD2 file to disk and
assert the helper resolves it to IFC4X3 (regression for the original
startswith iteration-order bug). An end-to-end test drives
bpy.ops.bim.execute_ifc_patch with an in-memory IfcLamp source and
verifies the on-disk IFC2X3 file contains a single
IfcBuildingElementProxy with ObjectType "IfcLamp/COMPACTFLUORESCENT"
and the original GlobalId preserved.
Generated with the assistance of an AI coding tool.
ExtractElements: expand the `query` docstring to cover the exclusion
syntax (`!` on entity classes, `!=` on attribute / pset / material /
classification / location / group facets) and the "seed with a broad
include before subtracting" gotcha — entity-class exclusion does not
auto-seed from "all elements", so a bare `! IfcSlab` query returns
nothing.
FixArchiCADToRevitDoorSwings: guard the `IfcIndexedPolyCurve.Segments`
loop against the IFC4 case where Segments is absent (a polyline
through all coords in declared order). Previously crashed on
`None.__iter__`.
Generated with the assistance of an AI coding tool.
The Migrate recipe previously crashed mid-loop with the cryptic
`RuntimeError: Entity with name '' not found in schema 'IFC2X3'` when
asked to downgrade an IFC4 or IFC4X3 file to IFC2X3 — the
class_4_to_2x3 mapping marks IFC4-only geometry / element classes with
an empty-string sentinel and the old code blindly forwarded that to
create_entity. Real files routinely contain IfcPolygonalFaceSet,
IfcTriangulatedFaceSet, IfcIndexedPolyCurve, IfcLamp, IfcPipeSegment,
IfcGeographicElement, etc.
The recipe now runs a preprocessing pipeline when the target is IFC2X3
and the source is IFC4 or IFC4X3:
- DowngradeIndexedPolyCurve flattens IfcIndexedPolyCurve to IfcPolyline
for the whole file (arcs included — see below).
- IfcPolygonalFaceSet / IfcTriangulatedFaceSet are converted directly
to IfcFacetedBrep at the entity level via
ifcopenshell.util.shape_builder.polygonal_face_set_to_faceted_brep,
preserving topology including IfcIndexedPolygonalFaceWithVoids inner
bounds. IfcShapeRepresentation carriers have their RepresentationType
tag updated from "Tessellation" to "Brep".
- Orphan source-only geometry instances (left over after the rewires)
are purged iteratively via
geometry_classes_introduced_after(target, source).
The Migrator is invoked with fallback_element_to_proxy=True so
IFC4-only IfcElement subclasses (IfcLamp, IfcPipeSegment,
IfcGeographicElement, ...) become IfcBuildingElementProxy in the
output. A post-pass encodes "<OriginalClass>/<PredefinedType>" into
ObjectType (e.g. "IfcLamp/COMPACTFLUORESCENT") when ObjectType is
empty, so the lost subclass identity survives the downgrade as
searchable text.
The migration loop now collects per-entity failures into a list rather
than crashing on the first; a summary RuntimeError fires at end if any
failed, naming up to 20 with their inverse references. Successful
migrations log a single count line via self.logger.
DowngradeIndexedPolyCurve extended:
- Arc segments (IfcArcIndex) are flattened via
ifcopenshell.util.shape_builder.arc_to_polyline_points with
ARC_SUBDIVISION=16 chord points per arc.
- Multi-index IfcLineIndex segments handled correctly.
- Absent Segments list (IFC4 polyline-through-all-coords case) handled.
Test coverage: 11 tests across the two recipes covering all four
preprocessing branches, the IFC4X3 source gate, the ObjectType
encoding (incl. author-supplied ObjectType preservation), the summary
RuntimeError shape, and the arc subdivision.
Generated with the assistance of an AI coding tool.
Adds the IFC-library primitives the ifcpatch Migrate recipe needs for a
defensive IFC4 / IFC4X3 -> IFC2X3 downgrade without each caller
reinventing the wheel.
In ifcopenshell.util.schema:
- Migrator(fallback_element_to_proxy=False) opt-in: when True, IFC4-only
IfcElement subclasses (IfcLamp, IfcPipeSegment, IfcGeographicElement,
...) migrate to IfcBuildingElementProxy instead of raising. Default
preserves the strict failure-on-unmappable contract for existing
callers (classification API, etc.).
- geometry_classes_introduced_after(target, source) derives the
IfcRepresentationItem subclasses present in `source` but absent in
`target` directly from the loaded schemas. Cached per pair. Replaces
hand-curated class lists that drift with each IFC update.
ifc4_only_geometry_classes() retained as an alias.
- generate_default_value synthesises a unit IfcAxis2Placement2D /
IfcAxis2Placement3D when downgrading entities whose Position became
required in the target schema (IfcIShapeProfileDef and friends in
IFC2X3).
- Enum-mismatch detection upgraded from string-matched RuntimeError to a
structural check via ifcopenshell.util.attribute.get_enum_items so
upgrade paths still surface real bugs loudly.
In ifcopenshell.util.shape_builder:
- polygonal_face_set_to_faceted_brep converts IfcPolygonalFaceSet /
IfcTriangulatedFaceSet (IFC4-only) directly to IfcFacetedBrep,
preserving topology including IfcIndexedPolygonalFaceWithVoids inner
bounds. Validates inputs at the boundary.
- arc_to_polyline_points approximates a circular arc through three
points with a chord polyline of configurable subdivisions. Tolerates
floating-point noise on planar Z. Raises on non-planar or invalid
inputs.
Test coverage: 47 unit tests across schema + shape_builder lanes
covering each helper directly (no transitive-only coverage), including
regression pins for the IFC4X3-prefix ordering invariant in
get_fallback_schema and the strict-default Migrator contract.
Generated with the assistance of an AI coding tool.
Add include_linked_ifc toggle on BIMSceneClipBoxProperties so the cap
pipeline can also bisect meshes inside Project > Links collection-instance
empties. Off by default - linked IFCs may carry the entire site or
structural backbone, and capping them adds per-mesh bisect cost on every
clip-box edit.
The new iterator composes instance.matrix_world @ inner.matrix_world as
the effective world placement so caps land in the active scene rather
than at the linked library's local origin. Linked-mesh cache entries are
namespaced with a 'link:' prefix to avoid collisions with top-level
scene objects.
Generated with the assistance of an AI coding tool.
Promotes the private _fill_quads_alpha helper from
bim/module/model/decorator.py to tool.Blender.draw_quads so any feature
decorator can reuse the same TRIS-batch path.
The new utility accepts an optional outline_color so callers can draw
fill, outline, or both in a single call. Migrates the only existing
caller (WallGizmoPreviewDecorator in model/wall.py) to the public API
and removes the local helper.
Generated with the assistance of an AI coding tool.
Add source-based clip box presets — a dropdown menu next to the Add
Clip Box button lets the user pre-size a clip box to the bounding box
of a chosen IFC source: a spatial element, IFC class, type, material,
profile, drawing camera frustum, status, system, group, or zone. The
picker dialog uses prop_with_search so files with hundreds of materials
or types remain browsable.
Add interactive face resize handles — six near-invisible click-target
quads render on the active clip box when its empty is the active
object. Dragging a face grows or shrinks the box one-sided on that
axis; the opposite face stays fixed. Ctrl+Click on a face aligns the
viewport to look at that face, following Blender's numpad-view
convention applied to the box's local axes so rotated boxes align
orthogonally to the screen. The gizmos honour negative-scale empties
so the visible cube and the clickable handles stay aligned.
Add settings and info menus — a gear-icon menu next to the Enable
Clipping / Show Caps toggles exposes per-file preferences (cap only
IFC products, show face handles); an info-icon menu adjacent documents
the gizmo gestures. A quick-access toggle row also appears in the
viewport Overlay popover, greyed out when no clip box exists, and
orphaned clip-box list entries now expose an X button so users can
recover from external host-empty deletions.
Plumbing: cap rebuild fires synchronously on gizmo release and
clip-box selection change, so the cross-section overlay re-forms
without waiting for the depsgraph debounce; cap eligibility honours
the "Only IFC Products" toggle. Includes 121 tests covering source
resolution, drag math, face visibility, gizmo registration, and the
view-alignment up-axis convention.
Generated with the assistance of an AI coding tool.
Adds a viewport overlay that traces the connected element path from
the selected wall or MEP element. Walls follow IfcRelConnectsPathElements
and draw each connected wall's reference axis with endpoint dots; MEP
elements follow IfcRelConnectsPorts and draw each segment's axis plus
a port-to-port spider for each fitting.
The new BIMModelProperties.show_paths toggle (Element Paths in the
Bonsai Decorators group of Blender's viewport overlay popover) drives
install / uninstall of both decorators on flip and on file load,
mirroring the show_slab_direction wiring. The popover row also
surfaces the pre-existing BIMSystemProperties.should_draw_decorations
toggle (System Decorations) so both connectivity overlays sit
together.
Dot colors split free endpoints (decorator_color_special, blue by
default) from junction nodes (decorator_color_selected, green by
default) so dangling chain tips read apart from interior joins. Walls
classify endpoints by IFC topology: rels expose RelatingConnectionType /
RelatedConnectionType and ATPATH dots use tool.Wall.path_connection_location_world
for the canonical T-meets join. MEP keeps the geometric classifier
because port positions coincide exactly across fitting + segment.
Generated with the assistance of an AI coding tool.
Edit-mode click-select rejected verts inside the clip volume
because clip_bb stayed at the first-arm view; the depsgraph and
PRE_VIEW handlers updated clip_planes but skipped the
view3d.clip_border call that refreshes clip_bb. Schedule a full
re-arm at transform-commit, IFC reload, and view drift.
The empty's wireframe was clipped by its own planes when rotated
at non-trivial scale because the 1e-6 absolute margin can't
absorb float-precision drift that scales with the box's world
half-extent. Add a 1e-5 relative expand.
Generated with the assistance of an AI coding tool.
Per IFC4+, IfcContext is the abstract supertype of IfcProject and
IfcProjectLibrary; library-only files legitimately contain only
IfcProjectLibrary as their root context. Bonsai assumed an IfcProject
was always present at three crash sites: the parent-library enum
(reported in #8183), RefreshLibrary's tree view, and AddProjectLibrary.
Introduce tool.Project.get_root_context() that prefers IfcProject and
falls back to IfcProjectLibrary, and route the three sites through it.
get_parent_library() now returns None for a root IfcProjectLibrary;
get_project_hierarchy() and the EditProjectLibrary parent-swap branch
handle that. AddProjectLibrary creates the nested sub-library via
IfcRelNests when the root is an IfcProjectLibrary, matching the
existing convention for library-under-library nesting.
For the separate "Open IFC Project" path, abort with a friendly error
pointing users to Project Setup -> Project Library -> Select Library
File instead of letting set_units() crash deep in the importer.
Closes#8183.
Partly generated with the assistance of an AI coding tool.
IfcQuantity* carries an optional Formula (IfcLabel) documenting how a
quantity was derived. Export it alongside each quantity so it survives
in the Quantities column.
The per-quantity entry shape grows from [name, value] to
[name, value, formula], which stays backward compatible for positional
consumers reading index 0/1. Formula is read with a schema-safe getattr
(it does not exist on IfcPhysicalComplexQuantity, nor in IFC2X3) and is
coalesced to "" when absent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a viewport clip box feature to Bonsai: a user-controllable oriented bounding box that hides everything outside its 6 faces and draws filled cross-section caps where IFC product geometry intersects the planes.
Quality and coordination > Sandbox > Clip Box
A clip box hides everything outside a user-controllable oriented
bounding box, with cross-section caps drawn where IFC product
geometry intersects the planes. The box is hosted on a Blender
empty (CUBE display); its matrix_world is the single source of
truth — G/R/S edits the empty and the viewport clip planes track.
State persists through IFC save/load via a project-level pset
(IfcProject.BBIM_ClipBoxes) so the boxes survive without binding
to any IfcRoot entity (avoids the IFC scale-lock / strip).
UI: BIM_PT_clip_box under the Sandbox tab. Prominent Enable
Clipping + Show Caps toggles at top, then Add, then a UIList with
per-row duplicate / remove icons. Scene-level enabled / show_caps
so the "hide everything outside" intent applies file-wide;
enabled is intentionally not persisted to the pset so reopening
an IFC never silently hides geometry. Adding a clip box arms
clipping so the user immediately sees the cut.
Default spawn at the 3D cursor with scale 10 (a 20 m cube) so the
volume covers a typical building storey or two rather than the
meaningless 2 m unit cube.
Modal-aware: depsgraph + draw-handler paths gate per-frame side
effects on tool.Blender.is_transform_modal_active so dragging
G/R/S on the box only writes the pset once on commit, not per
frame. Shift+D / Alt+D / Ctrl+Shift+D on a clip box gets adopted
as a first-class entry via the collection-to-list sync.
Cap eligibility is gated on IfcElement (walls, slabs, doors, …)
so spatial structure (IfcSpace, IfcBuildingStorey, IfcSite) and
annotations / grids never sprout solid fills at clip boundaries.
Cap rebuild is debounced behind a 1 s quiet window so external
gizmo drags (and any other burst of non-Bonsai depsgraph updates)
collapse to one rebuild on release. Bonsai's own G/R/S keeps the
snappy on-release feel via a modal-end fast-path. The relevance
filter compares a per-Object matrix hash against a baseline so a
plain selection click — which Blender quirkily flags as a
transform update — doesn't churn the cache or flash the caps off.
Edit mode short-circuits both the rebuild scheduler and the draw
handler entirely.
Caps use the evaluated mesh (modifier stack applied) and a
session/matrix/clip-box-hash cache so a typical scene only
re-bisects meshes whose geometry actually changed.
Performance: every per-frame poller (refresh, depsgraph handlers,
draw handlers) short-circuits on the cheapest available check
first — cap_cache emptiness for the post-view draw handler,
scene_props.enabled for the rest — so a session with clipping
disabled pays only one boolean read per tick.
Known v1 limitations documented in tests / docstrings: hollow
profiles cap as solid discs (single-ring tessellation only),
non-watertight inputs may produce degenerate caps, quad-view
untested, Cycles / EEVEE render not supported (GPU-overlay only).
Generated with the assistance of an AI coding tool.
Bisects a BMesh against a set of planes (clear_outer per plane),
then fills the cut edges as cap faces tagged via a BMesh int layer
so the tag survives subsequent bisects. Cut edges are grouped into
connected components before filling so a hollow profile's outer +
inner loops produce two separate cap faces instead of a single
welded outer face that hides the hole. Pre-welds T-junctions
introduced by IFC Boolean meshes so the cut closes into a fillable
loop.
Callers are responsible for input mesh quality. Non-watertight
inputs (terrain, single-shell surfaces) may produce degenerate
cap faces — that's an accepted user-supplied data limitation
which can be revisited if real-world feedback shows it matters.
Used by the clip-box feature to compute cross-section caps per IFC
product mesh.
Generated with the assistance of an AI coding tool.
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.
* Tolerate stale array child/parent GUIDs
A real-world IFC project (an arrayed door whose host got deleted
externally) crashed Bonsai's project load with "Instance with
GlobalId not found" inside setup_arrays.
tool.Blender.get_object_from_guid declared Optional return but let
RuntimeError propagate; callers iterating BBIM_Array child lists then
crashed instead of skipping. Honour the documented contract by
returning None on miss, matching the convention used by every other
by_guid lookup helper in tool/array.py, tool/ifc.py, tool/geometry.py.
Sweep the four user-action sites that resolve array child/parent
GUIDs without a guard - they shared the same bug class but were
reachable from different operators (regenerate_array, RegenerateArray
clear, duplicate_ifc_objects, process_arrays). An already-missing
entity is the desired terminal state for each, so the fix is
try/except RuntimeError: continue/skip.
setup_arrays now also collects each parent with at least one stale
child GUID into IfcImporter.broken_arrays, surfaced via a new Project
panel banner mirroring the existing pending_opening_recut UX. The
banner reports the count and offers "Select Elements" to navigate to
the affected array parents and a Dismiss button.
constrain_children_to_parent was being called once per layer inside
setup_arrays' for loop even though it always iterates all layers
internally - lifted out of the loop (pre-existing N x perf bug
that the stale-GUID print exposed).
Regression tests:
- test_returns_none_when_guid_not_in_file pins the get_object_from_guid
Optional contract.
- test_remove_array_tolerates_stale_child_guid injects a fake child
GUID into BBIM_Array.Data and asserts bim.remove_array completes
cleanly.
Generated with the assistance of an AI coding tool.
* Black: wrap long bl_description in dismiss_pending_array_repair
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.
ProfileDecorator.draw_faces (used by the roof path-edit overlay) and
SystemDecorator.draw_faces called bmesh.ops.triangulate on the live
bmesh — both mutated the input and produced ear-clip fans that rendered
as visible streaks across n-gon roof faces at alpha 0.1. The opening
DecorationsHandler edit-mode branch had a separate bug: it computed
triangles from obj.data.calc_loop_triangles() while iterating the
edit-mode bmesh, so any topology added mid-edit desynced the indices.
Centralise the correct draw path on tool.Blender.draw_bmesh_face_tris
(wraps bm.calc_loop_triangles, non-mutating, beauty triangulator) and
route all three call-sites through it. A forward-compat AST guard walks
every *Decorator / DecorationsHandler class under bim/module/ and pins
the no-bmesh.ops.triangulate rule against future regressions.
Generated with the assistance of an AI coding tool.
A user clicking the pen icon on a typed-product occurrence whose body
representation is mapped from its type would silently mutate every
sibling occurrence's geometry. Add a confirmation dialog at the pen-icon
dispatcher (the single chokepoint every feature routes through) showing
the sibling count, with a session-scoped suppress checkbox.
The check is read-only: tool.Model.get_sibling_occurrence_count wraps
tool.Geometry.get_elements_by_representation against the resolved body
rep and subtracts self + type. A forward-compat AST guard pins the
dispatcher monopoly so any future feature that binds pen_gizmo directly
to a feature-specific enable op fails the test before merge.
Generated with the assistance of an AI coding tool.
Once a bend was created, the only way to retune start_length /
end_length / radius was to delete and recreate from scratch.
EnableBendPreviewFromBend re-opens the preview on an existing
parametric bend: it walks the bend's ports to resolve the two
connected segments, reads start / end length and radius from the
bend type's BBIM_Fitting pset, and sets editing_bend_id on the
preview props. MEPAddBend then deletes the old bend + its port
connections (single undo step) before the recreate path runs, so
finish replaces the bend in place and cancel discards the edit
without touching the original.
GizmoMEPActions surfaces a pen icon on single bend-fitting
selections via the new _active_is_bend_fitting predicate; the icon
dispatches the new operator. Mirror of the wall fillet re-edit
flow (EnableWallFilletPreviewFromCorner + editing_corner_id in
CreateWallFillet).
Test coverage: registration probe for the new operator, an attached
editing_bend_id field probe on the preview umbrella, and a
parametrized truth-table for the _is_bend_fitting predicate
(IfcFlowFitting with BEND PredefinedType, with other PredefinedType,
with no type, IfcFlowSegment, IfcWall, None).
Generated with the assistance of an AI coding tool.
Wall topology mutations (merge / join / extend-to-wall / unjoin /
fillet) applied to a Bonsai array child are silently overwritten by
the next ``regenerate_array``; merge also orphans a GUID listed in
the parent's ``BBIM_Array.Data``. Add a central
``tool.Blender.Modifier.any_selected_is_array_child`` predicate and
gate the five wall topology gizmo groups plus the six bound operators
behind it. Operator gating is defence in depth against keymap / F3
invocation paths that bypass the gizmo.
The base ``_wall_gizmo_poll_gate`` keeps its loose two-check shape
(viewport gizmos + no preview). A new
``_wall_topology_gizmo_poll_gate`` wraps it with the array-child
filter and is what the topology gizmos use. Host-opening gizmos
deliberately stay on the loose gate: openings authored on a child
are preserved through ``regenerate_array`` and track with the
replicated instance.
A forward-compat AST guard walks wall.py for ``GizmoGroup`` subclasses
and asserts each routes its poll through the tighter gate or the
central predicate, with an allow-list for the parametric-edit and
preview-owner exceptions. New wall topology gizmos inherit the
contract by construction.
Generated with the assistance of an AI coding tool.
Selecting a Bonsai-parametric IfcDoor now shows the swing arc(s)
without entering edit mode. A new viewport decorator polls on the
active object, reads the door's BBIM_Door pset, and draws the same
arcs the parametric door swing gizmo would draw — matching the
hinge / panel-width / x-mirror contract minus the is_editing gate.
A forward-compat test walks every door operation type and cross-
checks the readonly decorator's arc selection against the gizmo's
swing-arc config table, so future enum additions fail in both
surfaces simultaneously.
Also disables the inherited 8-pass dark halo on GizmoArc: an open
curve has no enclosed silhouette, so the offset passes read as
ghost arcs rather than a uniform outline. The arc's own cross-
section thickness keeps it legible without the halo.
Generated with the assistance of an AI coding tool.
The MEP bend feature's IfcSweptDiskSolid representation produces
geometrically correct output but fails to round-trip through the
OpenCascade geometry kernel (upstream issue #8106) — the body is
dropped on the next file load. Until upstream is fixed, MEPAddBend
captures the bend centerline in world space before the segments are
extended (otherwise the post-extension axes no longer reach the
original intersection and arc reconstruction is wrong), then after
the fitting is placed it hand-meshes the bend body and swaps the
type's swept-disk representation for an IfcTessellatedFaceSet via
tool.Geometry.export_mesh_to_tessellation + tool.Model.
replace_object_ifc_representation.
The centerline includes the straight start_length / end_length legs
in addition to the arc so the bend covers the full segment-to-
segment span. Sweep uses parallel-transport framing — each ring's
(right, up) basis is rotated by the minimum rotation that maps the
previous tangent to the current one, eliminating the twist a fixed
world-axis reference produces when the tangent crosses the
reference. Cross-section orientation seeds from the source segment's
matrix_world local +X / +Y so asymmetric IfcRectangleProfileDef
ducts land with XDim / YDim on the same axes the segment expects;
parallel transport then preserves that alignment around the arc.
Centerline radius is radius + profile_dim[lateral_axis] to match
MEPAddBend's ref_point_radius — without this offset, the bend legs
fall short of the extended segments by profile_dim * tan(angle/2).
Face winding is left to the caller to correct via
bmesh.ops.recalc_face_normals on the closed bend tube.
Two FIXME(#8106) markers (capture site + helper call site) so both
can be dropped once upstream lands a swept-disk round-trip fix.
Generated with the assistance of an AI coding tool.
IfcTriangulatedFaceSet/IfcPolygonalFaceSet were introduced in
Fix#7992: IFC4 and do not exist in IFC2X3. Previously, requesting an
IfcTessellatedFaceSet representation in an IFC2X3 file silently
fell back to a faceted brep after unassigning material sets.
Add a guard in the update_representation operator (user-facing
error) and in the add_representation API (ValueError) so the
unsupported request is caught instead of failing silently.
Generated with the assistance of an AI coding tool.
GizmoWallEdition gains a fourth cursor-anchored icon that
spawns a perpendicular branch wall from the cursor's
orthogonal projection on the source wall axis. Click forms
a T-junction; shift+click forms an L-corner with the source
wall trimmed at the projection, keeping its longer portion.
The branch inherits the source's spatial container and
centerline baseline so its authored axis matches the source's
alignment rather than the type's default.
Also includes a floor-plane preview quad for the new gizmo,
a floor-Z cross line on the split preview for top-down
visibility, a small bump to QUAD_ALPHA for clearer preview
fills, and a stacking-offset helper that centralises the
cursor-row screen-up step across three call sites.
Generated with the assistance of an AI coding tool.
The MEP one-shot operators (join, unjoin variants, terminal removal,
path-select, obstruction add/remove) had no viewport surface. This
commit adds GizmoMEPActions — the icon-action gizmo group that
surfaces them as billboarded icons around selected MEP elements.
Three anchor regions: a horizontal row above the bbox top
(selection-cardinality icons), per-port endpoints for the three-state
lock / unjoin icons (open lock for PORT_FREE, closed for
PORT_TERMINAL, unjoin for PORT_JOINED — resolved per-frame from
port_connection_state), and the predicted join location
(compute_mep_join_location, shared with the bend preview) for the
join / unjoin_pair pair. Unjoin icons render at full
DEFAULT_BILLBOARD_SCALE with warning-red hover; endpoint lock icons
shrink so the lock row stays subordinate to the row icons. The
group hides itself entirely while a bend preview is active.
MEPAddObstruction grew a position enum (CURSOR / START / END) and a
mode enum (ADD / REMOVE / TOGGLE) so the gizmo can target a specific
port without touching the cursor and dispatch ADD or REMOVE based on
the click target — the lock_open icons drive ADD with position
pinned, the lock_closed icons drive bim.mep_remove_terminal_fitting.
Without the new fields the gizmo wiring (op_props.position = ...)
crashed at setup() with AttributeError on the obstruction operator.
validate_bend_preconditions extracts the type-match and profile-kind
checks MEPAddBend enforces so EnableBendPreview surfaces the
rejection immediately — the user no longer tunes a preview only to
learn at commit time that the segments use an unsupported profile
(e.g. IfcArbitraryClosedProfileDef).
Generated with the assistance of an AI coding tool.
Four discrete one-shot operators driven by the MEP segment's port
state. mep_unjoin_at_port deletes the IfcFlowFitting bridging a
segment's named port to a second element when the port is in the
JOINED state. mep_remove_terminal_fitting deletes the terminal
fitting at a port (closed-lock state) and dispatches by fitting
type — OBSTRUCTION fittings go through MEPGenerator.remove_obstruction
so the segment absorbs the freed length, other terminal fittings go
through the standard delete path. mep_unjoin_pair finds the single
fitting bridging two selected MEP segments and deletes it.
select_mep_path_members walks the connected MEP network from the
active element via IfcRelConnectsPorts and replaces the selection
with every reachable member. Foundation for the MEP Actions gizmo
group which surfaces these operators as icon affordances around
selected segments.
Generated with the assistance of an AI coding tool.
The extend icon used a pure screen-space billboard that always
pointed +X across the screen — the arrow ran horizontally
regardless of the pipe / duct's orientation. The new
billboarded_along_axis helper rotates the gizmo about the camera-
forward axis so its local +X aligns with the segment's local +Z
projected onto the screen, keeping the icon camera-facing but
visually following the extrusion direction. The flip-mirror branch
now reads from cursor-vs-current-end along the segment axis (not
screen-X), so the arrow points away from the current endpoint
regardless of viewport orientation. The split icon stacks
perpendicular to the rotated extend arrow in screen space so the
two don't overlap.
The decorator's green preview line no longer clamps the cursor
projection to min_projected_length — it follows the raw projection
so the line stays visible when the cursor crosses behind the
segment origin (the user still sees where they're pointing even
though the operator floors the actual commit).
Generated with the assistance of an AI coding tool.
The bend preview gizmo group (commit 2) populated a Scene draft but
the user saw nothing in the viewport until they hit finish — they
had to commit blindly. This commit ports the BendPreviewDecorator
(centerline arc + two leg projections on valid geometry, warning-red
axes on invalid in-segment intersections) and the interactive
GizmoBendPreview group (three dimension widgets for start_length /
end_length / radius plus validate / cancel icons). The bend axis
math lives in a pure compute_bend_preview_polylines helper, fed
into both the gizmo group's per-frame positioning and the GPU
decorator's draw path. MEPSegmentExtendPreviewDecorator lands at
the same time because it shares the decorator install / uninstall
plumbing — renders the extend-to-cursor preview line for the
GizmoPipeSegmentEdition / GizmoDuctSegmentEdition extend icons
when hovered, clamping the projected endpoint to the operator's
minimum so the preview matches where the commit lands. The
MEPJoinSegments dispatcher routes two selected MEP segments to
mep_add_transition (parallel) or enable_bend_preview (non-parallel)
— the F3 search entry point that makes the bend preview testable
before the gizmo-icon dispatch lands.
11 new tests in test_mep_bend_preview.py cover the geometry helper
truth table (parallel rejection, right-angle happy path, near-
collinear rejection, in-segment invalid_axes), the
_intersection_past_near parametrized boundary, registration probes
for the lifecycle operators / join dispatcher / gizmo group /
decorator, and the FinishBendPreview RuntimeError catch contract.
6 extend-preview-line tests (deferred from commit 3) join the
existing 35 in test_mep_segment_edition.py.
Generated with the assistance of an AI coding tool.
Spatial.get_root_element walks aggregate / nest / filled-void /
voided-element chains and core.assign_container assigns the container
to whatever the walk returns. For an IfcDoor the filled-void hop
redirects to the IfcOpeningElement, then voided-element to the host
wall, so a user who selects a door and runs bim.assign_container ends
up targeting the wall — and silently no-ops on the door if the wall is
already in the target storey.
Per IFC4 / IFC4.3 (IfcDoor, IfcWindow): the spatial containment of a
filling is defined independently of the filling relationship. Major
exporters (Revit, ArchiCAD, Tekla, Allplan) emit independent
ContainedInStructure on doors / windows accordingly. Drop the
filled-void / voided-element hops from the walk; aggregate and nest
remain — those are true sub-part relationships where the parent
legitimately owns the container.
New TestGetRootElement in test/tool pins the new contract (filling
resolves to itself) plus the retained aggregate / nest / loose-element
paths so a future PR that re-adds either hop is caught. Two new
TestAssignContainer cases in test/core pin filling-to-self through the
core layer and per-element can_contain filtering.
Generated with the assistance of an AI coding tool.
Pipe and duct segments had no parametric-edit affordance — the only
length edit path was a property panel value with no live preview.
This commit ports the per-segment parametric edit triad
(enable / finish / cancel) plus a cursor-anchored extend operator
and a cursor-projected split operator into one gizmo group per
segment type. The two PropertyGroups (BIMPipeSegmentProperties,
BIMDuctSegmentProperties) host the draft length plus snap fields
so cancel / no-op-finish restore the segment to its exact pre-edit
visual state including a non-identity pre-edit scale. Length
commits are written through DumbProfileJoiner.set_depth and
auto-dispatch bim.regenerate_distribution_element so adjacent
fittings track the port move. The split operator preserves
downstream port connectivity and runs through tool.Ifc.run for
single-step undo. The two segment types are now first-class
entries in tool.Parametric.EDIT_TYPES, which resolves the FIXME
on auto-commit-on-save dispatch.
35 unit tests cover predicate truth tables, segment_world_length
geometry, preview-via-scale / restore-scale helpers, gizmo class
wiring, lifecycle operator registration, dimension matrix_position
rotation respect, and lifecycle drift-handling. The 6 extend-
preview-line decorator tests stay deferred until the bend preview
decorator commit lands MEPSegmentExtendPreviewDecorator.
Generated with the assistance of an AI coding tool.
Parametric gizmos (wall/door/window/stair/roof/array/MEP) recompute
matrix_basis every frame from obj.matrix_world. While Blender's
transform modal (G/R/S and the Bonsai macro overrides) drags the
matrix, the gizmos slide off-cursor and fight the transform overlay.
Detect via context.window.modal_operators (Blender 4.2+) — the
collection of running modal operators. Gate poll() (forward-compat)
and draw_prepare() (production path: gizmo.hide=True preserves the
GizmoGroup across the drag instead of destroying it). Cover the
Bonsai macro override for G key (and Shift/Alt/Ctrl+Shift+D) by
matching the BIM_OT_* macro idnames that surface in modal_operators.
Forward-compat test walks every parametric-edit module for GizmoGroup
subclasses and asserts poll returns False with the detector mocked,
so new gizmo groups inherit the hide automatically.
Generated with the assistance of an AI coding tool.
Every preview operator (commit + cancel for both bend and wall
fillet) was inlining the same 3-4 line cleanup: set is_active to
False, zero every *_id IntProperty. The new clear_preview_state
helper in preview_base.py introspects bl_rna and applies that
contract generically — adopters become a single call. Two new tests
pin the contract: every *_id IntProperty zeroes, non-id fields stay.
Generated with the assistance of an AI coding tool.
Extract the pending_opening_recut tracking, three operators (apply /
dismiss / select), Project-panel banner, and the sibling
multi-instance warning banner (its backend helpers already landed
on this branch) from commit a85ed6032 on gizmos-8088.
All tool.* dependencies (Geometry.reimport_element_representations,
Blender.set_objects_selection, Array.*) and IfcImporter.gross_elements
are already on this branch -- no other diffs from a85ed6032 are
pulled.
The source's narrow except-tuple paraphrase comments are trimmed
to keep only the durable "don't swallow programmer errors" note,
per CLAUDE.md s4a.
Tests: 5 bim-lane tests in test/bim/module/project/
test_pending_opening_cuts.py covering apply happy-path + missing
entity, dismiss, select happy-path + cancellation.
Generated with the assistance of an AI coding tool.
MEPAddBend exists on the main flow but commits bend geometry with
hardcoded defaults (start_length=0.1, end_length=0.1, radius=0.2)
with no opportunity to tune before commit. The new scene-level
BIMBendPreviewProperties hosts a draft (start_segment_id,
end_segment_id, start_length, end_length, radius); EnableBendPreview
populates it from the two selected MEP segments after asserting they
are non-parallel, FinishBendPreview dispatches MEPAddBend with the
tuned values and clears the draft, CancelBendPreview discards it.
Scene-level placement follows CLAUDE.md 2.9: a bend creates a new
fitting entity between two segments, so neither segment alone owns
the draft. Foundation for the upcoming bend preview gizmo group and
decorator.
Generated with the assistance of an AI coding tool.
When an operator mutated IFC then raised mid-execute the user was left
staring at a raw traceback with the IFC graph captured by the active
transaction but the Blender side stale. Blender does not push an undo
step for a raised operator (the same gap that the CANCELLED-modal arm
patches via bpy.ops.ed.undo_push), so the WARNING the framework can
emit is only honest if it pushes that undo step too. The framework
now detects partial state via ifc_file.transaction.operations,
pushes a Recover undo step, then reports a WARNING naming Ctrl+Z so
the recovery path is discoverable. The bespoke try/except wrapper in
UnjoinWallPathConnection becomes redundant and is retired in the
same change.
Generated with the assistance of an AI coding tool.
Three files flagged by black --check on the lint-formatting job:
* bim/module/geometry/operator.py — single-arg `.update(...)` rejoined
onto one line under the 120-char budget.
* test/bim/module/model/test_wall_gizmos.py — same join on a
_make_path_rel call.
* test/modal/test_modal.py — pre-existing baseline noise picked up
via the upstream merge: PEP-8 blank-line separators between top-
level functions, `0.68+` → `0.68 +`, double quotes, trailing
whitespace stripped.
No behavioural change; pure whitespace.
Generated with the assistance of an AI coding tool.
The toggle_openings icon lived outside the IconSlot layout — each
host (wall, roof) declared an ad-hoc setup_pen_row_toggle_openings_icon
+ update_pen_row_toggle_openings_icon pair, and GizmoArrayEdition
queried a hardcoded _FEATURE_IDLE_MAX_X dict to position past it.
On an arrayed wall the dict was shadowed: find_for_element returns
"array" before "wall" in EDIT_TYPES order, the wall reservation was
never consulted, and the first per-layer ARRAY icon (local X=0.37)
landed 13cm from the wall's toggle_openings (X=0.50) — visually on
top of each other.
Promote idle-row icons into the slot system instead of patching the
dict:
* IconSlot gains an Optional visible_when predicate for state-driven
visibility (toggle_openings only when the host carries openings).
* BaseParametricGizmoGroup gains idle_slots: ClassVar[tuple[IconSlot]]
+ _idle_slot_x_positions() + _idle_row_right_edge() helpers; the
setup + idle-branch positioning loops mirror the existing
feature_slots path.
* Wall and roof declare toggle_openings as an idle_slot and drop
their ad-hoc setup/update calls.
* GizmoArrayEdition's _resolve_feature_idle_max_x walks
BaseParametricGizmoGroup.REGISTRY and takes the max
_idle_row_right_edge() across peers whose poll passes — no more
hardcoded dict, no more find_for_element-order shadowing.
* setup_pen_row_toggle_openings_icon + update_pen_row_toggle_openings_icon
helpers deleted from drawing/gizmos.py.
* 3 forward-compat AST guards pin the new contract.
Also bundles an unrelated array-test fix: TestUsingArrays in
test/tool/test_model.py was asserting against bpy.context.selected_objects
which is a fragile signal after remove_array / apply_array. A new
_array_objects() helper filters bpy.data.objects via the BIM_Array
pset's IfcActuator type instead.
Layout on an arrayed wall after the fix:
pen X = 0.00
toggle X = 0.50 (idle_slot 0)
array[0] X = 0.87 (one ICON_ARRAY_GAP past idle row)
array[1] X = 1.27
All separated by the standard inter-icon spacing.
Generated with the assistance of an AI coding tool.
In plan view world-Z collapses to zero on screen, so every wall-edit
icon anchored on the floor — the projected 3D cursor, wall endpoints,
wall-to-wall corners, IfcRelConnectsPathElements connection points —
projects onto the click target it represents. The result on a typical
extend / split / unjoin action: the icon sits on top of the cursor
crosshair (or the corner the user wants to click), defeating precise
positioning.
Add shared ``gizmo.top_down_clearance(context, billboard_rot)`` to
bim/module/drawing/gizmos.py: returns a screen-up Vector in top-down
view (cosine cone around world Z, matching ``is_view_top_down``) and a
zero Vector elsewhere, so call sites apply it unconditionally before
``billboarded_at``. Default distance 0.4 m aligns with the inter-icon
stack spacing already used by GizmoWallJoinIntersection so single
icons and stack bases land at consistent screen-up positions when
multiple groups render around the same wall endpoint.
Apply at the seven wall-edit anchor sites:
* GizmoWallEdition cursor stack (top-down branch only — non-top-down
already stacks along world-Z at structural points clear of the
cursor).
* GizmoWallExtendVertically (single icon at wall origin endpoint,
active-object Z elevation).
* GizmoWallJoinIntersection corner stack base + merge midpoint.
* GizmoWallUnjoinSingle link-toggle pool (one icon per IFC path
connection, previously sitting exactly on the connection point).
* GizmoWallFilletReedit pen icon at fillet corner.
* GizmoWallFilletToggleOpenings.
The clearance is a pure visual offset — bound operators still read
the world-space anchor (cursor / endpoint / connection point) at
execute time, so the action's target is unaffected.
Also tighten GizmoWallUnjoinSingle: gate poll on ``props.is_editing``
so the link-toggle icons only surface during the wall edit lifecycle
(matching every other edit-row icon), and downsize them via a new
``ICON_SCALE = 0.35`` constant since 16 of them at default scale
cluttered the viewport on path-heavy walls.
ruff + black clean. Wall gizmos test lane 14/14 pass.
Generated with the assistance of an AI coding tool.
Some distributions (e.g. Fedora) ship only a shared RocksDB that exports
RocksDB::rocksdb-shared rather than RocksDB::rocksdb. The CMake target
selection now falls back to the shared target when the static one is absent.
Newer RocksDB also changed DB::Open and DB::OpenForReadOnly to take
std::unique_ptr<DB>* instead of DB**. IfcFile.cpp uses SFINAE tag dispatch
to build against both old and new APIs without version detection.
CGAL 6.x deleted operator< from Point_d, so std::map<Point_d, ...>
no longer compiles. Adds a custom lexicographic comparator and updates
the three affected maps in snap_halfspaces and snap_halfspaces_2.
bim/handler.py was importing two feature-module internals
(wall_offset_gizmos.clear_caches, preview_base.discard_pending_previews)
to drain load-transient parametric state alongside the existing
tool.Parametric.heal_stale_edit_flags() call inside
_apply_save_file_invariants. Each new parametric drain added one
top-level import and one inline call — every load_post drain leaked
into handler.py's namespace.
Hide all three drains behind tool.Parametric.on_load_post(scene),
sited adjacent to heal_stale_edit_flags. The two feature-module
imports become late imports inside on_load_post — same pattern as
refresh_post_commit's existing `import bonsai.bim.handler` — which
sidesteps the tool.parametric -> bim.module.model.preview_base ->
bonsai.tool registration-time cycle.
The forward-compat AST contract that pinned "every module-scope
GenerationKeyedCache + clear_caches MUST be drained on load_post"
follows the call site to its new home — the test now walks
tool.Parametric.on_load_post instead of _apply_save_file_invariants.
No behaviour change. 45/45 affected bim tests pass
(test_handler_forward_compat, test_preview_base,
test_wall_offset_gizmos, test_parametric_registry).
ruff + black clean on all touched files.
Generated with the assistance of an AI coding tool.
Three feature-specific decorators previously lived in
bim/module/model/decorator.py despite owning state only their
home module reads:
* ArrayPreviewDecorator + ArraySelectionHighlightDecorator +
draw_array_layer_children_bbox -> array.py (read array
edit-state props and walk BBIM_Array psets)
* WallGizmoPreviewDecorator + draw_wall_partner_bbox -> wall.py
(dereference wall.py-private classes and helpers via lazy
imports)
decorator.py keeps cross-cutting infrastructure
(BoundingBoxDecorator, SlabDirectionDecorator, WallAxisDecorator,
WallFilletPreviewDecorator, PolylineDecorator, ProductDecorator)
and the shared bbox primitives (bbox_world_edges,
draw_polyline_segments, _BBOX_EDGES, _stroke_lines_alpha,
_fill_quads_alpha) that several feature files now import.
handler.py and gizmos.py update their import paths; the
wall-feature lazy imports inside WallGizmoPreviewDecorator
methods collapse to direct references now that the decorator
lives in wall.py.
No behaviour change. Wall lane 37/37, array lane 15/15, wall
forward-compat 6/6, parametric-registry 8/8 still pass.
Generated with the assistance of an AI coding tool.
Three concerns sharing the same architectural theme (collapse inline
bbox / edit-state lookups, drop overrides that re-do base-class work):
== Bbox helpers and array operator DRY ==
* tool/blender.py: add a "dimensions" tuple key to both
get_object_bounding_box and get_object_world_bounding_box return
dicts. The (max - min) per-axis extent — which callers previously
computed via local helpers — is now a key alongside min_x / max_x
/ min_point / max_point / center. Distinct from Blender's built-in
obj.dimensions (which folds object-level scale): the local variant
is the intrinsic mesh bbox extent; the world variant is the
matrix_world-applied AABB.
* bim/module/model/array.py: drop the local _bbox_dims helper; the
two callers now read tool.Blender.get_object_bounding_box["dimensions"]
directly.
* Rename _parent_geometry_changed -> _array_children_need_rebuild.
The old name suggested "did the parent change just now", implying
the function was a parent-edit-finish trigger. It actually runs
only inside the array-edit-finish path as a drift safety net (the
upstream-deliberate design — see commit 83d97d7e9 "Fix #7616. Make
regenerate array an operator instead of an array preference" —
means the array doesn't auto-regen when its parent geometry edits
finish). New name matches the call-site phrasing
``if X: _wipe_array_children(layers)`` and clarifies that this is
a children-state check, not a parent-edit trigger.
* Extract _resolve_array_edit_props(context) — returns the active
object's array props during an active edit lifecycle, or None.
Collapses the obj-active-then-is-editing prologue (3 lines + return)
to one resolver call across 4 sites: ToggleArrayMethod.execute,
AdjustArrayCount.execute, RemoveArrayLayerFromEdit._execute and
.poll. Each call site shrinks from 7 lines to 3.
* Migrate two inline bbox reads inside GizmoArrayEdition to the new
dict keys: get_axis_world_face_center collapses the manual
xs/ys/zs min/max + center math to bbox["center"] + bbox["max_x"] /
["max_y"] / ["max_z"]; get_element_height collapses
``max(corner[2] for corner in obj.bound_box)`` to
tool.Blender.get_object_bounding_box(obj)["max_z"].
The _BBOX_EQUALITY_EPS = 1e-5 tolerance stays inline as a single-
consumer constant — no other call site needs tolerance-equality on
dimension tuples, so extracting it to a shared util would be
speculative abstraction.
== Drop dead code ==
* GizmoArrayEdition.update_editing_gizmos override + its
_has_other_parametric_type helper: redundant with
hide_pen_button = True at line 1024. The base class already hides
the pen in every idle case (when hide_pen_button is truthy) AND in
every editing case (unconditionally). The override's conditional
hide-when-parametric only re-hid a pen that was already hidden in
both branches. Removes the only remaining path that could re-show
the array's pen icon; array-edit entry is now uniformly via the
per-layer ARRAY icons (which is the documented preferred
affordance, see the hide_pen_button comment).
* _wall_fillet_preview_active in wall.py: defined but never called.
_wall_fillet_props (the sibling thin-wrapper around
preview_base.get_preview_props) is heavily used; the
is_preview_active wrapper was added speculatively and never picked
up a consumer.
Generated with the assistance of an AI coding tool.
Bundled bug fixes + the forward-compat AST guard that prevents the
underlying class of bug from coming back.
* bim/module/model/wall.py: FinishEditingWall._execute early-returns
CANCELLED when props.is_editing is False. Without this guard, a
failed enable (e.g. on a wall without IfcMaterialLayerSetUsage)
leaves is_editing False but a press on finish still walked the
sub-ops below, which dereferenced layer-set-dependent state and
crashed.
* tool/model.py: Model.offset_wall now guards against
ifcopenshell.util.element.get_material returning None before
calling .is_a("IfcMaterialLayerSetUsage"). Fixes the pre-existing
test/bim/module/model/test_wall_header_refresh.py crash that has
been the only failing test in the wall lane since this branch
started.
* bim/handler.py: _apply_save_file_invariants drains
wall_offset_gizmos.clear_caches() on load_post. The module-scope
GenerationKeyedCache instance survives the .blend reload; without
the drain the cache may serve entries whose bpy_struct references
point into the freed bpy.data of the previous file.
* test/bim/test_handler_forward_compat.py: AST-walk test that
enumerates every bim/module/model/*.py source declaring both a
module-scope GenerationKeyedCache assignment AND a top-level
clear_caches function, and asserts each module appears as a
<module>.clear_caches() call in _apply_save_file_invariants. Pins
the contract: any future module-scope geom cache that exposes
clear_caches must wire into the load_post drain.
* test/bim/feature/model.feature + test/bim/test_feature.py: wall
edit-lifecycle scenarios switch from "add cube + assign as
IfcWallType" to "load the demo construction library + add an
occurrence of the WAL100 wall type", so the parametric edit runs
against a real LAYER2 wall with IfcMaterialLayerSetUsage rather
than a vanilla-mesh promotion that lacks one. The demo-library
step also picks the schema-matching library file (IFC2X3 /
IFC4 / IFC4X3) so the appended types remain valid across schemas.
Door saved-height assertion updates from 2.5 → 2500 to reflect
that BBIM_Door pset stores project units (METRIC_MM in the
empty-project fixture).
Generated with the assistance of an AI coding tool.
Fixes builds with newer GCC/libstdc++ that no longer provide <cstdint>,
<cstring>, <cfloat>, <memory>, <algorithm> etc. transitively. Also
disambiguates visit<> calls in taxonomy.h with the full namespace and
casts the character value in IfcCharacterDecoder to uint32_t to silence
ambiguous overload warnings.
When HDF5 is found via its CMake config file, the code previously hardcoded
the hdf5_cpp-static target. On distributions that ship only shared HDF5
(e.g. Fedora rawhide where the config file was added in a newer package),
this caused a link failure. Now checks for hdf5_cpp-static, hdf5_cpp-shared,
and hdf5::hdf5_cpp-shared in order, falling back to module-mode discovery.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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>
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.
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.
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.
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.
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.
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.
When validate_type selected a preferred_item from remaining_items
(e.g. the sole IfcBooleanResult in a representation), it left that
item in the list. The subsequent Items filter removed every item,
leaving Items=[] and causing guess_type to return
"MappedRepresentation" — silently corrupting the representation.
Also removes temporary debug print statements added during
investigation of the wall-to-slab extension workflow.
Generated with the assistance of an AI coding tool.
Re-running the operator on the same wall/slab pair created
additional IfcPolygonalFaceSet booleans each time. Now each
wall's existing booleans are removed before re-clipping, and
previously connected slabs are merged with the new selection
so no earlier clips are silently discarded.
Generated with the assistance of an AI coding tool.
When Shift+G is pressed on a LAYER3 element, any LAYER2 walls
connected via IfcRelConnectsElements(TOP) are now re-clipped
to the slab's updated geometry after recalculate_slab runs.
Generated with the assistance of an AI coding tool.
extend_walls_to_underside now accepts multiple slab/roof
objects in a single operation — all selected non-LAYER2 IFC
elements are treated as clip targets, all LAYER2 elements as
walls. Placement sync is done once upfront; each wall is then
clipped against every selected slab before reloading.
Also adds bim.regenerate_wall_to_underside (Shift+G): after
moving a slab, re-clips connected walls using the existing
IfcRelConnectsElements(TOP) relationship. Old booleans are
removed via remove_representation_item before re-clipping.
Generated with the assistance of an AI coding tool.
When extend_walls_to_underside is applied to a wall and the
roof/slab is later moved, pressing Shift+G now re-clips the
wall to the slab's new position.
The IFC relationship created by connect_wall_to_slab
(IfcRelConnectsElements, Description="TOP") is used to look
up which slabs a wall is clipped to. On regeneration, the
existing manual booleans (IfcPolygonalFaceSet operands) are
cleanly removed via remove_representation_item, then
clip_wall_to_slab is re-applied for each connected slab.
Shift+G on a LAYER2 wall that has a TOP connection now calls
bim.regenerate_wall_to_underside; walls without a connection
continue to call bim.recalculate_wall as before.
Generated with the assistance of an AI coding tool.
When the operator was called twice on the same wall for a
ridge roof, the two IfcPolygonalFaceSet clip solids shared
an exact ridge edge (kissing-solid). OCCT produced spurious
extra vertices at the coincident boundary.
Fix by building the clip solid from a rectangle on the slope
plane that extends slightly past the face edge (1 project
unit margin) rather than the exact face footprint. Adjacent
slope solids now volumetrically overlap at the ridge instead
of sharing a boundary face, which OCCT handles correctly.
Generated with the assistance of an AI coding tool.
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.
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.
ifc.get_object(element) can return None for IFC elements that aren't loaded as Blender objects (e.g., decomposed sub-elements).
The loop now skips those instead of passing None into collector.assign().
Cheers!
The temporary-offset workaround (#7408, commit bd57cc8735) subtracts the
directrix centroid (`mean`) from the curve points before building the
sweep near the origin, then must add it back to restore the original
location. The restore negated the sign — `Move(-mean)` instead of
`Move(+mean)` — placing the swept solid at -mean (mirrored through the
origin) rather than its true position.
Only triggers for polyline directrixes (`is_polyhedron()`) whose centroid
is more than 100 m from the origin (`mean.norm() > 1e2`), so models
centered near the origin are unaffected. Models that keep absolute site
coordinates (e.g. many Revit/ODA IFC exports) render affected swept
solids — reinforcing bars, pipes — at a mirrored phantom location far
from the rest of the model.
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.
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.
Three test files covering PR4's new surfaces — preview registry,
wall-gizmo poll behaviour, fillet operator registration. Every test
walks the live registry or class hierarchy instead of hard-coding
preview keys, operator names, or helper function names, so adding a
new preview / wall gizmo group / fillet operator exercises the same
invariants without test edits.
test_preview_base.py (6 tests):
* RegistryContract: every PREVIEW_CANCEL_OPS entry resolves to a
callable cancel operator on bpy.ops.bim.
* GetPreviewPropsTolerance: get_preview_props returns None for
contexts without a scene (regression guard for the SimpleNamespace
bug fixed in commit ee63137c6).
* ActivationCycle (registry-driven loop): any_preview_active toggles
with each registered preview's is_active flag;
discard_pending_previews clears every active flag across every
registered preview.
* SaveOnDiscardWired: locates the bim.save_project operator
dynamically and verifies its execute path references the discard
helper by its actual __name__.
test_wall_gizmo_poll_gate.py (4 tests):
* WallGizmoGroupsHideDuringPreview: walks the wall module for
bpy.types.GizmoGroup subclasses (skips preview-owner exceptions
whose bl_idname contains 'preview'), mocks any_preview_active to
True, and asserts every discovered gizmo's poll returns False.
* BaseParametricGizmoPollHidesDuringPreview: mirrors the test for
the cross-feature parametric framework base class.
test_fillet_operators.py (3 tests):
* FilletOperatorsRegistered: at-least-four-ops + every-discovered-op-
is-callable. Catches accidental deregistration.
* EnableRejectsIneligibleSelection: poll returns False without a
selection so the operator is greyed-out in menus.
State-clearing tests via bpy.ops.bim.cancel_wall_fillet_preview() are
deliberately omitted — the operator early-returns when context.screen
is unattached and prior tests in the model lane can leave the screen
in that state, making the dispatch path inherently flaky. Live testing
covers the behaviour.
Net: 13 tests pass cleanly in both single-file and full model lane.
Generated with the assistance of an AI coding tool.
deepdiff 9.1.0 added cachebox<6,>=5.2 as a direct runtime dep.
cachebox 5.2.3 only publishes macOS x86_64 wheels for macosx_10_12+,
incompatible with the macos py311 build's --platform macosx_10_10_x86_64.
The daily build's linux-wheel safeguard fires when the resulting
cachebox-*-manylinux_*.whl leaks into the macOS / windows wheels folder
(builds run on ubuntu-latest and cross-build via pip download --platform).
Pin deepdiff to <9.1 (resolves to 9.0.0, no cachebox transitive dep) as
the minimal hotfix. Long-term cleanup: bump the macos py311 platform tag
from 10_10 to 10_13 (matching py312/py313) and re-flag this line with the
standard \$(PYPI_PLATFORM) --only-binary=:all: pattern used by brickschema
and python-socketio.
Partly generated with the assistance of an AI coding tool.
Save-path:
* SaveProject._execute (project/operator.py) now calls
preview_base.discard_pending_previews(context.scene) right after
tool.Parametric.commit_pending_edits(). Previews are session-
transient — discard rather than commit. Sibling gizmo polls gate
on each preview's is_active flag; a stuck flag persisted through
the save would silently hide them on reload. Mirrors the pattern
already in gizmos-8088.
Preview-active gate hardening:
* preview_base.get_preview_props tolerates contexts without a
``scene`` attribute. Pre-existing tests use SimpleNamespace mocks
for the context; the previous getattr(context.scene, ...) raised
AttributeError before the inner default kicked in.
Test update:
* test_wall_header_refresh.test_geom_generation_invalidates_wall_geom_cache
patches tool.Wall.read_geometry instead of the now-deleted local
wall._read_wall_geometry (commit 7e5e7b8d6 migrated the call site).
Generated with the assistance of an AI coding tool.
When override_mode_set_edit encounters an unsupported profile (Couldn't
import profile), deselect the object so Tab continues to cycle cleanly.
Also restores the selection that existed before entering aggregate mode
when finally tabbing out, via save/restore_previous_selection().
Following the pattern from 586f9be077, deselect the active object after
exiting item mode so Tab continues to cycle cleanly. Also deselects
parametric LAYER1/LAYER2 items that cannot be edited directly, avoiding
the need to manually deselect before Tab-cycling out of aggregate mode.
Two cohesive cleanups in one commit.
A. Migrate wall.py to PR3-absorbed tool methods (fixes bug 4: pen icon
missing on fillet corner walls):
PR3 shipped tool.Wall.read_geometry + tool.Wall.validate_for_parametric_edit
but wall.py kept local duplicates predating that work. The local
_read_wall_geometry guards on tool.Blender.Modifier.is_wall (LAYER2-only)
while the tool method guards on tool.Parametric.is_path_connectable_wall
(LAYER2 OR fillet corner). Consequence: _get_wall_geom_cached → local
_read_wall_geometry returned None for every fillet corner →
GizmoWallFilletReedit.position_gizmos hit `if geom is None: hide` →
pen icon was unreachable for every fillet corner the user created.
Three _read_wall_geometry callers migrated to tool.Wall.read_geometry
(_read_wall_state_into_props, _get_wall_geom_cached,
GizmoWallJoinIntersection.position_gizmos). Two
_validate_wall_for_parametric_edit callers migrated to
tool.Wall.validate_for_parametric_edit (_maybe_resync_wall_props_from_ifc,
EnableEditingWall._execute). Local helpers deleted; docstring references
updated.
B. Drop over-restrictive gizmo gates (fixes bug 1: join icons missing
when walls intersect away from endpoints):
GizmoWallJoinIntersection.position_gizmos no longer hides itself when
the projected intersection lands further than MAX_DISTANCE_TO_ENDPOINT_
FACTOR (0.75 wall lengths) from any endpoint. The remaining
PARALLEL_DOT_THRESHOLD (cos 2°) gate via project_axis_intersection
returns None for near-parallel walls and is the only correctness bound;
distance from endpoints is a UI concern, not a geometric one.
GizmoWallFilletReedit.poll drops the has_a / has_b ConnectedFrom +
ConnectedTo guard — the IsFilletCorner pset is the authoritative signal.
EnableWallFilletPreviewFromCorner.execute already separately validates
both neighbour connections and reports a user-facing error if either
side is disconnected.
Generated with the assistance of an AI coding tool.
Three live-session regressions surfaced after the fillet feature
landed.
Sister gizmos competed with the active preview:
* preview_base.any_preview_active(context): new helper iterates the
PREVIEW_CANCEL_OPS registry and returns True if any preview is open.
Future previews registered there automatically gate sister gizmos.
* BaseParametricGizmoGroup.poll (gizmos.py): short-circuits on
any_preview_active so every parametric gizmo (door/window/stair/
roof/railing/wall edition) hides during ANY preview.
* The 4 wall gizmo groups with explicit polls (GizmoWallAddOpening,
GizmoWallExtendVertically, GizmoWallJoinIntersection,
GizmoWallUnjoinSingle) + GizmoWallFilletReedit gain the same gate.
DRY: extract _wall_gizmo_poll_gate(context):
* 5 wall gizmo polls each duplicated the 2 pre-flight checks
(viewport-gizmos enabled + no preview active). The helper centralises
them — each poll becomes a single short-circuit line followed by its
per-feature selection inspection.
ESC cancels the active preview:
* try_cancel_active_preview already existed in preview_base since PR3
but had no caller. Hooked into OverrideEscape.execute (geometry/
operator.py) as a new elif branch — same keymap that already cancels
pen gizmo edit mode + item mode + edit mode + aggregate mode. Order
in the branch chain matters: try preview cancel before falling back
to try_canceling_editing_modifier_parameters_or_path so the in-
flight preview wins over a stale modifier-edit cancel attempt.
Generated with the assistance of an AI coding tool.
In BIM_PT_type_attributes and BIM_PT_object_attributes (when
the active object is a type), attribute value buttons now use
"type.<Attr>" as the selector key so the operator finds
matching occurrences via their relating type rather than the
occurrence's own (often unset) attributes.
Generated with the assistance of an AI coding tool.
After selecting objects in the same container, copy a `location="Name"`
filter query to the clipboard and report it — consistent with the same
behaviour in SelectSimilarType, SelectSimilarAggregate, SelectIfcClass,
and SelectSimilarMaterial.
Generated with the assistance of an AI coding tool.
When a `+`-separated filter group returns no results, `FacetTransformer.facet_list`
was skipping the reset of `has_additive_facet_in_current_list` because the reset
was inside the `if self.elements:` guard. The stale flag caused the next group's
`add_default_elements()` to bail out early, leaving its element set empty and
silently dropping every subsequent group from the result.
Move the flag reset outside the guard so it always fires regardless of whether
the group produced any results.
In the Placement panel, show Location and Rotation X/Y/Z
each on their own row beneath a header label. When the IFC
file uses imperial units, display a read-only feet-and-inches
label alongside each Location input field.
Generated with the assistance of an AI coding tool.
Display general and calculated stair parameters (Width,
Height, Tread Run, Tread Rise, Length, etc.) formatted
to the IFC file's configured length unit rather than
raw numeric values.
Generated with the assistance of an AI coding tool.
End-to-end fillet flow on top of the helpers + recreate_wall hook
(landed in the previous commit). Users select two LAYER2 walls, click
the fillet entry icon, drag the live radius widget, and validate to
replace the corner with a curved LAYER2 corner wall (banana body).
Operators (5):
* EnableWallFilletPreview: 2-wall selection → validates LAYER2 +
straight axis + zero-slope + intersect-or-joined state → seeds the
preview props with a default radius computed from the shorter
available leg.
* FinishWallFilletPreview: dispatches CreateWallFillet with the tuned
radius; clears preview state on FINISHED, preserves it on failure so
the user can re-tune without re-selecting.
* CancelWallFilletPreview: clears preview state, no IFC mutation.
* EnableWallFilletPreviewFromCorner: pen-icon re-edit on an existing
fillet corner — pre-fills the preview from the corner's BBIM_Wall
pset + walks the inverse graph to recover wall A and wall B.
* CreateWallFillet: deletes any prior corner + A↔B path connection,
shortens A and B to the tangent points, instantiates a corner wall
from A's type, unassigns the swept-layer material/type (the explicit
banana body MUST own its geometry), assigns the dominant material,
rebuilds the body, sets a straight 2-point chord axis, stores
BBIM_Wall.IsFilletCorner+FilletRadius, reconnects A and B to the
corner with NOTDEFINED on the corner's side.
Gizmo groups (2 new + entry icon on existing):
* GizmoWallFilletPreview: visible while a preview is active. Bundles
a radius_dim widget at the arc apex, a trim_dim widget along wall A
expressing the same DOF via the leg setback distance
(trim = |radius| * tan(sweep/2)), and validate / cancel icons
anchored above the apex in screen-up.
* GizmoWallFilletReedit: pen-icon entry on an existing fillet corner
wall (single-selection, BBIM_Wall.IsFilletCorner set, both neighbour
connections present). Mutually exclusive with an active preview.
* GizmoWallJoinIntersection now stacks a fillet entry icon
(VIEW3D_GT_fillet → bim.enable_wall_fillet_preview) above the
existing join/unjoin icon in the joined and intersect state branches.
Property + decorator infrastructure:
* prop.py: BIMWallFilletPreviewProperties (Scene-level draft) +
BIMPreviewProperties umbrella with only the wall_fillet pointer.
The umbrella is the seam preview_base.py (landed in PR3) already
reads via getattr(scene, "BIMPreviewProperties", None).
* decorator.py: _stroke_lines_alpha helper + WallFilletPreviewDecorator.
Polls is_active; renders leg projections + arc + arc-center
construction lines from tool.Wall.compute_wall_fillet_geometry.
* __init__.py: registers operators + gizmo groups + property groups +
wires Scene.BIMPreviewProperties.
* handler.py: WallFilletPreviewDecorator.install/uninstall in
_install_decorators — always installed, self-polls on is_active.
Drive-by: extract gizmo.get_screen_up(billboard_rot) helper —
the local +Y of a billboard rotation is the camera's screen-up world
direction. Replaces 4 inline `billboard_rot @ Vector((0.0, 1.0, 0.0))`
sites added across the fillet feature's gizmo groups.
Generated with the assistance of an AI coding tool.
Eleven module-level helpers in wall.py that the upcoming wall-fillet
operators + gizmo groups depend on. Each is self-contained or
references only helpers earlier in the file; the operators and
gizmos themselves land in follow-up commits.
* _wall_fillet_props / _wall_fillet_preview_active /
_wall_fillet_preview_walls: thin read-side accessors over the
BIMPreviewProperties.wall_fillet pointer (added with the
operators commit). Safe today: get_preview_props returns None
until the pointer is attached.
* _walls_have_zero_slope_for_fillet: validates that input walls
are vertical (x_angle ~ 0); slanted-extrusion fillets require
swept-along-curve geometry the banana profile builder doesn't
support.
* _build_curved_corner_body_representation: builds the banana
(annular sector) IfcExtrudedAreaSolid as a polyline-tessellated
IfcIndexedPolyCurve.
* _apply_fillet_corner_geometry: positions the corner wall at
tangent_a and rebuilds its body. Shared by the creation operator
and the regenerate path.
* _resolve_two_walls: pulls (active, other) from a 2-wall
selection, validates both as LAYER2 + straight-axis + not-already-
a-fillet-corner.
* _pick_dominant_wall_material: returns the thickest layer's
material from an element's IfcMaterialLayerSet / Usage.
* regenerate_fillet_corner_wall: re-runs the geometry build from
BBIM_Wall.FilletRadius + current neighbour layer parameters.
Called by tool.Model.recreate_wall when the IsFilletCorner pset
is set; the FIXME(PR4) placeholder in recreate_wall is dropped.
* _wall_fillet_gizmo_x_matrix: 4x4 placement matrix with local +X
aligned to a world-space direction; used by the fillet preview
gizmo group.
Centralises the IsFilletCorner pset read as
tool.Parametric.is_fillet_corner_wall — replaces 3 inline
get_pset(element, "BBIM_Wall", "IsFilletCorner") sites
(tool.Model.recreate_wall, tool.Model.recalculate_walls,
tool.Parametric.is_path_connectable_wall) plus the new
_resolve_two_walls call.
Generated with the assistance of an AI coding tool.
The framework's parametric-edit icon row currently binds an array
icon to bim.add_array_from_feature_edit, but the supporting per-
feature add-array flow and gizmo positioning haven't fully landed.
Showing the icon today lets the user click it and trigger a half-
wired flow.
Force the icon hidden inside the props.is_editing branch of
BaseParametricGizmoGroup.update_editing_gizmos. The else-branch
(not editing) already hides it, so this just mirrors that behavior
during edit mode. Drop this gate when array integration completes
to re-enable the icon position + visibility plumbing.
Generated with the assistance of an AI coding tool.
The extend-X / extend-Z icons in GizmoWallEdition's cursor row are
billboarded toward the camera; without orientation polish they
always point in the same screen-space direction regardless of which
wall endpoint the click will move (or whether the cursor sits above
or below the wall top). New helper mirrors the icon's local-X (extend-X)
or local-Y (extend-Z) axis so each arrow points toward the end it
will move:
* Extend-X: walk wall midpoint to figure out which endpoint stays
fixed (cursor past midpoint → ATSTART stays; cursor before midpoint
→ ATEND stays). Project the fixed endpoint into screen-space and
flip the arrow when the gizmo's anchor sits on the same side.
* Extend-Z: flip when the cursor is below the wall top (within
EXTEND_FLIP_EPSILON tolerance).
Called once per resolved cursor gizmo from
``GizmoWallEdition._update_cursor_gizmos``, after the gizmo's
``matrix_basis`` is set by ``gizmo.billboarded_at``. Reuses
``gizmo.should_flip_extend_arrow`` + ``EXTEND_FLIP_MIRROR_X/Y`` +
``EXTEND_FLIP_EPSILON`` already on tool.
Generated with the assistance of an AI coding tool.
GizmoWallJoinIntersection's unjoin only fires when exactly two walls
are selected and surfaces one icon at their shared corner — useless
when the wall has 3+ joins and the user wants to disconnect just one.
* UnjoinWallPathConnection: surgical counterpart to UnjoinWalls.
Disconnects the active wall from a single partner wall identified
by IFC GlobalId (invariant under Blender-object renames + file
save/reload + undo). Walks both inverse arrays of the active wall
for the specific IfcRelConnectsPathElements joining the pair —
matches DumbWallJoiner.split's pattern and avoids disconnect_path's
direction-sensitivity. Resyncs both walls' draft props after the
recreate_wall pass.
* GizmoWallUnjoinSingle: activates on exactly-one selected
LAYER2 wall. Preallocates a pool of 16 unjoin icons (Blender forbids
gizmo allocation outside setup(); ATSTART + ATEND + ATPATH rels are
rarely more than a handful). Per-frame, iterates _iter_path_connections,
positions one billboarded icon at each join via
tool.Wall.path_connection_location_world, and hides the rest. Each
visible icon's bound operator carries the partner GlobalId, so a
click removes only that one rel.
* model/__init__.py: register both classes alphabetically.
Mutually exclusive with GizmoWallJoinIntersection via poll() — that
group requires len(selected) == 2; this one requires 1.
Generated with the assistance of an AI coding tool.
The single-wall unjoin gizmo needs to enumerate every
IfcRelConnectsPathElements a wall participates in, regardless of which
side of the rel the wall was authored on, and place an icon at each
join's physical location. Two helpers carry that work:
_path_connection_location_world wraps core.compute_path_connection_location
at the Vector boundary. _iter_path_connections walks ConnectedTo +
ConnectedFrom, normalises orientation to (other, self_ct, other_ct),
and filters non-wall partners + None refs so per-frame gizmo positioning
survives malformed IFC.
Generated with the assistance of an AI coding tool.
After a one-shot wall IFC mutation (unjoin / split / merge / extend /
join-at-corner …) the always-visible gizmos on the OTHER side of the
join can be left reading stale ``BIMWallProperties`` — the IFC
geometry moved but the draft props that drive the gizmo handles still
point at the pre-mutation numbers, so a subsequent edit-mode enter
shows the wall at its old length / position.
* New ``_maybe_resync_wall_props_from_ifc(obj)``: re-primes a single
wall's draft props from current IFC, with guards for non-walls,
non-parametric walls, and walls in an active draft session (the
draft is then the source of truth, not IFC). Must run from an
operator ``_execute`` — ID writes from gizmo refresh raise.
* New ``_resync_walls_after_mutation(objs)``: iterates the above
across a selection.
* Six existing mutation operators gain a resync call after their
``core.*`` / ``DumbWallJoiner`` mutation completes:
UnjoinWalls, ExtendWallsToUnderside, ExtendWallsToWall, SplitWall,
MergeWall, JoinWallsIntersection. MergeWall resyncs only the
surviving wall — the active wall is the deletion target.
Generated with the assistance of an AI coding tool.
Bundle of bugs surfaced when exercising the new gizmo framework
end-to-end in a live Blender session after the
bim/module/drawing/gizmos.py refactor + TypeAccessor/CycleType/PickType
mixins landed.
Register / annotation resolution
* parametric_lifecycle.py: hoist `entity_instance` import out of
TYPE_CHECKING so typing.get_type_hints resolves the
Callable[[entity_instance], bool] annotation at operator registration
(CycleDoorType, CycleWindowType, CycleStairType failed with NameError).
Clarify the INTERFACE return contract on the picker entry-point so
readers see why the gizmo step stays off the undo stack.
Framework callable contracts
* model/wall.py, door.py, window.py, stair.py: migrate `props_getter`
and `element_checker` from bl_idname strings to bound classmethods
on tool.Model / tool.Parametric. BaseParametricGizmoGroup.get_props
expects a callable; the string form raised TypeError on first
gizmo poll.
* model/door.py, model/stair.py: drop the dead `prop_path=` operator
kwarg from create_arc_gizmo / create_icon_gizmo call sites. The
framework helper blindly setattrs every kwarg onto the operator's
OperatorProperties, but ToggleDoorSwing / ToggleStairProperty don't
declare prop_path — the setattr raised mid-setup_element_specific_gizmos,
so self.gizmo_door_type / self.lock_gizmo never got assigned and
every subsequent draw_prepare tornadoed AttributeError. Nothing
reads op.prop_path anywhere; the kwarg was dead data.
Dispatcher operators
* model/array.py: add EnableEditingParametric (the framework pen-icon
dispatcher that routes to a per-feature edit operator by bl_idname
string) and AddArrayFromFeatureEdit (binds the framework's array
icon to bim.add_array on the current parametric draft).
* model/__init__.py: register both new operators.
Per-frame robustness
* drawing/gizmos.py: guard BaseParametricGizmoGroup.draw_prepare with
is_setup_complete() — matches the existing guard in refresh() and
in BaseSchematicGizmoGroup.draw_prepare(). Defense-in-depth: when
any subclass's setup raises mid-way, draw_prepare now no-ops cleanly
instead of per-frame AttributeError-tornadoing on whatever attribute
the failed setup phase was meant to populate.
* model/decorator.py: guard ProfileDecorator.__call__ against
context.active_object is None. The decorator is a per-frame
viewport draw handler; deselecting or deleting the active object
while it's installed crashed on obj.mode access. Treat None the
same as "no longer in edit mode" — uninstall + fire the exit
callback if present.
* geometry/data.py: ViewportData.load() populates `data` before
flipping `is_loaded`, so a raise from cls.mode() no longer leaves
the class flag-set but data-empty for subsequent reads.
Generated with the assistance of an AI coding tool.
Three concerns bundled into one cohesive refactor of gizmos.py
(splitting them surgically requires intermediate commits with
duplicate same-named classes that Python can't parse):
1. Framework primitives — StaticTrisGizmoMixin + TexturedQuadGizmoMixin
replace the older TrisGizmoMixin. New module-level helpers:
_get_static_tris_shader / _get_static_tris_batch / clear_static_
tris_cache for cached GPU batch reuse, _draw_outline_and_body for
the shared outline-then-body render path, draw_tris_with_outline
as the public wrapper. billboarded_at(world_pos, billboard_rot,
scale) is the canonical billboard-matrix helper; should_flip_extend_
arrow encapsulates the view-aware mirror decision for extend
gizmos; get_warning_color_from_prefs reads the user's warning
color.
2. Config classes — BaseValueGizmoConfig (shared visibility + dimension-
text contract), CountGizmoConfig (array N indicator),
DimensionGizmoConfig (length / height / depth labels), IconActionConfig
(icon-only gizmos that invoke an operator on click). DimensionRenderer
draws the actual numeric label using BLF.
3. Icon classes — each rewritten on StaticTrisGizmoMixin so they share
the cached GPU batch + outline-then-body render path:
GizmoLockOpen / GizmoLockClosed (replacing the single-state
GizmoLock), GizmoArc, GizmoFillet, GizmoWallCornerIcon,
GizmoWallTeeIcon, GizmoPen / GizmoValidate / GizmoCancel (the
parametric-edit triad), GizmoPlus / GizmoMinus / GizmoTrash,
GizmoArrayParent / GizmoArrayAll / GizmoArrayLayerIndicator (array
context indicators with a small digit-rendering helper for the "xN"
count label), GizmoMerge / GizmoSplit / GizmoUnjoin (wall-join
icons), and GizmoMenu (textured-quad icon-action menu trigger).
The legacy TrisGizmoMixin, GizmoLock, and DimensionDrawConfig are
removed; downstream callers in subsequent PR4 commits swap to the
new mixin and config classes when their feature operators land.
CycleTypeMixin / PickTypeMixin / TypeAccessorBase live in
bim.parametric_lifecycle (previous commit). The three mixins are
re-exported from gizmos.py here so feature-module access via
``gizmo.<MixinName>`` keeps working until PR5 cleanup drops the
re-exports.
bim/module/drawing/__init__.py is updated in the same commit to
register the 11 new gizmo classes (GizmoLockOpen / GizmoLockClosed /
GizmoFillet / GizmoWallCornerIcon / GizmoWallTeeIcon / GizmoTrash /
GizmoArrayParent / GizmoArrayAll / GizmoArrayLayerIndicator /
GizmoUnjoin / GizmoMenu) — without that, the new classes exist in
gizmos.py but aren't usable as bpy gizmo types.
Generated with the assistance of an AI coding tool.
Three operator mixins for type-selection ops on parametric features
(door type-cycle, window type-pick, stair type-cycle, railing
type-pick, roof type-cycle, etc.). Each shares the same contract:
* ``element_checker`` validates the active object is the expected
IFC type
* ``props_getter`` resolves the BIM<Name>Properties group
* ``type_literal`` is the Literal type whose args drive the enum
* ``type_attr`` is the PropertyGroup field to read/write
* ``skip_element_check=True`` bypasses element validation (for
operators that target a non-IFC context)
CycleTypeMixin shift-click reverses direction (forward by default).
PickTypeMixin opens a popup menu and routes the picked value
through execute() so F6 redo / EXEC_DEFAULT reach the apply path.
The PickType modal-handler dance waits for LEFTMOUSE release before
opening the menu when invoked mid-click (e.g. from a gizmo's
target_set_operator) so Blender's drag-through-pick gesture doesn't
commit an accidental item.
Ships standalone — the next commit's gizmos.py framework refactor
re-exports these names from bonsai.bim.parametric_lifecycle so
gizmo modules can spell ``gizmo.CycleTypeMixin`` / ``gizmo.PickTypeMixin``.
Concrete operator subclasses land in subsequent PR4 commits per
feature (door / window / stair / railing / roof).
Generated with the assistance of an AI coding tool.
2026-05-27 23:06:42 +02:00
625 changed files with 50548 additions and 6888 deletions
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -33,5 +33,13 @@ DATA;
#26=IFCSIMPLEPROPERTYTEMPLATE('2iwERDOW55Pf4hCbuFRe1Q',$,'FillMode','Method to fill areas seen in projection',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('1YF$qLzBzF19Io8aB2N8cE',$,'CutMode','Method for cutting geometry',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#29=IFCSIMPLEPROPERTYTEMPLATE('0lP6Y8q9v2QhDnR4sT7uVx',$,'PerspectiveShiftX','Horizontal perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
#30=IFCSIMPLEPROPERTYTEMPLATE('2mR8b1NcW5EoFyG7hJ9kLp',$,'PerspectiveShiftY','Vertical perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
#31=IFCSIMPLEPROPERTYTEMPLATE('1cFVJnqT13m8ItkMHaI1tp',$,'UseEdgeClassification','Enable the boundary/outline/sharp/crease/flush SVG edge classification scheme (issue #3668). When false, drawings use the original unclassified linework.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#32=IFCSIMPLEPROPERTYTEMPLATE('2kB$mxBgnBUvhjh0Ti0c4P',$,'RenderCreases','Whether to render ''crease'' (concave) edges. Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#33=IFCSIMPLEPROPERTYTEMPLATE('3MSIJNW$T8r9Hl12kk0BY$',$,'ValleyAngleMinDegrees','Minimum concave dihedral deviation from flat, in degrees, for a projection edge to be classified as ''crease'' rather than ''flush''.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
#34=IFCSIMPLEPROPERTYTEMPLATE('2epSGfC4bFM9gb1X7zBIp4',$,'RenderSharp','Whether to render ''sharp'' (convex) edges. Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#35=IFCSIMPLEPROPERTYTEMPLATE('3TZwsEjkr5WRDKcgrYzSIA',$,'RidgeAngleMinDegrees','Minimum convex dihedral deviation from flat, in degrees, for a projection edge to be classified as ''sharp'' rather than ''flush''.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
#36=IFCSIMPLEPROPERTYTEMPLATE('2Jua$lO754vgZOkBoHM2gA',$,'RenderFlush','Whether to render ''flush'' edges (dihedral deviation below both ridge/valley thresholds). Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.