Compare commits

...

30 Commits

Author SHA1 Message Date
Petru Conduraru 2e3d4486ca IfcCenterLineProfileDef: implement profile mapping
The mapping for IfcCenterLineProfileDef was a stub returning nullptr,
so any swept solid using this profile produced no geometry. The
dispatch then fell through to the IfcArbitraryOpenProfileDef parent
mapping, whose open zero-area loop fails the face cast in the sweep
mappings anyway.

The taxonomy offset_curve item sketched in the earlier commented-out
attempt is not implemented by any kernel (AbstractKernel throws
not_implemented_error, the OpenCASCADE loop converter throws for it),
so the offset is instead computed at mapping time. The centerline is
offset to both sides by Thickness/2 in the profile plane and closed
with end caps, using straight miter joins at polyline vertices. This
preserves the constant thickness along the curve that the profile
definition prescribes, which the circular arc joins of a generic
offset algorithm (the pre-0.8 BRepOffsetAPI_MakeOffset path) would
violate, matching the design note from the earlier implementation.

Centerlines with curved (arc or spline) segments are detected and
rejected with a warning for now, keeping the previous no-geometry
behavior for those.

Verified with IFC4 and IFC2X3 test files, straight and L-shaped
centerlines: output solids are watertight, mitered, and have exactly
the expected thickness, extents and volume.

This is the geometry-side prerequisite for the sheet-from-centerline
authoring discussed in #2077.

Generated with the assistance of an AI coding tool.
2026-07-25 08:34:14 +03:00
Petru Conduraru 89523999b3 Bonsai: fix UnboundLocalError crash in polyline angle calculation
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.
2026-07-25 13:57:51 +10:00
Petru Conduraru 51ab38de27 Fix ci-bonsai-daily: configure unmerged_blobs mock in git_mergetool tests (#8574)
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>
2026-07-24 23:21:18 +01:00
Petru Conduraru fbe36532a0 ifcdiff: fix crash when exporting property diffs to JSON
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.
2026-07-25 07:47:13 +10:00
Andrej730 2f1b2f9638 ifcwrap: use swig shadowing for keeping reference to Element 2026-07-24 21:51:21 +05:00
Andrej730 9001cca078 ifcwrap: exclude internal geometry pointers
Still available as `int(xxx.this)`.
2026-07-24 21:51:21 +05:00
Andrej730 ec558dc57e stub: add logger_or_root arg type 2026-07-24 18:37:25 +05:00
Andrej730 2db5658386 ifcwrap: hide guess_file_type from Python as unused 2026-07-24 18:22:59 +05:00
Andrej730 88c8bd032f ifcwrap: fix breaking validate_stub (824c1fc)
It's ignoring underscore prefixed functions as not actually used.
Removing underscore to keep it happy without adding new exceptions.
2026-07-24 18:00:54 +05:00
Andrej730 3d8654acfd ifcwrap: ignore newly added conversion settings structs (183e4c4) 2026-07-24 17:59:44 +05:00
Andrej730 4a20b67038 IfcSchema: provide arg names for register_schema, schema_by_name 2026-07-24 16:48:51 +05:00
Andrej730 b14df627d7 ci: fix failing test for ifc5d 2026-07-24 16:15:54 +05:00
dependabot[bot] 0828c6ba92 build(deps): bump ty from 0.0.61 to 0.0.63
Bumps [ty](https://github.com/astral-sh/ty) from 0.0.61 to 0.0.63.
- [Release notes](https://github.com/astral-sh/ty/releases)
- [Changelog](https://github.com/astral-sh/ty/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ty/compare/0.0.61...0.0.63)

---
updated-dependencies:
- dependency-name: ty
  dependency-version: 0.0.63
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:46:39 +05:00
Andrej730 a586c7f695 black . 2026-07-24 15:43:59 +05:00
Andrej730 0bad5a9389 ty: add ignores 2026-07-24 15:43:59 +05:00
dependabot[bot] 0ce400cace build(deps): bump gersemi from 0.26.1 to 0.28.0
Bumps [gersemi](https://github.com/BlankSpruce/gersemi) from 0.26.1 to 0.28.0.
- [Release notes](https://github.com/BlankSpruce/gersemi/releases)
- [Changelog](https://github.com/BlankSpruce/gersemi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BlankSpruce/gersemi/compare/0.26.1...0.28.0)

---
updated-dependencies:
- dependency-name: gersemi
  dependency-version: 0.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:29:41 +05:00
dependabot[bot] 91ed59311b build(deps): bump ruff from 0.15.22 to 0.16.0
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.22 to 0.16.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.22...0.16.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:29:22 +05:00
dependabot[bot] 1906481a01 Bump svelte from 5.53.6 to 5.55.8 in /src/ifctester/webapp
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.53.6 to 5.55.8.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.55.8/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.55.8
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:06:42 +05:00
dependabot[bot] 73bf238232 Bump uuid and hyperid in /src/ifctester/webapp
Removes [uuid](https://github.com/uuidjs/uuid). It's no longer used after updating ancestor dependency [hyperid](https://github.com/mcollina/hyperid). These dependencies need to be updated together.


Removes `uuid`

Updates `hyperid` from 3.3.0 to 4.0.0
- [Release notes](https://github.com/mcollina/hyperid/releases)
- [Commits](https://github.com/mcollina/hyperid/compare/v3.3.0...v4.0.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 
  dependency-type: indirect
- dependency-name: hyperid
  dependency-version: 4.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:06:21 +05:00
dependabot[bot] a85d5cc990 Bump lxml from 4.9.1 to 6.1.0 in /src/ifcopenshell-python
Bumps [lxml](https://github.com/lxml/lxml) from 4.9.1 to 6.1.0.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.9.1...lxml-6.1.0)

---
updated-dependencies:
- dependency-name: lxml
  dependency-version: 6.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:04:15 +05:00
dependabot[bot] c16aec2cb0 Bump ws and engine.io-client in /src/ifctester/webapp
Bumps [ws](https://github.com/websockets/ws) and [engine.io-client](https://github.com/socketio/socket.io). These dependencies needed to be updated together.

Updates `ws` from 8.17.1 to 8.21.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.17.1...8.21.0)

Updates `engine.io-client` from 6.6.3 to 6.6.6
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/engine.io-client@6.6.3...engine.io-client@6.6.6)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.21.0
  dependency-type: indirect
- dependency-name: engine.io-client
  dependency-version: 6.6.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:03:56 +05:00
dependabot[bot] 76be31561e build(deps-dev): bump immutable in /src/ifctester/webapp
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.5 to 5.1.9.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.5...v5.1.9)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 15:01:00 +05:00
dependabot[bot] a111c68d44 Bump devalue from 5.6.4 to 5.8.1 in /src/ifctester/webapp
Bumps [devalue](https://github.com/sveltejs/devalue) from 5.6.4 to 5.8.1.
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.6.4...v5.8.1)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.8.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 14:59:19 +05:00
dependabot[bot] 62f627ecc6 Bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 14:59:09 +05:00
dependabot[bot] 279e16f2ab build(deps-dev): bump tar from 7.5.16 to 7.5.21 in /src/ifctester/webapp
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.16 to 7.5.21.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.16...v7.5.21)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.21
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 14:58:54 +05:00
dependabot[bot] 3d68d0f0e5 build(deps-dev): bump postcss in /src/ifctester/webapp
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.4 to 8.5.22.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.4...8.5.22)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.22
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 14:58:50 +05:00
Petru Conduraru e759135608 Bonsai: cache-bust webui static assets so shipped JS/CSS changes reach users
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.
2026-07-24 11:32:34 +02:00
Petru Conduraru 1df738d968 ifc5d: match cost schedule export columns to the Bonsai cost panel (#6251)
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.
2026-07-24 11:32:34 +02:00
Petru Conduraru 98c28a1f30 ifc5d: professional grade ODS/XLSX cost schedule export #6251
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>
2026-07-24 11:32:34 +02:00
Petru Conduraru 21122c0d28 Resolve nested complex quantity paths in the selector (#2041)
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>
2026-07-24 10:31:38 +02:00
67 changed files with 641 additions and 308 deletions
+2 -2
View File
@@ -21,12 +21,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/build-outputs
path: ./build
+2 -2
View File
@@ -9,13 +9,13 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
path: IfcOpenShell
- name: Checkout Build Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/build-outputs
path: ifcopenshell_build
+2 -2
View File
@@ -35,12 +35,12 @@ jobs:
aws --version
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/build-outputs
path: ./build
+2 -2
View File
@@ -35,12 +35,12 @@ jobs:
aws --version
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/build-outputs
path: ./build
+2 -2
View File
@@ -27,12 +27,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/build-outputs
path: ${{ matrix.deps_dir }}
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6 # https://github.com/actions/checkout
- uses: actions/checkout@v7 # https://github.com/actions/checkout
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-tags: true
fetch-depth: 0
+2 -2
View File
@@ -65,7 +65,7 @@ jobs:
config:
short_name: macos
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
@@ -98,7 +98,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout bonsai_unstable_repo repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/bonsai_unstable_repo
token: ${{ secrets.IFCOPENBOT_TOKEN }}
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
config:
short_name: macos
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: '3.11'
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: '3.11'
@@ -21,7 +21,7 @@ jobs:
date: ${{ steps.date.outputs.date }}
verdate: ${{ steps.verdate.outputs.verdate }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set env
run: echo ok go
@@ -75,7 +75,7 @@ jobs:
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
fi
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-22.04
needs: activate
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
@@ -86,7 +86,7 @@ jobs:
name: Docker Build, Tag, Push
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
lfs: true
@@ -47,7 +47,7 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: '3.11'
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+2 -2
View File
@@ -10,9 +10,9 @@ jobs:
publish_website:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Checkout ifctester_org_static_html
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/ifctester_org_static_html
token: ${{ secrets.IFCOPENBOT_TOKEN }}
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
MIN_BLENDER_PY_VERSION: "3.11"
steps:
- name: Action - checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Action - install python
uses: actions/setup-python@v6
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout IfcOpenShell
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
@@ -25,7 +25,7 @@ jobs:
echo "name=$(basename $WHEEL)" >> $GITHUB_OUTPUT
- name: Checkout wasm-wheels
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/wasm-wheels
path: wasm-wheels
+2 -2
View File
@@ -43,7 +43,7 @@ jobs:
CLICOLOR_FORCE: "1"
CMAKE_COLOR_DIAGNOSTICS: "ON"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
@@ -257,7 +257,7 @@ jobs:
cd ../ifcdiff && make test || ERROR=1
cd ../ifcpatch && make test || ERROR=1
pip install -e ../ifc5d --no-deps
pip install odfpy xlsxwriter
pip install odfpy openpyxl
cd ../ifc5d && make test || ERROR=1
pip install -e ../ifcquery --no-deps
cd ../ifcquery && make test || ERROR=1
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
+2 -2
View File
@@ -27,12 +27,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout (recursive)
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Checkout intermediate Pages repo
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/aichat_ifcopenshell_org_static_html
ref: gh-pages
@@ -7,7 +7,7 @@ jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
@@ -27,12 +27,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout (recursive)
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Checkout intermediate Pages repo
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: IfcOpenShell/wasm_ifcopenshell_org_static_html
ref: gh-pages
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
- name: Install C++ dependencies
+5 -2
View File
@@ -121,7 +121,7 @@ import tarfile
import threading
from datetime import datetime
ssl._create_default_https_context = ssl._create_unverified_context
ssl._create_default_https_context = ssl._create_unverified_context # ty:ignore[invalid-assignment]
import time
from collections.abc import Generator, Sequence
@@ -697,7 +697,10 @@ def build_dependency(
compr = "xz"
else:
raise RuntimeError("fix source for new download type")
download_tarfile = tarfile.open(name=download_tarfile_path, mode=f"r:{compr}")
# ty: false positive bug upstream.
download_tarfile = tarfile.open(
name=download_tarfile_path, mode=f"r:{compr}"
) # ty:ignore[no-matching-overload]
# tarfile seriously doesn't have a function to retrieve the root directory more easily
extract_dir_name = os.path.commonprefix([x for x in download_tarfile.getnames() if x != "."])
# run([tar, "--exclude=\"*/*\"", "-tf", download_name], cwd=build_dir).strip() no longer works
+3 -3
View File
@@ -1,5 +1,5 @@
black==26.3.1
ruff==0.15.22
ruff==0.16.0
poethepoet
ty==0.0.61
gersemi==0.26.1
ty==0.0.63
gersemi==0.28.0
+52 -6
View File
@@ -10,6 +10,7 @@ if bonsai_lib_path:
import argparse
import base64
import json
import urllib.parse
import xml.etree.ElementTree as ET
import pystache
@@ -18,8 +19,53 @@ from aiohttp import web
sio_port = 8080 # default port
def get_asset_version() -> str:
"""A cache-busting token appended to locally served static asset URLs.
Browsers otherwise keep serving a stale cached copy of static/js and
static/css after Bonsai ships a code change, until the user does a hard
refresh. Using the Bonsai version (which includes the build's commit
hash) means the token changes on every shipped update.
"""
if bonsai_version:
return urllib.parse.quote(bonsai_version, safe="")
# Fallback for standalone runs without BONSAI_VERSION set (e.g. running
# sioserver.py directly outside of Blender): derive a token from the
# newest mtime among the static assets, so it still changes whenever the
# shipped files change.
static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
latest_mtime = 0
for root, _dirs, files in os.walk(static_dir):
for name in files:
latest_mtime = max(latest_mtime, int(os.path.getmtime(os.path.join(root, name))))
return f"dev-{latest_mtime}"
asset_version = get_asset_version()
@web.middleware
async def no_cache_static_middleware(request: web.Request, handler):
"""Force revalidation of locally served static assets.
Query-string version stamping (see `asset_version`) busts the cache for
the HTML-referenced entry points, but JS files that statically import
other local modules (e.g. cost.js/gantt.js importing utilities/costui.js)
reference those modules by an un-stamped relative path. Marking all
/static/ and /jsgantt/ responses as no-cache makes browsers always
revalidate with the server (a cheap conditional GET / 304 when nothing
changed), so nested imports also pick up shipped changes without
requiring a hard refresh.
"""
response = await handler(request)
if request.path.startswith("/static/") or request.path.startswith("/jsgantt/"):
response.headers["Cache-Control"] = "no-cache, must-revalidate"
return response
sio = socketio.AsyncServer(cors_allowed_origins="*", async_mode="aiohttp", max_http_buffer_size=10000000)
app = web.Application()
app = web.Application(middlewares=[no_cache_static_middleware])
sio.attach(app)
@@ -199,28 +245,28 @@ class BlenderNamespace(socketio.AsyncNamespace):
async def schedules(request):
with open("templates/index.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
return web.Response(text=html_content, content_type="text/html")
async def costing(request):
with open("templates/costing.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
return web.Response(text=html_content, content_type="text/html")
async def sequencing(request):
with open("templates/gantt.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
return web.Response(text=html_content, content_type="text/html")
async def documentation(request):
with open("templates/drawings.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
return web.Response(text=html_content, content_type="text/html")
@@ -229,7 +275,7 @@ async def documentation(request):
async def demo(request):
with open("templates/demo.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
return web.Response(text=html_content, content_type="text/html")
@@ -250,6 +250,14 @@ export class CostUI {
},
});
CostUI.addRibbonButton({
text: "Download CSV",
icon: "fa-solid fa-file-csv",
callback: () => {
CostUI.downloadCsv();
},
});
CostUI.addRibbonButton({
text: "Hide Schedules",
icon: "fa-regular fa-eye-slash",
@@ -523,6 +531,81 @@ export class CostUI {
}
}
static downloadCsv() {
const tables = document.querySelectorAll("table[id^='cost-items-']");
if (tables.length === 0) {
alert("No cost schedule loaded to export!");
return;
}
tables.forEach((table) => {
const scheduleId = table.id.split("-").pop();
const csv = CostUI.tableToCsv(table);
if (csv === null) {
return;
}
const nameEl = document.querySelector(
"#cost-schedule-container-" + scheduleId + " .form-header span"
);
const scheduleName = nameEl
? nameEl.textContent
: "cost_schedule_" + scheduleId;
CostUI.triggerCsvDownload(csv, scheduleName + ".csv");
});
}
static tableToCsv(table) {
const escapeCsvCell = (value) => {
const text = (value === null || value === undefined ? "" : value)
.toString()
.trim();
if (/[",\n]/.test(text)) {
return '"' + text.replace(/"/g, '""') + '"';
}
return text;
};
const cellText = (cell) => {
const input = cell.querySelector("input");
return input ? input.value : cell.innerText;
};
// The Actions column only holds buttons (edit/delete/etc), not data.
const isDataColumn = (column) => column && column !== "Actions";
const headerCells = Array.from(table.querySelectorAll("thead th")).filter(
(th) => isDataColumn(th.getAttribute("data-column"))
);
if (headerCells.length === 0) {
return null;
}
const rows = [headerCells.map((th) => escapeCsvCell(th.textContent)).join(",")];
table.querySelectorAll("tbody tr").forEach((row) => {
const cells = Array.from(row.children).filter((cell) =>
isDataColumn(cell.getAttribute("data-column"))
);
if (cells.length === 0) {
return; // e.g. the "No cost items found" placeholder row.
}
rows.push(cells.map((cell) => escapeCsvCell(cellText(cell))).join(","));
});
return rows.join("\n");
}
static triggerCsvDownload(csvContent, filename) {
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
static createCostTable({ costSchedule, currency, callbacks }) {
const preferences = CostUI.getColumnPreferences();
const isScheduleOfRates = costSchedule.PredefinedType === "SCHEDULEOFRATES";
@@ -7,7 +7,7 @@
<link
rel="stylesheet"
type="text/css"
href="/static/css/cost.css"
href="/static/css/cost.css?v={{v}}"
id="index-stylesheet"
/>
<link
@@ -21,7 +21,7 @@
/>
<script
type="text/javascript"
src="/static/js/jquery.min.js"
src="/static/js/jquery.min.js?v={{v}}"
></script>
<script
type="text/javascript"
@@ -34,7 +34,7 @@
<script>
var SOCKET_PORT = {{port}};
</script>
<script type="module" defer src="./static/js/cost.js"></script>
<script type="module" defer src="./static/js/cost.js?v={{v}}"></script>
</head>
<body>
<nav>
@@ -12,14 +12,14 @@
/>
<!-- here we request the CSS file from the server, -->
<!-- using registered static path in the server -->
<link rel="stylesheet" href="/static/css/demo.css" id="demo-stylesheet" />
<link rel="stylesheet" href="/static/css/demo.css?v={{v}}" id="demo-stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
/>
<script
type="text/javascript"
src="/static/js/jquery.min.js"
src="/static/js/jquery.min.js?v={{v}}"
></script>
<script
type="text/javascript"
@@ -33,7 +33,7 @@
</script>
<!-- here we request the JS file from the server-->
<!-- using registered static path in the server -->
<script defer src="./static/js/demo.js"></script>
<script defer src="./static/js/demo.js?v={{v}}"></script>
</head>
<body>
<!-- the navigation bar at the top of the page. -->
@@ -15,7 +15,7 @@
/>
<link
rel="stylesheet"
href="/static/css/drawings.css"
href="/static/css/drawings.css?v={{v}}"
id="drawings-stylesheet"
/>
<link
@@ -24,7 +24,7 @@
/>
<script
type="text/javascript"
src="/static/js/jquery.min.js"
src="/static/js/jquery.min.js?v={{v}}"
></script>
<script
type="text/javascript"
@@ -45,7 +45,7 @@
<script>
var SOCKET_PORT = {{port}};
</script>
<script defer src="./static/js/drawings.js"></script>
<script defer src="./static/js/drawings.js?v={{v}}"></script>
</head>
<body>
<nav>
@@ -9,25 +9,25 @@
type="image/x-icon"
href="https://bonsaibim.org/assets/images/favicon-blender.png"
/>
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css" />
<link rel="stylesheet" href="/static/css/gantt.css" id="gantt-stylesheet" />
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css?v={{v}}" />
<link rel="stylesheet" href="/static/css/gantt.css?v={{v}}" id="gantt-stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
/>
<script
type="text/javascript"
src="/static/js/jquery.min.js"
src="/static/js/jquery.min.js?v={{v}}"
></script>
<script
type="text/javascript"
src="https://cdn.socket.io/4.0.0/socket.io.min.js"
></script>
<script type="text/javascript" src="./jsgantt/jsgantt.js"></script>
<script type="text/javascript" src="./jsgantt/jsgantt.js?v={{v}}"></script>
<script>
var SOCKET_PORT = {{port}};
</script>
<script type="module" defer src="./static/js/gantt.js"></script>
<script type="module" defer src="./static/js/gantt.js?v={{v}}"></script>
</head>
<body>
<nav class="no-print">
@@ -9,7 +9,7 @@
type="image/x-icon"
href="https://bonsaibim.org/assets/images/favicon-blender.png"
/>
<link rel="stylesheet" href="/static/css/index.css" id="index-stylesheet" />
<link rel="stylesheet" href="/static/css/index.css?v={{v}}" id="index-stylesheet" />
<link
rel="stylesheet"
id="tabulator-stylesheet"
@@ -21,7 +21,7 @@
/>
<script
type="text/javascript"
src="/static/js/jquery.min.js"
src="/static/js/jquery.min.js?v={{v}}"
></script>
<script
type="text/javascript"
@@ -34,7 +34,7 @@
<script>
var SOCKET_PORT = {{port}};
</script>
<script defer src="./static/js/index.js"></script>
<script defer src="./static/js/index.js?v={{v}}"></script>
</head>
<body>
<nav>
+3 -3
View File
@@ -168,12 +168,12 @@ class Polyline(bonsai.core.tool.Polyline):
distance = (mouse_vector - last_point).length
if distance < 0:
return
angle, orientation_angle, angle_round_threshold = None, None, None
angle, orientation_angle = None, None
angle_round_threshold = 1000 # Avoids rounding when distance is too big
if distance > 0:
angle = tool.Cad.angle_3_vectors(
second_to_last_point, last_point, mouse_vector, new_angle=None, degrees=True
)
angle_round_threshold = 1000 # Avoids rounding when distance is too big
# Round angle to the nearest 0.05
angle = round(angle / 0.05) * 0.05 if distance < angle_round_threshold else angle
@@ -189,7 +189,7 @@ class Polyline(bonsai.core.tool.Polyline):
angle = 0
orientation_angle = 0
if input_ui:
assert angle is not None and orientation_angle is not None and angle_round_threshold is not None
assert angle is not None and orientation_angle is not None
if should_round:
angle_snap = tool.Snap.get_angle_snap_value(context)
angle = angle_snap * round(angle / angle_snap) if distance < angle_round_threshold else angle
+3 -3
View File
@@ -644,9 +644,9 @@ class Style(bonsai.core.tool.Style):
) -> tuple[tuple[float, float, float], float]:
if material.node_tree:
nodes = material.node_tree.nodes
output_node = next(
(n for n in nodes if n.type == "OUTPUT_MATERIAL" and n.is_active_output), None
) or next((n for n in nodes if n.type == "OUTPUT_MATERIAL"), None)
output_node = next((n for n in nodes if n.type == "OUTPUT_MATERIAL" and n.is_active_output), None) or next(
(n for n in nodes if n.type == "OUTPUT_MATERIAL"), None
)
if output_node:
result = cls._color_from_shader_socket(output_node.inputs["Surface"])
if result:
+4
View File
@@ -497,6 +497,8 @@ class TestGitMergetool:
with tempfile.TemporaryDirectory() as tmpdir:
ifc_path = os.path.join(tmpdir, "model.ifc")
mock_repo = mock.MagicMock()
# A clean mergetool resolution leaves no unmerged blobs in the index.
mock_repo.index.unmerged_blobs.return_value = {}
IfcGitRepo.repo = mock_repo
result = IfcGit.git_mergetool("ifcmerge", ifc_path)
assert result is None
@@ -511,6 +513,8 @@ class TestGitMergetool:
report_path = ifc_path + ".ifcmerge"
open(report_path, "w").close()
mock_repo = mock.MagicMock()
# A clean mergetool resolution leaves no unmerged blobs in the index.
mock_repo.index.unmerged_blobs.return_value = {}
IfcGitRepo.repo = mock_repo
result = IfcGit.git_mergetool("ifcmerge", ifc_path)
assert result is None
+28
View File
@@ -56,3 +56,31 @@ class TestValidateInput(NewFile):
# Angle.
assert subject.validate_input("25", "A") == (True, "25.0")
class TestCalculateDistanceAndAngle(NewFile):
def test_it_does_not_crash_when_distance_is_zero_and_should_round(self, monkeypatch):
# Regression test for #8597: right after placing the first polyline
# point, the initial mouse sample can equal the last placed point
# (distance == 0), e.g. entering the viewport on a YZ plane wall.
# angle_round_threshold used to only be assigned in the
# `distance > 0` branch, crashing when should_round reads it here.
# get_increment_snap_value requires a real 3D viewport rv3d, which
# is unrelated to this bug, so it's stubbed out for a headless run.
monkeypatch.setattr(tool.Snap, "get_increment_snap_value", classmethod(lambda cls, context: 1.0))
polyline_props = tool.Model.get_polyline_props()
mouse_point = polyline_props.snap_mouse_point.add()
mouse_point.x, mouse_point.y, mouse_point.z = 0, 0, 0
tool_state = subject.create_tool_state()
tool_state.is_input_on = False
tool_state.use_default_container = False
tool_state.plane_method = "YZ"
input_ui = subject.create_input_ui(input_options=["D", "A", "X", "Y", "Z"])
subject.calculate_distance_and_angle(bpy.context, input_ui, tool_state, should_round=True)
assert input_ui.get_number_value("D") == 0
assert input_ui.get_number_value("A") == 0
+99 -38
View File
@@ -386,12 +386,71 @@ class Ifc5Dwriter:
"PredefinedType": cost_schedule.PredefinedType,
}
# Presentation formats (.ods / .xlsx) mirror exactly what the Bonsai cost
# panel shows for a cost item: ID (Identification), Name, Quantity,
# Value (RateSubtotal) and the calculated Total Cost. Everything else
# (internal bookkeeping columns, Description, Unit, per-category cost
# breakdowns) is bonsai/csv2ifc round-trip plumbing and stays out of the
# presentation formats. The .csv format keeps the full column set since
# csv2ifc reads those extra columns back in on import.
PRESENTATION_COLUMNS = ("Identification", "Name", "Quantity", "RateSubtotal", "TotalPrice")
# Header text as shown in presentation formats, matching the Bonsai cost
# panel's own column labels (see BIM_UL_cost_items_trait.draw_header).
PRESENTATION_LABELS = {
"Identification": "ID",
"RateSubtotal": "Value",
"TotalPrice": "Total Cost",
}
def multiply_cells(self, cell1, cell2):
return "={}*{}".format(cell1, cell2)
def sum_cells(self, list_of_cells):
return "=SUM({})".format(",".join(list_of_cells))
def get_visible_headers(self, schedule_id: int) -> list[str]:
"""Internal column keys shown in presentation formats, in panel order."""
headers = self.sheet_data[schedule_id]["headers"]
return [h for h in self.PRESENTATION_COLUMNS if h in headers]
def get_display_label(self, column: str) -> str:
"""Header text to write for a column in presentation formats."""
return self.PRESENTATION_LABELS.get(column, column)
def is_numeric_column(self, column: str) -> bool:
return column in ("Quantity", "RateSubtotal", "TotalPrice") or column.endswith(" Cost")
def get_total_price_formula(self, schedule_id: int, cost_item_index: int, first_data_row: int) -> Union[str, None]:
"""Spreadsheet formula for the TotalPrice cell of a cost item, or None for a plain value.
Sum items get ``=SUM(...)`` over the TotalPrice cells of their direct
children, leaf items with a quantity and a rate get ``=Quantity*RateSubtotal``.
Assumes one cost item per row, in ``cost_items`` order, starting at
``first_data_row`` (1-based).
"""
items = self.sheet_data[schedule_id]["cost_items"]
headers = self.get_visible_headers(schedule_id)
if "TotalPrice" not in headers:
return None
item = items[cost_item_index]
col = lambda name: self.column_indexes[headers.index(name)]
if item["ItemIsASum"]:
prefix = item["Hierarchy"] + "."
child_rows = [
first_data_row + i
for i, other in enumerate(items)
if other["Hierarchy"].startswith(prefix) and "." not in other["Hierarchy"][len(prefix) :]
]
if child_rows:
total_col = col("TotalPrice")
return self.sum_cells(["{}{}".format(total_col, r) for r in child_rows])
return None
if "Quantity" in headers and "RateSubtotal" in headers and item.get("Quantity") and item.get("RateSubtotal"):
row = first_data_row + cost_item_index
return self.multiply_cells("{}{}".format(col("Quantity"), row), "{}{}".format(col("RateSubtotal"), row))
return None
def get_cell_position(self, schedule_id, attribute):
def get_position_in_list(item, item_list):
try:
@@ -482,32 +541,25 @@ class Ifc5DOdsWriter(Ifc5Dwriter):
assert False, type
row.addElement(cell)
def add_cost_item_rows(table, cost_data):
first_data_row = 6 # 3 metadata rows, 1 blank row, 1 header row.
def add_cost_item_rows(table, cost_data, cost_item_index):
row = TableRow()
self.row_count += 1
style = self.colours.get(cost_data["Index"])
for i, column in enumerate(self.sheet_data[cost_schedule.id()]["headers"]):
if column == "Total Price" and cost_data["Quantity"] != 0 and cost_data["Rate Subtotal"]:
cell_quantity = self.get_cell_position(cost_schedule.id(), "Quantity")
cell_subtotal_rate = self.get_cell_position(cost_schedule.id(), "Rate Subtotal")
value = self.multiply_cells(cell_quantity, cell_subtotal_rate)
cell = TableCell(formula=value, stylename=self.colours.get(cost_data["Index"]))
for column in self.get_visible_headers(cost_schedule.id()):
value = cost_data.get(column, "")
formula = None
if column == "TotalPrice":
formula = self.get_total_price_formula(cost_schedule.id(), cost_item_index, first_data_row)
if formula:
cell = TableCell(formula=formula, stylename=style)
elif self.is_numeric_column(column) and isinstance(value, (int, float)):
cell = TableCell(valuetype="float", value=value, stylename=style)
else:
value = cost_data.get(column, "")
cell = TableCell(valuetype="string", stylename=self.colours.get(cost_data["Index"]))
cell = TableCell(valuetype="string", stylename=style)
cell.addElement(P(text=value))
# TODO:FIX QUANTITY AND COST TO SHOW AS NUMBERS AND CURRENCIES
# elif "Cost" in column or "Rate" in column:
# value = cost_data.get(column, "")
# cell = TableCell(valuetype="string", stylename=self.colours.get(cost_data["Index"]))
# cell.addElement(P(text=value))
# # cell.addElement(P(text=u"${}".format(value))) # The current displayed value
# print("Should add rate ", "${}".format(value))
# elif "Quantity" in column:
# value = cost_data.get(column, "")
# cell = TableCell(valuetype="float", stylename=self.colours.get(cost_data["Index"]))
# print("Should add quantity",value)
# cell.addElement(P(text=value))
row.addElement(cell)
table.addElement(row)
@@ -534,20 +586,22 @@ class Ifc5DOdsWriter(Ifc5Dwriter):
table.addElement(new)
header_row = TableRow()
for header in self.sheet_data[cost_schedule.id()]["headers"]:
add_cell(type="text", value=header, row=header_row, style="fed8b1")
for header in self.get_visible_headers(cost_schedule.id()):
add_cell(type="text", value=self.get_display_label(header), row=header_row, style="fed8b1")
table.addElement(header_row)
self.row_count = 5
for cost_item_data in self.sheet_data[cost_schedule.id()]["cost_items"]:
add_cost_item_rows(table, cost_item_data)
for i, cost_item_data in enumerate(self.sheet_data[cost_schedule.id()]["cost_items"]):
add_cost_item_rows(table, cost_item_data, i)
self.doc.spreadsheet.addElement(table)
class Ifc5DXlsxWriter(Ifc5Dwriter):
def write(self) -> None:
import xlsxwriter
# openpyxl rather than xlsxwriter: it is what ifccsv already uses and
# what ships with Bonsai, so XLSX export works out of the box there.
import openpyxl
super().write()
os.makedirs(self.output, exist_ok=True)
@@ -558,24 +612,31 @@ class Ifc5DXlsxWriter(Ifc5Dwriter):
else:
file_name += cost_schedule.Name or ""
self.file_path = os.path.join(self.output, "{}.xlsx".format(file_name))
self.workbook = xlsxwriter.Workbook(self.file_path)
self.workbook = openpyxl.Workbook()
self.workbook.remove(self.workbook.active)
for cost_schedule in self.cost_schedules:
self.write_table(cost_schedule)
self.workbook.close()
self.workbook.save(self.file_path)
def write_table(self, cost_schedule):
worksheet = self.workbook.add_worksheet(self.sheet_data[cost_schedule.id()]["Name"])
headers = self.sheet_data[cost_schedule.id()]["headers"]
for i, header in enumerate(headers):
worksheet.write(0, i, header)
import re
row = 1
for cost_item_data in self.sheet_data[cost_schedule.id()]["cost_items"]:
col = 0
sheet_id = cost_schedule.id()
title = re.sub(r"[\[\]:*?/\\]", "_", self.sheet_data[sheet_id]["Name"])[:31]
worksheet = self.workbook.create_sheet(title)
headers = self.get_visible_headers(sheet_id)
worksheet.append([self.get_display_label(h) for h in headers])
first_data_row = 2 # Row 1 is the header.
for i, cost_item_data in enumerate(self.sheet_data[sheet_id]["cost_items"]):
row = []
for header in headers:
worksheet.write(row, col, cost_item_data.get(header, ""))
col += 1
row += 1
formula = None
if header == "TotalPrice":
formula = self.get_total_price_formula(sheet_id, i, first_data_row)
# openpyxl treats strings starting with "=" as formulas.
row.append(formula if formula else cost_item_data.get(header, None))
worksheet.append(row)
class Ifc5DPdfWriter(Ifc5Dwriter):
+8 -4
View File
@@ -20,10 +20,14 @@ dependencies = [
"typing_extensions",
]
[project.optional-dependencies]
advanced = [
"typst",
]
[project.optional-dependencies]
advanced = [
"typst",
]
spreadsheet = [
"odfpy",
"openpyxl",
]
[project.urls]
Homepage = "http://ifcopenshell.org"
+28
View File
@@ -120,6 +120,34 @@ class TestCsv2Ifc:
assert len(list(Path(temp_csv_dir).glob("*.ods"))) == 1
assert len(list(Path(temp_csv_dir).glob("*.xlsx"))) == 1
def test_xlsx_columns_match_cost_panel(self):
"""ODS/XLSX are presentation formats: they must show exactly what the
Bonsai cost panel shows (ID, Name, Quantity, Value, Total Cost), no
internal bookkeeping columns, no Description/Unit, no per-category
cost breakdown. See #6251."""
import openpyxl
ifc_file = self.setup_ifc_file()
csv_filepath = Path(__file__).parent.parent / "sample_cost_schedule_house_FR.csv"
ifc5d.csv2ifc.Csv2Ifc(str(csv_filepath), ifc_file).execute()
with tempfile.TemporaryDirectory("w") as temp_dir:
writer = ifc5d.ifc5Dspreadsheet.Ifc5DXlsxWriter(ifc_file, temp_dir)
writer.write()
workbook = openpyxl.load_workbook(next(Path(temp_dir).glob("*.xlsx")))
worksheet = workbook.active
headers = [cell.value for cell in next(worksheet.iter_rows())]
assert headers == ["ID", "Name", "Quantity", "Value", "Total Cost"]
# A leaf item (has quantity and value) gets Quantity * Value.
leaf_row = next(row for row in worksheet.iter_rows(min_row=2) if row[0].value == "DB.1.1")
assert leaf_row[4].value == "=C{}*D{}".format(leaf_row[0].row, leaf_row[0].row)
# A parent/sum item gets the sum of its direct children's Total Cost.
parent_row = next(row for row in worksheet.iter_rows(min_row=2) if row[0].value == "DB.1")
assert parent_row[4].value.startswith("=SUM(")
class TestSerialiseCostQuantities:
def test_quantity_name_with_special_characters_round_trips_as_json(self):
+2 -2
View File
@@ -36,7 +36,7 @@ import ifcopenshell.util.representation
import ifcopenshell.util.selector
import numpy as np
from deepdiff import DeepDiff
from orderly_set import OrderedSet
from orderly_set import StableSet
__version__ = version = "0.0.0"
@@ -257,7 +257,7 @@ class IfcDiff:
def json_dump_default(self, obj):
# result of DeepDiff may contain ordered sets
if isinstance(obj, (OrderedSet, set)):
if isinstance(obj, (StableSet, set)):
return list(obj)
return json.JSONEncoder.default(None, obj)
+28
View File
@@ -16,9 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import json
import os
import tempfile
import ifcopenshell
import ifcopenshell.api.context
import ifcopenshell.api.geometry
import ifcopenshell.api.pset
import ifcopenshell.api.root
import ifcopenshell.api.unit
import ifcopenshell.util.representation
@@ -94,6 +99,29 @@ class TestIfcDiff:
assert ifc_diff.deleted_elements == set()
assert ifc_diff.change_register == {wall.GlobalId: {"attributes_changed": True}}
def test_property_diff_exports_to_json(self):
# Regression test for #8905: comparing "property" relationships makes
# DeepDiff report a dictionary_item_added as a SetOrdered, which
# json.dump couldn't serialise, crashing export() with no results.
ifc_file = setup_project()
wall = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcWall", name="Foo")
new_file = ifc_file.from_string(ifc_file.to_string())
wall_new = new_file.by_id(wall.id())
pset = ifcopenshell.api.pset.add_pset(new_file, product=wall_new, name="Pset_WallCommon")
ifcopenshell.api.pset.edit_pset(new_file, pset=pset, properties={"FireRating": "2HR"})
ifc_diff = ifcdiff.IfcDiff(ifc_file, new_file, relationships=["property"])
ifc_diff.diff()
assert ifc_diff.change_register[wall.GlobalId]["properties_changed"]
with tempfile.TemporaryDirectory() as tmp_dir:
output = os.path.join(tmp_dir, "diff.json")
ifc_diff.export(output)
with open(output) as f:
results = json.load(f)
assert "Pset_WallCommon" in str(results["changed"][wall.GlobalId]["properties_changed"])
def test_changed_geometry(self):
ifc_file = setup_project()
wall = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcWall", name="Foo")
+102 -58
View File
@@ -21,66 +21,110 @@
#define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry;
#include "../profile_helper.h"
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCenterLineProfileDef* inst) {
return nullptr;
/*
const double d = inst->Thickness() * length_unit_ / 2.;
auto f = taxonomy::make<taxonomy::face>();
auto ofc = taxonomy::make<taxonomy::offset_curve>();
ofc->basis = map(inst->Curve());
ofc->offset = d;
// @todo
// f->children.push_back(ofc);
return f;
*/
const double eps = settings_.get<settings::Precision>().get();
// @todo we still need to handle this in the geometry libraries
/*
TopoDS_Wire wire;
if (!convert_wire(inst->Curve(), wire)) return false;
// BRepOffsetAPI_MakeOffset insists on creating circular arc
// segments for joining the curves that constitute the center
// line. This is probably not in accordance with the IFC spec.
// Although it does not specify a method to join segments
// explicitly, it does dictate 'a constant thickness along the
// curve'. Therefore for simple singular wires a quick
// alternative is provided that uses a straight join.
TopExp_Explorer exp(wire, TopAbs_EDGE);
TopoDS_Edge edge = TopoDS::Edge(exp.Current());
exp.Next();
if (!exp.More()) {
double u1, u2;
Handle(Geom_Curve) curve = BRep_Tool::Curve(edge, u1, u2);
Handle(Geom_TrimmedCurve) trim = new Geom_TrimmedCurve(curve, u1, u2);
Handle(Geom_OffsetCurve) c1 = new Geom_OffsetCurve(trim, d, gp::DZ());
Handle(Geom_OffsetCurve) c2 = new Geom_OffsetCurve(trim, -d, gp::DZ());
gp_Pnt c1a, c1b, c2a, c2b;
c1->D0(c1->FirstParameter(), c1a);
c1->D0(c1->LastParameter(), c1b);
c2->D0(c2->FirstParameter(), c2a);
c2->D0(c2->LastParameter(), c2b);
BRepBuilderAPI_MakeWire mw;
mw.Add(BRepBuilderAPI_MakeEdge(c1));
mw.Add(BRepBuilderAPI_MakeEdge(c1a, c2a));
mw.Add(BRepBuilderAPI_MakeEdge(c2));
mw.Add(BRepBuilderAPI_MakeEdge(c2b, c1b));
face = BRepBuilderAPI_MakeFace(mw.Wire());
} else {
BRepOffsetAPI_MakeOffset offset(BRepBuilderAPI_MakeFace(gp_Pln(gp::Origin(), gp::DZ())));
offset.AddWire(wire);
offset.Perform(d);
face = BRepBuilderAPI_MakeFace(TopoDS::Wire(offset));
if (d < eps) {
logger_.Warning("GEO", 403, "Thickness below precision for:", inst);
return nullptr;
}
return true;
*/
auto mapped = map(inst->Curve());
if (!mapped) {
return nullptr;
}
auto crv = taxonomy::dcast<taxonomy::loop>(mapped);
if (!crv || crv->children.empty()) {
logger_.Warning("GEO", 404, "Unsupported centerline curve for:", inst);
return nullptr;
}
if (!crv->is_polyhedron()) {
logger_.Warning("GEO", 404, "Unsupported curved centerline segments for:", inst);
return nullptr;
}
std::vector<Eigen::Vector2d> pts;
pts.reserve(crv->children.size() + 1);
for (auto& e : crv->children) {
if (e->start.which() != 1 || e->end.which() != 1) {
logger_.Warning("GEO", 404, "Unsupported parametric trims on centerline for:", inst);
return nullptr;
}
Eigen::Vector2d p = boost::get<taxonomy::point3::ptr>(e->start)->ccomponents().head<2>();
Eigen::Vector2d q = boost::get<taxonomy::point3::ptr>(e->end)->ccomponents().head<2>();
if (pts.empty()) {
pts.push_back(p);
} else if ((pts.back() - p).norm() > eps) {
logger_.Warning("GEO", 405, "Unsupported discontinuous centerline for:", inst);
return nullptr;
}
pts.push_back(q);
}
if (pts.size() < 2 || (pts.front() - pts.back()).norm() < eps) {
logger_.Warning("GEO", 405, "Unsupported closed or degenerate centerline for:", inst);
return nullptr;
}
const size_t n = pts.size();
std::vector<Eigen::Vector2d> normals(n - 1);
for (size_t i = 0; i < n - 1; ++i) {
Eigen::Vector2d t = pts[i + 1] - pts[i];
const double l = t.norm();
if (l < eps) {
logger_.Warning("GEO", 405, "Degenerate centerline segment for:", inst);
return nullptr;
}
t /= l;
normals[i] = Eigen::Vector2d(-t.y(), t.x());
}
// Straight miter joins at the vertices, because the profile prescribes a
// constant thickness along the curve, which the arc joins of a generic
// offset algorithm would violate.
std::vector<Eigen::Vector2d> miters(n);
miters.front() = normals.front();
miters.back() = normals.back();
for (size_t i = 1; i < n - 1; ++i) {
const double denom = 1. + normals[i - 1].dot(normals[i]);
if (denom < 1.e-9) {
logger_.Warning("GEO", 405, "Centerline reverses onto itself for:", inst);
return nullptr;
}
miters[i] = (normals[i - 1] + normals[i]) / denom;
}
std::vector<Eigen::Vector2d> polygon;
polygon.reserve(2 * n);
for (size_t i = 0; i < n; ++i) {
polygon.push_back(pts[i] - d * miters[i]);
}
for (size_t i = n; i-- > 0;) {
polygon.push_back(pts[i] + d * miters[i]);
}
double twice_area = 0.;
for (size_t i = 0; i < polygon.size(); ++i) {
const auto& a = polygon[i];
const auto& b = polygon[(i + 1) % polygon.size()];
twice_area += a.x() * b.y() - b.x() * a.y();
}
if (twice_area < 0.) {
std::reverse(polygon.begin(), polygon.end());
}
std::vector<taxonomy::point3::ptr> ps;
ps.reserve(polygon.size() + 1);
for (auto& p : polygon) {
ps.push_back(taxonomy::make<taxonomy::point3>(p.x(), p.y(), 0.));
}
ps.push_back(ps.front());
auto loop = polygon_from_points(ps);
auto face = taxonomy::make<taxonomy::face>();
face->children = { loop };
return face;
}
@@ -73,13 +73,6 @@ FILTERED_CARTESIAN_QUOTIENT: Any
EXACT_PREDICATES: Any
EXACT_CONSTRUCTIONS: Any
FT_AUTODETECT: Any
FT_IFCSPF: Any
FT_IFCXML: Any
FT_IFCZIP: Any
FT_ROCKSDB: Any
FT_UNKNOWN: Any
CURVES: Any
SURFACES_AND_SOLIDS: Any
CURVES_SURFACES_AND_SOLIDS: Any
@@ -133,7 +126,6 @@ class BRepElement(Element):
def calculate_projected_surface_area(self, along_x, along_y, along_z): ...
@property
def geometry(self) -> BRep: ...
def geometry_pointer(self): ...
@property
def surface_area(self): ...
@property
@@ -625,7 +617,6 @@ class TriangulationElement(Element):
def __init__(self, *args): ...
@property
def geometry(self) -> Triangulation: ...
def geometry_pointer(self): ...
class TtlWktSerializer(WriteOnlyGeometrySerializer):
def __init__(self, filename, geometry_settings, settings, logger=None): ...
@@ -1799,21 +1790,20 @@ def flatten(deep): ...
def get_feature(x): ...
def get_info_cpp(v, include_identifier=True): ...
def get_log(): ...
def guess_file_type(fn): ...
def helmert_curve_point(A0, A1, A2, s): ...
def kind_to_string(k): ...
def less(arg1, arg2): ...
def line_segments_to_polygons(s, eps, segments): ...
def map_shape(settings, instance): ...
def logger_or_root(logger) -> logger: ...
def logger_or_root(logger: logger | None) -> logger: ...
def nary_union(sequence): ...
def new_IfcBaseClass(schema_identifier: str, name: str) -> entity_instance: ...
def open(fn: str, readonly: bool = False, logger=None) -> file: ...
def parse_ifcxml(filename, logger=None): ...
def polygons_to_svg(*args): ...
def read(data): ...
def register_schema(arg1): ...
def schema_by_name(arg1: str) -> schema_definition: ...
def register_schema(schema: schema_definition) -> None: ...
def schema_by_name(schema_name: str) -> schema_definition: ...
def schema_names() -> tuple[str, ...]: ...
def serialise(schema_name, shape_str, advanced=True): ...
def set_feature(x, v): ...
@@ -1,4 +1,4 @@
lxml==4.9.1
lxml==6.1.0
numpy==1.22.0
regex==2021.4.4
xmlschema==1.6.4
@@ -540,8 +540,15 @@ def _get_element_value(element: ifcopenshell.entity_instance, keys: list[str]) -
value = results or None
if value and len(value) == 1:
value = value[0]
elif key in value:
value = value[key]
else:
value = value.get(key, None)
# A nested complex quantity/property (IfcPhysicalComplexQuantity /
# IfcComplexProperty) is represented as a dict whose nested members
# live under a "properties" sub-dict. Descend into it so that nested
# values are reachable with the natural "Qto.Complex.Nested" path.
subprops = value.get("properties")
value = subprops.get(key, None) if isinstance(subprops, dict) else None
elif isinstance(value, (list, tuple, set)): # If we use regex
if isinstance(key, str) and key.isnumeric():
try:
@@ -195,6 +195,37 @@ class TestGetElementValue(test.bootstrap.IFC4):
assert subject.get_element_value(element, "/Pset_.*Common/.Status") == ["New"]
assert subject.get_element_value(element, "/Pset_.*Common/.Status.0") == "New"
def test_selecting_a_nested_complex_quantity(self):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
complex_quantity = self.file.create_entity(
"IfcPhysicalComplexQuantity",
Name="Layer1",
Discrimination="layer",
HasQuantities=[
self.file.create_entity("IfcQuantityLength", Name="Width", LengthValue=0.1),
self.file.create_entity("IfcQuantityLength", Name="Height", LengthValue=2.5),
],
)
quantity = self.file.create_entity(
"IfcElementQuantity",
GlobalId=ifcopenshell.guid.new(),
Name="Qto_Custom",
Quantities=[complex_quantity, self.file.create_entity("IfcQuantityArea", Name="NetArea", AreaValue=5.0)],
)
self.file.create_entity(
"IfcRelDefinesByProperties",
GlobalId=ifcopenshell.guid.new(),
RelatedObjects=[element],
RelatingPropertyDefinition=quantity,
)
# A simple quantity in the same set still resolves normally.
assert subject.get_element_value(element, "Qto_Custom.NetArea") == 5.0
# Nested quantities of a complex quantity are reachable with the natural path.
assert subject.get_element_value(element, "Qto_Custom.Layer1.Width") == 0.1
assert subject.get_element_value(element, "Qto_Custom.Layer1.Height") == 2.5
# The explicit "properties" path is preserved for backwards compatibility.
assert subject.get_element_value(element, "Qto_Custom.Layer1.properties.Width") == 0.1
class TestFilterElements(test.bootstrap.IFC4):
def test_selecting_by_globalid(self):
+2 -2
View File
@@ -513,11 +513,11 @@ class IFC_PARSE_API schema_definition {
IfcUtil::IfcBaseClass* instantiate(const IfcParse::declaration* decl, IfcEntityInstanceData&& data) const;
};
IFC_PARSE_API const schema_definition* schema_by_name(const std::string&);
IFC_PARSE_API const schema_definition* schema_by_name(const std::string& schema_name);
IFC_PARSE_API std::vector<std::string> schema_names();
IFC_PARSE_API void register_schema(schema_definition*);
IFC_PARSE_API void register_schema(schema_definition* schema);
IFC_PARSE_API void clear_schemas();
} // namespace IfcParse
+50 -76
View File
@@ -9,7 +9,7 @@
"version": "0.0.0",
"dependencies": {
"eventemitter3": "^5.0.1",
"hyperid": "^3.3.0",
"hyperid": "^4.0.0",
"lucide-svelte": "^0.542.0",
"socket.io-client": "^4.8.1",
"svelecte": "^5.2.0",
@@ -25,7 +25,7 @@
"clsx": "^2.1.1",
"mode-watcher": "^1.1.0",
"sass-embedded": "^1.89.0",
"svelte": "^5.53.6",
"svelte": "^5.55.8",
"svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.5",
"tailwind-merge": "^3.3.0",
@@ -1605,9 +1605,9 @@
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
@@ -1625,7 +1625,7 @@
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-builder": {
@@ -1715,41 +1715,24 @@
}
},
"node_modules/devalue": {
"version": "5.6.4",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz",
"integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==",
"license": "MIT"
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"version": "6.6.6",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz",
"integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"ws": "~8.21.0",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
@@ -1821,12 +1804,20 @@
"license": "MIT"
},
"node_modules/esrap": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
"version": "2.2.9",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz",
"integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"peerDependencies": {
"@typescript-eslint/types": "^8.2.0"
},
"peerDependenciesMeta": {
"@typescript-eslint/types": {
"optional": true
}
}
},
"node_modules/eventemitter3": {
@@ -1883,14 +1874,12 @@
}
},
"node_modules/hyperid": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/hyperid/-/hyperid-3.3.0.tgz",
"integrity": "sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/hyperid/-/hyperid-4.0.0.tgz",
"integrity": "sha512-vjhh0CG44/iGjcNEjebL0BpRDR+C7d1KZ1Yqf92ralTOO5JjZiGHQT0bjDaKuNu1kk3jB+5jGFhNbxkT0wcQmA==",
"license": "MIT",
"dependencies": {
"buffer": "^5.2.1",
"uuid": "^8.3.2",
"uuid-parse": "^1.1.0"
"buffer": "^6.0.3"
}
},
"node_modules/ieee754": {
@@ -1914,9 +1903,9 @@
"license": "BSD-3-Clause"
},
"node_modules/immutable": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
"dev": true,
"license": "MIT"
},
@@ -2326,9 +2315,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"dev": true,
"funding": [
{
@@ -2365,9 +2354,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz",
"integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==",
"version": "8.5.22",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
"integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
"dev": true,
"funding": [
{
@@ -2385,7 +2374,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.16",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -2977,9 +2966,9 @@
}
},
"node_modules/svelte": {
"version": "5.53.6",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.6.tgz",
"integrity": "sha512-lP5DGF3oDDI9fhHcSpaBiJEkFLuS16h92DhM1L5K1lFm0WjOmUh1i2sNkBBk8rkxJRpob0dBE75jRfUzGZUOGA==",
"version": "5.55.8",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.8.tgz",
"integrity": "sha512-4D6lyrMHmDaZalQOEBMCWCCidyZjSnec14/oPn0k627G6goxcck9xqMwz1tFLlQz+ZFvtTTHfFOlUayuAz0z6Q==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
@@ -2991,9 +2980,9 @@
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.3",
"devalue": "^5.8.1",
"esm-env": "^1.2.1",
"esrap": "^2.2.2",
"esrap": "^2.2.4",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
@@ -3177,9 +3166,9 @@
}
},
"node_modules/tar": {
"version": "7.5.16",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
"version": "7.5.21",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.21.tgz",
"integrity": "sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -3241,21 +3230,6 @@
"node": ">=14.17"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/uuid-parse": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz",
"integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==",
"license": "MIT"
},
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
@@ -3358,9 +3332,9 @@
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
+2 -2
View File
@@ -22,7 +22,7 @@
"clsx": "^2.1.1",
"mode-watcher": "^1.1.0",
"sass-embedded": "^1.89.0",
"svelte": "^5.53.6",
"svelte": "^5.55.8",
"svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.5",
"tailwind-merge": "^3.3.0",
@@ -34,7 +34,7 @@
},
"dependencies": {
"eventemitter3": "^5.0.1",
"hyperid": "^3.3.0",
"hyperid": "^4.0.0",
"lucide-svelte": "^0.542.0",
"socket.io-client": "^4.8.1",
"svelecte": "^5.2.0",
+18 -25
View File
@@ -21,6 +21,8 @@
%ignore stream_or_filename::stream;
%ignore boost::hash_value;
%ignore IfcGeom::BRepElement::geometry_pointer;
%ignore IfcGeom::TriangulationElement::geometry_pointer;
// This is only used for RGB colours, hence the size of 3
%typemap(out) const double* {
@@ -254,6 +256,22 @@ namespace {
%include "../ifcgeom/Converter.h"
%include "../ifcgeom/ConversionResult.h"
%include "../ifcgeom/ConversionSettings.h"
// Keep the owning element alive while its geometry is referenced (#1124).
%define GEOMETRY_WITH_BACKREF(cls)
%feature("shadow") cls::geometry %{
@property
def geometry(self):
result = $action(self)
result._parent = self
return result
%}
%enddef
GEOMETRY_WITH_BACKREF(IfcGeom::TriangulationElement)
GEOMETRY_WITH_BACKREF(IfcGeom::SerializedElement)
GEOMETRY_WITH_BACKREF(IfcGeom::BRepElement)
%include "../ifcgeom/IfcGeomElement.h"
%include "../ifcgeom/IfcGeomRepresentation.h"
%include "../ifcgeom/Iterator.h"
@@ -791,27 +809,9 @@ struct ShapeRTTI : public boost::static_visitor<PyObject*>
};
%extend IfcGeom::TriangulationElement {
%pythoncode %{
# Hide the getters with read-only property implementations
# Keep the owning element alive while its geometry is referenced (#1124).
def _geometry_with_backref(self, _f=geometry):
result = _f(self)
result._parent = self
return result
geometry = property(_geometry_with_backref)
%}
};
%extend IfcGeom::SerializedElement {
%pythoncode %{
# Hide the getters with read-only property implementations
# Keep the owning element alive while its geometry is referenced (#1124).
def _geometry_with_backref(self, _f=geometry):
result = _f(self)
result._parent = self
return result
geometry = property(_geometry_with_backref)
%}
};
%extend IfcGeom::BRepElement {
@@ -834,13 +834,6 @@ struct ShapeRTTI : public boost::static_visitor<PyObject*>
}
%pythoncode %{
# Hide the getters with read-only property implementations
# Keep the owning element alive while its geometry is referenced (#1124).
def _geometry_with_backref(self, _f=geometry):
result = _f(self)
result._parent = self
return result
geometry = property(_geometry_with_backref)
volume = property(calc_volume_)
surface_area = property(calc_surface_area_)
%}
+9
View File
@@ -163,10 +163,19 @@
%ignore MaxOffset;
%ignore MaxOffsetDeviation;
%ignore ApplyOffset;
%ignore SvgRidgeAngleMinDegrees;
%ignore SvgValleyAngleMinDegrees;
%ignore SvgEmitFlushEdges;
%ignore SvgUseEdgeClassification;
%ignore SvgRenderCreaseEdges;
%ignore SvgRenderSharpEdges;
%ignore XmlSerializerFactory;
%ignore JsonSerializerFactory;
%ignore filetype;
%ignore guess_file_type;
// Triangulated representation helper struct
%ignore EdgeKey;