remove_representation took its type-level branch for any mapped representation,
which no-ops when the map is not one of the element type's RepresentationMaps --
a floating/shared map (typical of Revit imports where the type's RepresentationMaps
is empty). The occurrence's mapped rep was then unremovable: unassign_representation
on the type finds no matching map, and remove_representation can't delete the
geometry still referenced by the floating map.
Take the type-level branch only when the object is the type itself, or the mapped
rep is genuinely anchored to the type (new tool.Geometry.is_mapped_representation_of_type).
A floating mapped rep now falls through to occurrence-level removal, which
unassigns it from just that occurrence and lets remove_deep2 GC the map once its
last user is gone. No change for local reps or type-anchored mapped reps.
Fixes#9216.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Linux "basic dependencies" omitted libeigen3-dev even though
ifcgeom requires Eigen3 (find_package Eigen3 REQUIRED) and the
cmake snippet already passes -DEIGEN_DIR=/usr/include/eigen3.
macOS Homebrew line already installs eigen.
Closes#6903
Generated with the assistance of an AI coding tool.
test_file's parametrize list was filtered with `sys.argv[1] in
os.path.basename(fn)`, reading the raw process argv instead of a
pytest-native option. Under a bare `pytest` invocation sys.argv[1] is
pytest's own first CLI token, never a match, so the 138-fixture EXPRESS
rule corpus in test/fixtures/rules collapses to an empty parametrize and
pytest reports it as a single skipped test rather than an error. Under
CI's actual invocation (pytest -p no:pytest-blender -n $NPROCS test ...)
sys.argv[1] is "-p", which happens to substring-match 47 of the 138
fixtures, so CI has been silently running a coincidental 34% slice of
the corpus with no signal anything was wrong.
Replaced the module-level list comprehension with a pytest_generate_tests
hook plus a --rule CLI option (added via a new test/conftest.py). This
runs the full corpus by default under any pytest invocation, still
allows filtering to one rule for local debugging via --rule, and no
longer collides with pytest's own argv.
Verified all 138 fixtures collect and pass under the fixed harness
(63 fail- fixtures each raise a violation, 75 pass- fixtures raise none).
Generated with the assistance of an AI coding tool.
mcp 2.0.0 (unpinned in CI and in the ifcmcp[mcp] extra) renamed
mcp.server.fastmcp.FastMCP to mcp.server.mcpserver.MCPServer, which
ifcmcp does not support yet. server.py caught the resulting
ModuleNotFoundError with a bare except Exception and silently
reported it as FastMCP not installed, masking the real breakage
until the ifcmcp test suite failed in CI.
Pinned mcp to >=1.0,<2 in both ci.yml and ifcmcp's pyproject.toml
mcp extra, confirmed the full ifcmcp test suite (70 tests) passes
against mcp 1.29.0, and confirmed the genuinely-not-installed path
still raises the expected ImportError. Also narrowed the except
clause to ImportError only so an unrelated future bug in that
import block surfaces instead of being swallowed as "not installed".
Generated with the assistance of an AI coding tool.
FullBufferImpl and PagedFileImpl both open the file and then use the handle
without ever testing it:
auto stream = _wfopen(fn_wide, L"rb"); // null when the file is missing
fseek(stream, 0, SEEK_END); // null goes straight to the CRT
buf_.resize((size_t)ftell(stream));
Opening a path that does not exist therefore hands a null FILE* to the CRT. On
MSVC that does not return an error: the runtime terminates the process
immediately (fastfail, exit code 0xC0000409). No exception is thrown, no stack
unwinding starts, so a caller cannot defend with try/catch — the host
application simply dies. On glibc it is undefined behaviour as well.
This is reachable through the ordinary entry point, because guess_file_type()
answers FT_IFCSPF for a path that does not exist (its own comment calls this
"just weird, but for consistency with earlier behaviour"), so a missing path
flows into the reader rather than being reported.
The fix is to leave the reader empty when the open fails. Both implementations
then behave like a zero-length file: size() is 0 and get() throws out_of_range
for any position, so the parse fails and IfcFile::good() reports it, which is
what a caller can actually handle. PagedFileImpl's destructor already tested
fp_ for null, so the possibility was known — only the constructor did not check.
Verified by reading a non-existent path through IfcParse::IfcFile: the
constructor returns and good() reports the failure, where before the process
died with 0xC0000409 and no output.
create(timestamp=0) computed the FILE_NAME timestring with
`d.get("timestamp") or time.time()`, which treats 0 (a legitimate
epoch timestamp) as unset because 0 is falsy. The header ended up
with the current wall-clock time in FILE_NAME while IFCOWNERHISTORY
correctly stored CreationDate=0, an inconsistent pair of dates in
the same file. Switched to an explicit None check so an explicit
timestamp of 0 is honoured the same way any other explicit
timestamp is.
Generated with the assistance of an AI coding tool.
The profile mapping builds its points as
profile_helper(m4, {
{{-x, -y}, {f2}},
...
where `f2` is a `double` and profile_point's second member is a
`boost::optional<double>`. In recent Boost (somewhere between 1.85 and 1.91)
optional's converting constructor became explicit, and an explicit constructor
cannot be used in copy-initialization — which is what a braced element is. So
every one of these call sites stops compiling:
MSVC 19.4x: error C2664: cannot convert argument 2 from
'initializer list' to 'const std::vector<profile_point>&'
clang-cl 22: error: chosen constructor is explicit in copy-initialization
Twelve translation units are affected (IfcCShapeProfileDef,
IfcIShapeProfileDef, IfcLShapeProfileDef, IfcTShapeProfileDef,
IfcUShapeProfileDef, IfcZShapeProfileDef, IfcAsymmetricIShapeProfileDef,
IfcCraneRailAShapeProfileDef, IfcRectangleProfileDef,
IfcRectangleHollowProfileDef, IfcRoundedRectangleProfileDef,
IfcTrapeziumProfileDef), roughly 100 call sites in total.
Adding one overload that takes the double directly fixes all of them without
touching a single call site, and changes nothing for existing code: the
optional overload still wins wherever an optional is passed.
Verified by building schemas 2x3;4;4x3_add2 with MSVC 2022 against Boost
1.91 and OCCT 7.9.3 — IfcParse, IfcGeom, the schema mappings and
geometry_kernel_opencascade all archive cleanly. Without this, the same build
against Boost 1.85 succeeds, which is what identified Boost as the variable.
Removing translate_obj_to_z_location from the existing-IfcSpace
regeneration branch. The ShapeBuilder rewrite (d8de62308) builds
geometry in local space preserving obj.matrix_world, making the
translate call redundant — it adds z on top of the already-correct
location.z, producing 2*z.
Add test_regenerate_space_preserves_z_location to cover the
regeneration path with a non-zero Z elevation.
Generated with the assistance of an AI coding tool.
The example block was copy pasted verbatim from ExtractPropertiesToSQLite,
so it named the wrong recipe and wrote a .sqlite file. These docstrings are
what ifcpatch surfaces as CLI and UI help, so anyone following the example
for AGS2IFC got a recipe name that does not match the one they selected.
Also state that the input file is not read and that a new IFC4X3 model is
built, since that is not obvious from the signature and the recipe creates
its own project rather than patching the one passed in.
Generated with the assistance of an AI coding tool.
angle_round_threshold was only assigned inside the `distance > 0`
branch of calculate_distance_and_angle, but read unconditionally
whenever should_round is True. When the mouse sample coincides with
the last placed point (distance == 0), such as the first mouse move
after placing a wall's start point on a YZ plane view, this crashed
the modal wall tool.
angle_round_threshold is a fixed cutoff unrelated to whether distance
is currently zero, so it is now assigned once before the branch.
Fixes#8597.
Generated with the assistance of an AI coding tool.
test_returns_none_when_report_file_absent/empty build a MagicMock repo
without configuring index.unmerged_blobs(), so it returned a truthy
MagicMock and git_mergetool's load-bearing "unresolved conflicts remain"
fallback (tool/ifcgit.py:646-647) returned that list instead of None -
failing "assert [] is None". The production fallback is correct and
intentionally left untouched; the tests just misrepresented the
"mergetool resolved cleanly" scenario they are named for. Set
mock_repo.index.unmerged_blobs.return_value = {} in both.
Verified in headless Blender: test/tool/test_ifcgit.py::TestGitMergetool
2 failed / 1 passed -> 3 passed.
This change was made with the assistance of an AI tool.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
DeepDiff's dictionary_item_added/set_item_added results are a
deepdiff.helper.SetOrdered instance, which subclasses orderly_set's
StableSetEq rather than the OrderedSet class json_dump_default checked
for, so the property relationship check always crashed export() with
"Object of type SetOrdered is not JSON serializable". Check against
StableSet, the common base class shared by every orderly_set set
flavour, instead.
Fixes#8905
Generated with the assistance of an AI coding tool.
Browsers were caching /static/js and /static/css for the standalone
webui (costing, gantt, drawings, index, demo pages) indefinitely, so a
shipped JS fix (e.g. the Download CSV button) would only reach a user
after a manual hard refresh.
Two changes, applied consistently across all five webui pages.
1. Every locally served link/script tag in the pystache templates now
carries a ?v=<bonsai version> query string, falling back to a static
asset mtime hash when BONSAI_VERSION isn't set (e.g. running
sioserver.py standalone). Since get_bonsai_version() includes the
build's commit hash, the token changes on every shipped update.
2. Responses under /static/ and /jsgantt/ now carry
Cache-Control: no-cache, must-revalidate. This covers what query
stamping alone can't reach: cost.js and gantt.js statically import
utilities/costui.js by a fixed relative path with no query string, so
that nested module still needed server side revalidation to pick up
changes.
Verified against a live aiohttp instance of sioserver.py: rendered
HTML for all five routes shows the stamped URLs, and the token
changes when BONSAI_VERSION changes between two server runs. A
conditional GET against a static file with a stale If-Modified-Since
header confirms the cheap 304 revalidation path still works.
Also used this instance plus a real headless Chromium (Playwright) to
click test the previously untested Download CSV button on the costing
page. The ribbon renders it correctly, and clicking it (with a
synthetic cost-items table injected into the DOM to stand in for a
connected Blender's data) triggers a real Blob download with the
correct filename and CSV content. No bug found, the button works as
intended.
AI-generated with Claude Code.
Stefano's final ask on #6251 was specific: the ODS/XLSX export should
show exactly what the cost panel shows, ID (Identification), Name,
Quantity, Value, Total Cost, no more, no less. The previous fix in
this PR removed the internal bookkeeping columns but still exported
Description, Unit and a per-category cost breakdown (Labor Cost,
Material Cost, etc), none of which appear in the panel.
Presentation formats (.ods/.xlsx) now use an explicit allow-list of
columns instead of a block-list of internal ones, and relabel headers
to match the panel's own wording (ID / Value / Total Cost). The .csv
format is unchanged: csv2ifc still reads back the extra bookkeeping
columns for the import round trip, which is why it keeps them.
Also add a "Download CSV" button to the browser costing view
(Generate spreadsheet browser), which previously only offered a
clipboard-based Copy Selected. It reuses the already-rendered table
(respecting the user's column visibility settings) and triggers a
real file download, dropping only the UI-only Actions column.
AI-generated with Claude Code; reviewed and tested by Petru Conduraru.
Three defects reported against the Costing tab export:
1. XLSX export crashed with ModuleNotFoundError: xlsxwriter was never
bundled with Bonsai. Port the writer to openpyxl, which ifccsv
already uses and Bonsai already ships, so it works out of the box.
2. Every ODS cell was written as a string (numbers as text), and the
formula branch was dead code: it compared against 'Total Price' /
'Rate Subtotal' while the headers are 'TotalPrice' / 'RateSubtotal'.
Numeric columns are now typed float cells and TotalPrice becomes a
real formula: Quantity*RateSubtotal on leaf items, SUM over the
direct children's TotalPrice cells on sum items.
3. Internal bookkeeping columns (Id, ItemIsASum, Hierarchy, Index,
Quantities) leaked into the presentation formats. ODS/XLSX now hide
them; CSV keeps them since csv2ifc consumes them for the round trip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get_element_value could not reach the members of an IfcPhysicalComplexQuantity
(or IfcComplexProperty) by their natural path. util.element expands a complex
quantity into a dict whose nested members live under a "properties" sub-dict,
but the selector's dict navigation only looked at the top level, so
"Qto_Custom.Layer1.Width" returned None and IfcCsv exported nothing for it.
Only the internal "Qto_Custom.Layer1.properties.Width" path worked.
When a key is not a direct member of the value dict, descend into its
"properties" sub-dict so nested quantities/properties resolve with the
natural "Set.Complex.Nested" path. Direct keys still take priority, so the
explicit ".properties." path stays backward compatible and the regex branch
is untouched.
Verified: Qto_Custom.Layer1.Width -> 0.1 and Layer1.Height -> 2.5 (were
None), the sibling simple NetArea still resolves, the legacy .properties.
path still works, and IfcCsv now exports the nested value. test_selector.py:
38 passed (adds test_selecting_a_nested_complex_quantity).
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Fix ci-bonsai-daily: ProjectLibraryData duplicate parent-library enum
parent_libraries_enum() adds an explicit entry for get_root_context(),
then loops over cls.data["project_libraries"] (all IfcProjectLibrary
entities) and appends each. For a library-only file (no IfcProject),
get_root_context falls back to the top-level IfcProjectLibrary itself,
so the root is appended twice with the same enum key (its STEP id),
which Blender EnumProperty requires to be unique -> the data load
asserts. Normal project files are unaffected (root is an IfcProject
whose id never collides with a library id).
Skip library_id == root.id() in the loop (dedup by id, the colliding
key). Verified in headless Blender:
test_project_library_data.py::TestLibraryOnlyFile goes from 1 failed /
5 passed to 6 passed.
This change was made with the assistance of an AI tool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Bonsai: repair library files missing the required IfcProject, not just the symptom
Per the IFC Project Context concept template, every project data set (library
files included) shall contain exactly one IfcProject, and IfcProjectLibrary
instances are assigned to it via IfcRelDeclares. There is no such thing as a
spec-valid file rooted on IfcProjectLibrary alone.
get_root_context() (added in 260a387069, #8184) treated a missing IfcProject
as license to use the top-level IfcProjectLibrary as the file's root context
instead. That invalid premise is why project_libraries() (which walks every
IfcProjectLibrary, root included) then re-added that same entity, producing
the duplicate, colliding enum key this PR originally papered over with a
dedup guard.
Add tool.Project.ensure_project_context(), which repairs a file missing
IfcProject by creating one and declaring the file's root-level
IfcProjectLibrary instances to it, and tool.Project.open_library_file(),
which opens a library file through that repair. Route all three
IfcStore.library_file load sites in SelectLibraryFile through it. Downstream
code (get_root_context, ProjectLibraryData, RefreshLibrary,
AddProjectLibrary) now always operates on a spec-valid model, so the
duplicate enum entry cannot occur; the previous one-line dedup guard in
parent_libraries_enum() is kept only as cheap defense in depth for callers
that bypass the load-time repair, not as the fix.
Rework test_project_library_data.py: the previous _make_library_only_file()
fixture built an invalid library-only model and asserted that as correct
behaviour. Replace it with a spec-valid fixture (IfcProject + IfcProjectLibrary
declared to it) for the downstream tests, and a malformed fixture used only to
exercise the new repair path.
Verified live in headless Blender (isolated profile): reproduced the original
duplicate-enum-key failure mode, then confirmed ensure_project_context/
open_library_file repair a malformed file and ProjectLibraryData,
refresh_library and add_project_library all operate correctly on the result,
with no duplicate keys and no regression on already-valid files or IFC2X3.
This change was made with the assistance of an AI tool.
* Bonsai: stop supporting library-only files, do not repair them
Per Moult's feedback: if the IFC is invalid, our default position is to not
support it, not to patch around it. A library file with no IfcProject is
invalid IFC (Project Context concept template requires exactly one
IfcProject), and it is not ubiquitous: every library file bonsai ships under
bim/data/libraries has an IfcProject with the IfcProjectLibrary declared to
it via IfcRelDeclares. The single #8183 report is an outlier, not a common
authoring pattern worth accommodating.
Remove tool.Project.ensure_project_context() and open_library_file() (the
load-time repair added in the previous commit here) and revert
SelectLibraryFile's three load sites to plain ifcopenshell.open. Simplify
get_root_context() back to returning ifc_file.by_type("IfcProject")[0]
directly, no IfcProjectLibrary fallback: a file without IfcProject now raises
IndexError instead of being silently treated as valid. AddProjectLibrary's
nest-under-library branch is now dead code (root_context is always an
IfcProject) and is removed. The one-line enum dedup guard from the original
commit here is also removed: since get_root_context can only return an
IfcProject or raise, an IfcProject id can never collide with a library id, so
the guard has nothing left to guard against.
Rework test_project_library_data.py: drop the invalid _make_library_only_file
fixture and its tests, which asserted an unsupported model as correct
behaviour. Replace with a single spec-valid fixture matching bonsai's own
shipped library files (IfcProject + IfcProjectLibrary declared to it), used
for the ci-bonsai-daily regression test and the refresh/add-library
operators, plus one explicit test that get_root_context raises for a file
without IfcProject, documenting that this input is intentionally
unsupported rather than silently tolerated.
Verified live in headless Blender (isolated profile, source-loaded, never
the real profile): confirmed the removed methods are gone, that a
library-only file now raises instead of being handled, that
ProjectLibraryData/refresh_library/add_project_library all work correctly
on a spec-valid model with unique enum keys, and spot-checked that every
library file under bim/data/libraries already has an IfcProject.
This change was made with the assistance of an AI tool.
* Bonsai: inline get_root_context, trim docstrings, confirm get_parent_library unchanged
Per Moult's round 3 review. get_root_context added nothing over
ifc_file.by_type("IfcProject")[0], which is guaranteed by the IFC Project
Context concept template; remove it and inline the call at its three sites
(operator.py's RefreshLibrary and AddProjectLibrary, data.py's
parent_libraries_enum). Trim the get_parent_library docstring to one line;
its logic is untouched by this PR, byte for byte identical to origin/v0.8.0,
and still returns None only when project_library has neither Nests nor
HasContext, never for a library declared directly to IfcProject.
Rework test_project_library_data.py to match: replace the two
get_root_context-specific tests with one that exercises the real call site
(ProjectLibraryData.parent_libraries_enum raising IndexError for a file
without IfcProject), and add an explicit test that get_parent_library
returns None for a genuinely orphaned library. Also drop a long inline
comment that restated what the test body already shows.
Verified live in headless Blender (isolated profile, source-loaded, never
the real profile): all 17 test/bim/module/project tests pass, including the
new get_parent_library None-for-orphan case. Ran the full test/bim suite
before and after on the identical harness: 82 failed/1335 passed both times,
same failing tests (all pre-existing, unrelated to this module).
This change was made with the assistance of an AI tool.
* Bonsai: fix EditProjectLibrary leaving stale declarations after reparenting
Per Moult's round 4 review. The assertion change (get_parent_library(root)
now returns the IfcProject instead of None) is correct: in the old
library-only test model a top-level library had neither IfcRelNests nor
IfcRelDeclares, so None meant "top level". In the new spec-valid model a
top-level library is always declared to the guaranteed IfcProject via
IfcRelDeclares, so get_parent_library correctly resolves it through the
HasContext branch instead of falling through to None. get_project_hierarchy
already keys top-level libraries under the project for exactly this reason,
so the library tree still renders correctly.
Auditing every caller found one real bug in EditProjectLibrary, which
Gorgious56 originally wrote for the library-only model. Its move-library
logic assumed a top-level library (previous_parent_library is None) needed
no cleanup before nesting it under a new parent, and that unnesting a
library back to the project needed no new relationship because it was
"already assigned by default". Both assumptions relied on a top-level
library never actually holding a IfcRelDeclares, which is no longer true.
Reproduced live: moving a project-declared library under another library
left its old IfcRelDeclares dangling alongside the new IfcRelNests (an
invalid double parentage), and moving a nested library back to the project
left it with neither relationship, orphaning it out of the tree entirely.
Fixed by tearing down whichever of IfcRelDeclares/IfcRelNests the library
previously had before establishing whichever one the new parent requires,
instead of assuming which prior state applies.
Added tests: get_parent_library resolving a nested sub-library to its
library parent (the third contract case alongside project-declared and
orphaned), and both EditProjectLibrary reparenting directions, which fail
without the operator.py fix and pass with it.
Verified live in headless Blender (isolated profile, source-loaded, never
the real profile): all 20 test/bim/module/project tests pass. Ran the full
test/bim suite before and after on the identical harness: 123 failed/1294
passed before, 123 failed/1297 passed after, identical failing test names
in both runs (diffed), the extra 3 passes are the new tests above.
This change was made with the assistance of an AI tool.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
on_depsgraph_update and on_depsgraph_update_caps are registered together
as persistent depsgraph handlers (bim/module/clip_box/__init__.py:50-52).
on_depsgraph_update guards with `if cls._file_loading: return`, but the
sibling on_depsgraph_update_caps did not, so a depsgraph tick during the
file-load window still ran it. Beyond the failing test, this can re-arm a
cap-rebuild bpy.app.timers callback in the exact load window _on_load_pre
cancels timers for, against regions whose GPU state is not yet wired.
Add the same _file_loading guard as the first check.
Verified in headless Blender:
test_clip_box.py::TestRefreshTimerLifecycle::test_depsgraph_update_no_op_while_loading
1 failed -> passed.
This change was made with the assistance of an AI tool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Link IFC with 'Use Cache' unchecked crashed with FileNotFoundError
when no .ifc.cache.blend existed yet (a fresh link). Regression from
35e3d9c42, which refactored the cache-clear guard from
'if not self.use_cache and blend_filepath.exists()' into
should_clear_cache() but dropped the existence check on the
not-use_cache path, so os.remove() ran on a non-existent file.
Check blend_filepath.exists() first in should_clear_cache() so the
remove is never attempted when there is nothing to clear, while
keeping the query-mismatch cache invalidation intact.
Fixes#8350
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When replacing a style on an item whose previous IfcStyledItem wraps its styles
in the deprecated IfcPresentationStyleAssignment, and the assignment is not
being reused (use_style_assignment is False, e.g. an IFC4 file authored by
AVEVA E3D), the else branch called remove_same_type_styles(style_assignment)
with style_assignment still None, raising
AttributeError: 'NoneType' object has no attribute 'Styles'. Operate on style_,
the assignment found in the current iteration, instead of the accumulator.
Verified red-green with a minimal IFC4 file using IfcPresentationStyleAssignment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
compare() recursed into list values passing the negated comparison through,
so != meant "at least one item differs" and both = and != matched the same
elements on any multi-valued property (e.g. an enumerated property with two
values selected). Strip the negation for the per-item comparison and negate
the aggregate instead, so != means "no item equals" and stays the complement
of =. The same applies to !*=.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Optimise recipe imported `toposort`, a third-party PyPI package that
is not bundled with Bonsai, so running the recipe there raised
`ModuleNotFoundError: No module named 'toposort'`.
Replace it with the standard library `graphlib.TopologicalSorter`
(available since Python 3.9), which provides the same dependencies-first
ordering guarantee the recipe relies on: forward-referenced instances are
mapped before the instances that reference them. The dependency-graph
dict format ({node: {predecessors}}) is identical between the two, so the
graph construction is unchanged. Drop `toposort` from ifcpatch's
dependencies since it is no longer used.
Verified with toposort NOT installed: the Optimise recipe now runs and
deduplicates correctly (IfcParseExamples_test.ifc 88 -> 63 instances, all
6 products preserved, output reopens cleanly).
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tool.Bsdd.identifier_url() (pset/ui.py pset name check in the Property
Sets panel) reads client.baseurl unconditionally, but the test stub
never had that attribute, so any scenario that opens the Property Sets
panel dies with AttributeError under the stub. The boolean.feature
scenarios only surfaced this once their STEP id failures were fixed,
the id failure had been masking it. Mirror the real bsdd.Client
default so identifier_url() resolves to the standard identifier URL.
This change was made with the assistance of an AI tool.
The two boolean.feature scenarios pinned representation item objects by
absolute STEP id (Item/IfcHalfSpaceSolid/90, the BBIM_Boolean pset text
[91]). Those ids shift every time any earlier entity allocation in an
empty project changes (latest instance: #8577 moved 90 to 86), so this
cluster re-breaks on unrelated commits.
Make the object-name and panel-text BDD steps run their argument through
replace_variables, the same substitution 'the variable' and the
connection steps already use, and have boolean.feature capture the real
ids from the IFC file (by_type(...)[0].id()) into variables at the point
the entities are created. The steps stay strict: the substituted name
must still resolve to exactly the named object, there is no wildcard
matching. Substitution is a no-op for every existing feature string
without a {variable} placeholder.
This change was made with the assistance of an AI tool.
Two independent test-harness/fixture defects in test/bim/test_feature.py:
- OperatorSpy had no bl_rna, so any BDD step that redraws a panel calling
helper.draw_filter() (which tests "module" in op.bl_rna.properties)
crashed with AttributeError. Give OperatorSpy a bl_rna property that
forwards to the real registered operator class
(bpy.types[bl_idname].bl_rna), matching live UILayout.operator()
semantics. Fixes test_select_all_walls and test_edit_filter_query.
- The shared "I create default MEP types" step looked up
bpy.data.objects["IfcDistributionPort/Port"], but port creation never
sets port.Name, so tool.Loader.get_name deterministically names the
object "IfcDistributionPort/Unnamed". Update the literal. Fixes the MEP
scenarios (connect/transition/bend) that share this setup.
Verified in headless Blender: OperatorSpy scenarios 2 passed (were
AttributeError); MEP test_connect_mep_elements* go from
KeyError 'IfcDistributionPort/Port' to passing.
This change was made with the assistance of an AI tool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pset names containing spaces (e.g. "SOLIDWORKS Custom Properties") were
not quoted when building selector keys in SelectSimilarData, causing
get_element_value to fail when the operator ran. Now wraps pset names
and property names in double quotes if they contain spaces, consistent
with the selector syntax used elsewhere.
Generated with the assistance of an AI coding tool.
Per aothms's review comment: this file's schema was already changed
independently on v0.8.0 since this branch was created, so this PR's own
edit conflicts with it. Reverting to the current upstream version of the
fixture; the bsdd.py rate-limiting fix is untouched.
bsdd.py: the Client made every request with a bare requests.get, so a single
429 from the (unauthenticated, aggressively rate limited) bSDD API failed the
whole test. Route requests through a Session with a mounted urllib3 Retry
(5 attempts, backoff, honouring Retry-After) for 429/5xx, matching how a
resilient API client should behave, not just papering over the test.
ColumnPSetsOfSets.ifc: FILE_SCHEMA was accidentally changed from IFC4X3_ADD2
to IFC2X3 in a7738eeb64 (an unrelated logger refactor), a one line collateral
edit to this fixture. The file's DATA section still uses IFCPROPERTYSETDEFINITIONSET,
an IFC4+ only type. Parsing it against IFC2X3 threw "Entity ... not found in
schema", which silently fell back to interpreting the value as a raw nested
aggregate instead of the intended defined-type wrapper, producing the
double-nested tuple that broke test_stream, test_file and test_rocks in
test_streaming_rocksdb_and_simpletyperefs.py. Restoring the original schema
declared when the fixture was added (ff3fa48332) fixes all three.
Generated with the assistance of an AI coding tool.
Bsdd.get_dictionaries() unconditionally did cls.client = bsdd.Client(),
replacing whatever client was already set - including the
bSDDClientStub the BDD suite injects at module load
(test_feature.py: tool.Bsdd.client = bSDDClientStub()) to avoid live
network calls. Because "Load bSDD Dictionaries" is the first step of
every bsdd.feature scenario, the stub was discarded before its fixture
data ("LCA", "BonsaiTestDict") could ever be returned.
The re-init is unnecessary: bsdd.Client.__init__ only sets baseurl and
blank tokens, and the next line already updates baseurl defensively via
hasattr. Drop the clobbering assignment; reuse whichever client is
already set.
Verified in headless Blender: bsdd scenarios (load dictionaries, search
all/single dictionary) go from 3 failed ("Could not see LCA/
BonsaiTestDict") to 3 passed.
This change was made with the assistance of an AI tool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The choco dir was renamed from choco/blenderbim to choco/bonsai back in
2024 (Rename choco dir), but choco_release.py's BLENDERBIM_DIR constant
was never updated, so the daily choco release job crashes immediately
with FileNotFoundError trying to os.chdir into the now nonexistent
choco/blenderbim directory.
Release tags also moved from a bare blenderbim-YYMMDD scheme to
bonsai-X.Y.Z-alphaYYMMDDHHMM, so the tag-prefix strip used to build the
package version still looked for the old "blenderbim-" prefix and left
it untouched, embedding the raw tag (including the already-present
"-alpha" segment) into the nuspec version field, which the template
then doubled up with its own "-alpha" suffix, producing an invalid
NuGet version string. Both are fixed together since the second bug
would otherwise surface as soon as the first one is unblocked.
The pre-commit black hook also reformatted pre-existing whitespace
drift in choco_release.py (this file sits outside CI's lint scope, so
it had never been auto-formatted before); that reformatting is
incidental to satisfying the local hook, not part of the fix itself.
Generated with the assistance of an AI coding tool.
replace_attribute() rewrites references inside aggregate attributes via
element.walk(), but never checked whether the replacement value was
already present elsewhere in the same aggregate. For an EXPRESS SET
(e.g. IfcProject.RepresentationContexts, IfcRelAggregates.RelatedObjects)
this can leave the same reference listed twice, which is invalid IFC.
LIST and BAG aggregates legitimately allow duplicates, so a blanket dedup
would be wrong; only SET-typed attributes are deduplicated, determined at
runtime from the schema declaration (IfcOpenShell#8706 review comment).
The SET/LIST/BAG check is cached per (schema, class, attribute index), and
the dedup pass itself only runs when a cheap linear pre-check finds the
replacement value already present in the aggregate, so the common case
(no duplicate produced) pays only that pre-check, not a hash-set rebuild.
Benchmarked against a 23MB (431k entities) and a 104MB (2.4M entities) IFC
model against a large SET attribute: worst case adds well under 1ms per
call; the realistic case (merging duplicate contexts, matching the PR
#8706 scenario) shows no measurable regression.
Fixes the root cause flagged in IfcOpenShell#8706 (Moult), obviating the
need for MergeDuplicateContexts' own manual aggregate-dedup pass for that
scenario.
Generated with the assistance of an AI coding tool.
Fixes issue #3910's documentation gap. IsDefinedBy() returns
IfcRelDefinesByProperties relationship objects, not the property set
itself, and RelatingPropertyDefinition() must be used to reach the
IfcPropertySet or IfcElementQuantity. Properties can also come from an
element's type via IsTypedBy() -> RelatingType() -> HasPropertySets(),
a path that is easy to miss because it works differently. Adds a
worked, beginner-commented, compilable example covering both paths.
Generated with the assistance of an AI coding tool.
The ifcopenshell take-off engine left every Qto_EarthworksFillBaseQuantities
value null, so Bonsai added the qset with no numbers on IFC4X3 models. Map the
geometrically derivable quantities using the slab axis convention: Length on
local X, Width on local Y, Depth on local Z, and the net solid volume as the
compacted (Fill) or undisturbed (Cut) volume. LooseVolume and Weight stay
unmapped because they need soil bulking and density factors absent from
geometry. Bring IfcEarthworksCut to parity with the Blender engine and wire
IfcReinforcedSoil on both engines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
assign_cost_item_quantity skipped every IfcSpatialElement, which also
swallowed IfcSpace. Spaces are legitimate quantifiable objects, so their
Qto_SpaceBaseQuantities (for example GrossFloorArea) were never picked up
and count based cost items fell back to 0. Keep skipping spatial
containers (site, building, storey) but allow IfcSpace.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The headless "IfcOpenShell" calculator had all Qto_SpaceBaseQuantities
formulas set to null for IfcSpace in both IFC4QtoBaseQuantities.json and
IFC4X3QtoBaseQuantities.json, so qto.py's `if not formula: continue`
skipped every quantity, no geometry task was queued, and spaces never
appeared in results (elements_quantified: 0). The Blender calculator
already computes these; they were just never ported to the
ifcopenshell.util.shape-backed calculator.
Map the eight computable quantities to existing util.shape functions,
mirroring the Blender calculator semantics (no new shape.py code):
GrossFloorArea=gross_get_footprint_area, NetFloorArea=net_get_footprint_area,
GrossCeilingArea=gross_get_top_area, NetCeilingArea=net_get_top_area,
GrossPerimeter=gross_get_footprint_perimeter, GrossVolume=gross_get_volume,
NetVolume=net_get_volume, Height=net_get_z.
Left null (matching the Blender ruleset, not guessed): GrossWallArea,
NetWallArea, NetPerimeter (Blender stub), and FinishFloor/CeilingHeight
(Blender derives these from sibling IfcCovering decomposition geometry,
which this per-element calculator architecture can't reach).
Verified on IFC4 (4x3 space extruded 2.5m): before -> {} / elements_quantified 0;
after -> GrossFloorArea 12, GrossPerimeter 14, Height 2.5, GrossVolume 30,
etc. - all exact matches to the extrusion. IFC4X3 formulas are identical
and the formula->function resolution is schema-agnostic.
Scope: fixes the IfcSpace case (the issue title). The 12 other all-null
classes noted in the issue (IfcDoor, IfcSite, IfcRailing, ...) are left as
follow-up.
This change was made with the assistance of an AI tool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ifc5d IfcOpenShell (geometry-based) Qto engine computed
Qto_CoveringBaseQuantities using axis-agnostic heuristics:
- GrossArea/NetArea: gross_get_max_side_area / net_get_max_side_area,
the largest of the X/Y/Z projected side areas.
- Width: gross_get_min_xyz, the smallest of the X/Y/Z dimensions.
The Blender Qto engine instead already used
EPset_Parametric.LayerSetDirection (AXIS2 for wall-like coverings,
AXIS3 for floor/ceiling-like coverings) to pick the correct axis via
get_covering_gross_area/get_covering_net_area/get_covering_width in
bonsai/bim/module/qto/calculator.py.
For any covering whose length isn't the largest dimension (e.g. a
short wall-covering strip, or a small covering patch), the two
engines' heuristics can pick different faces/axes entirely, giving
different Width/Area values for the same element - this is what was
reported in #6728.
Fix: give the IfcOpenShell engine the same layer-set-direction
awareness. Added IfcOpenShell.get_covering_parametric_axis/
get_covering_area/get_covering_width (dispatched as internal
functions, like the existing get_weight/get_segment_length), and
wired gross_get_covering_area/net_get_covering_area/
gross_get_covering_width into the IfcCovering rules in
IFC4QtoBaseQuantities.json and IFC4X3QtoBaseQuantities.json.
The AXIS2 area/width formulas (get_side_area, net_get_y) intentionally
match the simpler formulas already used for Qto_WallBaseQuantities in
this same rule set (net_get_side_area/net_get_y), rather than
replicating the Blender engine's more elaborate get_lateral_area/
get_width (min(X,Y)) helpers, consistent with how the two engines
already diverge for regular walls without being considered a bug.
Verified with a standalone script driving ifc5d.qto.IfcOpenShell
directly against synthetic AXIS2/AXIS3 IfcCovering geometry: for
typical proportions old and new formulas agree, and for
disproportionate coverings (thin dimension not the smallest/largest)
the old formulas picked the wrong axis while the new ones correctly
track the covering's LayerSetDirection, matching the Blender engine.
Did not verify through the full Blender/Bonsai UI, as it would have
required registering the addon in the machine's shared Blender
profile, which is unsafe while other agents may have it loaded.
Generated with the assistance of an AI coding tool.
copy_cost_item appends the copy to the inverse relationships of the
original cost item, which for a root cost item includes the source
schedule's IfcRelAssignsToControl. copy_cost_schedule then assigned that
same cost item to the new schedule as well, so the copies showed up in
both schedules and deleting them from one removed them from the other.
Unassign the copy from the source schedule before assigning it to the
new one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes#4443. The Wall/Slab/other authoring tools (BimTool subclasses)
already bind Shift+Q to bim.perform_quantity_take_off via hotkey_S_Q,
but the Spatial tool has its own separate keymap/operator
(bim.spatial_hotkey) that never registered a Q entry, forcing users to
switch tools just to (re)calculate quantities for a selected element.
Added the same Shift+Q keymap entry and a matching hotkey_S_Q handler
to the Spatial tool, mirroring BimTool's existing behavior exactly
(including the same selected-objects guard).
The other part of the request, a bulk "calculate all quantities"
entry point, already exists today: bim.perform_quantity_take_off
computes quantities for every IfcElement when no objects are
selected, exposed via the Scene > Quantity Take-off panel regardless
of which workspace tool is active, so no change was needed there.
AI-generated, reviewed and tested by BIMvoice.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
can_nest() only permitted IfcElement-to-IfcElement pairs, so nesting
two IfcElementType objects (e.g. an IfcElementAssemblyType nesting a
component IfcDoorType) was silently rejected. IfcRelNests.RelatingObject/
RelatedObjects are typed as the general IfcObjectDefinition in the
schema, so type-to-type nesting is schema legal, IfcOpenShell's core
nest.assign_object API already handles it generically, and the Nest
UI panel is driven purely by ifcopenshell.util.element.get_nest/
get_components (IFC data queries, not Blender collection structure),
so once the relationship exists it displays correctly with no other
changes needed.
Extended is_compatible_class to also accept a same-kind IfcTypeProduct
pair. Mixing an occurrence element with a type is intentionally still
rejected, that isn't a real modeling pattern.
Verified live in headless Blender: type-to-type nesting now creates
a real IfcRelNests and the Nest panel's own data functions reflect
it correctly; mixing an occurrence with a type is still rejected;
existing element-to-element nesting is unaffected.
Generated with the assistance of an AI coding tool.
Root cause: the IfcOpenShell-geometry-engine calculator ruleset
(IFC4QtoBaseQuantities.json and IFC4X3QtoBaseQuantities.json) left
IfcWall's GrossFootprintArea/NetFootprintArea mapped to null, so
these two quantities were silently omitted from Qto_WallBaseQuantities
whenever that ruleset was used. The generic gross_get_footprint_area
and net_get_footprint_area formulas already exist and are already
wired up for IfcSlab in the same files, so this was a missing mapping,
not a missing implementation.
Fixes#7029.
Generated with the assistance of an AI coding tool.
This reverts commit b61f809731.
This commit was probably using not updated build, currently latest build is e333c1c and can confirm that it has `logger_or_root` added and `delete_same_facet_edge_pairs` removed.
Footings are authored two ways with different local axis conventions. Beam-like
footings (STRIP_FOOTING, FOOTING_BEAM) are a profile extruded along local Z, so
Length is local Z and the cross section sits on local X (Width, horizontal) and
local Y (Height, vertical). Slab-like footings (PAD_FOOTING, PILE_CAP) have their
footprint on local X/Y and their thickness (Height) on local Z.
The engine rule set is keyed per IfcFooting and cannot branch on predefined type,
so the previous static rule (Height=net_get_z, Length=net_get_max_xy, Width=null)
swapped Length and Height for beam-like footings and never emitted Width.
Add predefined-type-aware get_footing_length/width/height to the IfcOpenShell and
Blender calculators, and point the IfcFooting rule at them in all four IFC4/IFC4X3
ios/Blender rule files.
Confirmed by authoring footings through the real Bonsai generators and measuring
world-axis orientation: a beam-like footing with a 0.3 wide by 0.6 tall cross
section and 6.0 run reports Length 6.0, Width 0.3, Height 0.6, with the 0.3
physically horizontal and 0.6 physically vertical; a 2.0x1.5x0.3 pad reports
Length 2.0, Width 1.5, Height 0.3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
See #6835. Qto_OpeningElementBaseQuantities came out axis-scrambled for
openings authored in a Z-up local frame (X along the voided wall, Y
through it, Z vertical), which is how Bonsai authors every wall opening:
- The IfcOpenShell engine mapped Height to the local Y extent and Depth
to the local Z extent, so a 0.9 x 2.0 door opening with Bonsai's
default 1.2m void depth reported Height 1.2 and Depth 2.0, and Area
(max side area) picked the through-wall side, 2.4 instead of 1.8.
This matches the wrong Height=1.2/Area=1.2 screenshots reported for a
1x1 window opening in #6835.
- The Blender engine mapped opening Width to get_length, which returns
the longest bounding box edge, i.e. the opening height for typical
door openings (the same defect 4adaf0d fixed for IfcDoor Width), and
get_opening_depth used min(x, y), which returns the opening width
whenever the width is smaller than the void depth.
The IfcOpenShell engine now has opening-aware internal calculators
(get_opening_width/height/depth/area) that detect horizontal (slab
style) openings with the same heuristic as the Blender calculator, so
slab opening depths keep reporting the slab thickness. The Blender
ruleset uses get_x for opening Width, and get_opening_depth measures the
through-element Y extent for vertical openings.
Door and window quantities themselves are addressed separately: the
Blender engine door Width was fixed in 4adaf0d, and the remaining
door/window defects (door not quantified on the IfcOpenShell engine,
inflated areas) are fixed by the attribute-based calculators in #8389.
Generated with the assistance of an AI coding tool.
falken10vdl reviewed 16b1b4e7b1 on #8843 and pointed out that tagging
every area for redraw was overkill. The actual problem was that the
Object Material panel and the scene Materials list read from plain
python caches (ObjectMaterialData and MaterialsData) that only get
invalidated when the Materials editing UI list is reloaded, which
never happens while you are not in editing mode. The redraw itself was
never the issue, closing the rename dialog already triggers one.
Removed the tag_redraw loop from RenameMaterial and instead call the
existing bonsai.bim.module.material.data.refresh() function from
core.rename_material, unconditionally, through a new tool.Material.refresh()
method. This is the same invalidate-on-next-load mechanism already used
by every other module's Data classes, just wired up for this operator
too, instead of introducing a new one.
Also updates the core tests to prescribe the new unconditional refresh()
call, and adds tool-layer coverage for tool.Material.refresh().
Generated with the assistance of an AI coding tool.
theoryshaw tested #8843 and asked for the new name to show up right
away instead of needing a manual refresh. The Object Material panel
and the scene Materials list both already re-read live IFC data on
their next draw (tool.Ifc.Operator purges those caches after every
IFC-mutating operator), so the button text was correct on the next
redraw. What was missing was the redraw itself: the material name is
a plain button label, not an RNA property Blender tracks, so nothing
told the Properties editor to repaint after the rename dialog closed.
Tag every area for redraw once the rename completes, the same pattern
used elsewhere in Bonsai for popup-triggered edits that need an
immediate repaint.
Also adds core-layer test coverage for rename_material, which had
none.
Generated with the assistance of an AI coding tool.
Adds a "Rename Material" entry to the context menu that already
extends every button in the properties editor (UI_MT_button_context_menu),
triggered when right-clicking a material name button
(bim.select_by_material) that points to a real IfcMaterial. This
gives a quick entry point to renaming from the Object Material panel
without navigating to the scene Materials list.
This follows the pattern that #6680's thread converged on: theoryshaw
requested a right-click entry (rather than a pencil icon or
double-click) that keeps the existing single-click select-by-material
behaviour intact. falken10vdl is the issue's assignee; this is offered
as a starting point for that discussion, not a replacement for it.
Generated with the assistance of an AI coding tool.
Add an "Is Selected" checkbox to each target-view category header in
BIM_UL_drawinglist that toggles selection for all drawings in the
category. The toggle only affects drawings currently visible in the
list (honoring the show_drawings_on_sheets_only filter), and the header
checkbox reflects the aggregate selection state of its drawings.
Also make category headers more obvious: wrap them in a box() for a
distinct inset background and make the header name clickable to
expand/contract the category (same as the disclosure triangle).
Ref: #8825
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds a "Show Only Drawings on Sheets" toggle below the drawing list. When
enabled, the list is filtered to drawings referenced by at least one sheet
(target-view headers with no sheeted drawings are hidden too), and
bim.select_all_drawings only acts on the visible/filtered drawings.
A drawing is considered sheeted when its drawing document Location matches a
document reference Location on any SHEET-scoped IfcDocumentInformation.
Filtering is computed live so it reflects sheet edits without reloading.
Closes#8823
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs: cover the Blender 5.1 / Python 3.13 transition in installation guides
The system requirements still listed Blender 4.3-4.5 with Python 3.11
only, and nothing documented the pitfall from issue 7623: importing
preferences into a Blender whose Python version changed carries over an
incompatible Bonsai build that silently fails to load. Document the two
Python generations, that Get Extensions picks the matching build
automatically while manual zip installs do not, and the
uninstall-reinstall step that resolves the upgrade case.
Generated with the assistance of an AI coding tool.
* docs: keep it simple, only Blender 5.1 and 5.2 with Python 3.13
Per review, drop the descriptive text and the Python 3.11 line.
Generated with the assistance of an AI coding tool.
* Keep real cause in wrapper ImportError
When the compiled wrapper exists for the current interpreter but fails
to load (for example a glibc version mismatch, as on AWS Lambda in
issue 5927), the bare except rewrote the error into the misleading
"IfcOpenShell not built for '<platform>'" message. Environments such
as AWS Lambda or the Blender add-on dialog only surface the final
exception message, so the actual cause was invisible and undiagnosable.
Keep the "not built for" message only when no matching binary is
present, and otherwise include the original loader error, chaining the
cause in both branches.
This change was AI-generated.
Fixes#5927
* Simplify wrapper import failure to a single message
Per review feedback, drop the filesystem scan and the two message
variants. Always raise the classic "IfcOpenShell not built for
'<platform>'" message with the original exception appended in
parentheses, still chained as the cause. Environments that only show
the final exception message (AWS Lambda, the Blender add-on dialog)
now surface the real loader error, such as the glibc version mismatch
in issue 5927, without any extra logic.
This change was AI-generated.
The jsGantt-improved library that renders Bonsai's Gantt chart already
ships full support for an "Hour" granularity (column width, header
labels in every bundled language, hour-aware rendering math). Bonsai's
config only exposed Day/Week/Month/Quarter, with a comment claiming
Hour caused browser issues even with vUseSingleCell enabled.
Headless Chrome testing against the same library version shows that
claim no longer holds once vUseSingleCell is active (as Bonsai already
configures it at 10000): Hour-format charts render without errors from
typical schedules up through fairly extreme ones (5000 tasks across a
3 year span rendered in about 2.4s). The failure mode the old comment
described only reproduces with vUseSingleCell disabled, which is not
how Bonsai runs it.
Task start/finish times already flow through to the chart unmodified
as raw ISO datetimes (tool/sequence.py create_new_task_json), so any
schedule authored with real hour-level timestamps, for example an
imported MS Project/P6/Excel schedule or one written directly through
ifcopenshell-python, can now be viewed at hour granularity. Verified
live with a night shift schedule crossing midnight, rendered correctly
with no console errors.
Note: Bonsai's own "Edit Task Time" UI currently always snaps
ScheduleStart/ScheduleFinish to 09:00/17:00 regardless of the hour
entered (ifcopenshell/api/sequence/edit_task_time.py), and work
calendars only encode working days, not working hours. So authoring a
genuine hour-precision schedule through that UI is still not possible;
this change only unlocks viewing hour-level data that already exists
in the model. Fixing the editor and calendar model is a separate,
larger design decision for a maintainer.
Addresses #2772.
Generated with the assistance of an AI coding tool.
Point scriptSalome.py at templates/salome/ and the bonded scripts at
_deprecated/, matching the current tree so README links resolve.
Generated with the assistance of an AI coding tool.
* Bonsai: move annotations between drawings when reassigning their group
Assigning an IfcAnnotation to a group that represents another drawing
previously left the annotation in both drawings at once: it stayed in
its old drawing group, its Blender object stayed in the old drawing
collection, and it kept the old camera depth, so the reassignment
appeared to do nothing useful. Issue #2966 documents the seven step
manual workaround users needed instead.
The assign group operator now detects when the target group represents
a drawing (via the new tool.Drawing.get_group_drawing, the inverse of
get_drawing_group), unassigns the annotation from its previous drawing
group, moves its object into the new drawing collection, and places it
on the new drawing camera plane. The target camera is imported on
demand when it has not been loaded yet, matching the pattern used by
the activate drawing operator.
Generated with the assistance of an AI coding tool.
* Bonsai: add one click copy of annotations to another drawing (#2966)
Duplicating an annotation into a different drawing used to require a
seven step manual process: loading groups in scene properties, copying
the object, fixing its group assignment by hand, and repositioning it
onto the target camera plane. A plain Blender duplicate is not enough
because the copy keeps pointing at the same IFC entity, and the Shift D
override, while it does create a genuine new entity through
root.copy_class, leaves the duplicate in the source drawing group,
collection, and camera depth.
The new copy annotation to drawing operator packages the proven recipe
already used by duplicate drawing into one action: duplicate through
tool.Geometry.duplicate_ifc_objects, unassign the copy from the source
drawing group, assign it to the chosen target group, place it on the
target camera plane at the same world XY, and file it into the target
drawing collection. The originals are left untouched and the user's
selection is restored. The target camera is imported on demand when it
has not been loaded yet.
The operator shows a target drawing dropdown and is reachable from the
annotation tool sidebar when an annotation is selected, and from the
drawings panel. Annotations already in the target drawing are skipped
and reported.
The orchestration lives in core.drawing.copy_annotations_to_drawing
with prophecy tests covering the copy, the skip, and the camera import
branches. Verified live in headless Blender 5.1: the copy is a new
IfcAnnotation with its own GlobalId and IfcTextLiteral, both texts are
editable independently, and everything survives save and reload with
each annotation loading in its own drawing.
Generated with the assistance of an AI coding tool.
Fixes#4395.
Root cause: the SVG cut-linework merge step that fuses adjacent
elements' cut polygons together (per the pset-driven JoinCriteria
setting) was hardcoded to only IfcWall and IfcSlab. IfcCovering cut
shapes were skipped unconditionally, so adjacent coverings never
joined, leaving a visible seam/broken corner in section drawings
regardless of JoinCriteria.
Fix: added an EPset_Drawing.JoinClasses property, following the
exact same user-overridable pattern already used by
EPset_Drawing.BringToFront - a comma-separated list of IFC classes
to join, defaulting to "IfcWall,IfcSlab" (unchanged behavior) when
unset. Users can override per-drawing to add IfcCovering (or any
other class) when they want it joined too. Kept this opt-in rather
than hardcoding IfcCovering into the default list, since joining a
thin finish layer the same way as a thick wall/slab could produce
unwanted mitring in some cases - the user decides per drawing.
Verified live against the reporter's own attached file
(ifcovering joining.ifc) and its cached section linework: with
JoinClasses unset, two separate closed paths reproduce the reported
seam exactly. With JoinClasses = "IfcWall,IfcSlab,IfcCovering", the
two coverings merge into a single closed polygon with the internal
seam removed. Confirmed IfcSlab join behavior is unchanged in both
runs.
Generated with the assistance of an AI coding tool.
Co-authored-by: Dion Moult <dionmoult@gmail.com>
reimport_element_representations() built a fresh
ifcopenshell.geom.settings() without copying deflection_tolerance /
angular_tolerance from the IfcImportSettings it had just
constructed, and never passed geometry_library to either the
iterator() or create_shape() calls it makes. As a result, exiting
Item/edit mode (which reaches this function via
switch_representation) silently fell back to IfcOpenShell's
hard-coded mesher defaults (0.001 linear deflection, ~50x finer than
the project's default of 0.05) and the default geometry kernel,
instead of the project's configured tolerance and Geometry Library.
This made geometry visibly change quality after a no-op Tab into and
back out of edit mode, since the reload path was unintentionally far
more precise (and used a different kernel) than the initial import.
Both settings, and geometry_library, are now taken from the
IfcImportSettings instance already built at the top of the function,
so a reload matches the original import.
Refs #5685.
Generated with the assistance of an AI coding tool.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
theoryshaw's follow-up on #6944: after the profile/curve reconstruction fix
(previous commit), the arc/circle marker for a freshly Shift+D-duplicated
loop wouldn't appear until leaving and re-entering Edit Mode.
Root cause: ProfileDecorator groups arc/circle vertices purely by
IFCARCINDEX/IFCCIRCLE vertex-group index every draw call (it has no cache
to go stale, it fully recomputes from the live edit-mesh bmesh each frame).
Duplicating a loop copies its vertex-group weights onto the new geometry,
since Blender allocates no new group for a duplicate, so the source loop
and its live duplicate land in the same dict entry. That entry then fails
the "exactly 2 verts per circle / 3 per arc" check and is skipped entirely,
so BOTH the original and the duplicate stop being drawn until the mesh is
reimported and gets fresh, distinct groups.
Verified live in headless Blender: built a bmesh with an IFCCIRCLE loop and
an IFCARCINDEX loop, then ran bmesh.ops.duplicate on each (the same
bmesh-level operation underlying Shift+D) and called ProfileDecorator's
draw method directly. Before this change, duplicating either loop dropped
both the original and the duplicate from the decorator (0 circle/arc
batches drawn instead of 2). After, both draw immediately, with no change
to the non-duplicated case (still 1) or to genuinely distinct loops (5
independent circles still resolve to 5, not merged). 500-circle timing is
unchanged (~14.3ms/draw before and after), so the added connectivity split
is not a hot-path regression.
Added test/bim/module/model/test_profile_decorator_duplicate_loop.py
pinning the new _connected_components helper's behavior for single and
duplicated circle/arc loops.
This contribution was produced with the assistance of an AI coding tool.
auto_detect_profiles had the identical defect fixed in the previous
commit: duplicating a circle/arc loop in Edit Mode reuses the same
IFCCIRCLE/IFCARCINDEX vertex group index for the new geometry, and this
sibling function (used for curve/annotation editing rather than profile
voids) tallied group membership across the whole mesh instead of per
loop, so it also rejected a legitimately duplicated loop as malformed.
Applied the identical fix: scope the group-count sanity check to each
connected edge loop, computed after the loops are built rather than in
the initial whole-mesh vertex pass. Kept the existing forked-loop check
(more than 2 edges per vertex) in the first pass since it is unrelated
to group counting.
Verified live in headless Blender: constructed two 2-vertex IFCCIRCLE
loops sharing one vertex group index (the exact state Blender's Edit
Mode duplicate produces) and called auto_detect_curves directly.
Before this change it returned (False, "CIRCLE"); after, it returns two
valid IfcCircle curves.
Generated with the assistance of an AI coding tool.
Duplicating a circular or filleted-arc void in the profile CAD editor
(Shift+D on the loop's vertices) reused the same IFCCIRCLE/IFCARCINDEX
vertex group index for the new geometry, since Blender's mesh duplicate
copies vertex group weights but does not allocate a new group. On exit
from Edit Mode, auto_detect_profiles tallied group membership across the
whole mesh rather than per loop, so a group meant to hold exactly 2 (circle)
or 3 (arc) vertices ended up with double that, failing its sanity check
and blocking the edit with an "INVALID PROFILE" popup. Fixes#6944.
Scope the sanity check to each connected edge loop instead, matching how
the loops are actually converted into IfcCircle/arc segments below. Also
explicitly reject an arc/circle vertex tagged onto an isolated vertex with
no edges at all, which the old whole-mesh count also caught.
Verified live in headless Blender against the issue's repro file
(IfcFurniture "Slab.004", IfcArbitraryProfileDefWithVoids with three
IfcCircle voids): entering the profile editor, duplicating one void's
2-vertex loop and moving it produced an "INVALID PROFILE" popup before
this change, and now produces a valid profile (the original 3 voids
intact, plus the duplicate as a 4th void or a separate solid profile
depending on whether it still falls inside the outer boundary).
test/tool/test_model.py passes unchanged (32 passed, 1 pre-existing
unrelated failure present on both before and after).
Generated with the assistance of an AI coding tool.
Both files were merged unformatted and fail the Black formatter step
on every branch, keeping ci-lint red repo-wide.
Generated with the assistance of an AI coding tool.
The ci-lint workflow's ty steps fail on every branch because base
v0.8.0 has four diagnostics.
ty check (bonsai):
- root/operator.py: bpy.data.objects.get() can return None, so
UnlinkObject._execute could put None in its objects list and crash
on the first attribute access when an unknown object name is passed.
Handle the miss explicitly, which also satisfies the declared
list[bpy.types.Object] type.
- tool/sequence.py: ty does not narrow Literal types through
membership tests on list literals, so the assert_never() exhaustive
check was flagged. Use tuple literals, which ty narrows, keeping the
exhaustiveness check intact.
ty check (ios):
- draw.py: arrange_polygons was called through conditional argument
splats that let the same call site work against pre-April-2026
wrappers lacking arrange_polygon_settings and the logger parameter.
No runtime bug for current builds, but the dynamic splats cannot be
typed against the fixed 3-parameter signature. Drop the old-build
workaround and call the current signature directly, following the
precedent of 3d8115ebc5 which dropped similar old-build workarounds
in ifcopenshell.file. Verified against a current wrapper build that
the direct call arranges polygons and serializes to SVG, with and
without a logger.
- Optimise.py: igraph is an optional dependency with a guarded import
and a toposort fallback, but it was missing from the ios type-check
venv so ty could not resolve it. Add it to type-check-requirements
next to the toposort fallback that is already listed.
After this, poe ty-bonsai and poe ty-ios both pass cleanly.
Generated with the assistance of an AI coding tool.
The script called Polyline.get_mode() on every modelspace entity, but
that method only exists on POLYLINE entities, so any typical DXF
containing lines, circles or text crashed with AttributeError before
converting anything. Test for POLYLINE polyface meshes with
dxftype()/is_poly_face_mesh instead and skip other entities with a
message, only create the spatial containment relation when products
exist, and take the input/output paths from the command line (matching
obj2ifc.py) instead of a hardcoded input.dxf/test.ifc.
Fixes#2151
This change was written with the assistance of an AI coding tool.
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.
#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.);
@@ -35,5 +35,12 @@ DATA;
#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.);
#37=IFCSIMPLEPROPERTYTEMPLATE('1zM9sia2L8RQDnWZxgUwlZ',$,'JoinClasses','Comma separated list of IFC classes whose cut linework will be joined together when they meet (e.g. mitred at a corner).\X2\000A\X0\Defaults to ''IfcWall,IfcSlab'' if not set. Override to also join other classes, such as ''IfcWall,IfcSlab,IfcCovering''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.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.