Compare commits

...

226 Commits

Author SHA1 Message Date
Andrej730 75ec717162 build_rocky: try --shared 2026-08-28 18:57:54 +05:00
Andrej730 ac3ee016e7 build-all: fix LIBRARY_EXT not considering macos
Led to error during OpenCOLLADA build:
```
2026-08-28 13:33:34,391 make[2]: *** No rule to make target `/Users/runner/work/IfcOpenShell/IfcOpenShell/build/Darwin/arm64/10.15/install/pcre-shared-8.41/lib/libpcre.so', needed by `lib/libOpenCOLLADABaseUtils.dylib'.  Stop.
2026-08-28 13:33:34,391 make[2]: *** Waiting for unfinished jobs....
```
2026-08-28 18:51:24 +05:00
Andrej730 565393e0ec build-all: use kernel as gmp mirror
As it seems more stable than the other ones.
2026-08-28 18:51:24 +05:00
Andrej730 c13471a8af build-all: fix missing cgal dependency on boost
boost does have `"boost" in targets` guard, so building just `cgal` previously would fail
2026-08-28 18:51:24 +05:00
Andrej730 75c93ce5be build_rocky: use dtolnay/rust-toolchain action 2026-08-28 18:13:57 +05:00
Andrej730 0594e88de1 build-all: set CMAKE_INSTALL_PREFIX automatically for all dependencies
The only tricky one was occt, but it seems `INSTALL_DIR` is always initialized with `CMAKE_INSTALL_PREFIX` if not provided, so it will work fine.
2026-08-28 18:13:57 +05:00
Andrej730 cf60ed3c59 package-zip-archives: --shared flag
Currently adding `-shared-` suffix to all dependencies instead of overriding, to avoid mixups during transition period.
2026-08-28 18:13:57 +05:00
Andrej730 ecd328e42e package-zip-archives: skip previously installed runtime deps 2026-08-28 18:13:57 +05:00
Andrej730 c5a24a3f47 build-all: provide LD_LIBRARY_PATH when testing wrapper import 2026-08-28 18:13:57 +05:00
Andrej730 7997d5a2c8 build-all: consider lib64 during examples testing 2026-08-28 14:20:10 +05:00
Andrej730 3a67602bd2 IfcParseExamples: fix gcc warning about not covered enum cases 2026-08-27 19:19:11 +05:00
Andrej730 2a9786bac2 package-zip-archives: --occt-shared
Packing OCCT shared libraries, seems to work. Though still got to do a test in CI before implementing `--shared`.
2026-08-27 18:48:58 +05:00
Andrej730 df31069743 package-zip-archives: target patching and checking more precisely
E.g. to ensure patching of qt libraries is not covering up issues with other runtime libraries.
2026-08-27 18:48:58 +05:00
Andrej730 48eddeec01 package-zip-archives: patch rpath for all runtime libs, not just qt
Unsure if it's required, but just to be safe and to avoid warnings showing up during `check_runtime_dependencies`.
2026-08-27 18:48:58 +05:00
Andrej730 453bf9b8e8 package-zip-archives: add libs to ignore during check_runtime_dependencies 2026-08-27 18:48:58 +05:00
Andrej730 705aacf659 package-zip-archives: check runtime deps for python wrapper too 2026-08-27 18:48:58 +05:00
Andrej730 94fa2dea42 package-zip-archives: make python wrapper packaging rerunnable
Now we have temporary staging area and each run works exactly the same, without affecting actuall installation.
2026-08-27 17:56:15 +05:00
Andrej730 57f0f89993 remove_tr1.patch: add a note 2026-08-27 17:07:35 +05:00
Andrej730 ab8567fdca build-deps: point to the existing remove_tr1.patch
Tested that it applies cleanly even though commit is outdated.
2026-08-27 17:07:21 +05:00
Andrej730 57eb5f474e build-all: always apply tr1 removal patch
Just to use consistent patches between the builds. It was previously guarded by `WASM`, but it was a dead code - `OpenCOLLADA` is skipped on wasm, so it was never exercised.

Regarding the "specializing std::hash outside of the std:: namespace" issue on gcc - it was caused by patch missing fixes for `COLLADABU_HASH_NAMESPACE_OPEN` and `COLLADABU_HASH_NAMESPACE_CLOSE`. So in theory it should have also result in an error in clang or in an invalid code/ub. Either way, now it's fixed.
2026-08-27 17:07:11 +05:00
Andrej730 ec0e0ab3ff run-cmake.bat: revert special handling for USE_CCACHE
Since it's part of normal cmake options and no need to alter PATH to disable ccache.
2026-08-27 16:54:01 +05:00
Andrej730 25ffdc3943 cmake: adjust ccache messages given ut's now optional 2026-08-27 16:54:01 +05:00
Andrej730 147c87a7be cmake: move USE_CCACHE closer to other options 2026-08-27 16:54:01 +05:00
Andrej730 3251c1b225 Enable ccache explicitly for builds #9359
Since it's now off by default
2026-08-27 16:54:01 +05:00
Andrej730 d2da2988a4 cmake: fix buidling using rocksdb built as RelWithDebInfo #9359
See logs below for example issue I've met locally when I had just `RocksDBTargets-relwithdebinfo.cmake`.
Providing a list of configs makes it try to use matching config first and only then try `Release` as a fallback, otherwise it was now requiring `Release` builds.

```
CMake Error in CMakeLists.txt:
  IMPORTED_LOCATION not set for imported target "RocksDB::rocksdb"
  configuration "RelWithDebInfo".
```
2026-08-27 16:54:01 +05:00
Andrej730 5903c3c895 Normalize whitespaces in yml files 2026-08-27 16:54:01 +05:00
Andrej730 028497d4c0 package-zip-archives: use logger to control logs verbosity 2026-08-27 10:26:06 +05:00
Andrej730 b6b8d27f53 package-zip-archives: fix using wrong pattern for detecting SONAME
See example below - `Shared library` is used to declarate dependency, `Library soname` is used to declare `SONAME`.
```
 0x0000000000000001 (NEEDED)             Shared library: [libifcopenshell.plugin.so]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [ld-linux-x86-64.so.2]
 0x000000000000000e (SONAME)             Library soname: [libifcopenshell.parse.so]
```
2026-08-27 10:26:06 +05:00
Richard Brice 3b908aff79 Guard get_style() against non-IfcMaterial material sets lacking HasRepresentation 2026-08-26 09:14:44 -07:00
Richard Brice 05ba93ab61 Modifications to build C++ with Visual Studio 2026 and the v145 toolset. (#9359)
* Modifications to build C++ with Visual Studio 2026 and the v145 toolset.

* Fixes linker settings for rocksdb for Debug and Release builds

* module is a C++ 20 keyword. Explicitly stating namespace allows cpp20 projects to build against the library

* Fixes crash when initializing an object with the initialize function when some of the attributes are empty, {}, or omitted, std::nullopt

* cleanup for vs2026 v145 toolset per @aothms review

* Fixes bug, IfcCurveSegment.setStartLength was setSegmentLength in alignment_helper.cpp

* Bumps boost to 1.92
2026-08-26 09:10:41 -07:00
Andrej730 05e5a375f5 pyodide/meta.yaml: drop redundant --py313
Noticed after 5b00c8b45 - build started to break. `--py313` is not needed here, since wasm build doesn't really depend on the provided Python versions and just builds the version it picks up from `pyodide`.
2026-08-26 14:49:03 +05:00
dependabot[bot] 15deb1c02c build(deps): bump actions/setup-python from 6 to 7
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 14:11:39 +05:00
dependabot[bot] cc78d0bcc7 build(deps): 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-08-26 14:11:28 +05:00
dependabot[bot] 9246e9f1be Bump python-jose from 3.3.0 to 3.4.0 in /src/opencdeserver/api/app
Bumps [python-jose](https://github.com/mpdavis/python-jose) from 3.3.0 to 3.4.0.
- [Release notes](https://github.com/mpdavis/python-jose/releases)
- [Changelog](https://github.com/mpdavis/python-jose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mpdavis/python-jose/compare/3.3.0...3.4.0)

---
updated-dependencies:
- dependency-name: python-jose
  dependency-version: 3.4.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 14:10:52 +05:00
Andrej730 12338714aa package-zip-archives: provide arch suffixes and use them to determine build type
E.g. to avoid picking up wasm build that's also available locally
2026-08-26 13:03:30 +05:00
Andrej730 99c3304365 package-zip-archives: add colors for readibility 2026-08-26 12:34:36 +05:00
Andrej730 087f24185f package-zip-archives: consider installed qt libs to install/ifcopenshell/lib 2026-08-26 12:34:04 +05:00
Andrej730 aa4a73f62c package-zip-archives: clean up leftovers from previous failed local run 2026-08-26 12:34:04 +05:00
Andrej730 feba4cedb6 package-zip-archives: print current step 2026-08-26 12:33:58 +05:00
Andrej730 be05a04c9e package-zip-archives: get hash using git rev-parse for local runs 2026-08-26 12:02:01 +05:00
Andrej730 13bc8308b1 CI: unify script for packaging zips, port to Python 2026-08-25 18:55:14 +05:00
Andrej730 e104e7486c OCCT: avoid using deprecated Triangles method
Deprecated since OCCT 7.6.0
2026-08-25 18:55:14 +05:00
Andrej730 3103b3c747 Add fake-bpy-module to Bonsai dev environment 2026-08-25 18:55:14 +05:00
Andrej730 ea1671933f Bump tools 2026-08-25 18:55:14 +05:00
Andrej730 3c74891c4e BonsaiViewer: add --version cli option
Also useful for testing if all dependencies are found, without launching the viewer.
2026-08-25 18:16:13 +05:00
Andrej730 42dccbc74b build-all: support Python 3.15 2026-08-25 14:02:43 +05:00
Ryan Schultz 151aa2c45a Bonsai: sort drawing scale dropdown by scale ratio (#9357)
The imperial list ran the architectural scales from 1'=1'-0" down to
1/128"=1'-0", then restarted at 1"=10' for the engineering scales. Merge
both groups into a single sequence ordered by ratio, largest scale first.
The metric list was already ordered by ratio and is unchanged.

Also fix the enum cache invalidation, which compared the cached list's
length against hardcoded 13/31 while the imperial list has 32 entries, so
switching a scene from imperial back to metric kept showing imperial
scales. Track the unit system the cache was built for instead.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:02:22 -05:00
Dion Moult d808104725 ci: test the daily bonsai build against the branch that built it
The bare clone fetched the default branch, so a v0.9.0 daily would be
smoke-tested and pytested against v0.8.0 scripts and tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:25:42 +10:00
Dion Moult b71b217814 ci: trigger bonsai and ifcsverchok dailies from v0.9.0
Daily builds for user testing now come from the v0.9.0 branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:25:10 +10:00
Dion Moult d9f218eacf Bump build e333c1c > ad113e1
First v0.9.0alpha0 binary set, so the version prefix moves with it.
The bump trackers had drifted (bonsai's OLD pointed at 3e7b739 while
ifcopenshell-python pinned e333c1c), so this was done by hand;
'make bump' works again from here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:25:04 +10:00
Dion Moult 0faba0fdd8 bonsai: drop Intel macOS builds
Upstream binary builds no longer produce macos64 zips (build_osx builds
arm64 only since wgpu Qt), and Blender dropped Intel Mac support in 5.0,
so there is nothing left to package for that platform.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:24:53 +10:00
Dion Moult ad113e1283 ifcviewer: latch the motion contribution cull instead of flip-flopping
The coarse motion threshold (15 px vs the 3 px still floor) followed the
per-frame "did the camera move" test directly. During a slow drag on a
janky main thread — the 66-model web session at 20 fps, mouse events
coalesced — some frames see no camera change, so the cull alternated
between thresholds every few frames: 84% of the visible set vanishing
and reappearing (139k <-> 22k objects in the log), with a full
visible-set re-upload at each flip feeding the very jank that caused the
gaps. On screen it read as the model sporadically jumping and returning
while orbiting slowly, easing as streaming and caching settled — which
is exactly how it was reported.

The motion state now latches: any camera movement arms it, and it only
drops after 250 ms of stillness, with the render loop kept alive over
the hold so the fine-threshold re-cull actually runs in an on-demand
loop. A drag degrades once at its start and restores once shortly after
it ends. Measured with a deliberately gappy scripted drag: two
transitions for the whole drag where each 120 ms pause previously
flipped it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult 4a761b51f5 ifcviewer: pack the cull-hot instance fields and skip unchanged culls
On the single-threaded web build the CPU cull WAS the frame: 52-60 ms
of a 60 ms frame at 640k instances (desktop hides the same cost across
cores via std::async, which web cannot use without COOP/COEP+pthreads).

Two changes, both also helping desktop:

- ModelGpuData::CullInstance packs the six AABB floats and three ids the
  cull reads into 40 contiguous bytes. InstanceInfo is 232 bytes with
  the AABB 200 bytes away from the ids, so the walk paid two or three
  cache lines per instance. Rebuilt by rebuildCullInstances at model
  apply and inside uploadInstanceRecords, which every recompose,
  transform and colour-override change already funnels through.
  Measured on web: 94 ns/instance -> 36 ns/instance during a continuous
  orbit (~2.6x).

- render() re-culls only when a cull input changed: the camera, a
  cull-relevant setting (contribution px, LOD px, x-ray, HiZ on/off), a
  fresh HiZ pyramid, or scene_epoch_ — bumped by chunk residency,
  visibility, colours, transforms, model add/remove/hide/unload. A
  frame requested for an overlay redraw, pick feedback, or a streaming
  tick where nothing landed draws from the buffers the last cull
  uploaded and skips the walk entirely. Benchmarks are exempt so bench
  numbers keep measuring the real cull.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult 73f8e6aea7 ifcviewer-web: complete the OPFS cache in the background
Filling only from the viewer's reads meant the cache converged on the
bytes the camera had needed — a user had to orbit every model into view
(unloading others to get there) before an entry could finish. Now a
filling entry fetches its uncovered spans in order, 8 MB at a time,
whenever the viewer has been quiet for 1.5 s, yielding the moment real
reads resume so interactive streaming always wins. A 42 MB model that
levelled off at 85% viewed now completes seconds after load with no
interaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult e5072460cd ifcviewer-web: OPFS model cache behind addUrl(url, {cache: true})
Streaming a federation over the network re-downloads everything on every
visit: browsers do not populate their HTTP cache from ranged fetches
(measured at 0 of 78 range requests served from cache even with a strong
ETag). Host pages have started hand-rolling OPFS caches against the
library's own source seam — this is the second app to port the same ~350
lines — so the capability moves into the library.

The design keeps what those pages got right: the cache fills FROM THE
VIEWER'S OWN RANGED READS (no second download, and only bytes the camera
actually needed), entries are keyed by a hash of the URL and validated
by ETag (falling back to Last-Modified + size), and a byte-span ledger
guarantees a partial copy is never mistaken for a whole one. What it
fixes: writes go through a FileSystemSyncAccessHandle in an inline
worker — positional writes with no copy-on-open, where the pages'
createWritable({keepExistingData}) paid a whole-file copy per flush
(quadratic as the cache fills) and buffered up to 48 MB per model in JS
to compensate — the handle's exclusive lock makes a second tab fall back
to plain network instead of corrupting the entry; a complete copy now
opens when the server is unreachable (offline was dead before despite
the bytes being local); and entry names are hashes, where prefix-matched
sanitised names could delete a sibling model's cache.

viewer.cacheInfo() reports entries and the storage estimate;
viewer.clearCache(url?) drops one or all. Browsers without OPFS or sync
handles, servers without validators, and second tabs all degrade to
exactly today's network streaming.

Verified: the sample round-trips to zero range requests on reload, and a
42 MB model goes from 108 range requests to 5 on the second visit — the
85% the camera had viewed comes off disk, coverage honestly reports
incomplete for the bytes streaming never needed. The test server now
sends a content-hash ETag so the specs exercise real validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult 311b75a955 ifcviewer: carve the margin out of the cache on the first driver growth refusal
On web there is no device-memory query, so the budget sat at the wasm
heap cap while the pool grew until Chrome's GPU process refused
(observed at 1920 MB on a 66-model session). Nothing acted on that
refusal: the cache kept the last byte, and the next attachment
reallocation (orbit resize, 76 MB) had to fail first — a few frames of
invalid-TextureView errors — before pressure feedback carved out room.

The refusal IS the query-less platform's device report. render() now
answers the first one by lowering the budget by the required-tier
margin and shrinking the pool to it, so attachments and model buffers
find headroom without ever failing. Desktop gets the same fallback for
drivers GpuMemory cannot answer for.

Reproduced under Playwright with a native process squeezing the GPU:
Chrome refuses at 512 MB, the margin (256 MB) is released on the next
frame, and the session continues with zero uncaptured WebGPU errors —
previously the same squeeze produced invalid-view frames before
recovery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult 6f24133d35 ifcviewer-web: stream getObjects() per model instead of one scene-sized JSON
getObjects() is on every real host page's path — a click hands back an
object id, and resolving it to a GlobalId/name/type needs the element
tables (the JS layer also builds its GUID index from this call). The
implementation materialised a vector of ElementRef (three fresh
std::strings per element), serialised the entire scene into one JSON
string grown by +=, and UTF8ToString'd the whole thing — several
hundred MB simultaneously alive at ~600k elements. The wasm heap never
returns pages, so that transient became the session's permanent floor.

ViewportCore::visitModelElements hands out one model's elements as
slices into its string table (no per-element copies), and the export
serialises straight from those, one model per batch, reusing one string
whose capacity grows only to the largest model. The JS side accumulates
batches and resolves the same array as before — the page API is
unchanged. Peak is now one model's JSON instead of the scene's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult 091b4d4113 ifcviewer: bound the CPU triangle shadow and the cull scratch to residency
The wasm heap grew past 2 GB on a 66-model session (surfacing first as
the setBindGroup 2 GB TypeError, fixed separately) because CPU memory
attached to loaded geometry never shrank while the GPU pool did:

- mesh_triangles_cache — the dequantised positions + LOD0 indices the
  surface raycasts and measurement tools read — was filled once per mesh
  on first residency (gated on mesh_local_volumes == 0) and never
  released, converging over a session to the whole federation's geometry
  on the heap: 12 B/vertex + 4 B/index, 400 MB - 1 GB at this scale. And
  on web nothing reads it at all (no measurement tools yet).
- Every chunk's cull scratch was reserved at model load (20 B/instance
  scene-wide) and the scratch + uploaded mirrors survived eviction.
- Cull ran the HiZ test and emitted VisibleDrawGpu entries — then
  uploaded them — for non-resident chunks render() cannot draw.

Now the shadow follows GPU residency: a per-mesh resident-chunk refcount
(the spatial planner may duplicate a mesh into several chunks) is
counted up in applyStreamedChunk and down in unloadChunk, releasing the
mesh's entry at zero and refilling from the chunk bytes on the next
residency. mesh_local_volumes (8 B/mesh) is kept across eviction so the
Volume tool still covers evicted meshes. Hosts opt in via
ViewportHost::wantsCpuMeshTriangles(): Qt yes, web no until the tools
are ported — so on web the shadow costs nothing.

Cull stops at the streaming counters for non-resident chunks, the eager
scratch reserve is gone, and unloadChunk releases the scratch and
uploaded mirrors. Clearing the mirrors also fixes a real staleness bug
in unload/load: the model's cull buffers are recreated on load, and a
stale mirror would make the memcmp dirty-check skip the first upload
into the fresh (garbage) buffer.

The heartbeat log reports the shadow (cpuTris). Measured on a 3-model /
990 MB scene: shadow tracks residency (493 MB at a 530 MB resident set,
flat over minutes of streaming churn; previously monotonic), unload
drops it to zero, reload refills it (verified via readbackMeshTriangles
round trip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult 06d87e21a7 ifcviewer-web: never hand setBindGroup the whole wasm heap as dynamic offsets
Emscripten's generated WebGPU shim implements the dynamic-offset path of
wgpuRenderPassEncoderSetBindGroup as

    pass.setBindGroup(index, group, HEAPU32, ptr >>> 2, count);

where HEAPU32 is the view over the entire wasm linear memory. Browsers
validate the byte length of that whole backing buffer, not the slice
actually read, and refuse anything over 2 GB. This build lets the heap
grow to 4 GB because large federations need it, so on a big enough
session (66 models) every dynamic-offset draw — the axis gizmo, section
gizmo and overlay lines, all drawn every frame — throws

    TypeError: GPURenderPassEncoder.setBindGroup: Argument 3 can't be an
    ArrayBuffer or an ArrayBufferView larger than 2 GB

on every frame for the life of the page.

ifcviewer::setBindGroupDynamic copies the handful of offsets into a
small Uint32Array on web (HEAPU32.slice, not subarray, which would alias
the heap again) and forwards straight through natively. The five
dynamic-offset call sites route through it. A Playwright spec spies on
setBindGroup and asserts the largest buffer it is ever handed is the
offsets themselves (4 bytes), where the shim previously passed the full
268 MB heap 35 times in three seconds of idle rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult b7f1f1728f ifcviewer-web: expose frame stats and per-model unload/load to the page
The memory work (cache budget, pressure handling, unloadModel) lives in
ViewportCore and so already ran in the wasm, but the page could not see
or use any of it: the web host had no onFrameStats, and there were no
bindings for residency.

- WebViewportHost latches the last FrameStats; ifcv_get_frame_stats_c
  hands them to JS as doubles, and viewer.stats() returns {fps,
  frameTimeMs, objects, triangles, drawCalls, vram{used, capacity,
  budget}, workingSet{chunks, chunksMissing, missingBytes}} — the same
  figures BonsaiViewer's status bar shows. Device-wide VRAM is omitted:
  there is no query for it on web.
- viewer.unloadModel / loadModel / modelUnloaded / modelVramBytes, keyed
  by source id like the other per-model calls.
- The demo page shows a GPU memory line that turns into a "full: N of M
  visible chunks not loaded" notice once a shortfall persists for 3 s,
  and each model's MB with an Unload/Load button.
- memory.spec.mjs covers stats() and the unload/load round trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult 8074541057 Surface VRAM shortfall to the user and let them unload models
When the geometry in view needs more GPU memory than the cache can
hold, the viewer keeps the largest on-screen chunks resident and streams
the rest as the camera moves. That is the right degradation, but it was
invisible: nothing told the user the scene did not fit, and the only
lever was removing or hiding models, neither of which is "keep it in the
federation but stop spending GPU memory on it".

Viewer core:
- ModelGpuData::unloaded, with drawable() = !hidden && !unloaded now the
  test every cull / draw / pick / streaming pass uses. unloadModel evicts
  every chunk and releases the model's own buffers; loadModel recreates
  them from the CPU mirrors (no disk read) and lets chunks stream back.
  Recompose keeps the CPU instances current while a model is unloaded so
  a reload sees up-to-date transforms. The MeshGpu/InstanceGpu record
  builders are factored out so load and reload share them.
- FrameStats reports the camera's working set: chunks wanted, how many
  of those are not resident, and their bytes.
- modelVramBytes / isModelUnloaded accessors, forwarded by ViewportWindow.

BonsaiViewer:
- Models tree gains a memory column (name | MB | eye) refreshed once a
  second and on load-state changes; unloaded models read "unloaded" in
  italics. The viewport stays the single authority for the state;
  SessionState only carries the modelLoadStateChanged notification.
- Context menu: "Unload Model" / "Load Model", distinct from hide and
  remove, reporting the MB freed in the status bar.
- Status bar notice, independent of the perf-stats toggle, once the
  shortfall has persisted for 3 s (a moment of missing chunks after any
  camera move is normal): "GPU memory full: N of M visible chunks (X MB)
  not loaded", with a tooltip pointing at Unload. The perf label also
  shows "N/M chunks waiting".

Verified on the GPU: unloading a 497 MB model frees it immediately with
the others still rendering; reloading streams all 180 chunks back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult d755ca3a59 bonsaiviewer: show the pool's capacity in the VRAM readout, budget alongside
The readout showed used over budget, which reads as impossible once the
pool legitimately sits a sub-buffer above a lowered budget (releasing
it would undershoot). Show used/capacity, and the budget only when it
differs from capacity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult 777b728205 ifcviewer: move the live budget only on sustained device readings
A 66-model session oscillated with a ~4 s period — 298 releases in one
log: the pool grew to its ceiling, the next report read ~83 MB free, the
budget dropped and the pool shrank, the reading rebounded, the budget
rose and the pool re-grew, reloading the same chunks each time. Objects
flickered on and off continuously.

The report includes transients the viewer itself creates: the upload
staging behind a burst of chunk loads (~170 MB in that session) and a
released sub-buffer the driver has not yet reclaimed. A budget that
followed every reading fed those straight back into growth decisions.

GpuBudget::update now bounds the cache outright on the first device
report and afterwards moves only on sustained readings: lower when free
memory is below half the margin on two consecutive scheduled reports,
raise when it is above 1.5× the margin on two, and nothing in between.
Transients drain well within a poll interval, so a momentary low never
reaches the pool, while a process that really took memory still does a
second later. A refused allocation (onPressure) is never deferred.

Verified in the saturated regime (working set ~990 MB against a 683 MB
budget, continuous streaming): zero releases over 75 s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult 24616ed655 ifcviewer: stop the live budget from over-shrinking and from going stale
A 66-model session showed the pool reach a 1938 MB ceiling, the next
poll lower the budget to 1756, and the shrink drop 402 MB (73+73+256)
for a 182 MB excess, which the pool then spent seconds re-growing. Two
causes.

The release granularity is whole sub-buffers but the shrink ran "until
capacity ≤ target", so the last 36 MB of excess cost a 256 MB
sub-buffer. shrinkToCapacity now never undershoots — it releases only
while doing so keeps capacity ≥ target, leaving a sub-buffer's worth of
excess for the margin to absorb — and the pressure path uses a separate
releaseAtLeast(bytes), whose contract is the opposite: free at least
what the failed allocation needs, whatever the granularity. Resident
geometry is also only evicted once the pool is over budget by half the
margin (GpuBudget::shrinkTarget), so report jitter does not trigger a
shrink-and-reload.

The ceiling was a second old when the pool grew into it, and the upload
staging that rides on growth had pushed device free memory to ~74 MB —
below the driver's observed refusal point — before the next scheduled
poll. pollDeviceMemory now re-derives the budget immediately after any
sub-buffer is added, so the next growth decision sees the device as it
is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult 6201c4052b ifcviewer: make the geometry cache budget live, not fixed at startup
The budget was derived once at init as device free minus a reserve sized
for attachments at 4K plus margin. On a 1440p surface that idled ~550 MB
of VRAM the user's hardware could have spent on geometry, and it never
followed the device as other processes came and went.

Now, on the same once-a-second device poll that feeds the status bar,

    budget = cache capacity + device free - margin

is recomputed and applied: the pool's growth ceiling moves with it, and
the pool yields whole sub-buffers when the device has less to give than
the pool holds. The attachments are eager, so at any poll they are
already inside "used" at the actual surface size; a resize that no
longer fits is answered by the existing pressure path rather than by a
permanent reserve.

The margin is 256 MB for later required allocations plus a learned part:
drivers refuse while still reporting memory free (the original crash
refused 59 MB with 221 MB "free"), so a pressure event records how much
reported-free memory proved unusable and update() stops short of it from
then on, instead of growing straight back into the same refusal.

Web is unchanged: fixed heap cap plus pressure. On the test machine the
idle-device budget goes from 1609 MB to 2212 MB; with another process
holding 1 GB mid-session the budget follows it down and back up without
evicting geometry the device could still hold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:13 +10:00
Dion Moult ab99024307 ifcviewer: budget the geometry cache and make required allocations fallible
Loading enough models drove the chunk pool to the driver's refusal point,
after which the first click aborted: the pick attachments are allocated
lazily, wgpu-native reported their OOM as a validation error nobody
observed, and the invalid views reached wgpuQueueSubmit, which panics
across the FFI boundary. Two policy defects compounding: the cache was
allowed to take the last byte, and nothing but the pool's own growth was
treated as fallible.

GPU memory is now two tiers. Required allocations (per-pixel attachments,
a model's metadata buffers, readback staging) are eager, deterministic
and fallible; the chunk pool is an elastic cache that grows only to a
budget and yields whenever a required allocation fails.

- GpuBudget (pure, unit-tested): desktop derives the budget from the
  driver's free-memory report minus a reserve for the attachments at 4K;
  web keeps the wasm-heap cap; either lowers it on pressure. The budget's
  source differs per platform, the mechanism does not.
- GpuAllocScope: the OOM/Validation error-scope dance in one place,
  synchronous on wgpu-native, provisional on Dawn-web. BufferPool's
  inline copy now uses it.
- BufferPool::shrinkToCapacity releases whole sub-buffers newest-first
  after the owner empties them; growth clamps to the budget instead of
  overshooting.
- ViewportCore::allocateRequired runs any required creation under a
  scope and, on failure, lowers the budget, evicts and releases cache
  sub-buffers, waits for the device to reclaim them, and retries until
  it fits or the cache is at its floor. Pick attachments are created with
  the other attachments in configureSurface; render() skips a frame
  rather than submit invalid views; a model whose buffers cannot fit is
  not loaded instead of aborting.

Verified on a 4 GB GeForce: the pool clamps itself at the derived budget
(256+256+67 MB for a 579 MB budget) and, in a standalone check against
the real device, a pool grown to the driver's refusal point observes a
failed required allocation, releases 320 MB and succeeds on retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:12 +10:00
Dion Moult b7d2b2fa3a bonsaiviewer: show pool and device VRAM in the performance stats
FrameStats gains the geometry pool's used/capacity bytes and, on desktop,
the device-wide used/total reported by the driver (NVML via dlopen, or
amdgpu/i915 sysfs, matched to the wgpu adapter's vendor/device id so a
switchable-graphics laptop reports the card wgpu actually picked). The
device query is polled once a second, not per frame. Web has no VRAM
query, so the device figure is omitted there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:12 +10:00
Andrej730 5fa14c3dea IfcAlignment examples: fix Wunknown-pragmas
gcc was complaining, because it couldn't recognize msvc pragmas
2026-08-24 19:18:00 +05:00
Andrej730 7fa8506fac build-all: test examples if they were built 2026-08-24 19:11:10 +05:00
Andrej730 9d68e7b9ca build-all: support --occt-shared 2026-08-24 19:11:10 +05:00
Andrej730 9ffbfe0dbb build-all: fix missing is_on_off
Update build-all.py
2026-08-24 19:10:28 +05:00
Andrej730 5b00c8b451 build-all: add --help and verify args 2026-08-24 18:50:23 +05:00
Andrej730 a5b6f83a3d Normalize whitespaces in cmake files 2026-08-24 18:50:23 +05:00
Andrej730 908d85a51a IfcAdvancedHouse: report an error in case serialization fails
Instead of a crash
2026-08-24 18:50:23 +05:00
Thomas Krijnen 9089a20ce3 Add branch filter for v0.9.0 in CI workflow #9336 2026-08-22 13:21:43 +02:00
Thomas Krijnen d4a5420851 Rename job to publish_ifctester_org 2026-08-22 13:18:20 +02:00
Thomas Krijnen 87bc6bfbab Add string decode/encode api 2026-08-22 13:12:36 +02:00
Petru Conduraru a5e94cf0d8 Version and SOVERSION for geometry_serializer 2026-08-22 13:04:57 +02:00
Dion Moult 2c1d445d5b ifcviewer-web: mint session model ids when a load is requested
A federated pick could be attributed to the wrong file. The model slot a
host sees — ElementRef::model_index, modelProgress's index — is a rank in
session_model_id order, and on web that id was minted at the END of the
sidecar read chain, after three network round trips. So the ranking was
the order the models' reads happened to finish in, not the order the host
added them. With ~40 similarly-sized models over HTTP, adjacent models
swapped and a click reported its neighbour's file; the host page then
asked for a GUID the file does not contain.

Mint the id at the top of loadSidecarMetadataWeb instead, which runs
synchronously from load_sidecar_from_source_c and therefore in the order
the host asked for its models. A load that fails partway just abandons
its id, and the ranks compact over the surviving models as before.

Positions are still positions, though: if one model fails to load, every
later index shifts down one and a host mapping index into its own list
silently drifts again. So also carry the source id — the handle the host
minted itself when it registered the file — through ElementRef into the
pick payload and getObjects rows, and document it as the way to attribute
an object to a file. ModelGpuData::web_source_id defaults to -1 now, since
0 is a real source id and cannot double as "none".

The test server grows a ?delay=<ms> knob so a test can force the losing
interleaving: georef-a is added first and served slowly, and its objects
must still come back as model 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 11:30:56 +10:00
Dion Moult d86f89090b ifcviewer-web: draw the axis indicator (corner gizmo + orbit pivot)
The desktop viewport draws an RGB triad in the bottom-left corner and a
second one at the orbit target while navigating; the web build drew
neither. Both lived in the Qt-coupled OverlayRenderer, which only
ViewportWindow drives — the web host no-ops the overlay hooks — so the
wasm build had no path to them at all.

Lift them into AxisIndicatorRenderer, a Qt-free renderer in
IfcViewerCore, and drive it from ViewportCore::render for desktop and
web alike. Same move SectionGizmoRenderer already made; the drawing code
is unchanged apart from swapping qDegreesToRadians for CameraMath's kPiF.

Pivot visibility moves to the core with it: it was a QTimer on
ViewportWindow, so the afterglow couldn't follow the gizmo across. It is
now a Stopwatch deadline next to the drawing, with render() requesting
frames until an armed afterglow expires. Hosts keep the same three
triggers (on for orbit/pan drags, off on release, 600 ms on wheel).

The web demo shell's log overlay sat exactly on top of the corner gizmo,
so it shifts right of the 110 px box.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 11:16:41 +10:00
Andrej730 973f61c6dc express.h: remove non-snake case aliases (schemas are regenerated now) 2026-08-19 20:17:51 +05:00
Andrej730 f47aa4d81a Manually update other schemas 2026-08-19 20:17:51 +05:00
Andrej730 fa9a3383aa Use snake case in cpp consistently 2026-08-19 20:17:51 +05:00
Andrej730 104591a80b Normalize whitespaces in the codebase 2026-08-19 20:17:50 +05:00
Andrej730 030e6e5bb4 Regen cpp schemas 2026-08-19 20:17:50 +05:00
Andrej730 a031310a66 express/run.bat: rewrite in Python 2026-08-19 20:17:50 +05:00
Andrej730 436e3f7b2a Header_section_schema-definitions.h: use IFC_SCHEMA_API 2026-08-19 20:17:50 +05:00
Andrej730 335d571854 gltf_serializer: refactor proj code into a separate call 2026-08-19 20:17:49 +05:00
Andrej730 f78b380b71 express/mapping: fix breaking generation after f23db9440f
It was failing to map `int64_t` to `Argument_INT` enum.
2026-08-19 19:38:30 +05:00
Andrej730 2cebc3f60b typing 2026-08-19 19:38:30 +05:00
Andrej730 0a8159505d pyproject: black to format all files by default 2026-08-19 19:38:30 +05:00
Andrej730 301fba5a8b Bump ty to 0.0.72 2026-08-19 19:38:30 +05:00
Andrej730 ba90cf220d black, ruff 2026-08-19 19:38:29 +05:00
Andrej730 6318892a97 Script to check whitespace issues in the codebase 2026-08-19 19:33:08 +05:00
Dion Moult 1a6336bd20 Output test audits to sqlite 2026-08-18 13:50:14 +10:00
Richard Brice 511584b36f Allows key point referents to be nested to the parent alignment in the reusing horizontal scenario 2026-08-17 08:03:00 +10:00
Richard Brice f65de78c46 Strengthens implementation of station_to_string. Adds alignment name to stationing referent. 2026-08-17 08:03:00 +10:00
myoualid 59b957daff fixes to sequence.create_baseline:
- assert isinstance(res, list) was wrong because duplicate_task returns a tuple not a list
- removed overkill assertion anyway as the usecase is already typed.
- setting optional name or reuse planned schedule name
- usecase now returns created baseline work schedule
2026-08-17 08:03:00 +10:00
Thomas Krijnen 81a0941d5a Apply suggestion from @aothms 2026-08-17 08:03:00 +10:00
BelGraDev dba735f1ee Fixed error when accessing the UnitType attribute in convert_file_length_units 2026-08-17 08:03:00 +10:00
Andrej730 e100cf5a34 Fix examples linking errors for shared build (incorrect attributes order)
E.g. IfcAdvancedHouse:
```
/usr/bin/x86_64-linux-gnu-ld.bfd: CMakeFiles/IfcAdvancedHouse.dir/IfcAdvancedHouse.cpp.o: in function `main':
IfcAdvancedHouse.cpp:(.text.startup.main+0x137): undefined reference to `hierarchy_helper<Ifc4x3_add2>::addBuilding(Ifc4x3_add2::IfcSite, Ifc4x3_add2::IfcOwnerHistory)'
/usr/bin/x86_64-linux-gnu-ld.bfd: IfcAdvancedHouse.cpp:(.text.startup.main+0x7c7): undefined reference to `hierarchy_helper<Ifc4x3_add2>::getRepresentationContext(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
/usr/bin/x86_64-linux-gnu-ld.bfd: IfcAdvancedHouse.cpp:(.text.startup.main+0x931): undefined reference to `hierarchy_helper<Ifc4x3_add2>::getRepresentationContext(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
```

Noticed by addressing gcc warning gcc warning that attribute order is incorrect:
```
//src/ifcparse/hierarchy_helper.i:721:31: warning: attribute ignored in explicit instantiation ‘class hierarchy_helper<Ifc2x3>’ [-Wattributes]
  721 | template IFC_SCHEMA_API class hierarchy_helper<IfcSchema>;
      |                               ^~~~~~~~~~~~~~~~~~~~~~~~~~~
//src/ifcparse/hierarchy_helper.i:721:31: note: no attribute can be applied to an explicit instantiation
```
2026-08-14 17:38:47 +05:00
Andrej730 665502cbc5 .gitignore: ignore compile_commands.json at root for clang convenience 2026-08-14 15:37:05 +05:00
Andrej730 252831d7f0 .clang-tidy: drop removed AnalyzeTemporaryDtors
Resolves the error below. This option was removed in clang 18.
```
.clang-tidy:4:1: error: unknown key 'AnalyzeTemporaryDtors'
AnalyzeTemporaryDtors: false
```
2026-08-14 15:34:42 +05:00
Andrej730 3a6055a558 build-all: document undocumented args 2026-08-14 13:03:20 +05:00
Andrej730 7b1b0b986c build-all: use global constants for flags consistently 2026-08-14 13:03:20 +05:00
Andrej730 cd34d92fdb build-all: add flag to build examples
Useful to reproduce issues with examples locally
2026-08-14 12:54:31 +05:00
Andrej730 1391c7d974 Bump pyodide version to fix the build
0.29.3 have an older version of micropip and is affected by https://github.com/pyodide/pyodide/issues/6177
2026-08-14 12:08:32 +05:00
Andrej730 223d6da3b1 Reapply "build_pyodide: try more recent pyodide-build"
This reverts commit 1a931ddfd9.
2026-08-14 12:03:29 +05:00
Andrej730 171e899eb0 Add script to quickly pack wasm wheel after local build-all 2026-08-14 12:03:29 +05:00
Andrej730 e2561ffa3b black, sort imports 2026-08-14 10:17:02 +05:00
Dion Moult 4b87ab5d0d Fix warnings in test suite due to undeclared wall pytest marker 2026-08-14 06:44:11 +10:00
Thomas Krijnen 8cc36f0d4d Add link dependency on native build to resole example failure 2026-08-13 05:13:35 +02:00
Dion Moult 13cc190849 Update georef tests to not hardcode the results of vert[0] used in auto origin detection.
Because vert[0] can change based on kernel output, we now assert that 1)
origins are on a vert, any vert, and 2) both blender coords and map
coords are what we expect. I manually visually verified all tests
against Blender 5.1 + stable 0.8.5 to check that actual behaviour hasn't
changed, only tests need updating.
2026-08-13 11:20:26 +10:00
Dion Moult b71354ce19 Fix assigning a plain material to an occurrence as a layer set
Assigning a material to an occurrence with a set material type has raised
"IfcMaterial cannot be assiged as a IfcMaterialLayerSetUsage" since the
default changed to assigning usages to occurrences. The type is upgraded to a
usage but the material is passed on unchanged, and material.assign_material
only accepts a material for a usage when that material is already the set,
whereas the Object Materials dropdown gives us a plain IfcMaterial. Pass
nothing in that case and let the API make the set, as it does when asked for
a usage with no material.

Look the set up past the usage afterwards, so the material the user picked is
added to it. get_material returns the usage, which is not a material set, so
neither branch of the repair below matched and the picked material was
dropped, leaving the set empty.

This is a stopgap and is commented as such: the real problem is that
assign_material builds sets with no items in them and ignores the material it
was given, which is not valid IFC and leaves callers patching up after it.

Also register "I evaluate expression" as a Then step. It has only ever been a
Given and a When, so the last line of the scenario covering this could never
run; it is the only Then of its kind in the suite.

test/bim goes from 16 failures to 15, with none introduced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 10:28:15 +10:00
Dion Moult 17042f6f80 ifc4d: make ScheduleIfcGenerator's boilerplate file actually work
create_boilerplate_ifc sets self.file and self.work_plan and returns
nothing, but create_ifc assigned its result back over self.file, so any
caller that did not supply a file got None and crashed on the next
create_entity. Call it for its side effects, as csv2ifc and csv4d2ifc
already do.

That alone only moved the failure along: the boilerplate builds a file and a
work plan but no IfcProject, and add_work_calendar looks for an IfcContext.
Create one, matching csv4d2ifc's copy of the same method, which has both
lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:30:27 +10:00
Dion Moult beb0db89e5 ifc4d: rewrite the MS Project importer onto ScheduleIfcGenerator
msp2ifc parsed the XML and built the IFC itself, so a programme read
differently depending on whether it came out of MS Project or P6. It now
parses only, and hands the parsed programme to ScheduleIfcGenerator the way
p62ifc does. Calendars, statuses, task times and resources are therefore the
shared ones, and a reader no longer has to know which tool planned the
schedule.

Three things MS Project does differently needed handling rather than sharing.

It has no work breakdown structure: there is one flat task list and an
OutlineLevel column, and a task with anything indented under it is a summary
whose dates are rolled up rather than planned. Those become IfcTasks without
an IfcTaskTime, as a P6 WBS node does. Summaries and leaves also interleave,
and a planner expects a summary to stay where they put it, so the tree is
walked in export order instead of through create_tasks, which sorts nodes
ahead of activities. And a link may hang off a summary, which P6 cannot do,
so create_rel_sequences resolves both ends against summaries too --
IfcRelSequence relates two IfcProcesses and does not require a time on
either.

Calendar handling flattens what MS Project stores as differences against a
base calendar, since IfcWorkCalendar has no such notion, and reads holidays
from whichever of the two spellings the export uses rather than both.
Recurring exceptions are skipped, because the recurrence is not readable from
the export and guessing wrong silently moves every date computed from the
calendar.

In common.py the UDF and activity-code property set names become class
attributes. They keep their P6 names by default, but MS Project's extended
attributes are not P6 user-defined fields and now land in
MSP_ExtendedAttribute rather than under a name that says P6. IsMilestone
likewise prefers a source that states it outright -- MS Project has a
Milestone flag -- and falls back to the zero-duration test, which is all P6
gives us, so the other importers are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:24:56 +10:00
Dion Moult 572f718007 Use tool.Blender to get selected objects 2026-08-11 17:06:37 +10:00
Dion Moult 6bab0603e6 by_type now returns tuple - update annotations and fix failing tests 2026-08-11 17:06:37 +10:00
Dion Moult b408e64e5e Fix failing test due to declaration() instead of declaration 2026-08-11 17:06:37 +10:00
Dion Moult f580f7255f Type elements are hidden after assignment by default now. So rewrite tests to either use non-types or explicitly select types. 2026-08-11 17:06:37 +10:00
Dion Moult b252cd25f8 Give the web viewer a federation: false origin and per-model transforms
Models now resolve to global coordinates, which alone would make things worse:
composed per-instance transforms are float32, and around six million metres
that quantises at roughly half a metre. So the first model to load also sets a
false origin, derived from where its geometry actually sits, unless a host has
set one itself.

WebFederation owns the concepts an .ifcfed carries — a federation unit, a false
origin, a per-model transform and display name — without the file format. The
desktop Federation class is a document model whose sources are local filesystem
paths, which mean nothing in a browser; a host page that wants .ifcfed can parse
the JSON and drive these calls.

Models are keyed by the JS source id rather than the session model id. The
source id exists the moment a File or URL is registered, whereas the session id
is minted inside the async range-read chain, so keying on it lets a transform be
set before the model has streamed and applied when it arrives — the model never
visibly jumps. loadSidecarMetadataWeb gained a completion callback to carry that
id back out, and addFile/addUrl now return the source id and fire onModelLoaded,
where before they were fire-and-forget with no handle and no completion signal.

The embedded sample bypasses the source registry, so it is bound separately;
otherwise the guess never runs for a page that only ever shows the sample.

georef-a and georef-b are the regression fixture: two boxes whose different map
conversions resolve to the same real-world point, so a viewer that applies them
draws one box's worth of scene and one that ignores them spans 707 m. They carry
two meshes each because reorderSidecarByMorton bails out below two and then
writes no chunk table, and a sidecar without one cannot stream over byte ranges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:06:37 +10:00
Dion Moult 935562142e Apply a model's coordinate operation when its sidecar loads
.ifcview has carried the model's CoordinateOperation since v11 and the
streaming reader has always parsed it, but applyCachedModel ignored it. The
matrix only ever reached the scene because BonsaiViewer pushes it after every
load via setModelCoordinateOperation. Nothing does that on web, so every model
rendered in its local coordinates and two federated models with differing map
conversions came out misaligned.

Seed the matrix and the unit scales from the sidecar, and recompose the model
afterwards. Seeding alone is not enough: the instance transforms in a sidecar
are baked with identity federation matrices, and applyCachedModel uploads them
as-is. The recompose also fixes a second case that had nothing to do with
georeferencing — a model loaded while a federated false origin was already in
force kept its unshifted transforms.

ModelGpuData gains the unit scales because composeModelTransformation needs
them to lift a transform's anchor point into metres, and on a sidecar-only load
there is no IFC to read them back from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:06:37 +10:00
Dion Moult d1d0fb4636 Move the federation transform math into IfcViewerCore
The value types and compose helpers in Federation.h were already Qt-free —
Eigen and std::string — but sat in the Qt half of the viewer, so the web build
could not reach them. Split them into FederationMath and add it to
IfcViewerCore, which the Emscripten build links.

What stays behind is what genuinely needs the dependencies: computeModelGeoref
reads an ifcopenshell::file, and the Federation class is a QObject that
persists .ifcfed. Federation.h includes the new header, so no caller changes.

FederationMath needs convert() to resolve a federation unit name to metres and
x_axis_to_angle_deg() to read grid north off a coordinate operation, hence the
helpers_math dependency added in the previous commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:06:37 +10:00
Dion Moult e0b226f4ca Split the schema-free half of the unit and geolocation helpers out
unit.h and geolocation.h both include ../ifcparse/express.h for the entity
walking they do, which puts the whole module out of reach of anything that
cannot link IfcParse. Most of what a viewer wants from them needs no IFC at
all: the unit conversion tables, and the Helmert parameters-to-matrix math.

Move those into unit_convert and geolocation_transform, and build them as a
new helpers_math target that `helpers` re-exports PUBLIC, so existing callers
keep working through the unchanged unit.h / geolocation.h includes. The new
target has no IfcParse or Qt dependency and so builds under Emscripten, where
the rest of this directory cannot.

One target rather than compiling the sources into each consumer: the glob in
this directory would otherwise put them in libhelpers.a as well, leaving two
copies of the same objects in any link that pulls both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:06:37 +10:00
Dion Moult 79bd3563de Refill the web chunk fetch pipeline from each load completion
Queued chunk loads waited for the next render frame to start, so streaming
advanced at frame cadence rather than as fast as the in-flight cap allowed.
driveStreamingLoads now queues whatever it could not start and every load
completion drains that queue, decoupling fetching from the render loop.

pumpWebChunkLoads is deliberately defined outside the __EMSCRIPTEN__ block
that holds the rest of the byte-range streaming code: driveStreamingLoads
calls it unconditionally and ViewportCore.h declares it unconditionally, so
desktop needs a definition to link against. The body guards itself instead
and compiles to a no-op off the web, where loads are not asynchronous.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:06:37 +10:00
Andrej730 4e887e1c59 Reapply 253918c
Fixes pyodide build. Reverted in af58eaf by accident?
2026-08-10 20:54:36 +05:00
Andrej730 fa9f3b5cb7 pyodide/order: update script after dlls rename 2026-08-10 20:50:04 +05:00
Andrej730 77dc679a6e pytest: fix warnings from using non-collections 2026-08-10 18:39:07 +05:00
Andrej730 ce9e2b94d5 Update plugins gitignore 2026-08-10 18:13:45 +05:00
Andrej730 58dcaed89a ruff: some util rules 2026-08-10 17:06:56 +05:00
Andrej730 109bd58384 ruff: use readable rule names in ignores 2026-08-10 17:06:56 +05:00
Andrej730 b6dccce12a ruff: use readable rule names in selectors 2026-08-10 17:06:56 +05:00
Andrej730 daa7d98b3f ruff: remove unused noqa
Most of them are actually correct, but they're not enforced in general on the repo, so using them blocks us from flagging `unused-noqa` for rules that we actually do use.
2026-08-10 17:06:56 +05:00
Andrej730 321760cea4 bcf: bump required Python version to 3.10
3.9 is EOL
2026-08-10 16:54:37 +05:00
Andrej730 7b9615f4e5 ruff: fix unsorted-dunder-all 2026-08-10 16:54:37 +05:00
Andrej730 ff22a9d1f3 ruff: fix deprecated-import 2026-08-10 16:54:37 +05:00
Andrej730 055f64fa9b ruff: fix quoted-annotation 2026-08-10 16:54:37 +05:00
Andrej730 7370d07db1 ruff: sort imports 2026-08-10 16:54:37 +05:00
Andrej730 717d6aa2af ruff: fix pyprojects using select instead of extend-select by mistake 2026-08-10 16:33:01 +05:00
Andrej730 69b0409aa0 build_pyodide: normalize version added to meta.yaml
Prevents error below:
```
ValueError: Version mismatch in ifcopenshell: version in meta.yaml is '0.9.0alpha0' but version from wheel name is '0.9.0a0'
```
2026-08-10 16:33:01 +05:00
Andrej730 4095d5c8d6 pyodide/meta.yaml: better document version placeholder 2026-08-10 16:33:01 +05:00
Andrej730 19a3707f72 build-all: build swig natively for pyodide 2026-08-10 15:11:36 +05:00
Andrej730 262117c4f8 build-all: drop unused kwargs in build_dependency 2026-08-10 15:11:36 +05:00
Andrej730 246fa24be0 build-all: reuse WASM constant for consistency 2026-08-10 15:11:36 +05:00
Andrej730 35d2fb43e2 build_osx: use uv run 2026-08-10 15:11:24 +05:00
Andrej730 f10f7eba83 build-all: use assert_never instead of ValueError 2026-08-10 15:11:24 +05:00
Andrej730 785936000a ruff: sort imports 2026-08-10 13:35:22 +05:00
Thomas Krijnen 83fc219a8a publish-cpp-api-docs.yml 2026-08-10 06:15:49 +02:00
Thomas Krijnen 7a1dcd07c8 Handle version postfixes 2026-08-10 05:19:17 +02:00
Thomas Krijnen b63137e859 Merge remote-tracking branch 'origin/v0.8.0' into ifcviewer-wgpu 2026-08-10 03:31:02 +02:00
Thomas Krijnen 3d15500976 run black 2026-08-09 14:17:35 +02:00
Thomas Krijnen 64aed6a766 try fix stub 2026-08-09 14:11:49 +02:00
Thomas Krijnen a08eed7ac9 swig ignore ifcopenshell::detail::performance_scope 2026-08-09 14:04:04 +02:00
Thomas Krijnen 076f46cfeb Further propagate logger so that test succeeds 2026-08-09 14:00:42 +02:00
Thomas Krijnen a353edb9e0 check_call() so that init errors surface earlier 2026-08-09 13:59:44 +02:00
Thomas Krijnen 30fb379e32 Remove cwd from import in case you're running tests like I do 2026-08-09 13:16:24 +02:00
Thomas Krijnen dbea3f0362 Reapply skip type bitmap after field reordering changes 2026-08-09 12:56:15 +02:00
Thomas Krijnen b5eca83357 Adapt for namespaces changes 2026-08-09 12:48:35 +02:00
Thomas Krijnen e5aaf7c602 Adapt for namespaces changes 2026-08-09 12:45:00 +02:00
Thomas Krijnen 9e53d0dcc9 Don't bind to reference in order not to overwrite entity instance storage in case of IfcPropertySetDefinitionSet 2026-08-09 12:42:35 +02:00
Thomas Krijnen 17c4d8faff Skip unavailable shape-stat kernels
Generated with the assistance of an AI coding tool.
2026-08-09 11:51:06 +02:00
Thomas Krijnen c9c7edd4d6 Require SWIG 4.1 in CMake
Generated with the assistance of an AI coding tool.
2026-08-09 10:58:05 +02:00
Thomas Krijnen 96653029cf Upgrade standalone CI to SWIG 4.2.1
Generated with the assistance of an AI coding tool.
2026-08-09 10:49:49 +02:00
Thomas Krijnen e9fffc221b Silence final compiler warnings
Generated with the assistance of an AI coding tool.
2026-08-09 10:03:41 +02:00
Thomas Krijnen a441757080 Use underscore plugin artifact names
Generated with the assistance of an AI coding tool.
2026-08-09 09:52:17 +02:00
Thomas Krijnen c818f48a47 Own completed iterator results uniquely
Generated with the assistance of an AI coding tool.
2026-08-09 09:14:41 +02:00
Thomas Krijnen 28c9c1d34d Silence remaining compiler warnings
Generated with the assistance of an AI coding tool.
2026-08-09 09:04:28 +02:00
Robert Sigmundsson f05dd4aea5 Fix #9278. calculate_unit_scale raises the SI prefix to the length exponent for prefixed SQUARE_METRE/CUBIC_METRE units.
An SI prefix attaches to the base unit symbol and the prefixed symbol is
raised to the power as a whole: DECI CUBIC_METRE is dm3 = a litre = 1e-3 m3,
not 0.1 m3. The scale factor previously applied the prefix multiplier
linearly for all IfcSIUnits, inflating volumes x100 and areas x10 for such
declarations (produced e.g. by MagiCAD for Revit MEP exports).

Following the reviewer note in #9278, the exponent is taken from the
derived attribute IfcSIUnit.Dimensions rather than from substring matching
on the unit name: the multiplier is raised to LengthExponent only when the
unit's dimensions are a pure power of length, so prefixed derived units
(KILO PASCAL, MEGA NEWTON) and non-length units (KILO GRAM) correctly keep
the linear multiplier. This matches the exponent handling already present
in convert() and named_dimensions in the same module.

Adds regression tests for prefixed AREAUNIT/VOLUMEUNIT and for the
linear-prefix behaviour of PRESSUREUNIT/MASSUNIT.
2026-08-09 08:41:15 +02:00
Thomas Krijnen dcfc22e29e Transfer iterator result ownership
Generated with the assistance of an AI coding tool.
2026-08-09 04:54:51 +02:00
Thomas Krijnen be3c2ee770 Expose geometry types in snake case
Generated with the assistance of an AI coding tool.
2026-08-09 04:44:18 +02:00
Thomas Krijnen fbfa51c451 Fix MSVC geometry build errors
Generated with the assistance of an AI coding tool.
2026-08-09 04:15:58 +02:00
Thomas Krijnen 19f3261dc3 Fixed by @Moult 2026-08-09 03:46:25 +02:00
Richard Brice 7ed8584edc Revised update_alignment_parameter_segment_tags to make EndTag optional 2026-08-08 10:35:20 -07:00
Thomas Krijnen 61f30dd200 Silence obvious compiler warnings
Generated with the assistance of an AI coding tool.
2026-08-08 17:08:26 +02:00
Thomas Krijnen b706121f53 Replace Boost function callbacks
Generated with the assistance of an AI coding tool.
2026-08-08 16:28:47 +02:00
Thomas Krijnen 99a09a2a3c Use snake case conversion result APIs
Generated with the assistance of an AI coding tool.
2026-08-08 16:09:30 +02:00
Thomas Krijnen 2ba55ba984 Flatten the geometry representation namespace
Generated with the assistance of an AI coding tool.
2026-08-08 15:56:41 +02:00
Thomas Krijnen 616c7a00d5 Inline conversion result vectors
Generated with the assistance of an AI coding tool.
2026-08-08 15:45:04 +02:00
Thomas Krijnen 8c003110fe Replace Boost shared pointers
Generated with the assistance of an AI coding tool.
2026-08-08 15:37:53 +02:00
Thomas Krijnen 4e49b640a7 Own iterator geometry results
Return independent geometry copies with unique ownership, preserve parent lifetimes, and teach the Python wrapper to own derived results. Keep serializer inputs non-owning and replace Collada's deferred object with copied triangulation elements.\n\nGenerated with the assistance of an AI coding tool.
2026-08-08 15:18:51 +02:00
Thomas Krijnen c30841aad6 Remove unused adaptor element path
The optional adaptor element list was never assigned, so simplify IfcConvert to use its geometry iterator unconditionally.

Generated with the assistance of an AI coding tool.
2026-08-08 15:00:48 +02:00
Thomas Krijnen 4597929df9 Remove _t suffixes from public types
Rename header-scope aliases, enums, and helper types while retaining descriptive names where dropping the suffix would create a collision.

Generated with the assistance of an AI coding tool.
2026-08-08 14:58:26 +02:00
Thomas Krijnen 2859c1ef17 Use value serialization in sphere example
Update the stale pointer-form example and pass the IFC file required by the current serialization API.

Generated with the assistance of an AI coding tool.
2026-08-08 14:21:10 +02:00
Thomas Krijnen 7ae6bf4374 Rename geometry and serializer files
Apply the rename manifest, normalize serializer filenames to the classes they define, and update includes and CMake source lists.

Generated with the assistance of an AI coding tool.
2026-08-08 14:20:05 +02:00
Thomas Krijnen 02481b3247 Wrap more classes into ifcopenshell:: namespace 2026-08-08 13:58:39 +02:00
Thomas Krijnen 2c47c9d4fa Irrelevant comment 2026-08-08 13:30:47 +02:00
Thomas Krijnen c2abc3f844 Remove old Java Native Interface code 2026-08-08 13:29:29 +02:00
Thomas Krijnen 4dcd644a32 Deleted unmigrated examples 2026-08-08 13:10:01 +02:00
Thomas Krijnen 6fea72b045 Run black 2026-08-08 12:42:25 +02:00
Thomas Krijnen 1573730f18 Disambiguate naming 2026-08-08 12:35:44 +02:00
Thomas Krijnen 8f4832651a Track patch rename 2026-08-08 12:30:26 +02:00
Thomas Krijnen af58eaf79f Last minute refactoring 2026-08-08 07:42:45 +02:00
Thomas Krijnen 8870ffb018 Rework c++ docs 2026-08-08 03:44:56 +02:00
Richard Brice c5ba22451f Adds update_alignment_parameter_segment_tags function 2026-08-07 14:33:04 -07:00
Andrej730 1a931ddfd9 Revert "build_pyodide: try more recent pyodide-build"
This reverts commit f7876a97ee.

There's some emscripten mismatch, will try to bump it later.
2026-08-07 20:04:46 +05:00
Andrej730 f7876a97ee build_pyodide: try more recent pyodide-build 2026-08-07 17:53:18 +05:00
Andrej730 4d0e5f6aee ifcopenshell.file: improve missing attribute error msg 2026-08-07 17:53:18 +05:00
Andrej730 dfc60196ec ifcwrap/cmake: fix using python:abc feature on older swig 2026-08-07 16:00:33 +05:00
Andrej730 ef4bba8b33 IfcGeomWrapper: remove stale IfcGeom::Matrix reference
It was removed long time ago in c78b289
2026-08-07 16:00:33 +05:00
Andrej730 e044b865dd pyodide/test_wheel: make it support modular wheels 2026-08-06 19:52:49 +05:00
Andrej730 47812b2d32 gitignore: ignore non-plugins shared objects too (--ifcopenshell-shared) 2026-08-06 19:49:54 +05:00
Andrej730 16e58f7369 build_pyodide: push built dependencies as build finished
As we do in build_rocky/build_osx already, allowing to push dependencies in case build succeeds but some later tests fail.
2026-08-06 19:49:54 +05:00
Andrej730 253918c100 serialization/cmake: remove dependency on geometry_kernel_opencascade
To fix wrapper depending on `ifcopenshell.geometry.kernel.opencascade.so` plugin, which breaks wasm module import (wrapper imported first and it fails because occt kernel isn't loaded yet and loading kernel first since it needs some symbols from the wrapper).

`geometry_serializer` seems to need `OpenCascadeConversionResult.h` from the kernel just to be able to refer to structs (`ConversionResultShape`, `OpenCascadeShape`) and include with relative path already allows it.
2026-08-06 19:49:54 +05:00
Andrej730 c57554a07e ifcwrap/cmake: dont link against geom kernels
Produces errors in wasm builds - we want to be able to load the wrapper first and all other dependencies should be pluggable. But loading wrapper first failed, because it depended on the kernels. Loading kernels first would also fail, since OCCT kernel is using using some symbols from core (`ifcopenshell::exception`) and in wasm they have to be resolved all during `dlopen`.
2026-08-06 19:49:54 +05:00
Andrej730 2aff2adb42 gitattributes: normalize .i swig templates line endings
IfcGeomWrapper.i was stored on index as CRLF and on Linux it sometimes produced distracting diffs, when working on the file.
2026-08-06 19:49:54 +05:00
Andrej730 3d05ccbd59 gitattributes: ensure platform dependent files are stored using correct line endings 2026-08-06 19:49:38 +05:00
Andrej730 027203008d black . 2026-08-06 19:11:27 +05:00
Dion Moult 74b405a9f7 Write GlobalId index when serializing to RocksDB
rocks_db_file_storage already exposes a `g|`-prefixed guid -> instance
name map, but RocksDbSerializer never populated it, so by_guid() on a
converted file always threw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 10:20:03 +10:00
Dion Moult 38e0e0e297 Compare entity instances in one file by identity again
entity_instance.file is a property backed by a fresh SWIG wrapper on every
access, and ifcopenshell::file had no __eq__, so `self.file != other.file`
in entity_instance.__eq__ compared two throwaway wrappers and was always
true - even for an instance against itself. Every entity comparison
therefore took the deep get_info() branch, making distinct but structurally
identical instances compare equal and leaving the final `return False`
unreachable. Bonsai's TestAddRepresentationItemToShapeAspect showed this as
two separate IfcShapeAspects being treated as one, so the stale aspect was
never removed.

Restore the file_pointer() pair that was commented out on both
ifcopenshell::file and express::Base - IfcParseWrapper.i already described
it as the way to "trace file ownership of instances on the python side" -
and give file the __eq__/__hash__ it was missing. The express::Base one
needs $self->file() now that file_ lives on instance_data. This also
repairs rocksdb_lazy_instance.__eq__, which already called file_pointer().

EXPRESS `=` is value comparison and `:=:` is instance comparison, but
rule_compiler emits `==` for both (see the @todo on process_rel_op), and
derived attributes build their operands in the shared global file, so rules
compare same-file instances and need value semantics. Restore those for the
duration of rule execution with settings.compare_instances_by_value,
alongside the existing unpack_non_aggregate_inverses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 10:20:03 +10:00
Richard Brice 048242783e Updates update_key_point_referents to confirm to CT 4.1.4.4.3 2026-08-05 07:26:27 -07:00
Andrej730 d1027c5877 build-all: fix missing CXXFLAGS in manual build (f8f4725)
In the mentioned commit I've dropped workaround, because issue upstream was resolved, but forgot to load flags from `pyodide` fro manual build.
2026-08-05 18:37:46 +05:00
Andrej730 d65df3665a split_pyodide_ifcopenshell_wheel: print some logs 2026-08-05 18:37:46 +05:00
Andrej730 393640b121 split_pyodide_ifcopenshell_wheel: add doc-string 2026-08-05 18:37:46 +05:00
Andrej730 5d6e3fbdc8 order_pyodide_wheel_shared_objects: explain why 2026-08-05 18:37:46 +05:00
Andrej730 24c3d855de build-all: drop workaround for expired mpfr ssl 2026-08-05 18:37:46 +05:00
Andrej730 ae4f5c92fb build-all: remove unnecessary quoting from annotations and Union 2026-08-05 18:37:46 +05:00
Andrej730 75a3bf0175 build-all: accept truthy/falsy values in more consistent way 2026-08-05 18:37:46 +05:00
Andrej730 beb5c1f7ed build_pyodide: use uv 2026-08-05 18:37:46 +05:00
Andrej730 8bdaa8c7cb build-all: ifcopenshell-shared
As we're not currently bundling dlls for all other external dependencies.
It seems `--shared`in CI previously  worked sort of by accident - since it was relying on the cached build outputs that were previously built statically.
2026-08-04 22:13:25 +05:00
Andrej730 3340d88b95 ifcwrap/cmake: add missing geometry_serializer runtime target installation
Caused errors like so:
```
Traceback (most recent call last):
  File "/__w/IfcOpenShell/IfcOpenShell/build/Linux/x86_64/install/python-3.13.6/lib/python3.13/site-packages/ifcopenshell/__init__.py", line 87, in <module>
    from . import ifcopenshell_wrapper
  File "/__w/IfcOpenShell/IfcOpenShell/build/Linux/x86_64/install/python-3.13.6/lib/python3.13/site-packages/ifcopenshell/ifcopenshell_wrapper.py", line 10, in <module>
    from . import _ifcopenshell_wrapper
ImportError: libifcopenshell.geometry.writer.so: cannot open shared object file: No such file or directory
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import inspect, ifcopenshell; print(inspect.getfile(ifcopenshell))
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/__w/IfcOpenShell/IfcOpenShell/build/Linux/x86_64/install/python-3.13.6/lib/python3.13/site-packages/ifcopenshell/__init__.py", line 89, in <module>
    raise ImportError("IfcOpenShell not built for '%s' (%s)" % (python_distribution, e)) from e
ImportError: IfcOpenShell not built for 'linux/64bit/python3.13' (libifcopenshell.geometry.writer.so: cannot open shared object file: No such file or directory)
```
2026-08-04 22:10:31 +05:00
Andrej730 0e6e3edaa6 build-all: small alignment fix 2026-08-04 20:57:45 +05:00
Andrej730 ccb9be0940 build_rocky: add comments to the script 2026-08-04 16:21:01 +05:00
Andrej730 82f0a74ab2 Build workflows: drop stale line
It's a stale line from original approach (b599ee1040) that was using `bin` as a stage area. Now zip is packed right to `~/output`.
2026-08-04 16:20:17 +05:00
Bruno Postle 6f3acc84ee ifcmcp: source tool descriptions from ifcquery/ifcedit instead of duplicating them
Alternative to #8955, for #8951 (23 of 25 ifcmcp tools reach MCP clients
with an empty description because FastMCP reads each wrapper's own
__doc__, and the server.py wrappers had none).

#8955 fixes this by hand-writing a new docstring directly onto each
server.py wrapper. Most of those wrappers are thin passthroughs to
IfcSession methods in core.py, which already had short docstrings, which
themselves mostly delegate to already-documented ifcquery/ifcedit
functions -- so that fix tripled up content across three layers that can
drift out of sync.

This instead enriches the true source (the ifcquery/ifcedit library
functions, useful independently of MCP) and has core.py's IfcSession
methods copy __doc__ from their delegate via a small _use_doc()
decorator, and server.py's tool registration pull description= from the
matching IfcSession method. Methods that aren't pure passthroughs
(session lifecycle, generic API/shape dispatch) keep their own
hand-written docs. Keeps #8955's regression test.

Generated with the assistance of an AI coding tool.
2026-08-03 12:52:55 +02:00
Richard Brice e077390e3d add update_key_point_referents to label key alignment points 2026-08-01 15:24:03 -07:00
Richard Brice 80cc603932 alignment: rename get_referent_nest to get_stationing_nest 2026-08-01 15:21:50 -07:00
737 changed files with 66721 additions and 65488 deletions
-1
View File
@@ -1,5 +1,4 @@
Checks: 'bugprone-*,cert-*,clang-analyzer-*,readability-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''
AnalyzeTemporaryDtors: false
FormatStyle: none
+4 -2
View File
@@ -19,6 +19,8 @@
# normalize the line endings of the following files
*.bat text eol=crlf
*.cmd text eol=crlf
*.cpp text
*.css text
*.csv text
@@ -28,20 +30,20 @@
*.gitkeep
*.h text
*.html text
*.i text
*.ifc text
*.json text
*.md text
*.po text
*.pot text
*.py text
*.sh text eol=lf
*.txt text
# files not normalized ATM
# bat
# bnf
# blend
# i
# ico
# mo
# mpass
+1 -1
View File
@@ -7,7 +7,7 @@ body:
label: Bug Description
placeholder: |
Describe what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
+383
View File
@@ -0,0 +1,383 @@
# /// script
# dependencies = [
# "pytest",
# ]
# ///
"""Check (and by default fix) whitespace issues in tracked source files:
- stray CR, e.g. 'hello\\rworld' -> 'helloworld'
- line ending mismatch, e.g. 'hello\\r\\n' -> 'hello\\n' (or vice versa)
- missing newline at end of file
- extra newline(s) at end of file
- trailing whitespace at end of line
"""
import argparse
import io
import os
import re
import subprocess
import sys
from collections.abc import Callable
from pathlib import Path
from typing import BinaryIO, Literal, cast
import pytest
class C:
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
RESET = "\033[0m"
CR = b"\r"
CRLF = b"\r\n"
LF = b"\n"
LineSeparator = Literal[b"\r\n", b"\n"]
SYSTEM_LINE_SEPARATOR = cast(LineSeparator, os.linesep.encode())
class Checker:
def __init__(self, newline: LineSeparator = SYSTEM_LINE_SEPARATOR) -> None:
self.newline = newline
self.issues = 0
def report(self, label: str, issue: str) -> None:
self.issues += 1
print(f"{label}: {C.RED}{issue}{C.RESET}")
def check_stray_cr(self, filepath: Path, check: bool) -> None:
with filepath.open("r+b") as f:
self._check_stray_cr(f, str(filepath), check)
def _check_stray_cr(self, f: BinaryIO, label: str, check: bool) -> None:
# a CR is "stray" if it isn't immediately followed by a LF, i.e. not part of a CRLF pair
# CRLF/CR mismatch will be reported separately.
stray_cr = re.compile(rb"\r(?!\n)")
content = f.read()
matches = list(stray_cr.finditer(content))
if not matches:
return
line_numbers = dict.fromkeys(content.count(b"\n", 0, m.start()) + 1 for m in matches)
for line_number in line_numbers:
self.report(f"{label}:{line_number}", "stray carriage return")
if check:
return
f.seek(0)
f.write(stray_cr.sub(b"", content))
f.truncate()
def check_line_endings_mismatch(self, filepath: Path, check: bool) -> None:
with filepath.open("r+b") as f:
self._check_line_endings_mismatch(f, str(filepath), check)
def _check_line_endings_mismatch(self, f: BinaryIO, label: str, check: bool) -> None:
NEWLINE = self.newline
def get_line_ending(line: bytes) -> LineSeparator | None:
if line.endswith(CRLF):
return CRLF
if line.endswith(LF):
return LF
# last line with no trailing newline at all; check_eof_newline handles that
return None
changed = False
fixed_lines = []
for line_number, line in enumerate(f, start=1):
found = get_line_ending(line)
if found in (NEWLINE, None):
fixed_lines.append(line)
continue
self.report(f"{label}:{line_number}", f"line ending mismatch (expected {NEWLINE!r}, found {found!r})")
changed = True
content = line[: -len(found)]
fixed_lines.append(content + NEWLINE)
if changed and not check:
f.seek(0)
f.write(b"".join(fixed_lines))
f.truncate()
def check_eof_newline(self, filepath: Path, check: bool) -> None:
with filepath.open("r+b") as f:
self._check_eof_newline(f, str(filepath), check)
def _check_eof_newline(self, f: BinaryIO, label: str, check: bool) -> None:
NEWLINE = self.newline
NEWLINE_SIZE = len(NEWLINE)
size = f.seek(0, os.SEEK_END)
if size == 0:
return
trailing_newlines = 0
while True:
pos = f.seek((-trailing_newlines - 1) * NEWLINE_SIZE, os.SEEK_END)
if f.read(NEWLINE_SIZE) != NEWLINE:
break
trailing_newlines += 1
if pos == 0:
break
if trailing_newlines == 0:
self.report(label, "missing newline at end of file")
if check:
return
f.seek(0, os.SEEK_END)
f.write(NEWLINE)
elif trailing_newlines > 1:
self.report(label, f"{trailing_newlines} trailing newlines at end of file")
if check:
return
f.truncate(size - (trailing_newlines - 1) * NEWLINE_SIZE)
def check_trailing_whitespaces(self, filepath: Path, check: bool) -> None:
with filepath.open("r+b") as f:
self._check_trailing_whitespaces(f, str(filepath), check)
def _check_trailing_whitespaces(self, f: BinaryIO, label: str, check: bool) -> None:
NEWLINE = self.newline
NEWLINE_SIZE = len(NEWLINE)
changed = False
fixed_lines = []
for line_number, line in enumerate(f, start=1):
has_newline = line.endswith(NEWLINE)
content = line[:-NEWLINE_SIZE] if has_newline else line
stripped = content.rstrip()
if stripped != content:
self.report(f"{label}:{line_number}", "trailing whitespace")
changed = True
fixed_lines.append(stripped + (NEWLINE if has_newline else b""))
if changed and not check:
f.seek(0)
f.write(b"".join(fixed_lines))
f.truncate()
CheckMethod = Callable[[Checker, BinaryIO, str, bool], None]
class TestChecker:
def _assert_check(
self,
method: CheckMethod,
content: bytes,
expected_issues: int,
fixed: bytes,
check: bool,
line_ending: LineSeparator,
*,
transform: bool = True,
) -> None:
checker = Checker(line_ending)
if line_ending == CRLF and transform:
content = content.replace(LF, CRLF)
fixed = fixed.replace(LF, CRLF)
buffer = io.BytesIO(content)
method(checker, buffer, "test", check)
assert buffer.getvalue() == (content if check else fixed)
assert checker.issues == expected_issues
@pytest.mark.parametrize(
("content", "expected_issues", "fixed"),
(
# OK
(b"", 0, b""),
(b"hello\n", 0, b"hello\n"),
(b"line1\r\nline2\n", 0, b"line1\r\nline2\n"),
# ERR
(b"hello\rworld\n", 1, b"helloworld\n"),
(b"a\rb\rc\n", 1, b"abc\n"),
(b"hello\r", 1, b"hello"),
),
)
@pytest.mark.parametrize("check", [False, True])
def test_check_stray_cr(self, content: bytes, expected_issues: int, fixed: bytes, check: bool) -> None:
# Don't parametrize by line endings, since in this case it doesn't matter.
self._assert_check(Checker._check_stray_cr, content, expected_issues, fixed, check, LF)
@pytest.mark.parametrize(
("content", "expected_issues", "fixed", "line_ending"),
(
# OK
(b"", 0, b"", LF),
(b"hello\n", 0, b"hello\n", LF),
(b"hello\r\n", 0, b"hello\r\n", CRLF),
# ERR
(b"hello\r\n", 1, b"hello\n", LF),
(b"a\nb\r\nc\n", 1, b"a\nb\nc\n", LF),
(b"a\r\nb\r\n", 2, b"a\nb\n", LF),
(b"hello\n", 1, b"hello\r\n", CRLF),
(b"a\r\nb\nc\r\n", 1, b"a\r\nb\r\nc\r\n", CRLF),
),
)
@pytest.mark.parametrize("check", [False, True])
def test_check_line_endings_mismatch(
self, content: bytes, expected_issues: int, fixed: bytes, line_ending: LineSeparator, check: bool
) -> None:
self._assert_check(
Checker._check_line_endings_mismatch, content, expected_issues, fixed, check, line_ending, transform=False
)
@pytest.mark.parametrize(
("content", "expected_issues", "fixed"),
(
# OK
(b"", 0, b""),
(b"hello\n", 0, b"hello\n"),
# ERR
(b"hello", 1, b"hello\n"),
(b"hello\n\n\n", 1, b"hello\n"),
(b"\n\n\n", 1, b"\n"),
),
)
@pytest.mark.parametrize("check", [False, True])
@pytest.mark.parametrize("line_ending", [LF, CRLF])
def test_check_eof_newline(
self, content: bytes, expected_issues: int, fixed: bytes, check: bool, line_ending: LineSeparator
) -> None:
self._assert_check(Checker._check_eof_newline, content, expected_issues, fixed, check, line_ending)
@pytest.mark.parametrize(
("content", "expected_issues", "fixed"),
(
# OK
(b"", 0, b""),
(b"hello\n", 0, b"hello\n"),
(b"hello", 0, b"hello"),
# ERR
(b" ", 1, b""),
(b"hello ", 1, b"hello"),
),
)
@pytest.mark.parametrize("check", [False, True])
@pytest.mark.parametrize("line_ending", [LF, CRLF])
def test_check_trailing_whitespaces(
self, content: bytes, expected_issues: int, fixed: bytes, check: bool, line_ending: LineSeparator
) -> None:
self._assert_check(Checker._check_trailing_whitespaces, content, expected_issues, fixed, check, line_ending)
@staticmethod
def run_tests(extra_args: list[str] | None = None) -> None:
pytest.main([__file__, *(extra_args or [])])
def existing_path(value: str) -> Path:
path = Path(value)
if not path.exists():
raise argparse.ArgumentTypeError(f"path not found: {value}")
return path
# Python files are covered by `black`.
PATTERNS = (
"*.cpp",
"*.h",
"*.i",
"*.cmake",
"*/CMakeLists.txt",
"*.yml",
)
REPO_ROOT = Path(subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip())
# Generated files; formatted by the express codegen, not by this script.
IGNORED_DIRS = (
REPO_ROOT / "src/ifcparse/schemas",
REPO_ROOT / "win/patches",
)
def get_tracked_files(root: Path | None = None) -> list[Path]:
output = subprocess.check_output(
["git", "ls-files", "--others", "--cached", "--exclude-standard", *PATTERNS],
cwd=root,
text=True,
)
base = root if root is not None else Path()
filepaths = []
for line in output.splitlines():
filepath = base / line
if not any(filepath.resolve().is_relative_to(d) for d in IGNORED_DIRS):
filepaths.append(filepath)
return filepaths
def main() -> int:
# anything after "--" is forwarded to pytest, e.g. `--test -- --capture=no`
argv = sys.argv[1:]
if "--" in argv:
split = argv.index("--")
argv, extra_args = argv[:split], argv[split + 1 :]
else:
extra_args = []
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=__doc__,
)
parser.add_argument("paths", type=existing_path, nargs="*", help="files or directories to check")
parser.add_argument(
"--check",
action="store_true",
help="only check for whitespace issues without applying fixes",
)
parser.add_argument(
"--test",
action="store_true",
help="run self-tests",
)
parser.add_argument(
"--verbose",
action="store_true",
help="print each checked path",
)
args = parser.parse_args(argv)
if args.test:
TestChecker.run_tests(extra_args)
return 0
if args.paths:
filepaths: list[Path] = []
for path in args.paths:
filepaths.extend(get_tracked_files(path) if path.is_dir() else [path])
else:
filepaths = get_tracked_files()
# dict.fromkeys() dedupes while preserving order, unlike set().
filepaths = list(dict.fromkeys(filepaths))
checker = Checker()
for filepath in filepaths:
if args.verbose:
print(f"checking {filepath}")
checker.check_stray_cr(filepath, args.check)
checker.check_line_endings_mismatch(filepath, args.check)
checker.check_eof_newline(filepath, args.check)
checker.check_trailing_whitespaces(filepath, args.check)
print(f"{len(filepaths)} file(s) checked.")
if not checker.issues:
color = C.GREEN
elif args.check:
color = C.RED
else:
color = C.YELLOW
outcome = "found" if args.check else "found and fixed"
print(f"{color}{checker.issues} issue(s) {outcome}.{C.RESET}")
return 1 if args.check and checker.issues else 0
if __name__ == "__main__":
sys.exit(main())
@@ -26,7 +26,7 @@ jobs:
working-directory: src/bonsaiviewer-autodesk
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
+7 -77
View File
@@ -35,6 +35,9 @@ jobs:
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Set up uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Install Dependencies
run: |
brew update
@@ -61,7 +64,7 @@ jobs:
- name: Unpack Dependencies
run: |
cd build
python ../nix/cache_dependencies.py unpack
uv run ../nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.23
@@ -102,7 +105,7 @@ jobs:
# INSTALL_RPATH to "@loader_path" on Apple.
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \
BUILD_BONSAIVIEWER=ON QT_DIR="${QT_DIR}" \
python3 ./nix/build-all.py -v --diskcleanup --shared ${MAC_INTEL} \
uv run ./nix/build-all.py -v --diskcleanup --shared ${MAC_INTEL} \
| tee build.log
- name: Upload Build Logs
@@ -119,7 +122,7 @@ jobs:
- name: Pack Dependencies
run: |
cd build
python ../nix/cache_dependencies.py pack
uv run ../nix/cache_dependencies.py pack
- name: Commit and Push Changes to Build Repository
run: |
@@ -132,80 +135,7 @@ jobs:
- name: Package .zip archives
run: |
VERSION=v`cat VERSION`
# packaging/build.py stages the connector binary + connector.json
# into dist/autodesk/; the .app loop below copies that folder into
# the bundle. Same on-disk shape as the Linux and Windows builds.
python3 src/bonsaiviewer-autodesk/packaging/build.py
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
test -d "$autodesk_connector_dir"
cd ./build/`uname`/*/10.15/install/ifcopenshell
mkdir -p ~/output
install_root="$PWD"
stage_runtime_payload() {
dest="$1"
while IFS= read -r runtime_file; do
cp -L "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" -type f \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
}
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
py_version_major=python-${numbers}$postfix
pushd . > /dev/null
cd $py_version
if [ ! -d ifcopenshell ]; then
mkdir ../ifcopenshell_
mv * ../ifcopenshell_
mv ../ifcopenshell_ ifcopenshell
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
stage_runtime_payload ifcopenshell
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip ifcopenshell
mv *.zip ~/output
popd > /dev/null
done
rm -f "$install_root"/bin/*.zip
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
exe=`basename "$exe_path"`
package_dir="$install_root/.package-${exe}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp "$exe_path" "$package_dir/"
stage_runtime_payload "$package_dir"
pushd "$package_dir" > /dev/null
zip -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip" .
popd > /dev/null
rm -rf "$package_dir"
done
# .app bundles (e.g. BonsaiViewer.app) live at the install-prefix
# root because their install rule uses `BUNDLE DESTINATION "."` —
# that's the layout Qt's macdeployqt expects. macdeployqt has
# already embedded the Qt frameworks inside each bundle during
# install/strip, so the only thing left to stage is the connector.
find "$install_root" -maxdepth 1 -type d -name "*.app" | while read app_path; do
app=`basename "$app_path" .app`
if [ "$app" = "BonsaiViewer" ]; then
# ConnectorDiscovery looks in applicationDirPath()/connectors,
# which for a bundle is Contents/MacOS.
mkdir -p "$app_path/Contents/MacOS/connectors"
cp -a "$autodesk_connector_dir" "$app_path/Contents/MacOS/connectors/"
fi
pushd "$install_root" > /dev/null
zip -qq -r "$HOME/output/${app}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip" "$(basename "$app_path")"
popd > /dev/null
done
uv run nix/package-zip-archives.py "macos${{ matrix.oldarch }}64" --shared
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
+31 -27
View File
@@ -8,6 +8,13 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Set up uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Install Python
# Installs latest Python version so it's preferred by uv over system Python.
run: uv python install
- name: Checkout Repository
uses: actions/checkout@v7
with:
@@ -26,7 +33,7 @@ jobs:
- name: Unpack Dependencies
run: |
cd ifcopenshell_build
python ../IfcOpenShell/nix/cache_dependencies.py unpack
uv run ../IfcOpenShell/nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.23
@@ -40,18 +47,6 @@ jobs:
NEW_FILE=`echo $FILE | sed "s/-/+${GITHUB_SHA:0:7}-/2"`
mv $FILE $NEW_FILE
- name: Order wheel shared objects
run: |
python ./IfcOpenShell/pyodide/order_pyodide_wheel_shared_objects.py dist/ifcopenshell-*.whl
- name: Split packages
run: |
VERSION=v`cat ./IfcOpenShell/VERSION`
mkdir -p dist-modular
python ./IfcOpenShell/pyodide/split_pyodide_ifcopenshell_wheel.py dist/ifcopenshell-*.whl ./dist-modular
cd dist-modular
zip -r -qq ifcopenshell-modular-${VERSION}-${GITHUB_SHA:0:7}-pyodide.zip *.whl
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v7
@@ -61,23 +56,10 @@ jobs:
ifcopenshell_build/*/*/logs/*.log
retention-days: 30
- name: Run wheel tests
run: |
cp -r IfcOpenShell/pyodide/test test
# venv set up in build_pyodide.sh.
source .venv/bin/activate
uv pip install pytest-pyodide
PYODIDE_ROOT_DIST=`pyodide config get pyodide_root`/dist
# `pytest-pyodide` requires pyodide in 'pyodide' directory in cwd, when running `pytest`.
cp -r $PYODIDE_ROOT_DIST test/pyodide
cp dist/ifcopenshell-*.whl test/pyodide
cd test
pytest --capture=no
- name: Pack Dependencies
run: |
cd ifcopenshell_build
python ../IfcOpenShell/nix/cache_dependencies.py pack
uv run ../IfcOpenShell/nix/cache_dependencies.py pack
- name: Commit and Push Changes to Build Repository
run: |
@@ -88,6 +70,28 @@ jobs:
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
git push || echo "Push failed"
- name: Order wheel shared objects
run: |
uv run ./IfcOpenShell/pyodide/order_pyodide_wheel_shared_objects.py dist/ifcopenshell-*.whl
- name: Split packages
run: |
VERSION=v`cat ./IfcOpenShell/VERSION`
mkdir -p dist-modular
uv run ./IfcOpenShell/pyodide/split_pyodide_ifcopenshell_wheel.py dist/ifcopenshell-*.whl ./dist-modular
cd dist-modular
zip -r -qq ifcopenshell-modular-${VERSION}-${GITHUB_SHA:0:7}-pyodide.zip *.whl
- name: Run wheel tests
run: |
# venv set up in build_pyodide.sh.
source .venv/bin/activate
ln -s "$PWD/dist" IfcOpenShell/dist
ln -s "$PWD/dist-modular" IfcOpenShell/dist-modular
cd IfcOpenShell/pyodide
./run_pytest.py setup
./run_pytest.py run
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
+6 -151
View File
@@ -33,14 +33,11 @@ jobs:
libXfixes-devel libXft-devel pango-devel cairo-devel libstdc++-static
git config --global --add safe.directory '*'
- name: Install Rust
# The bonsaiviewer-autodesk connector is a Rust crate; the "Package
# .zip archives" step below runs `cargo build --release` via
# packaging/build.py. Match the dedicated connector workflow's stable
# toolchain (dtolnay/rust-toolchain@stable).
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# The bonsaiviewer-autodesk connector is a Rust crate; the "Package
# .zip archives" step below runs `cargo build --release` via
# packaging/build.py. Match the dedicated connector workflow's stable
# toolchain, rather than whatever Rust the runner image happens to ship.
- uses: dtolnay/rust-toolchain@stable
- name: Install aws cli
run: |
@@ -110,149 +107,7 @@ jobs:
- name: Package .zip archives
shell: bash
run: |
VERSION=v`cat VERSION`
# bonsaiviewer-autodesk is now a Rust connector. packaging/build.py
# invokes `cargo build --release` and stages the binary +
# connector.json into dist/autodesk/. Same on-disk shape as the
# old PyInstaller flow so the symlink + zip steps below
# continue to work unchanged.
uv run src/bonsaiviewer-autodesk/packaging/build.py
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
test -d "$autodesk_connector_dir"
cd ./build/`uname`/*/install/ifcopenshell
mkdir -p ~/output
install_root="$PWD"
QT6_VERSION="${QT6_VERSION:-6.8.3}"
if [ -z "${QT_DIR:-}" ]; then
for qt_candidate in "$(dirname "$install_root")"/qt6-${QT6_VERSION}-*/${QT6_VERSION}/*; do
if [ -d "$qt_candidate/lib" ]; then
QT_DIR="$qt_candidate"
break
fi
done
fi
ensure_soname_links() {
dest="$1"
find "$dest" -maxdepth 1 -type f -name "*.so*" | while IFS= read -r shared_object; do
soname=$(readelf -d "$shared_object" 2>/dev/null | sed -n 's/.*(SONAME).*Shared library: \[\(.*\)\].*/\1/p' | head -n 1)
[ -n "$soname" ] || continue
[ -e "$dest/$soname" ] && continue
ln -s "$(basename "$shared_object")" "$dest/$soname"
done
}
stage_runtime_payload() {
dest="$1"
include_geometry_writers="${2:-1}"
while IFS= read -r runtime_file; do
if [ "$include_geometry_writers" != "1" ] && [[ "$(basename "$runtime_file")" == ifcopenshell.geometry.writer.* ]]; then
continue
fi
cp -P "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" \( -type f -o -type l \) \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
ensure_soname_links "$dest"
}
stage_qt_runtime_payload() {
exe_path="$1"
dest="$2"
[ -n "${QT_DIR:-}" ] && [ -d "$QT_DIR/lib" ] || return 0
if ! LD_LIBRARY_PATH="$QT_DIR/lib:${LD_LIBRARY_PATH:-}" ldd "$exe_path" 2>/dev/null | grep -q "libQt6"; then
return 0
fi
find "$QT_DIR/lib" -maxdepth 1 \( -type f -o -type l \) -name "*.so*" -exec cp -P {} "$dest/" \;
ensure_soname_links "$dest"
if [ -d "$QT_DIR/plugins" ]; then
pushd "$QT_DIR/plugins" > /dev/null
find . \( -type f -o -type l \) -name "*.so*" | while IFS= read -r plugin_file; do
mkdir -p "$dest/plugins/$(dirname "$plugin_file")"
cp -P "$plugin_file" "$dest/plugins/$plugin_file"
done
popd > /dev/null
if [ -d "$dest/plugins" ]; then
find "$dest/plugins" -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN/../..:$ORIGIN' {} \;
fi
fi
find "$dest" -maxdepth 1 -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
printf "[Paths]\nPrefix = .\n" > "$dest/qt.conf"
}
check_runtime_dependencies() {
package_dir="$1"
missing=0
while IFS= read -r binary_file; do
readelf -h "$binary_file" >/dev/null 2>&1 || continue
if ! env -u LD_LIBRARY_PATH ldd "$binary_file" > "$package_dir/.ldd.out" 2>&1; then
echo "ldd failed for $binary_file"
cat "$package_dir/.ldd.out"
missing=1
continue
fi
if grep -q "not found" "$package_dir/.ldd.out"; then
echo "Missing runtime dependencies for $binary_file"
grep "not found" "$package_dir/.ldd.out"
missing=1
fi
done < <(find "$package_dir" -type f \( -perm /111 -o -name "*.so" -o -name "*.so.*" \))
rm -f "$package_dir/.ldd.out"
if [ "$missing" -ne 0 ]; then
echo "Runtime dependency check found issues; continuing packaging."
fi
return 0
}
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
py_version_major=python-${numbers}$postfix
pushd . > /dev/null
cd $py_version
if [ ! -d ifcopenshell ]; then
mkdir ../ifcopenshell_
mv * ../ifcopenshell_
mv ../ifcopenshell_ ifcopenshell
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
stage_runtime_payload ifcopenshell
zip -y -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell
mv *.zip ~/output
popd > /dev/null
done
rm -f "$install_root"/bin/*.zip
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
exe=`basename "$exe_path"`
package_dir="$install_root/.package-${exe}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp "$exe_path" "$package_dir/"
patchelf --set-rpath '$ORIGIN' "$package_dir/$exe"
stage_runtime_payload "$package_dir" 0
stage_qt_runtime_payload "$exe_path" "$package_dir"
if [ "$exe" = "BonsaiViewer" ]; then
mkdir -p "$package_dir/connectors"
cp -a "$autodesk_connector_dir" "$package_dir/connectors/"
fi
check_runtime_dependencies "$package_dir"
pushd "$package_dir" > /dev/null
zip -y -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip" .
popd > /dev/null
rm -rf "$package_dir"
done
uv run nix/package-zip-archives.py linux64 --shared
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
+6 -151
View File
@@ -45,14 +45,11 @@ jobs:
libXfixes-devel libXft-devel pango-devel cairo-devel libstdc++-static
git config --global --add safe.directory '*'
- name: Install Rust
# The bonsaiviewer-autodesk connector is a Rust crate; the "Package
# .zip archives" step below runs `cargo build --release` via
# packaging/build.py. Match the dedicated connector workflow's stable
# toolchain (dtolnay/rust-toolchain@stable).
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# The bonsaiviewer-autodesk connector is a Rust crate; the "Package
# .zip archives" step below runs `cargo build --release` via
# packaging/build.py. Match the dedicated connector workflow's stable
# toolchain, rather than whatever Rust the runner image happens to ship.
- uses: dtolnay/rust-toolchain@stable
- name: Install aws cli
run: |
@@ -122,149 +119,7 @@ jobs:
- name: Package .zip archives
shell: bash
run: |
VERSION=v`cat VERSION`
# bonsaiviewer-autodesk is now a Rust connector. packaging/build.py
# invokes `cargo build --release` and stages the binary +
# connector.json into dist/autodesk/. Same on-disk shape as the
# old PyInstaller flow so the symlink + zip steps below
# continue to work unchanged.
uv run src/bonsaiviewer-autodesk/packaging/build.py
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
test -d "$autodesk_connector_dir"
cd ./build/`uname`/*/install/ifcopenshell
mkdir -p ~/output
install_root="$PWD"
QT6_VERSION="${QT6_VERSION:-6.8.3}"
if [ -z "${QT_DIR:-}" ]; then
for qt_candidate in "$(dirname "$install_root")"/qt6-${QT6_VERSION}-*/${QT6_VERSION}/*; do
if [ -d "$qt_candidate/lib" ]; then
QT_DIR="$qt_candidate"
break
fi
done
fi
ensure_soname_links() {
dest="$1"
find "$dest" -maxdepth 1 -type f -name "*.so*" | while IFS= read -r shared_object; do
soname=$(readelf -d "$shared_object" 2>/dev/null | sed -n 's/.*(SONAME).*Shared library: \[\(.*\)\].*/\1/p' | head -n 1)
[ -n "$soname" ] || continue
[ -e "$dest/$soname" ] && continue
ln -s "$(basename "$shared_object")" "$dest/$soname"
done
}
stage_runtime_payload() {
dest="$1"
include_geometry_writers="${2:-1}"
while IFS= read -r runtime_file; do
if [ "$include_geometry_writers" != "1" ] && [[ "$(basename "$runtime_file")" == ifcopenshell.geometry.writer.* ]]; then
continue
fi
cp -P "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" \( -type f -o -type l \) \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
ensure_soname_links "$dest"
}
stage_qt_runtime_payload() {
exe_path="$1"
dest="$2"
[ -n "${QT_DIR:-}" ] && [ -d "$QT_DIR/lib" ] || return 0
if ! LD_LIBRARY_PATH="$QT_DIR/lib:${LD_LIBRARY_PATH:-}" ldd "$exe_path" 2>/dev/null | grep -q "libQt6"; then
return 0
fi
find "$QT_DIR/lib" -maxdepth 1 \( -type f -o -type l \) -name "*.so*" -exec cp -P {} "$dest/" \;
ensure_soname_links "$dest"
if [ -d "$QT_DIR/plugins" ]; then
pushd "$QT_DIR/plugins" > /dev/null
find . \( -type f -o -type l \) -name "*.so*" | while IFS= read -r plugin_file; do
mkdir -p "$dest/plugins/$(dirname "$plugin_file")"
cp -P "$plugin_file" "$dest/plugins/$plugin_file"
done
popd > /dev/null
if [ -d "$dest/plugins" ]; then
find "$dest/plugins" -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN/../..:$ORIGIN' {} \;
fi
fi
find "$dest" -maxdepth 1 -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
printf "[Paths]\nPrefix = .\n" > "$dest/qt.conf"
}
check_runtime_dependencies() {
package_dir="$1"
missing=0
while IFS= read -r binary_file; do
readelf -h "$binary_file" >/dev/null 2>&1 || continue
if ! env -u LD_LIBRARY_PATH ldd "$binary_file" > "$package_dir/.ldd.out" 2>&1; then
echo "ldd failed for $binary_file"
cat "$package_dir/.ldd.out"
missing=1
continue
fi
if grep -q "not found" "$package_dir/.ldd.out"; then
echo "Missing runtime dependencies for $binary_file"
grep "not found" "$package_dir/.ldd.out"
missing=1
fi
done < <(find "$package_dir" -type f \( -perm /111 -o -name "*.so" -o -name "*.so.*" \))
rm -f "$package_dir/.ldd.out"
if [ "$missing" -ne 0 ]; then
echo "Runtime dependency check found issues; continuing packaging."
fi
return 0
}
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
py_version_major=python-${numbers}$postfix
pushd . > /dev/null
cd $py_version
if [ ! -d ifcopenshell ]; then
mkdir ../ifcopenshell_
mv * ../ifcopenshell_
mv ../ifcopenshell_ ifcopenshell
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
stage_runtime_payload ifcopenshell
zip -y -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip ifcopenshell
mv *.zip ~/output
popd > /dev/null
done
rm -f "$install_root"/bin/*.zip
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
exe=`basename "$exe_path"`
package_dir="$install_root/.package-${exe}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp "$exe_path" "$package_dir/"
patchelf --set-rpath '$ORIGIN' "$package_dir/$exe"
stage_runtime_payload "$package_dir" 0
stage_qt_runtime_payload "$exe_path" "$package_dir"
if [ "$exe" = "BonsaiViewer" ]; then
mkdir -p "$package_dir/connectors"
cp -a "$autodesk_connector_dir" "$package_dir/connectors/"
fi
check_runtime_dependencies "$package_dir"
pushd "$package_dir" > /dev/null
zip -y -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip" .
popd > /dev/null
rm -rf "$package_dir"
done
uv run nix/package-zip-archives.py linuxarm64 --shared
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
+1 -1
View File
@@ -64,7 +64,7 @@ jobs:
max-size: 5000MB
- name: Set up Python for connector build
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: '3.12'
+3 -12
View File
@@ -16,7 +16,7 @@ on:
- 'src/ifc5d/ifc5d/**'
- 'src/ifccityjson/**'
branches:
- v0.8.0
- v0.9.0
workflow_dispatch:
jobs:
@@ -51,19 +51,10 @@ jobs:
name: "Linux Build",
short_name: linux,
}
- {
name: "MacOS Build",
short_name: macos,
}
- {
name: "MacOS ARM Build",
short_name: macosm1,
}
exclude:
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
- pyver: py313
config:
short_name: macos
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
@@ -72,7 +63,7 @@ jobs:
python-version: '3.11'
- name: Get current version
id: version
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
- name: Compile
run: |
cd src/bonsai && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }}
@@ -128,7 +119,7 @@ jobs:
blender --command extension install-file -r user_default -e $bonsai_zip
blender --command extension list
git clone https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
git clone --branch ${{ github.ref_name }} --single-branch https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
# Reregister Bonsai.
# Note that running it in background might miss some errors
-9
View File
@@ -34,19 +34,10 @@ jobs:
name: "Linux Build",
short_name: linux,
}
- {
name: "MacOS Build",
short_name: macos,
}
- {
name: "MacOS ARM Build",
short_name: macosm1,
}
exclude:
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
- pyver: py313
config:
short_name: macos
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
@@ -34,21 +34,21 @@ jobs:
- name: Run conda cleaner
run: |
python - << EOF
import os
from datetime import datetime, timedelta
from binstar_client.utils import get_server_api
from binstar_client.errors import BinstarError
# Configuration
api_token = os.environ.get('ANACONDA_TOKEN')
pkg_name = 'ifcopenshell'
channel_name = 'ifcopenshell'
# Authenticate with Anaconda
aserver_api = get_server_api(token=api_token)
# Get the list of packages in the channel
def get_package(filter_package_name: str = None):
try:
@@ -59,12 +59,12 @@ jobs:
print(f"No packages found for {filter_package_name}.")
if len(user_packages) > 1:
raise ValueError(f"Found {len(user_packages)} package for {filter_package_name}. Will only support 1 package.")
return user_packages[0]
except BinstarError as err:
raise ValueError(f"Failed to fetch packages: {err}")
# Delete a package version
def delete_package(package_name, version):
try:
@@ -72,33 +72,33 @@ jobs:
print(f"Deleted {package_name} version {version}")
except BinstarError as err:
print(f"Failed to delete {package_name} version {version}: {err}")
# Main logic
def main():
package = get_package(pkg_name)
if not package:
print("No packages found.")
return
number_of_supported_versions = ${{ env.NUM_SUPPORTED_VERSIONS }}
package_name = package['name']
versions = package["versions"]
if len(versions) <= number_of_supported_versions:
print(f"Number of versions {len(versions)} is less than or equal to {number_of_supported_versions}.")
return
# sort the versions in descending order
print(f"Before reversal: {versions=}")
versions.reverse()
print(f"After reversal: {versions=}")
releases = versions[number_of_supported_versions:]
for release in releases:
delete_package(package_name, release)
main()
EOF
@@ -24,11 +24,13 @@ jobs:
- uses: actions/checkout@v7
- name: Set env
run: echo ok go
- name: Get current version
id: version
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
# Strip any trailing prerelease label and number; the dated alpha
# suffix is added below.
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
- name: Get current date
id: date
run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT
@@ -37,7 +39,7 @@ jobs:
id: verdate
run: echo "verdate=${{ steps.version.outputs.version }}alpha${{ steps.date.outputs.date }}" >> $GITHUB_OUTPUT
test:
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}
needs: activate
@@ -62,7 +64,7 @@ jobs:
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- name: set ARTIFACTS ENV vars
shell: bash
run: |
@@ -74,7 +76,7 @@ jobs:
elif [[ "$RUNNER_OS" == "Linux" ]]; then
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
fi
- uses: actions/checkout@v7
with:
submodules: recursive
+12 -11
View File
@@ -1,6 +1,6 @@
name: ci-ifcopenshell-docker
on:
on:
workflow_dispatch:
push:
tags:
@@ -37,13 +37,14 @@ jobs:
name: ccache
uses: hendrikmuhs/ccache-action@v1.2.23
-
-
name: Build ifcopenshell
run: |
mkdir build && cd build
cmake \
-DCMAKE_INSTALL_PREFIX=$PWD/install/ \
-DCMAKE_BUILD_TYPE=Release \
-DUSE_CCACHE=ON \
-DCMAKE_PREFIX_PATH=/usr \
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
-DBUILD_PACKAGE=On \
@@ -66,12 +67,12 @@ jobs:
../cmake
make -j $(nproc)
make install
-
-
name: Package
run: |
make package
working-directory: build
- name: Upload
- name: Upload
uses: actions/upload-artifact@v7
with:
# Artifact name
@@ -88,8 +89,8 @@ jobs:
- uses: actions/checkout@v7
with:
lfs: true
- name: Download
- name: Download
uses: actions/download-artifact@v8.0.1
with:
# Artifact name
@@ -100,17 +101,17 @@ jobs:
uses: docker/setup-qemu-action@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
-
uses: docker/setup-buildx-action@v4
-
name: Login to Dockerhub
uses: docker/login-action@v4
uses: docker/login-action@v4
with:
username: aecgeeks
password: ${{ secrets.DOCKER_HUB_TOKEN }}
-
-
name: Build container image
uses: docker/build-push-action@v7
with:
with:
context: artifacts
repository: aecgeeks/ifcopenshell
# Since the dispatch is set to `tag`, `github.ref_name` should evaluate to the pushed tag
+2 -2
View File
@@ -7,7 +7,7 @@ on:
- '.github/workflows/ci-ifcsverchok-build.yml'
- 'src/ifcsverchok/*'
branches:
- v0.8.0
- v0.9.0
jobs:
activate:
@@ -32,7 +32,7 @@ jobs:
python-version: '3.11'
- name: Get current version
id: version
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
- name: Get current date
id: date
run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT
+3 -1
View File
@@ -3,11 +3,13 @@ name: ci-ifctester-org
on:
workflow_dispatch:
push:
branches:
- v0.9.0
paths:
- src/ifctester/**
jobs:
publish_website:
publish_ifctester_org:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v7
+13 -2
View File
@@ -43,6 +43,7 @@ jobs:
sudo apt update
sudo apt-get install --no-install-recommends -y \
cmake \
bison \
gcc \
g++ \
libboost-date-time-dev \
@@ -61,13 +62,23 @@ jobs:
libocct-ocaf-dev \
libocct-visualization-dev \
libpcre3-dev \
libpcre2-dev \
libtbb-dev \
libxml2-dev \
libxi-dev \
occt-misc \
tcl-dev \
tk-dev \
swig
tk-dev
- name: Build SWIG
# IfcOpenShell requires SWIG 4.1+, ubuntu-22.04 ships 4.0.2.
run: |
sudo apt-get remove --purge -y swig swig4.0
git clone https://github.com/swig/swig --branch v4.2.1 --depth 1
cmake -S swig -B swig/build -DCMAKE_BUILD_TYPE=Release
cmake --build swig/build -j "$(nproc)"
sudo cmake --install swig/build
swig -version
- name: Configure minimal IfcOpenShell
run: |
+5 -4
View File
@@ -84,7 +84,7 @@ jobs:
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
${OCCT_CMAKE_DEPS} \
libcgal-dev libeigen3-dev
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.23
with:
@@ -121,7 +121,7 @@ jobs:
cd OpenCOLLADA
git checkout v1.6.68
patch -p1 --batch --forward -i ../nix/patches/opencollada/pr622_and_disable_subdirs.patch
patch -p1 --batch --forward -i ../nix/patches/opencollada/allow_static_libraries_config_on_unix.patch
patch -p1 --batch --forward -i ../nix/patches/opencollada/config_select_libs_by_use_shared.patch
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
@@ -176,7 +176,7 @@ jobs:
run: |
echo $Python3_ROOT_DIR
echo ${{ env.pythonLocation }}
mkdir build && cd build
cmake \
-DCMAKE_BUILD_TYPE=Release \
@@ -185,6 +185,7 @@ jobs:
-DPYTHON_EXECUTABLE:FILEPATH=${{ env.pythonLocation }}/bin/python \
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
-DUSE_MMAP=On \
-DUSE_CCACHE=ON \
-DBUILD_SHARED_LIBS=${{ matrix.build_shared_libs }} \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
-DGLTF_SUPPORT=On \
@@ -223,7 +224,7 @@ jobs:
cmake --build .
./IfcOpenHouse && test -f IfcOpenHouse.ifc
./IfcParseExamples IfcOpenHouse.ifc
./IfcAdvancedHouse && test -f IfcAdvancedHouse.ifc
./IfcAdvancedHouse && test -f IfcAdvancedHouse.ifc
./IfcAlignment && test -f FHWA_Bridge_Geometry_Alignment_Example.ifc
./IfcSimplifiedAlignment && test -f FHWA_Bridge_Geometry_Alignment_Example_Simplified.ifc
@@ -35,4 +35,4 @@ jobs:
external_repository: IfcOpenShell/bonsaibim_org_docs_unstable # Target repository
publish_branch: main # Branch to deploy to
cname: docs-unstable.bonsaibim.org # Custom domain for unstable docs
publish_dir: src/bonsai/docs/_build/html # Directory containing built docs
publish_dir: src/bonsai/docs/_build/html # Directory containing built docs
@@ -0,0 +1,87 @@
# This file was generated with the assistance of an AI coding tool.
name: Publish C++ API documentation
on:
push:
branches:
- v0.9.0
paths:
- '.github/workflows/publish-cpp-api-docs.yml'
- 'docs/cpp-api/**'
- 'src/ifcgeom/**'
- 'src/ifcparse/**'
- 'src/serializers/**'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: publish-cpp-api-docs
cancel-in-progress: false
jobs:
publish:
if: github.repository == 'IfcOpenShell/IfcOpenShell'
runs-on: ubuntu-24.04
steps:
- name: Checkout IfcOpenShell
uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: '3.10'
- name: Install documentation dependencies
run: |
sudo apt-get update
sudo apt-get install --yes doxygen graphviz
python -m pip install --requirement docs/cpp-api/requirements.txt
- name: Build C++ API documentation
working-directory: docs/cpp-api
run: |
export PROJECT_NUMBER="$(git rev-parse --short HEAD)"
python -m sphinx -M html . output -W --keep-going
- name: Checkout documentation repository
uses: actions/checkout@v7
with:
repository: IfcOpenShell/cpp_docs
ref: master
path: published-docs
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Replace published documentation
run: |
publish_tree="${RUNNER_TEMP}/published-docs-tree"
mkdir -p "${publish_tree}/v0.9.0-latest"
rsync --archive docs/cpp-api/output/html/ "${publish_tree}/v0.9.0-latest/"
touch "${publish_tree}/.nojekyll"
if [[ -f published-docs/CNAME ]]; then
cp published-docs/CNAME "${publish_tree}/CNAME"
fi
rsync --archive --delete --exclude='.git/' "${publish_tree}/" published-docs/
- name: Commit and push if changed
working-directory: published-docs
env:
SOURCE_SHA: ${{ github.sha }}
run: |
git config user.name 'IfcOpenBot'
git config user.email 'IfcOpenBot@users.noreply.github.com'
git add --all
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "Update C++ API docs from ${SOURCE_SHA:0:7}"
git push origin master
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
+4 -4
View File
@@ -37,7 +37,7 @@ jobs:
libtbb-dev nlohmann-json3-dev \
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
libcgal-dev opencollada-dev
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -77,7 +77,7 @@ jobs:
echo ::set-output name=deb::$( ls assets/*.deb | head -n 1 | xargs basename )
working-directory: build
env:
CHANGELOG_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CHANGELOG_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release
id: release
uses: actions/create-release@v1
@@ -101,7 +101,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
upload_url: ${{ steps.release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: build/assets/${{ steps.package.outputs.tgz }}
asset_name: ${{ steps.package.outputs.tgz }}
asset_content_type: application/x-gzip
@@ -111,7 +111,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
upload_url: ${{ steps.release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: build/assets/${{ steps.package.outputs.deb }}
asset_name: ${{ steps.package.outputs.deb }}
asset_content_type: application/vnd.debian.binary-package
+13 -3
View File
@@ -16,6 +16,9 @@
/src/ifcmax/out/
/src/ifcwrap/out/
/src/ifctester/webapp/public/pyodide/
# pyodide wheels
/dist/
/dist-modular/
/win/BuildDepsCache*.txt
@@ -108,9 +111,13 @@ src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
# plugins
src/ifcopenshell-python/ifcopenshell/ifcopenshell.document.*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell.geometry.*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell.parse.schema*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_document_*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_geometry_*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_parse_schema*.so
src/ifcopenshell-python/ifcopenshell/libifcopenshell.geometry.so
src/ifcopenshell-python/ifcopenshell/libifcopenshell.geometry.writer.so
src/ifcopenshell-python/ifcopenshell/libifcopenshell.parse.so
src/ifcopenshell-python/ifcopenshell/libifcopenshell.plugin.so
# apple
.DS_Store
@@ -119,6 +126,9 @@ src/ifcopenshell-python/ifcopenshell/ifcopenshell.parse.schema*.so
.clangd
# clangd cache
.cache
# Useful for symlinking json compilation database from cmake,
# allowing clang commands without `-p path/to/build`.
/compile_commands.json
# Brickschema
src/bonsai/bonsai/bim/schema/Brick.ttl
-3
View File
@@ -8,9 +8,6 @@
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
path = src/ifcopenshell-python/test/Sample-BIM-Files
url = https://github.com/IfcOpenShell/ids-test-files
[submodule "docs/cpp-api/assets/doxygen-awesome-css"]
path = docs/cpp-api/assets/doxygen-awesome-css
url = https://github.com/jothepro/doxygen-awesome-css.git
[submodule "src/ifcopenshell-python/ifcopenshell/simple_spf"]
path = src/ifcopenshell-python/ifcopenshell/simple_spf
url = https://github.com/IfcOpenShell/step-file-parser
+1 -1
View File
@@ -1 +1 @@
0.8.6
0.9.0alpha0
+57 -34
View File
@@ -36,6 +36,14 @@ file(READ "../VERSION" "RELEASE_VERSION_")
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
message(STATUS "Detected version '${RELEASE_VERSION}'")
# CMake's project(VERSION) only accepts numeric components. Keep the complete
# release identifier for build information, but use its numeric release part
# for PROJECT_VERSION, SOVERSION, and generated CMake package metadata.
string(REGEX MATCH "^[0-9]+\\.[0-9]+\\.[0-9]+" PROJECT_VERSION_NUMERIC "${RELEASE_VERSION}")
if(NOT PROJECT_VERSION_NUMERIC)
message(FATAL_ERROR "VERSION must start with a numeric major.minor.patch version: '${RELEASE_VERSION}'")
endif()
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
if(POLICY CMP0141) # 3.25+
@@ -55,9 +63,15 @@ endif()
# Include utility macros and functions
include(utilities.cmake)
# use extra version to make pre-release using eg semver
# Use a SemVer-compatible spelling for CPack artifact names. A trailing
# alphabetic label and number is separated from the numeric version by a
# hyphen: for example, 0.9.0alpha0 becomes 0.9.0-alpha0.
if(NOT DEFINED EXTRA_VERSION)
set(EXTRA_VERSION "-alpha.3")
if(RELEASE_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+([A-Za-z]+)([0-9]+)$")
set(EXTRA_VERSION "-${CMAKE_MATCH_1}${CMAKE_MATCH_2}")
else()
set(EXTRA_VERSION "")
endif()
endif()
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
@@ -119,7 +133,8 @@ option(WITH_ZSTD "Use Zstd compression in RocksDB writes" OFF)
option(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only instead of system-wide." OFF)
option(USE_DEBUG_PYTHON "Use debug binaries when building Debug IfcPython on Windows." OFF)
option(ADD_COMMIT_SHA "Add commit sha and branch in version number, requires git" OFF)
option(VERSION_OVERRIDE "Override the version defined in buildinfo.cpp with the file VERSION in the repository root" OFF)
option(VERSION_OVERRIDE "Use VERSION as the branch label when commit information is embedded" OFF)
option(USE_CCACHE "Use ccache as a compiler launcher if it is found" OFF)
set(
PYTHON_MODULE_INSTALL_DIR
@@ -127,15 +142,7 @@ set(
"Directory to install IfcPython package to. By default package is installed in found Python's site-packages."
)
if (VERSION_OVERRIDE)
file(READ "../VERSION" "RELEASE_VERSION_")
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
message(STATUS "Detected version '${RELEASE_VERSION}'")
else()
set(RELEASE_VERSION "0.8.0")
endif()
project(IfcOpenShell VERSION ${RELEASE_VERSION})
project(IfcOpenShell VERSION ${PROJECT_VERSION_NUMERIC})
# Make sure CMake modules in this project are found first
list(PREPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
@@ -172,26 +179,30 @@ if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM)
set(BUILD_IFCGEOM ON)
endif()
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
message(STATUS "`ccache` is found, using it as a compiler launcher.")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
if(MSVC)
# By default Visual Studio generators will use /Zi which is not compatible
# with ccache, so tell Visual Studio to use /Z7 instead.
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>")
# Not needed for Ninja.
if(CMAKE_GENERATOR MATCHES "Visual Studio")
file(COPY_FILE
${CCACHE_FOUND} ${CMAKE_BINARY_DIR}/cl.exe
ONLY_IF_DIFFERENT)
set(CMAKE_VS_GLOBALS
"CLToolExe=cl.exe"
"CLToolPath=${CMAKE_BINARY_DIR}"
"UseMultiToolTask=true"
)
if(USE_CCACHE)
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
message(STATUS "`USE_CCACHE` is enabled and `ccache` is found, using it as a compiler launcher.")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
if(MSVC)
# By default Visual Studio generators will use /Zi which is not compatible
# with ccache, so tell Visual Studio to use /Z7 instead.
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>")
# Not needed for Ninja.
if(CMAKE_GENERATOR MATCHES "Visual Studio")
file(COPY_FILE
${CCACHE_FOUND} ${CMAKE_BINARY_DIR}/cl.exe
ONLY_IF_DIFFERENT)
set(CMAKE_VS_GLOBALS
"CLToolExe=cl.exe"
"CLToolPath=${CMAKE_BINARY_DIR}"
"UseMultiToolTask=true"
)
endif()
endif()
endif()
else()
message(STATUS "ccache usage is disabled, set `USE_CCACHE=ON` to enable it.")
endif()
if(MSVC AND MSVC_PARALLEL_BUILD)
@@ -305,12 +316,25 @@ if (WITH_ROCKSDB)
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_ROCKSDB)
# See https://github.com/facebook/rocksdb/issues/981.
if(TARGET RocksDB::rocksdb)
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb)
set(IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET RocksDB::rocksdb)
elseif(TARGET RocksDB::rocksdb-shared)
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb-shared)
set(IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET RocksDB::rocksdb-shared)
else()
message(FATAL_ERROR "RocksDB found but neither RocksDB::rocksdb nor RocksDB::rocksdb-shared target exists")
endif()
# Our win/build-deps.cmd builds RocksDB separately per Debug/Release config into the
# same install prefix, so the imported target only ever has DEBUG and RELEASE listed in
# IMPORTED_CONFIGURATIONS. On a multi-config generator (Visual Studio), CMake maps any
# unmatched build config to the *first* entry of that list, which happens to be DEBUG
# (RocksDBTargets-debug.cmake sorts before RocksDBTargets-release.cmake). Without an
# explicit mapping, RelWithDebInfo and MinSizeRel builds would end up linking the
# /MDd-flavored rocksdb_d.lib into an /MD (NDEBUG) binary, causing a CRT/runtime-library
# mismatch that depends on nothing but that alphabetical ordering.
set_target_properties(${IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET} PROPERTIES
MAP_IMPORTED_CONFIG_RELWITHDEBINFO "RELWITHDEBINFO;RELEASE"
MAP_IMPORTED_CONFIG_MINSIZEREL "MINSIZEREL;RELEASE"
)
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE ${IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET})
if (WITH_ZSTD)
# @todo do we actually need the zstd include dir or rather just pass
@@ -693,8 +717,7 @@ endif()
# Documentation
if(BUILD_DOCUMENTATION)
set(CMAKE_MODULE_PATH "../docs/cmake")
add_subdirectory(../docs docs)
add_subdirectory(../docs/cpp-api docs/cpp-api)
endif()
if(BUILD_EXAMPLES)
+1
View File
@@ -52,6 +52,7 @@ macro(SET_INSTALL_SELF_RPATH _target)
endmacro()
function(ifcopenshell_plugin_target TARGET)
# Plug-ins are loaded by exact filename and should not receive a platform library prefix.
set_target_properties(${TARGET} PROPERTIES PREFIX "")
if((NOT WIN32) AND BUILD_SHARED_LIBS AND NOT WASM_BUILD AND NOT CREATE_BUNDLE AND NOT CMAKE_INSTALL_RPATH AND COMMAND SET_INSTALL_SELF_RPATH)
SET_INSTALL_SELF_RPATH(${TARGET})
+13 -33
View File
@@ -1,35 +1,15 @@
#Look for an executable called sphinx-build
find_program(SPHINX_EXECUTABLE NAMES sphinx-build DOC "Path to sphinx-build executable")
include(FindPackageHandleStandardArgs)
#Handle standard arguments to find_package like REQUIRED and QUIET
find_package_handle_standard_args(Sphinx "Failed to find sphinx-build executable" SPHINX_EXECUTABLE)
find_package(Doxygen REQUIRED)
#find_package(Sphinx REQUIRED)
find_program(
SPHINX_EXECUTABLE
NAMES sphinx-build
REQUIRED
DOC "Path to the sphinx-build executable"
)
set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR})
set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/docs/sphinx)
message(STATUS "SPHINX BUILD ${CMAKE_CURRENT_BINARY_DIR}")
file(MAKE_DIRECTORY ./output/doxygen)
if(DOXYGEN_FOUND)
add_custom_target(
Sphinx
ALL
COMMAND ${SPHINX_EXECUTABLE} -v -T -b html ${SPHINX_SOURCE} ${CMAKE_CURRENT_SOURCE_DIR}/output
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/output
COMMENT "Generating documentation with Sphinx"
)
# add_custom_target(ifcopenshell_python_docs ALL
# COMMAND make html
# WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../src/ifcblenderexport/docs
# OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/../src/ifcblenderexport/docs
# COMMENT "Generating documentation with Sphinx")
else(DOXYGEN_FOUND)
message("Doxygen need to be installed to generate the doxygen documentation")
endif(DOXYGEN_FOUND)
add_custom_target(
cpp_api_docs
COMMAND ${SPHINX_EXECUTABLE} -M html . output -W --keep-going
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Generating the IfcOpenShell C++ API documentation"
VERBATIM
)
+63 -22
View File
@@ -68,7 +68,7 @@ PROJECT_LOGO =
# entered, it will be relative to the location where doxygen was started. If
# left blank the current directory will be used.
OUTPUT_DIRECTORY = ./output
OUTPUT_DIRECTORY = ./output/doxygen
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096
# sub-directories (in 2 levels) under the output directory of each output format
@@ -852,7 +852,7 @@ WARNINGS = YES
# will automatically be disabled.
# The default value is: YES.
WARN_IF_UNDOCUMENTED = YES
WARN_IF_UNDOCUMENTED = NO
# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
# potential errors in the documentation, such as documenting some parameters in
@@ -901,7 +901,7 @@ WARN_IF_UNDOC_ENUM_VAL = NO
# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.
# The default value is: NO.
WARN_AS_ERROR = NO
WARN_AS_ERROR = FAIL_ON_WARNINGS
# The WARN_FORMAT tag determines the format of the warning messages that doxygen
# can produce. The string should contain the $file, $line, and $text tags, which
@@ -944,7 +944,6 @@ WARN_LOGFILE =
# Note: If this tag is empty the current directory is searched.
INPUT = ../../src/ifcgeom \
../../src/ifcgeom_schema_agnostic \
../../src/ifcparse \
../../src/serializers \
@@ -1001,7 +1000,7 @@ RECURSIVE = YES
# Note that relative paths are relative to the directory from which doxygen is
# run.
EXCLUDE =
EXCLUDE = ../../src/ifcparse/schemas
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
# directories that are symbolic links (a Unix file system feature) are excluded
@@ -1025,7 +1024,33 @@ EXCLUDE_PATTERNS =
# wildcard * is used, a substring. Examples: ANamespace, AClass,
# ANamespace::AClass, ANamespace::*Test
EXCLUDE_SYMBOLS =
EXCLUDE_SYMBOLS = "ifcopenshell::geom::opaque_number::*" \
ifcopenshell::entity::attribute_by_name_cmp \
ifcopenshell::impl::rocks_db_file_storage::rocksdb_types_iterator \
ifcopenshell::impl::in_memory_file_storage::type_iterator \
"util::string_buffer::*_item" \
util::string_buffer::item \
ifcopenshell::geom::layer_filter::wildcards_match \
ifcopenshell::paged_file_impl::entry \
ifcopenshell::token \
attribute_value::pointer_type \
INCLUDE_PARENT_PARENT_DIR \
POSTFIX_SCHEMA_ \
POSTFIX_SCHEMA__ \
STRINGIFY_ \
MAKE_INIT_FN_ \
MAKE_INIT_FN__ \
key_from_string \
add_ \
subtract_ \
multiply_ \
divide_ \
equals_ \
less_than_ \
negate_ \
ifcopenshell::geom::utils::create_cube \
ifcopenshell::geom::utils::create_polyhedron \
ifcopenshell::geom::utils::create_nef_polyhedron
# The EXAMPLE_PATH tag can be used to specify one or more files or directories
# that contain example code fragments that are included (see the \include
@@ -1236,7 +1261,7 @@ IGNORE_PREFIX =
# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
# The default value is: YES.
GENERATE_HTML = YES
GENERATE_HTML = NO
# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
@@ -1311,7 +1336,7 @@ HTML_STYLESHEET =
# documentation.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_EXTRA_STYLESHEET = assets/doxygen-awesome-css/doxygen-awesome.css
HTML_EXTRA_STYLESHEET =
# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
# other source files which should be copied to the HTML output directory. Note
@@ -2166,7 +2191,7 @@ MAN_LINKS = NO
# captures the structure of the code including all documentation.
# The default value is: NO.
GENERATE_XML = NO
GENERATE_XML = YES
# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
@@ -2303,7 +2328,7 @@ ENABLE_PREPROCESSING = YES
# The default value is: NO.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
MACRO_EXPANSION = NO
MACRO_EXPANSION = YES
# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
# the macro expansion is limited to the macros specified with the PREDEFINED and
@@ -2311,7 +2336,7 @@ MACRO_EXPANSION = NO
# The default value is: NO.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
EXPAND_ONLY_PREDEF = NO
EXPAND_ONLY_PREDEF = YES
# If the SEARCH_INCLUDES tag is set to YES, the include files in the
# INCLUDE_PATH will be searched if a #include is found.
@@ -2344,7 +2369,17 @@ INCLUDE_FILE_PATTERNS =
# recursively expanded use the := operator instead of the = operator.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
PREDEFINED =
PREDEFINED = IFC_PARSE_API= \
IFC_SCHEMA_API= \
IFC_GEOM_API= \
IFC_GEOMLIBRARY_API= \
IFC_GEOMSERIALIZATION_API= \
SERIALIZERS_API= \
"POSTFIX_SCHEMA(name)=name##_Schema" \
"Handle(name):=opencascade::handle<name>" \
kernel_=kernel \
Simplekernel_=Simplekernel \
inline=
# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
# tag can be used to specify a list of macro names that should be expanded. The
@@ -2353,7 +2388,22 @@ PREDEFINED =
# definition found in the source code.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
EXPAND_AS_DEFINED =
EXPAND_AS_DEFINED = kernel_ \
cgal_shape \
cgal_kernel \
cgal_placement \
cgal_point \
cgal_direction \
cgal_vector \
cgal_plane \
cgal_curve \
cgal_wire \
cgal_face \
cgal_polyhedron \
cgal_vertex_descriptor \
cgal_face_descriptor \
create_cube \
create_polyhedron
# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
# remove all references to function-like macros that are alone on a line, have
@@ -2731,15 +2781,6 @@ DOT_GRAPH_MAX_NODES = 50
MAX_DOT_GRAPH_DEPTH = 0
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
# files in one run (i.e. multiple -o and -T options on the command line). This
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
# this, this feature is disabled by default.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_MULTI_TARGETS = NO
# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
# explaining the meaning of the various boxes and arrows in the dot generated
# graphs.
+41 -18
View File
@@ -1,33 +1,56 @@
# IfcOpenShell C++ API documentation
This folder contains the setup to build the IfcOpenShell C++ API documentation from the source code.
This directory contains the Sphinx, Doxygen, Breathe, and Exhale configuration
for the IfcOpenShell C++ API reference. During a Sphinx build, Exhale runs
Doxygen, Breathe consumes the generated XML, and Exhale creates the API pages.
## Prerequisites
- Python 3.10 or newer
- [Doxygen](https://www.doxygen.nl/)
- [Graphviz](https://graphviz.org/)
Install the Python dependencies from this directory:
```shell
python -m pip install -r requirements.txt
```
Both `doxygen` and `dot` must be available on `PATH`. For the standard Windows
install locations, this can be done for the current PowerShell session with:
```powershell
$env:Path = "C:\Program Files\doxygen\bin;C:\Program Files\Graphviz\bin;$env:Path"
```
## Generating the documentation
> Prerequisites:
>
> Make sure to have [Doxygen](https://www.doxygen.nl) and [Graphviz](https://graphviz.org) installed into your `$PATH` variable.
>
> The documentation also use the [doxygen-awesome](https://jothepro.github.io/doxygen-awesome-css) theme as a git submodule.
Build with the command (from within the `/docs/cpp-api` folder):
From this directory, run:
```shell
$ doxygen
python -m sphinx -M html . output -W --keep-going
```
To include the current git commit hash into the build documentation, use the following command:
To include the current Git commit in Doxygen's project metadata, set
`PROJECT_NUMBER` before building. For example, in PowerShell:
```powershell
$env:PROJECT_NUMBER = git rev-parse --short HEAD
python -m sphinx -M html . output -W --keep-going
```
Or in a POSIX shell:
```shell
$ PROJECT_NUMBER=$(git rev-parse --short HEAD) doxygen
PROJECT_NUMBER=$(git rev-parse --short HEAD) python -m sphinx -M html . output -W --keep-going
```
This will extract the current commit hash in short version and sets the propper ENV variable used by doxygen.
Alternatively, configure the main CMake project with
`-DBUILD_DOCUMENTATION=ON` and build the `cpp_api_docs` target.
The generation of the documentation might take a while depending on your systems hardware, as it is configured to generate the Class graphs using .
The generated documentation is written to `output/html/index.html`. The
generated Doxygen XML and Exhale sources are kept under `output/` as build
artifacts.
The resulting documentation is located unter `/cpp-api/output/html` and can be directly accessed with your browser:
```shell
$ open ./output/html/index.html
```
The generated headers under `src/ifcparse/schemas` are intentionally excluded
from this documentation build.
+59
View File
@@ -0,0 +1,59 @@
# This file was generated with the assistance of an AI coding tool.
import warnings
from pathlib import Path
from shutil import rmtree
from sphinx.deprecation import RemovedInSphinx90Warning
warnings.filterwarnings("ignore", category=RemovedInSphinx90Warning, module=r"exhale\.configs")
generated_directories = (
Path(__file__).parent / "output" / "api",
Path(__file__).parent / "output" / "doxygen",
)
for generated_directory in generated_directories:
if generated_directory.is_dir():
rmtree(generated_directory)
project = "IfcOpenShell"
copyright = "2020, IfcOpenShell"
extensions = [
"breathe",
"exhale",
]
primary_domain = "cpp"
highlight_language = "cpp"
html_theme = "alabaster"
breathe_projects = {
"IfcOpenShell": "./output/doxygen/xml",
}
breathe_default_project = "IfcOpenShell"
exhale_args = {
"containmentFolder": "./output/api",
"rootFileName": "library_root.rst",
"rootFileTitle": "IfcOpenShell C++ API",
"doxygenStripFromPath": "../..",
"createTreeView": False,
"exhaleExecutesDoxygen": True,
"exhaleUseDoxyfile": True,
}
cpp_id_attributes = [
"IFC_PARSE_API",
"IFC_SCHEMA_API",
"IFC_GEOM_API",
"IFC_GEOMLIBRARY_API",
"IFC_GEOMSERIALIZATION_API",
"SERIALIZERS_API",
]
exclude_patterns = [
"output/doctrees",
"output/doxygen",
"output/html",
]
+9
View File
@@ -0,0 +1,9 @@
.. This file was generated with the assistance of an AI coding tool.
IfcOpenShell C++ API
====================
.. toctree::
:maxdepth: 2
output/api/library_root
+5
View File
@@ -0,0 +1,5 @@
# This file was generated with the assistance of an AI coding tool.
Sphinx==8.1.3
breathe==4.36.0
exhale==0.3.7
+532 -211
View File
File diff suppressed because it is too large Load Diff
+506
View File
@@ -0,0 +1,506 @@
#!/usr/bin/env -S uv run --script
# /// script
# ///
import argparse
import json
import logging
import os
import platform
import re
import shlex
import shutil
import subprocess
from pathlib import Path
from typing import Literal, NamedTuple
class C:
GREY = "\033[90m"
YELLOW = "\033[33m"
RED = "\033[31m"
RESET = "\033[0m"
class ColorFormatter(logging.Formatter):
COLORS = {
logging.DEBUG: C.GREY,
logging.WARNING: C.YELLOW,
logging.ERROR: C.RED,
}
def format(self, record: logging.LogRecord) -> str:
color = self.COLORS.get(record.levelno, C.RESET)
return f"{color}{super().format(record)}{C.RESET}"
handler = logging.StreamHandler()
handler.setFormatter(ColorFormatter("%(message)s"))
logging.basicConfig(level=logging.INFO, handlers=[handler])
logger = logging.getLogger(__name__)
def run(
*cmd: str,
cwd: Path | None = None,
env: dict[str, str] | None = None,
stderr: int | None = None,
) -> str:
logger.debug(f"$ {shlex.join(cmd)}")
return subprocess.check_output(cmd, cwd=cwd, env=env, stderr=stderr, text=True)
REPO_ROOT = Path(run("git", "-C", str(Path(__file__).parent), "rev-parse", "--show-toplevel").strip())
VERSION = "v" + (REPO_ROOT / "VERSION").read_text().strip()
def get_git_sha() -> str:
sha = os.getenv("GITHUB_SHA") or run("git", "rev-parse", "HEAD", cwd=REPO_ROOT).strip()
return sha[:7]
def is_platform(name: Literal["MAC", "LINUX"]) -> bool:
current = "MAC" if platform.system() == "Darwin" else "LINUX"
return current == name
def get_install_dir(arch_suffix: str) -> Path:
if is_platform("MAC"):
pattern = "Darwin/*/*/install"
else:
if "arm64" in arch_suffix:
pattern = "Linux/aarch64/install"
else:
pattern = "Linux/x86_64/install"
for data in (REPO_ROOT / "build").glob(pattern):
return data
raise Exception("No install dir found")
class RuntimeInfo(NamedTuple):
runtime_dirs: list[Path]
qt_dir: Path | None
def find_qt_dir(install_root: Path, qt6_version: str, qt6_install_root: str | None) -> Path | None:
search_root = Path(qt6_install_root).parent if qt6_install_root else install_root
for qt_candidate in search_root.glob(f"qt6-{qt6_version}-*/{qt6_version}/*"):
if (qt_candidate / "lib").is_dir():
return qt_candidate
return None
def get_runtime_info(install_root: Path, qt6_version: str) -> RuntimeInfo:
install_dirs_path = install_root / "install_dirs.json"
install_dirs: dict[str, str] = json.loads(install_dirs_path.read_text()) if install_dirs_path.is_file() else {}
# Qt is handled separately via `stage_qt_runtime_payload`.
qt6_install_root = None
if "qt6" in install_dirs:
qt6_install_root = install_dirs.pop("qt6")
qt_dir_env = os.getenv("QT_DIR")
qt_dir = Path(qt_dir_env) if qt_dir_env else find_qt_dir(install_root, qt6_version, qt6_install_root)
if ARGS.shared:
dependencies_to_stage = install_dirs.keys()
elif ARGS.occt_shared:
dependencies_to_stage = {"occt"}
else:
return RuntimeInfo([], qt_dir)
runtime_dirs = []
for name in dependencies_to_stage:
runtime_dir = Path(install_dirs[name])
assert "-shared-" in runtime_dir.name, f"Expected a shared build, found: {runtime_dir}"
runtime_dirs.append(runtime_dir)
return RuntimeInfo(runtime_dirs, qt_dir)
def ensure_soname_links(paths: list[Path]) -> None:
"""Ensure that all shared libraries in `paths` are present using their SONAMEs (at least as symlinks)."""
for shared_object in paths:
if not shared_object.is_file():
continue
try:
readelf_output = run("readelf", "-d", str(shared_object))
except subprocess.CalledProcessError:
continue
match = re.search(r"\(SONAME\).*Library soname: \[(.*)\]", readelf_output)
if not match:
continue
soname = match.group(1)
soname_path = shared_object.parent / soname
if soname_path.exists():
continue
soname_path.symlink_to(shared_object.name)
def is_shared_library(path: Path) -> bool:
name = path.name.lower()
return name.endswith((".so", ".dylib", ".dll")) or ".so." in name
def stage_runtime_payload(install_dir: Path, dest: Path, *, include_geometry_writers: bool = True) -> None:
"""Copy all libs from `install_dir/{bin,lib,lib64}` into `dest`."""
runtime_files = []
for runtime_dir_name in ("bin", "lib", "lib64"):
runtime_dir = install_dir / runtime_dir_name
if not runtime_dir.is_dir():
continue
for runtime_file in runtime_dir.rglob("*"):
if not (runtime_file.is_symlink() or runtime_file.is_file()):
continue
if not is_shared_library(runtime_file):
continue
if not include_geometry_writers and runtime_file.name.startswith("ifcopenshell.geometry.writer."):
continue
dest_file = dest / runtime_file.name
# Currently there's an overlap between dependencies installations.
# E.g. libraries from occt are installed to both `ifcopenshell/lib`
# (as part of `ifcopenshell_deploy_qt_runtime`)
# and to `occt-shared/lib`. So we skip previously installed binaries.
if dest_file.exists():
continue
shutil.copy(runtime_file, dest_file, follow_symlinks=False)
runtime_files.append(dest_file)
if not is_platform("MAC"):
ensure_soname_links(runtime_files)
for lib_so in runtime_files:
if lib_so.is_file():
run("patchelf", "--set-rpath", "$ORIGIN", str(lib_so))
def stage_qt_runtime_payload(exe_path: Path, dest: Path, qt_dir: Path | None) -> None:
"""Copy QT libs/plugins from `qt_dir` next to `exe_path`, if it depends on QT."""
def is_so_file(path: Path) -> bool:
return (path.is_file() or path.is_symlink()) and ".so" in path.name
if not qt_dir or not (qt_dir / "lib").is_dir():
return
# Skip executables that don't depend on QT (don't have `libQt6` referenced).
env = os.environ.copy()
env["LD_LIBRARY_PATH"] = f"{qt_dir / 'lib'}:{env.get('LD_LIBRARY_PATH', '')}"
try:
ldd_output = run("ldd", str(exe_path), env=env)
except subprocess.CalledProcessError:
return
if "libQt6" not in ldd_output:
return
# Copy all QT libs to `dest`.
qt_lib_files = []
for lib_file in (qt_dir / "lib").iterdir():
if is_so_file(lib_file):
dest_file = dest / lib_file.name
qt_lib_files.append(dest_file)
# Currently we install some qt libs to `install/ifcopenshell/lib` too,
# so there's a bit of overlap beteen stage_runtime and stage_qt_runtime,
# hence the skip.
if dest_file.exists():
continue
shutil.copy(lib_file, dest_file, follow_symlinks=False)
ensure_soname_links(qt_lib_files)
# Copy QT plugins.
plugins_dir = qt_dir / "plugins"
if plugins_dir.is_dir():
for plugin_file in plugins_dir.rglob("*"):
if not is_so_file(plugin_file):
continue
dest_plugin_file = dest / "plugins" / plugin_file.relative_to(plugins_dir)
dest_plugin_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(plugin_file, dest_plugin_file, follow_symlinks=False)
# Point plugins rpath to `dest`.
dest_plugins_dir = dest / "plugins"
if dest_plugins_dir.is_dir():
for plugin_so in dest_plugins_dir.rglob("*.so*"):
if plugin_so.is_file():
run("patchelf", "--set-rpath", "$ORIGIN/../..:$ORIGIN", str(plugin_so))
# Non-recursive, set rpath only for top-level libs.
for lib_so in qt_lib_files:
if lib_so.is_file():
run("patchelf", "--set-rpath", "$ORIGIN", str(lib_so))
qt_conf_path = dest / "qt.conf"
qt_conf_path.write_text("[Paths]\nPrefix = .\n")
KNOWN_EXCEPTIONS = frozenset(
(
# Optional Qt SQL driver plugins we don't ship the client libs for.
"libqsqlpsql.so",
"libqsqlmysql.so",
"libqsqlmimer.so",
"libqsqlodbc.so",
)
)
def check_runtime_dependencies(package_dir: Path) -> None:
"""Check all binaries in `package_dir` and report if they're still missing dependencies or are static."""
def is_executable_or_so(path: Path) -> bool:
name = path.name
return os.access(path, os.X_OK) or name.endswith(".so") or ".so." in name
missing = False
env = os.environ.copy()
env.pop("LD_LIBRARY_PATH", None)
for binary_file in package_dir.rglob("*"):
if not binary_file.is_file() or not is_executable_or_so(binary_file):
continue
# Skip non-binaries.
try:
run("readelf", "-h", str(binary_file), stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError:
continue
try:
ldd_output = run("ldd", str(binary_file), env=env, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
logger.error(f"ldd failed for {binary_file}")
logger.error(e.output)
missing = True
continue
if "not found" in ldd_output:
is_known = binary_file.name in KNOWN_EXCEPTIONS
log = logger.debug if is_known else logger.warning
log(f"Missing runtime dependencies for {binary_file}")
for line in ldd_output.splitlines():
if "not found" in line:
log(line)
if not is_known:
missing = True
# TODO: should error?
if missing:
logger.warning("Runtime dependency check found issues; continuing packaging.")
def package_python_wrapper(
py_dir: Path,
ifcopenshell_install_dir: Path,
github_sha: str,
output_dir: Path,
arch_suffix: str,
runtime_dirs: list[Path],
) -> None:
logger.info(f"Packaging python wrapper '{py_dir.name}'")
py_version = py_dir.name
postfix = "" if py_version[-1].isdigit() else py_version[-1]
# Match and convert `x.y` -> `xy`.
version_match = re.search(r"[0-9]+\.[0-9]+", py_version)
assert version_match
numbers = "".join(version_match.group().split("."))
py_version_major = f"python-{numbers}{postfix}"
package_dir = ifcopenshell_install_dir / f".package-{py_version_major}"
if package_dir.exists():
# Clean up previous local runs.
shutil.rmtree(package_dir)
package_dir.mkdir(parents=True)
ifcopenshell_dir = package_dir / "ifcopenshell"
ifcopenshell_dir.mkdir()
for item in py_dir.iterdir():
dest = ifcopenshell_dir / item.name
if item.is_dir():
shutil.copytree(item, dest, symlinks=True)
else:
shutil.copy(item, dest, follow_symlinks=False)
if not is_platform("MAC"):
for lib_so in ifcopenshell_dir.glob("*.so*"):
if lib_so.is_file():
run("patchelf", "--set-rpath", "$ORIGIN", str(lib_so))
# Cache from test run during build.
pycache_dir = ifcopenshell_dir / "__pycache__"
if pycache_dir.is_dir():
shutil.rmtree(pycache_dir)
for pyc_file in ifcopenshell_dir.rglob("*.pyc"):
pyc_file.unlink()
# TODO: packs qt libs also?
stage_runtime_payload(ifcopenshell_install_dir, ifcopenshell_dir)
for runtime_dir in runtime_dirs:
stage_runtime_payload(runtime_dir, ifcopenshell_dir)
if not is_platform("MAC"):
check_runtime_dependencies(ifcopenshell_dir)
zip_path = output_dir / f"ifcopenshell-{py_version_major}-{VERSION}-{github_sha}-{arch_suffix}.zip"
run("zip", "-y", "-r", "-qq", "-1", str(zip_path), "ifcopenshell", cwd=package_dir)
shutil.rmtree(package_dir)
def is_packageable_executable(path: Path) -> bool:
if not path.is_file() or not os.access(path, os.X_OK):
return False
return not (path.name.lower().endswith(".zip") or is_shared_library(path))
def package_executable(
exe_path: Path,
ifcopenshell_install_dir: Path,
github_sha: str,
output_dir: Path,
autodesk_connector_dir: Path,
qt_dir: Path | None,
runtime_dirs: list[Path],
arch_suffix: str,
) -> None:
exe = exe_path.name
logger.info(f"Packaging executable '{exe}'")
package_dir = ifcopenshell_install_dir / f".package-{exe}"
if package_dir.exists():
# Clean up previous local runs.
shutil.rmtree(package_dir)
package_dir.mkdir(parents=True)
shutil.copy(exe_path, package_dir / exe)
# TODO: kept `is_platform(MAC)` to retain original bash script behaviour,
# but is this guard needed or it should be always False?
stage_runtime_payload(ifcopenshell_install_dir, package_dir, include_geometry_writers=is_platform("MAC"))
for runtime_dir in runtime_dirs:
stage_runtime_payload(runtime_dir, package_dir)
# On macOS, rpath is already set at build time via CMake's INSTALL_RPATH, and
# QT apps are packaged as .app bundles (`package_app_bundle`) instead.
if not is_platform("MAC"):
run("patchelf", "--set-rpath", "$ORIGIN", str(package_dir / exe))
stage_qt_runtime_payload(exe_path, package_dir, qt_dir)
if exe == "BonsaiViewer":
connectors_dir = package_dir / "connectors"
connectors_dir.mkdir()
shutil.copytree(autodesk_connector_dir, connectors_dir / autodesk_connector_dir.name, symlinks=True)
check_runtime_dependencies(package_dir)
zip_path = output_dir / f"{exe}-{VERSION}-{github_sha}-{arch_suffix}.zip"
run("zip", "-y", "-qq", "-r", str(zip_path), ".", cwd=package_dir)
shutil.rmtree(package_dir)
def package_app_bundle(
app_path: Path,
install_root: Path,
github_sha: str,
output_dir: Path,
autodesk_connector_dir: Path,
arch_suffix: str,
) -> None:
"""Zip a `.app` bundle (e.g. BonsaiViewer.app) living at the install-prefix root.
Their install rule uses `BUNDLE DESTINATION "."` - that's the layout Qt's
macdeployqt expects. macdeployqt has already embedded the Qt frameworks
inside each bundle during install/strip, so the only thing left to stage
is the connector.
"""
app = app_path.stem
logger.info(f"Packaging app bundle '{app}'")
if app == "BonsaiViewer":
# ConnectorDiscovery looks in applicationDirPath()/connectors,
# which for a bundle is Contents/MacOS.
connectors_dir = app_path / "Contents" / "MacOS" / "connectors"
connectors_dir.mkdir(parents=True)
shutil.copytree(autodesk_connector_dir, connectors_dir / autodesk_connector_dir.name, symlinks=True)
zip_path = output_dir / f"{app}-{VERSION}-{github_sha}-{arch_suffix}.zip"
run("zip", "-qq", "-r", str(zip_path), app_path.name, cwd=install_root)
ARCH_SUFFIXES = ("linux64", "linuxarm64", "macosm164")
LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR")
class Args(NamedTuple):
arch_suffix: str
log_level: str
occt_shared: bool
shared: bool
ARGS: Args
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("arch_suffix", choices=ARCH_SUFFIXES, help="Zip filename suffix.")
# TODO: relax default to INFO once things get more stable.
parser.add_argument("--log-level", default="DEBUG", choices=LOG_LEVELS, help="Logging verbosity.")
parser.add_argument("--occt-shared", action="store_true", help="OCCT was built as shared libraries.")
parser.add_argument("--shared", action="store_true", help="Build was made with shared libraries.")
args = parser.parse_args()
global ARGS
ARGS = Args(
arch_suffix=args.arch_suffix,
log_level=args.log_level,
occt_shared=args.occt_shared,
shared=args.shared,
)
logger.setLevel(ARGS.log_level)
# bonsaiviewer-autodesk is now a Rust connector. packaging/build.py
# invokes `cargo build --release` and stages the binary +
# connector.json into dist/autodesk/. Same on-disk shape as the
# old PyInstaller flow so the symlink + zip steps below
# continue to work unchanged.
run("uv", "run", str(REPO_ROOT / "src/bonsaiviewer-autodesk/packaging/build.py"))
autodesk_connector_dir = REPO_ROOT / "src/bonsaiviewer-autodesk/dist/autodesk"
assert autodesk_connector_dir.is_dir()
# Locate the ifcopenshell install dir and stage QT6 alongside the zip output.
install_root = get_install_dir(ARGS.arch_suffix)
ifcopenshell_install_dir = install_root / "ifcopenshell"
output_dir = Path.home() / "output"
output_dir.mkdir(parents=True, exist_ok=True)
qt6_version = os.getenv("QT6_VERSION", "6.8.3")
runtime_dirs, qt_dir = get_runtime_info(install_root, qt6_version)
# Iterate over all built Python wrappers in `install/ifcopenshell/python-x.y.z`
# and zip them, bundling all dynamic libs from `lib`.
github_sha = get_git_sha()
for py_dir in sorted(ifcopenshell_install_dir.glob("python-*")):
package_python_wrapper(py_dir, ifcopenshell_install_dir, github_sha, output_dir, ARGS.arch_suffix, runtime_dirs)
# Iterate over all executables in `install/ifcopenshell/bin` and zip them.
# Each zip bundles dynamic libs from `lib` and also qt libs.
bin_dir = ifcopenshell_install_dir / "bin"
for exe_path in sorted(bin_dir.iterdir()):
if is_packageable_executable(exe_path):
package_executable(
exe_path,
ifcopenshell_install_dir,
github_sha,
output_dir,
autodesk_connector_dir,
qt_dir,
runtime_dirs,
ARGS.arch_suffix,
)
if is_platform("MAC"):
for app_path in sorted(install_root.glob("*.app")):
package_app_bundle(app_path, install_root, github_sha, output_dir, autodesk_connector_dir, ARGS.arch_suffix)
if __name__ == "__main__":
main()
+18 -4
View File
@@ -1,3 +1,13 @@
# Removing use of `tr1` namespace that might not be available on some systems.
#
# Current status on different systems:
# - msvc - removed `tr1` namespace in 14.51 (`_MSC_VER == 1951`)
# - gcc (with libstdc++) - currently neither deprecated nor removed, though there are plans to
# - clang (with libc++) - never had it
#
# One of the hunks in the patch is patching `_MSC_VER == 1500`, so it's not stricly needed,
# but kept it just so it will be easy to check the absense of any `tr1` use.
diff --git a/COLLADABaseUtils/include/COLLADABUhash_map.h b/COLLADABaseUtils/include/COLLADABUhash_map.h
index 8ab0fb9b..12503bfb 100644
--- a/COLLADABaseUtils/include/COLLADABUhash_map.h
@@ -27,11 +37,13 @@ index 8ab0fb9b..12503bfb 100644
- #define COLLADABU_HASH_MAP std::tr1::unordered_map
- #define COLLADABU_HASH_MULTIMAP std::tr1::unordered_multimap
- #define COLLADABU_HASH_SET std::tr1::unordered_set
- #define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
- #define COLLADABU_HASH_NAMESPACE_CLOSE }
+ #define COLLADABU_HASH_MAP std::unordered_map
+ #define COLLADABU_HASH_MULTIMAP std::unordered_multimap
+ #define COLLADABU_HASH_SET std::unordered_set
#define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
#define COLLADABU_HASH_NAMESPACE_CLOSE }
+ #define COLLADABU_HASH_NAMESPACE_OPEN std
+ #define COLLADABU_HASH_NAMESPACE_CLOSE
#define COLLADABU_HASH_FUN hash
@@ -107,12 +107,12 @@
#define COLLADABU_HASH_NAMESPACE_CLOSE
@@ -45,11 +57,13 @@ index 8ab0fb9b..12503bfb 100644
- #define COLLADABU_HASH_MAP std::tr1::unordered_map
- #define COLLADABU_HASH_MULTIMAP std::tr1::unordered_multimap
- #define COLLADABU_HASH_SET std::tr1::unordered_set
- #define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
- #define COLLADABU_HASH_NAMESPACE_CLOSE }
+ #define COLLADABU_HASH_MAP std::unordered_map
+ #define COLLADABU_HASH_MULTIMAP std::unordered_multimap
+ #define COLLADABU_HASH_SET std::unordered_set
#define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
#define COLLADABU_HASH_NAMESPACE_CLOSE }
+ #define COLLADABU_HASH_NAMESPACE_OPEN std
+ #define COLLADABU_HASH_NAMESPACE_CLOSE
#define COLLADABU_HASH_FUN hash
diff --git a/common/libBuffer/include/CommonFWriteBufferFlusher.h b/common/libBuffer/include/CommonFWriteBufferFlusher.h
index c7af45b2..fac4f133 100644
+10 -2
View File
@@ -28,5 +28,13 @@ since it's pure cmake without any additional moving parts.
- clone IfcOpenShell repo next to it to `IfcOpenShell` folder
- run `python nix/build-all.py -wasm -py-313` in `IfcOpenShell`
- it will produce Python package in `IfcOpenShell/ifcopenshell`
- run `pyodide build`
- it will produce a wheel in `IfcOpenShell/dist`
- run `python pyodide/build-all-pack-wheel-local.py`, it will
- clean up previous wheels
- run `pyodide build`
- prepare standalone and modular wheels
- produce final wheels in `IfcOpenShell/dist` and `IfcOpenshell/dist-modular`
- testing:
- ensure you're in pyodide environment
- `cd IfcOpenshell/pyodide`
- `./run_pytest.py setup`
- `./run_pytest.py run`
View File
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Intended to be run after nix/build-all.py has finished the wasm build."""
import shutil
import subprocess
from pathlib import Path
def get_repo_root() -> Path:
output = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True)
return Path(output.strip())
def run(cmd: list[str], **kwargs) -> None:
print("$", " ".join(cmd))
subprocess.check_call(cmd, **kwargs)
def main() -> None:
repo_root = get_repo_root()
shutil.rmtree(repo_root / "dist", ignore_errors=True)
shutil.rmtree(repo_root / "dist_modular", ignore_errors=True)
run(["pyodide", "build"], cwd=repo_root)
shutil.rmtree(repo_root / "ifcopenshell", ignore_errors=True)
(repo_root / "setup.py").unlink(missing_ok=True)
run(["git", "restore", "pyproject.toml"], cwd=repo_root)
wheel = next((repo_root / "dist").glob("ifcopenshell-*.whl"))
run(["uv", "run", "pyodide/order_pyodide_wheel_shared_objects.py", str(wheel)], cwd=repo_root)
run(
["uv", "run", "pyodide/split_pyodide_ifcopenshell_wheel.py", str(wheel), "dist-modular/"],
cwd=repo_root,
)
if __name__ == "__main__":
main()
+9 -7
View File
@@ -1,10 +1,8 @@
#!/usr/bin/bash
set -ex
PYODIDE_VERSION=0.29.3
PYODIDE_BUILD_VERSION=0.33.0
PYODIDE_XBUILDENV_ROOT="${HOME}/.cache/.pyodide-xbuildenv-${PYODIDE_BUILD_VERSION}"
PYODIDE_XBUILDENV="${PYODIDE_XBUILDENV_ROOT}/${PYODIDE_VERSION}"
PYODIDE_VERSION=0.29.4
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Script is assuming that it will be possible to execute it multiple times
# therefore we're clearing venv each time and ignoring existing 'emsdk' folder.
@@ -16,12 +14,14 @@ source .venv/bin/activate
# Install pyodide cross build environment.
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
uv pip install "pyodide-build==${PYODIDE_BUILD_VERSION}"
uv pip install -r "${SCRIPT_DIR}/requirements.txt"
# `uv run` is required, so xbuildenv would skip using `pip`.
uv run pyodide xbuildenv install "${PYODIDE_VERSION}"
uv run pyodide xbuildenv install-emscripten
EMSDK_ROOT="${PYODIDE_XBUILDENV}/emsdk"
# Cache path includes a hash segment that varies by pyodide-build version,
# so query it instead of constructing it manually.
EMSDK_ROOT=$(uv run pyodide config get emsdk_dir)
[ -f "${EMSDK_ROOT}/emsdk_env.sh" ] && source "${EMSDK_ROOT}/emsdk_env.sh"
[ -f "${EMSDK_ROOT}/../../emsdk_env.sh" ] && source "${EMSDK_ROOT}/../../emsdk_env.sh"
which emcc
@@ -29,8 +29,10 @@ emcc --version
mkdir -p packages/ifcopenshell
VERSION=`cat IfcOpenShell/VERSION`
# Normalize to the canonical PEP 440 form (e.g. 0.9.0alpha0 -> 0.9.0a0).
VERSION=`python3 -c "from packaging.version import Version; print(Version('$VERSION'))"`
cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell
sed -i s/0.8.0/$VERSION/g packages/ifcopenshell/meta.yaml
sed -i s/9.9.9/$VERSION/g packages/ifcopenshell/meta.yaml
# Use custom build ifcopenshell directory in build-all to make caching simpler
# Otherwise pyodide build path typically includes package version, so cached cmake configs might break.
+3 -2
View File
@@ -1,6 +1,7 @@
package:
name: ifcopenshell
version: 0.8.0
# Placeholder, replaced by build_pyodide.sh with the actual version from VERSION file.
version: 9.9.9
source:
# meta.yaml is placed as `packages/ifcopenshell/meta.yaml`.
@@ -8,7 +9,7 @@ source:
build:
script: |
BUILD_CFG=Release python nix/build-all.py -v --wasm --py313
BUILD_CFG=Release python nix/build-all.py -v --wasm
about:
home: http://ifcopenshell.org
+17 -5
View File
@@ -1,6 +1,18 @@
#!/usr/bin/env python3
# /// script
# ///
# This file was generated with the assistance of an AI coding tool.
"""Order Pyodide wheel shared objects so wasm side modules load safely."""
"""Order Pyodide wheel shared objects so wasm side modules load safely.
Pyodide's package loader loads a wheel's bundled ``.so`` files in the order
they appear in the wheel's zip.
If a ``.so`` that depends on symbols from another ``.so`` is loaded first,
loading fails with errors like
- "Failed to load dynamic library"
- "Dynamic linking error: cannot resolve symbol"
This is a known issue upstream - https://github.com/pyodide/pyodide/issues/6020.
"""
from __future__ import annotations
@@ -22,10 +34,10 @@ SCHEMA_ORDER = {
}
MAIN_SHARED_OBJECT_RE = re.compile(r"^_ifcopenshell_wrapper(?:\.|$)")
SCHEMA_PLUGIN_RE = re.compile(r"^ifcopenshell\.parse\.schema\.([^.]+)\.so$")
MAPPING_PLUGIN_RE = re.compile(r"^ifcopenshell\.geometry\.mapping\.([^.]+)\.so$")
DOCUMENT_PLUGIN_RE = re.compile(r"^ifcopenshell\.document\.[^.]+\.([^.]+)\.so$")
GEOMETRY_SERIALIZATION_PLUGIN_RE = re.compile(r"^ifcopenshell\.geometry\.serialization\.([^.]+)\.so$")
SCHEMA_PLUGIN_RE = re.compile(r"^ifcopenshell_parse_schema_(.+)\.so$")
MAPPING_PLUGIN_RE = re.compile(r"^ifcopenshell_geometry_mapping_(.+)\.so$")
DOCUMENT_PLUGIN_RE = re.compile(r"^ifcopenshell_document_[a-z0-9]+(?:_(.+))?\.so$")
GEOMETRY_SERIALIZATION_PLUGIN_RE = re.compile(r"^ifcopenshell_geometry_writer_(.+)\.so$")
def schema_key(schema: str) -> tuple[int, str]:
+1
View File
@@ -0,0 +1 @@
pyodide-build==0.39.0
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
import argparse
import shlex
import shutil
import subprocess
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent
DIST_DIRS = (
SCRIPT_DIR / "test/pyodide",
SCRIPT_DIR / "test/pyodide-modular",
)
WHEEL_SRCS = (
SCRIPT_DIR / "../dist",
SCRIPT_DIR / "../dist-modular",
)
def run(cmd: list, **kwargs) -> None:
print("$", shlex.join(str(part) for part in cmd))
subprocess.check_call(cmd, **kwargs)
def setup() -> None:
run(["uv", "pip", "install", "pytest-pyodide"])
# Copy pyodide installation so we can modify it locally just for tests.
pyodide_root = subprocess.check_output(["pyodide", "config", "get", "pyodide_root"], text=True).strip()
pyodide_root_dist = Path(pyodide_root) / "dist"
for dist_dir in DIST_DIRS:
if dist_dir.exists():
shutil.rmtree(dist_dir)
shutil.copytree(pyodide_root_dist, dist_dir)
def run_tests() -> None:
for dist_dir, wheel_src in zip(DIST_DIRS, WHEEL_SRCS):
if not wheel_src.exists():
raise RuntimeError(f"error: {wheel_src} does not exist")
# Clean up previous wheels.
for whl in dist_dir.glob("ifcopenshell*.whl"):
whl.unlink()
# Symlink new ones.
for whl in wheel_src.glob("ifcopenshell*.whl"):
(dist_dir / whl.name).symlink_to(whl.resolve())
for dist_dir in DIST_DIRS:
run(["pytest", f"--dist-dir={dist_dir}", "--capture=no"], cwd=SCRIPT_DIR)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("command", choices=["setup", "run"])
args = parser.parse_args()
if args.command == "setup":
setup()
else:
run_tests()
+16 -1
View File
@@ -1,5 +1,19 @@
#!/usr/bin/env python3
"""Split optional IfcOpenShell Pyodide payloads into separate wheels."""
# /// script
# ///
"""Split optional IfcOpenShell Pyodide payloads into separate wheels.
The main wheel bundles per-schema plugin ``.so`` files and pure Python
subpackages that most browser sessions probably don't need.
This splits each of those out into its own installable wheel,
so a Pyodide app can fetch just the base wheel plus whichever schema/plugin wheels it actually needs.
Resulting wheels (roughly):
- ifcopenshell.whl (main ifcopenshell.py files + _ifcopenshell_wrapper)
- ifcopenshell_pure_python.whl (api, express, python files only)
- splitted wheels with a single .so binary - e.g. `ifcopenshell_parse_schema_ifc4.whl`
"""
from __future__ import annotations
@@ -159,6 +173,7 @@ def build_wheel(
entries[record_name] = None
write_record(zf, entries, record_name)
print(f"Splitting wheel to '{out}'.")
return out
+2
View File
@@ -0,0 +1,2 @@
pyodide
pyodide-modular
View File
+22 -11
View File
@@ -1,18 +1,30 @@
import zipfile
from pathlib import Path
WHEEL_FILENAME = next(
p.name for p in (Path.cwd() / "pyodide").iterdir() if p.name.startswith("ifcopenshell-") and p.suffix == ".whl"
)
from ..order_pyodide_wheel_shared_objects import shared_object_sort_key
def test_ifcopenshell_import(selenium):
def _first_so_name(wheel_path: Path) -> str:
with zipfile.ZipFile(wheel_path) as zf:
for name in zf.namelist():
if name.endswith(".so"):
return Path(name).name
return wheel_path.name
def test_ifcopenshell_import(selenium, request):
dist_dir = Path(request.config.getoption("--dist-dir"))
wheel_paths = list(dist_dir.glob("ifcopenshell*.whl"))
wheel_paths.sort(key=lambda path: shared_object_sort_key(_first_so_name(path), 0))
WHEEL_NAMES = tuple(path.name for path in wheel_paths)
selenium.load_package("micropip")
# Important to test it with `micropip.install`
# without any dependencies loaded to ensure micropip will load them automatically.
selenium.run_async(
f"""
selenium.run_async(f"""
import micropip
await micropip.install(f"./{WHEEL_FILENAME}")
wheel_filenames = {WHEEL_NAMES!r}
for wheel_filename in wheel_filenames:
print(f"Loading {{wheel_filename}}...")
await micropip.install(f"./{{wheel_filename}}")
import ifcopenshell
from pathlib import Path
ifcopenshell.set_plugin_search_paths([str(Path(ifcopenshell.__file__).parent)])
@@ -24,5 +36,4 @@ def test_ifcopenshell_import(selenium):
wall.Name = "Test"
assert wall.Name == "Test", f"Entity name wasn't changed: {{wall}}"
print(wall)
"""
)
""")
+36 -14
View File
@@ -6,10 +6,6 @@ version = "0.0.0"
[tool.black]
line-length = 120
include = '''
src/.*.pyi?$
|nix/.*.pyi?$
'''
extend-exclude = '''
src/ifcopenshell-python/ifcopenshell/express/rules/*
|src/ifcopenshell-python/ifcopenshell/express/express_parser.py
@@ -18,6 +14,15 @@ extend-exclude = '''
|src/ifc2ca/templates/*
|src/svgfill
|src/exterior-shell-extractor
|choco/bonsai/tools/enable_blenderbim_addon.py
|choco/bonsai/tools/disable_blenderbim_addon.py
|docs/conf.py
|docs/generate_docs.py
|aws/lambda/example_handler/__init__.py
|conda/update_version_init.py
|test/bpy.py
|test/tests.py
|test/run.py
'''
[tool.pyright]
@@ -62,20 +67,30 @@ select = [
#
"FA", # future annotations
"UP", # pyupgrade
"RUF015", # next() > list_comprehension[0]
"RUF022", # sort __all__
"unnecessary-iterable-allocation-for-first-element",
"unsorted-dunder-all",
"I", # import sorting
"unused-noqa",
"rule-codes-in-selectors",
"noqa-comments",
"rule-codes-in-suppression-comments",
# General util rules.
"invalid-rule-code",
"redirected-noqa",
"invalid-pyproject-toml",
"invalid-suppression-comment",
]
ignore = [
"FA100", # Conflicts with Blender using annotations for props definitions.
# Conflicts with Blender using annotations for props definitions.
"future-rewritable-type-annotation",
# Maybe will enable later:
"UP007", # Optional to X | Y
"UP045", # Optional to X | None
"UP015", # Unnecessary mode argument
"UP028", # yield for -> yield from
"UP030", # implicit references for positional format fields
"UP031", # Replace % with .format
"UP032", # Replace .format with f-string
"non-pep604-annotation-union", # Union[X,Y] to X | Y
"non-pep604-annotation-optional", # Optional to X | None
"redundant-open-modes", # Unnecessary mode argument
"yield-in-for-loop", # yield for -> yield from
"format-literals", # implicit references for positional format fields
"printf-string-formatting", # Replace % with .format
"f-string", # Replace .format with f-string
]
[tool.ty.rules]
@@ -98,10 +113,13 @@ unresolved-attribute = "ignore"
invalid-argument-type = "ignore"
invalid-method-override = "ignore"
invalid-assignment = "ignore"
unsound-assignment = "ignore"
invalid-parameter-default = "ignore"
missing-override-decorator = "ignore"
invalid-yield = "ignore"
unsound-yield = "ignore"
invalid-return-type = "ignore"
unsound-return-statement = "ignore"
non-callable-init-subclass = "ignore"
not-iterable = "ignore"
possibly-missing-attribute = "ignore"
@@ -109,6 +127,8 @@ no-matching-overload = "ignore"
not-subscriptable = "ignore"
unsupported-dynamic-base = "ignore"
unsupported-operator = "ignore"
# `@persistent` is incorrectly annotated as `Any` in fake-bpy, needs to be resolved upstream.
dynamic-function-decorator-return = "ignore"
[tool.ty.environment]
extra-paths = [
@@ -170,6 +190,8 @@ dev-setup.help = "Install repo packages in editable mode"
ruff = "ruff check"
check-whitespace = "uv run .github/scripts/check-whitespace.py"
black = "black ."
ty.sequence = ["ty-bonsai", "ty-ios"]
+3 -3
View File
@@ -1,5 +1,5 @@
black==26.3.1
ruff==0.16.0
black==26.5.1
ruff==0.16.4
poethepoet
ty==0.0.63
ty==0.0.74
gersemi==0.28.0
+3 -3
View File
@@ -10,7 +10,7 @@ name = "bcf-client"
# author = "IfcOpenShell"
description = "BCF-XML file handler."
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.10"
keywords = ["IFC", "BCF", "BIM"]
dependencies = [
"xsdata>=24.4",
@@ -65,6 +65,6 @@ commands = pytest --cov --cov-report=term tests
[tool.ruff]
extend = "../../pyproject.toml"
lint.select = [
"F401", # unused imports
lint.extend-select = [
"unused-import", # unused imports
]
+17 -34
View File
@@ -42,10 +42,12 @@ endif
IS_STABLE:=FALSE
VERSION:=$(shell cat ../../VERSION)
VERSION_MAJOR:=$(shell cat '../../VERSION' | cut -d '.' -f 1)
VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2)
VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
VERSION_BASE:=$(shell sed -E 's/[[:alpha:]]+[0-9]+$$//' ../../VERSION)
VERSION_PYTHON:=$(shell sed 's/alpha/a/' ../../VERSION)
VERSION_SEMVER:=$(shell sed -E 's/([[:alpha:]]+)([0-9]+)$$/-\\1\\2/' ../../VERSION)
VERSION_DATE:=$(shell date '+%y%m%d')
VERSION_DAILY:=$(VERSION_BASE)a$(VERSION_DATE)
VERSION_SEMVER_DAILY:=$(VERSION_BASE)-alpha$(VERSION_DATE)
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
LAST_GIT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD)
@@ -67,32 +69,17 @@ endif # def PYVERSION
IFCMERGE_VERSION:=2026-04-07
ifdef PLATFORM
SUPPORTED_PLATFORMS := linux macos macosm1 win
SUPPORTED_PLATFORMS := linux macosm1 win
ifeq ($(filter $(PLATFORM),$(SUPPORTED_PLATFORMS)),)
$(error Unsupported PLATFORM=$(PLATFORM). Must be one of $(SUPPORTED_PLATFORMS))
endif
ifeq ($(PLATFORM),macos)
ifeq ($(PYVERSION),py313)
$(error Blender 5.1 with Python 3.13 doesn't support intel macOS.)
endif
endif
ifeq ($(PLATFORM), linux)
PYPI_PLATFORM:=--platform manylinux_2_17_x86_64
BLENDER_PLATFORM:=linux-x64
endif
ifeq ($(PLATFORM), macos)
ifeq ($(PYVERSION), py311)
PYPI_PLATFORM:=--platform macosx_10_10_x86_64
else
PYPI_PLATFORM:=--platform macosx_10_13_x86_64
endif
BLENDER_PLATFORM:=macos-x64
endif
ifeq ($(PLATFORM), macosm1)
PYPI_PLATFORM:=--platform macosx_11_0_arm64
BLENDER_PLATFORM:=macos-arm64
@@ -106,7 +93,7 @@ endif
endif # def PLATFORM
# Current build commit hash.
OLD:=3e7b739
OLD:=ad113e1
.PHONY: bump
bump:
ifndef NEW
@@ -192,10 +179,8 @@ endif
# Provides networkx graph analysis for project dependency calculations
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download networkx --dest=./wheels
# Required by IFCDiff
# Pinned <9.1: deepdiff 9.1.0 adds cachebox<6,>=5.2 which only ships macOS x86_64
# wheels for macosx_10_12+ and is incompatible with our macos py311 --platform
# macosx_10_10_x86_64 target. Revisit once the macos py311 platform tag is bumped
# to 10_13 (matching py312/py313).
# Pinned <9.1: deepdiff 9.1.0 adds the compiled dependency cachebox<6,>=5.2,
# which this platformless download cannot provide for every target platform.
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download "deepdiff<9.1" --dest=./wheels
# Required by IFCCSV and ifcopenshell.util.selector
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download lark --dest=./wheels
@@ -213,8 +198,6 @@ endif
# pyradiance is using different platform versions than defaults in our makefile.
ifeq ($(PLATFORM), linux)
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance --platform manylinux_2_28_x86_64 --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
else ifeq ($(PLATFORM), macos)
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance --platform macosx_10_13_x86_64 --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
else
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
endif
@@ -260,14 +243,14 @@ endif
cp pyproject.toml build/
ifeq ($(IS_STABLE), TRUE)
$(SED) "s/0.0.0/$(VERSION)/" build/bonsai/blender_manifest.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
$(SED) "s/0.0.0/$(VERSION_SEMVER)/" build/bonsai/blender_manifest.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/pyproject.toml
else
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
$(SED) "s/0.0.0/$(VERSION_SEMVER_DAILY)/" build/bonsai/blender_manifest.toml
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
$(SED) "s/7777777/$(LAST_GIT_BRANCH)/" build/bonsai/__init__.py
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/pyproject.toml
endif
# Blender 5.1+ requires Python 3.13.
@@ -279,9 +262,9 @@ endif
# Provides bonsai Add-on functionality
ifeq ($(IS_STABLE), TRUE)
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/pyproject.toml
else
$(SED) 's/version = "0.0.0"/version = "$(VERSION)a$(VERSION_DATE)"/' build/pyproject.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/pyproject.toml
endif
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m build
cp build/dist/*.whl build/wheels/
@@ -315,9 +298,9 @@ endif
rm -rf build/bonsai/libs/
ifeq ($(IS_STABLE), TRUE)
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-$(BLENDER_PLATFORM).zip ./bonsai
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION_SEMVER)-$(BLENDER_PLATFORM).zip ./bonsai
else
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-alpha$(VERSION_DATE)-$(BLENDER_PLATFORM).zip ./bonsai
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION_SEMVER_DAILY)-$(BLENDER_PLATFORM).zip ./bonsai
endif
mv build/bonsai*.zip dist/
+3 -8
View File
@@ -185,13 +185,10 @@ class IfcStore:
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
IfcStore.cache_path = cache_path
cache_path = Path(IfcStore.cache_path)
cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
settings = ifcopenshell.geom.settings()
cache_preexists = cache_path.exists()
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
IfcStore.cache_path, cache_settings, serializer_settings
)
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, settings)
if cache_preexists:
print(f"Successfully loaded existing cache: {cache_path.name}.")
else:
@@ -206,9 +203,7 @@ class IfcStore:
os.remove(IfcStore.cache_path)
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
IfcStore.cache_path, cache_settings, serializer_settings
)
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, settings)
print("New cache was created.")
except Exception as e:
print(f"Failed to create a cache: {str(e)}.")
+5 -5
View File
@@ -740,7 +740,7 @@ class IfcImporter:
self.update_progress((percent_average / 100 * progress_range) + start_progress)
shape = iterator.get()
if shape:
assert isinstance(shape, W.TriangulationElement)
assert isinstance(shape, W.triangulation_element)
product = self.file.by_id(shape.id)
self.create_product(product, shape)
results.add(product)
@@ -1079,9 +1079,9 @@ class IfcImporter:
def create_curve(
self,
element: ifcopenshell.entity_instance,
shape: Union[W.Triangulation, W.TriangulationElement],
shape: Union[W.triangulation, W.triangulation_element],
) -> bpy.types.Curve:
if isinstance(shape, W.TriangulationElement):
if isinstance(shape, W.triangulation_element):
geometry = shape.geometry
else:
geometry = shape
@@ -1112,11 +1112,11 @@ class IfcImporter:
def create_mesh(
self,
element: ifcopenshell.entity_instance,
shape: Union[W.Triangulation, W.TriangulationElement],
shape: Union[W.triangulation, W.triangulation_element],
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
) -> Union[bpy.types.Mesh, None]:
try:
if isinstance(shape, W.TriangulationElement):
if isinstance(shape, W.triangulation_element):
# shape is ShapeElementType
geometry = shape.geometry
else:
@@ -678,7 +678,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# Identify all potential building elements
# TODO: don't select everything, use AABB culling in Blender
building_elements = (
building_elements = list(
tool.Ifc.get().by_type("IfcWall")
+ tool.Ifc.get().by_type("IfcSlab")
+ tool.Ifc.get().by_type("IfcVirtualElement")
@@ -708,7 +708,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
while True:
tree.add_element(iterator.get_native())
shape = iterator.get()
assert isinstance(shape, W.TriangulationElement)
assert isinstance(shape, W.triangulation_element)
shapes[shape.id] = {
"verts": ifcopenshell.util.shape.get_vertices(shape.geometry),
"faces": ifcopenshell.util.shape.get_faces(shape.geometry),
@@ -348,10 +348,7 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
if self.obj_type == "Object":
if context.selected_objects:
objects = [o.name for o in context.selected_objects]
else:
objects = [context.active_object.name]
objects = [o.name for o in tool.Blender.get_selected_objects()]
else:
objects = [self.obj]
props = tool.Classification.get_classification_props()
@@ -516,7 +516,7 @@ def _world_segment_to_screen_pixels(
# ---------------------------------------------------------------------------
class BIM_GT_box_face_quad(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname convention
class BIM_GT_box_face_quad(bpy.types.Gizmo):
"""Near-invisible face-quad click target with drag-to-resize modal.
Geometry: a unit quad in the local XY plane at z=0. The adapter
@@ -620,7 +620,7 @@ class BIM_GT_box_face_quad(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname
return {"RUNNING_MODAL"}
class BIM_GT_box_face_outline(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname convention
class BIM_GT_box_face_outline(bpy.types.Gizmo):
"""Thin non-interactive colored edge outline for one face.
Drawn as 4 line segments in the face plane. The layout helper
@@ -160,7 +160,7 @@ def _make_face_set_cb(gz: Any, group: Any, axis: int, is_max: bool):
return setter
class OBJECT_GGT_bim_clip_box(bpy.types.GizmoGroup): # noqa: N801 — Blender bl_idname convention
class OBJECT_GGT_bim_clip_box(bpy.types.GizmoGroup):
"""Face-quad resize handles on the active clip box.
Renders six near-invisible click-target quads and six colored edge
@@ -987,7 +987,7 @@ class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper):
@classmethod
def poll(cls, context):
try:
import typst # noqa: F401
import typst # ruff: ignore[unused-import]
return True
except ModuleNotFoundError:
@@ -313,7 +313,7 @@ class CreateAllShapes(bpy.types.Operator):
failures.append(element)
print("***** FAILURE *****")
if shape:
assert isinstance(shape, W.TriangulationElement)
assert isinstance(shape, W.triangulation_element)
geom = shape.geometry
print(
f"Success {time.time() - start:.3f}s "
@@ -28,7 +28,7 @@ operators via ``target_set_operator``; drag handles inherit modal state
from ``GizmoMovable``.
"""
__all__ = [ # noqa: RUF022 (unsorted `__all__`)
__all__ = [ # ruff: ignore[unsorted-dunder-all]
"GizmoColor",
"GizmoAxis",
"TextAlignment",
@@ -5660,7 +5660,7 @@ class BaseParametricGizmoGroup:
"""
return 0.0
def _update_view_dependent_dimensions(self, context: bpy.types.Context, mw: Matrix, props) -> None: # noqa: ARG002
def _update_view_dependent_dimensions(self, context: bpy.types.Context, mw: Matrix, props) -> None:
"""Update overall_width, overall_height, and lining_offset based on view direction.
This base implementation handles the common pattern for door/window gizmos.
@@ -5837,7 +5837,7 @@ class BaseParametricGizmoGroup:
self.update_dimension_gizmos(mw, props)
self._refresh_element_specific(context, mw, props)
def _refresh_element_specific(self, context: bpy.types.Context, mw: "Matrix", props) -> None: # noqa: ARG002
def _refresh_element_specific(self, context: bpy.types.Context, mw: "Matrix", props) -> None:
"""Override for element-specific refresh logic.
Called from both refresh() (on state change) and draw_prepare() (per frame),
@@ -6344,7 +6344,7 @@ class BaseParametricGizmoGroup:
"""
return (0.0, 0.0)
def get_icon_y_offset(self, context: bpy.types.Context, mw: Matrix) -> float: # noqa: ARG002
def get_icon_y_offset(self, context: bpy.types.Context, mw: Matrix) -> float:
"""Get Y offset for icons based on view direction.
Uses get_icon_y_extent() to determine how far to offset icons based on
@@ -6546,9 +6546,7 @@ class BaseParametricGizmoGroup:
self._refresh_element_specific(context, mw, props)
def _update_dimension_gizmo_positions(
self, context: bpy.types.Context, mw: "Matrix", props # noqa: ARG002
) -> None:
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: "Matrix", props) -> None:
"""Update dimension gizmo positions based on view direction.
Override this method in subclasses to implement view-dependent
@@ -1406,31 +1406,28 @@ class CreateDrawing(bpy.types.Operator):
# Backwards compatibility with older ifcopenshell builds that don't expose these keys.
pass
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
self.serialiser_settings.set("svg-without-storeys", True)
self.serialiser_settings.set("svg-write-poly", True)
self.serialiser_settings.set("svg-poly", True)
self.svg_settings.set("svg-without-storeys", True)
self.svg_settings.set("svg-write-poly", True)
self.svg_settings.set("svg-poly", True)
# Objects with more than these edges are rendered as wireframe instead of HLR for optimisation
self.serialiser_settings.set("profile-threshold", 10000)
self.serialiser_settings.set("svg-xmlns", True)
self.serialiser_settings.set("svg-project", True)
self.serialiser_settings.set("auto-elevation", False)
self.serialiser_settings.set("auto-section", False)
self.serialiser_settings.set("print-space-names", False)
self.serialiser_settings.set("print-space-areas", False)
self.serialiser_settings.set("door-arcs", False)
self.serialiser_settings.set("svg-no-css", True)
self.serialiser_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
self.serialiser_settings.set("scale", str(self.scale))
self.serialiser_settings.set("svg-subtract-before", "always")
self.serialiser_settings.set("svg-prefilter", True) # See #3359
self.serialiser_settings.set("svg-unify-inputs", True)
self.serialiser_settings.set("svg-segment-projection", True)
self.svg_settings.set("profile-threshold", 10000)
self.svg_settings.set("svg-xmlns", True)
self.svg_settings.set("svg-project", True)
self.svg_settings.set("auto-elevation", False)
self.svg_settings.set("auto-section", False)
self.svg_settings.set("print-space-names", False)
self.svg_settings.set("print-space-areas", False)
self.svg_settings.set("door-arcs", False)
self.svg_settings.set("svg-no-css", True)
self.svg_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
self.svg_settings.set("scale", str(self.scale))
self.svg_settings.set("svg-subtract-before", "always")
self.svg_settings.set("svg-prefilter", True) # See #3359
self.svg_settings.set("svg-unify-inputs", True)
self.svg_settings.set("svg-segment-projection", True)
if target_view == "REFLECTED_PLAN_VIEW":
self.serialiser_settings.set("svg-mirror-y", True)
self.serialiser = ifcopenshell.geom.serializers.svg(
self.svg_buffer, self.svg_settings, self.serialiser_settings
)
self.svg_settings.set("svg-mirror-y", True)
self.serialiser = ifcopenshell.geom.serializers.svg(self.svg_buffer, self.svg_settings)
# tree = ifcopenshell.geom.tree()
# This instructs the tree to explode BReps into faces and return
# the style of the face when running tree.select_ray()
+12 -12
View File
@@ -50,11 +50,13 @@ from bonsai.bim.module.drawing.data import refresh as refresh_drawing_data
from bonsai.bim.prop import Attribute, BIMFilterGroup
diagram_scales_enum = []
diagram_scales_enum_system = None
def purge():
global diagram_scales_enum
global diagram_scales_enum, diagram_scales_enum_system
diagram_scales_enum = []
diagram_scales_enum_system = None
def update_target_view_doc(self: "DocProperties", context: bpy.types.Context) -> None:
@@ -123,14 +125,12 @@ def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> No
def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
global diagram_scales_enum
global diagram_scales_enum, diagram_scales_enum_system
assert context.scene
if (
len(diagram_scales_enum) < 1
or (context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13)
or (context.scene.unit_settings.system == "METRIC" and len(diagram_scales_enum) == 31)
):
if context.scene.unit_settings.system == "IMPERIAL":
system = context.scene.unit_settings.system
if len(diagram_scales_enum) < 1 or diagram_scales_enum_system != system:
diagram_scales_enum_system = system
if system == "IMPERIAL":
diagram_scales_enum = [
("CUSTOM", "Custom", ""),
("1'=1'-0\"|1/1", "1'=1'-0\"", ""),
@@ -144,21 +144,21 @@ def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context)
('1/4"=1\'-0"|1/48', '1/4"=1\'-0"', ""),
('3/16"=1\'-0"|1/64', '3/16"=1\'-0"', ""),
('1/8"=1\'-0"|1/96', '1/8"=1\'-0"', ""),
("1\"=10'|1/120", "1\"=10'", ""),
('3/32"=1\'-0"|1/128', '3/32"=1\'-0"', ""),
('1/16"=1\'-0"|1/192', '1/16"=1\'-0"', ""),
('1/32"=1\'-0"|1/384', '1/32"=1\'-0"', ""),
('1/64"=1\'-0"|1/768', '1/64"=1\'-0"', ""),
('1/128"=1\'-0"|1/1536', '1/128"=1\'-0"', ""),
("1\"=10'|1/120", "1\"=10'", ""),
("1\"=20'|1/240", "1\"=20'", ""),
("1\"=30'|1/360", "1\"=30'", ""),
('1/32"=1\'-0"|1/384', '1/32"=1\'-0"', ""),
("1\"=40'|1/480", "1\"=40'", ""),
("1\"=50'|1/600", "1\"=50'", ""),
("1\"=60'|1/720", "1\"=60'", ""),
('1/64"=1\'-0"|1/768', '1/64"=1\'-0"', ""),
("1\"=70'|1/840", "1\"=70'", ""),
("1\"=80'|1/960", "1\"=80'", ""),
("1\"=90'|1/1080", "1\"=90'", ""),
("1\"=100'|1/1200", "1\"=100'", ""),
('1/128"=1\'-0"|1/1536', '1/128"=1\'-0"', ""),
("1\"=150'|1/1800", "1\"=150'", ""),
("1\"=200'|1/2400", "1\"=200'", ""),
("1\"=300'|1/3600", "1\"=300'", ""),
@@ -72,11 +72,10 @@ class ExportOBJ(bpy.types.Operator):
# Conversion from IFC to OBJ
# Settings for obj
settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.SURFACES_AND_SOLIDS)
settings.set("apply-default-materials", True)
serializer_settings.set("use-element-guids", True)
settings.set("use-element-guids", True)
settings.set("use-world-coords", True)
ifc_file: ifcopenshell.file
@@ -90,7 +89,7 @@ class ExportOBJ(bpy.types.Operator):
obj_file_path = os.path.join(output_dir, "model.obj")
mtl_file_path = os.path.join(output_dir, "model.mtl")
serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings, serializer_settings)
serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings)
serialiser.setFile(ifc_file)
serialiser.setUnitNameAndMagnitude("METER", 1.0)
serialiser.writeHeader()
@@ -107,7 +106,7 @@ class ExportOBJ(bpy.types.Operator):
if iterator.initialize():
while True:
shape = iterator.get()
assert isinstance(shape, W.TriangulationElement)
assert isinstance(shape, W.triangulation_element)
materials = shape.geometry.materials
for material in materials:
+1 -1
View File
@@ -430,7 +430,7 @@ class SverchokData:
@classmethod
def has_sverchok(cls) -> bool:
try:
import sverchok # noqa: F401
import sverchok # ruff: ignore[unused-import]
return True
except ModuleNotFoundError:
+4 -6
View File
@@ -560,7 +560,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
)
update_door_modifier_representation(obj)
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
def _execute(self, context: bpy.types.Context) -> set[str]:
for obj in tool.Blender.get_selected_objects():
if not tool.Blender.Modifier.is_eligible_for_door_modifier(obj):
continue
@@ -638,7 +638,7 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
def _execute(self, context: bpy.types.Context) -> set[str]:
for obj in tool.Blender.get_selected_objects():
self.remove_door_on_object(obj)
return {"FINISHED"}
@@ -683,7 +683,7 @@ class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator):
return True
return False
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
def _execute(self, context: bpy.types.Context) -> set[str]:
obj = tool.Blender.get_active_object()
if not obj:
return {"CANCELLED"}
@@ -909,9 +909,7 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
setattr(self, f"gizmo_swing_arc_{cfg.name}", main)
setattr(self, f"gizmo_swing_arc_{cfg.name}_flip", flip)
def _refresh_element_specific(
self, context: bpy.types.Context, mw: Matrix, props: "BIMDoorProperties" # noqa: ARG002
) -> None:
def _refresh_element_specific(self, context: bpy.types.Context, mw: Matrix, props: "BIMDoorProperties") -> None:
"""Update door-specific swing arc gizmos."""
self.update_swing_gizmos(mw, props)
+2 -2
View File
@@ -765,7 +765,7 @@ class GizmoRoofEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
def is_element_type(cls, element: ifcopenshell.entity_instance) -> bool:
return tool.Parametric.is_roof(element)
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw, props) -> None: # noqa: ARG002
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw, props) -> None:
"""Anchor every dimension gizmo at the object origin. Each gizmo's
declared axis (height/slope along +Z, thickness along -Z) separates
them in 3D so they don't visually collide despite sharing a
@@ -776,7 +776,7 @@ class GizmoRoofEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
self.set_dimension_gizmo_position("angle", mw, origin, (0, 0, 1))
self.set_dimension_gizmo_position("roof_thickness", mw, origin, (0, 0, -1))
def get_element_height(self, props) -> float: # noqa: ARG002
def get_element_height(self, props) -> float:
"""Object-local Z of the mesh's topmost vertex, so the pen / validate /
cancel / cycle row anchors visibly above sloped or stepped roof
bodies rather than at the parametric ``props.height`` which may not
+3 -5
View File
@@ -405,7 +405,7 @@ class SetStairTreads(bpy.types.Operator):
bl_label = "Set Number of Treads"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]: # noqa: ARG002
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
obj = context.active_object
if not obj:
return {"CANCELLED"}
@@ -658,9 +658,7 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
self.tread_count_label_gizmo.alpha = 0.8
self.tread_count_label_gizmo.target_set_operator("bim.input_stair_treads")
def _refresh_element_specific(
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties" # noqa: ARG002
) -> None:
def _refresh_element_specific(self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties") -> None:
"""Update stair-specific lock and tread count gizmos. Lock positioning is
handled per-frame in the dimension-positioning hook."""
self.update_lock_gizmo(props)
@@ -707,7 +705,7 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
self.update_gizmo_visibility(self.tread_count_label_gizmo, props.is_editing)
def _update_dimension_gizmo_positions(
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties" # noqa: ARG002
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties"
) -> None:
"""Update dimension gizmo positions based on camera view direction."""
viewing_from_negative_y, viewing_from_negative_x = self._frame_view_dir
+2 -2
View File
@@ -2174,7 +2174,7 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
return (far, near)
def _update_dimension_gizmo_positions(
self, context: bpy.types.Context, mw: Matrix, props: "BIMWallProperties" # noqa: ARG002
self, context: bpy.types.Context, mw: Matrix, props: "BIMWallProperties"
) -> None:
"""Re-position length / height / height_end dimensions to the camera-facing
Y-side of the wall every frame. Mirrors the door & stair pattern: when the
@@ -2530,7 +2530,7 @@ def _perpendicular_wall_params(
return clamped_x, abs(cursor_local_y), side_sign
def _commit_pending_wall_edits_for_selection(context: bpy.types.Context) -> None: # noqa: ARG001
def _commit_pending_wall_edits_for_selection(context: bpy.types.Context) -> None:
"""Thin wall-scoped alias for ``tool.Parametric.commit_pending_edits_for_selection``.
Encapsulates the ``names=("wall",)`` filter so the registry name is
+1 -1
View File
@@ -538,7 +538,7 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Remove Window"
bl_options = {"REGISTER"}
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
def _execute(self, context: bpy.types.Context) -> set[str]:
obj = context.active_object
assert obj
element = tool.Ifc.get_entity(obj)
@@ -2442,7 +2442,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
if iterator.initialize():
while True: # Main loop.
shape = iterator.get()
assert isinstance(shape, W.TriangulationElement)
assert isinstance(shape, W.triangulation_element)
results.add(self.file.by_id(shape.id))
geometry = shape.geometry
@@ -2518,7 +2518,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
print("Finished", time.time() - start)
return {"FINISHED"}
def process_occurrence(self, shape: W.TriangulationElement) -> None:
def process_occurrence(self, shape: W.triangulation_element) -> None:
element = self.file.by_id(shape.id)
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
@@ -558,7 +558,7 @@ class IntegerInputDialogMixin:
return None
return props
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]: # noqa: ARG002
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
props = self._resolve_props(context)
if props is None:
return {"CANCELLED"}
+29 -2
View File
@@ -142,8 +142,35 @@ def assign_material(
else:
element_material_type = material_type
ifc.run("material.assign_material", products=[element], type=element_material_type, material=material)
assigned_material = material_tool.get_material(element)
# TODO: this whole dance is a stopgap and wants rewriting.
#
# material.assign_material creates material sets with no items in them,
# ignoring the material it was handed -- an IfcMaterialLayerSet with no
# MaterialLayers is not valid IFC, since the list is mandatory and
# [1:?]. So we repair it below, after the fact. Worse, the API rejects a
# plain IfcMaterial outright when asked for a usage, which is exactly
# what the Object Materials dropdown gives us, so we cannot even pass it
# on and have to let the API invent an empty set and then fill it in.
#
# The fix is for assign_material to build the set around the material it
# is given, rather than leaving an invalid one behind for its callers to
# patch up. That is a wider change than it looks: add_material_set has
# the same behaviour, and the create-empty-then-add-items idiom is
# spread through the API's own docstrings, examples and tests. Until
# that is untangled, keep the repair here where it is at least visible.
# Only a usage refuses a plain IfcMaterial; every other type still wants
# it, and IfcMaterial and IfcMaterialList cannot be created without it.
pass_material = material_tool.is_a_material_set(material) or not element_material_type.endswith("Usage")
ifc.run(
"material.assign_material",
products=[element],
type=element_material_type,
material=material if pass_material else None,
)
# A usage points at the set rather than being one, and it is the set
# that needs an item adding to it below.
assigned_material = material_tool.get_material(element, should_skip_usage=True)
assert assigned_material # Type checker.
if material_tool.is_a_material_set(material):
+1 -1
View File
@@ -651,7 +651,7 @@ class Material:
def get_default_material(cls): pass
def get_elements_by_material(cls, material): pass
def get_material_attributes(cls): pass
def get_material(cls, element, should_inherit: bool = False): pass
def get_material(cls, element, should_inherit: bool = False, should_skip_usage: bool = False): pass
def get_object_ui_active_material(cls): pass
def get_object_ui_material_type(cls): pass
def get_style(cls, material): pass
+1 -1
View File
@@ -17,7 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
#
# Ignore unused imports.
# ruff: noqa: F401
# ruff: file-ignore[unused-import]
from bonsai.tool.aggregate import Aggregate
from bonsai.tool.array import Array
+2
View File
@@ -25,6 +25,7 @@ import importlib
import math
import os
import platform
import re
import subprocess
import sys
import tempfile
@@ -1756,6 +1757,7 @@ class Blender(bonsai.core.tool.Blender):
repo_path = repo.working_tree_dir
assert repo_path
version_ = (Path(repo_path) / "VERSION").read_text().strip()
version_ = re.sub(r"[A-Za-z]+\d+$", "", version_)
commit_date = bonsai.get_last_commit_date()
assert commit_date
commit_date = datetime.fromisoformat(commit_date)
+2 -2
View File
@@ -1187,7 +1187,7 @@ class Geometry(bonsai.core.tool.Geometry):
if iterator and iterator.initialize():
while True:
shape = iterator.get()
assert isinstance(shape, W.TriangulationElement)
assert isinstance(shape, W.triangulation_element)
element = tool.Ifc.get().by_id(shape.id)
if obj := tool.Ifc.get_object(element):
# It's possible that there will be multiple shapes for the same context,
@@ -2179,7 +2179,7 @@ class Geometry(bonsai.core.tool.Geometry):
item = tool.Ifc.get().by_id(props.ifc_definition_id)
allowed_attributes = [
a.name()
for a in item.declaration().as_entity.all_attributes()
for a in item.declaration.as_entity().all_attributes()
if a.type_of_attribute()._is("IfcLengthMeasure")
]
+2 -2
View File
@@ -872,7 +872,7 @@ class Loader(bonsai.core.tool.Loader):
cls,
element: ifcopenshell.entity_instance,
representation: ifcopenshell.entity_instance,
shape: W.TriangulationElement,
shape: W.triangulation_element,
) -> bpy.types.Camera:
"""Create camera data.
@@ -1026,7 +1026,7 @@ class Loader(bonsai.core.tool.Loader):
@classmethod
def convert_geometry_to_mesh(
cls,
geometry: W.Triangulation,
geometry: W.triangulation,
mesh: bpy.types.Mesh,
verts: Optional[npt.NDArray[np.float64]] = None,
*,
+11 -2
View File
@@ -220,9 +220,14 @@ class Material(bonsai.core.tool.Material):
@classmethod
def get_material(
cls, element: ifcopenshell.entity_instance, should_inherit: bool = False
cls,
element: ifcopenshell.entity_instance,
should_inherit: bool = False,
should_skip_usage: bool = False,
) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.element.get_material(element, should_inherit=should_inherit)
return ifcopenshell.util.element.get_material(
element, should_inherit=should_inherit, should_skip_usage=should_skip_usage
)
@classmethod
def is_a_material_set(cls, material: ifcopenshell.entity_instance) -> bool:
@@ -326,6 +331,10 @@ class Material(bonsai.core.tool.Material):
@classmethod
def get_style(cls, material: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
if not material.is_a("IfcMaterial"):
# material may also be an IfcMaterialConstituentSet / IfcMaterialLayerSet /
# IfcMaterialProfileSet / IfcMaterialList, none of which have HasRepresentation.
return None
for material_representation in material.HasRepresentation:
for representation in material_representation.Representations:
for item in representation.Items:
+1 -1
View File
@@ -2459,7 +2459,7 @@ class Model(bonsai.core.tool.Model):
polygons = {}
for curve in curves:
geometry = ifcopenshell.geom.create_shape(settings, curve)
assert isinstance(geometry, W.Triangulation)
assert isinstance(geometry, W.triangulation)
v = ifcopenshell.util.shape.get_vertices(geometry, is_2d=True)
v = np.round(v, 4) # Round to nearest 0.1mm, otherwise things like circles don't polygonise reliably
edges = ifcopenshell.util.shape.get_edges(geometry)
+1 -1
View File
@@ -53,7 +53,7 @@ class Profile(bonsai.core.tool.Profile):
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile)
assert isinstance(shape, W.Triangulation)
assert isinstance(shape, W.triangulation)
verts = ifcopenshell.util.shape.get_vertices(shape)
if verts.size == 0:
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.")
@@ -76,6 +76,10 @@ Release
Notes:
- Typically all packages are released at once using the same version schema
- ``VERSION`` uses Python/PEP 440-compatible spelling. For example, an alpha
release may be ``0.9.0alpha0`` (canonicalized to ``0.9.0a0``); build scripts
derive numeric-only and SemVer forms such as ``0.9.0`` and
``0.9.0-alpha0`` where required.
- The ``README.md`` badges can serve as a visual reference for what versions have been released
- Corrective Release (if needed after a standard release):
+1 -1
View File
@@ -33,7 +33,7 @@ exclude = ["test*"]
[tool.ruff]
extend = "../../pyproject.toml"
lint.extend-select = [
"F401", # unused imports
"unused-import", # unused imports
]
[tool.ruff.lint.isort]
+1
View File
@@ -42,6 +42,7 @@ markers =
type
unit
void
wall
web
# Provide plugins explicitly, so it will be possible run tests with PYTEST_DISABLE_PLUGIN_AUTOLOAD.
+1
View File
@@ -1,3 +1,4 @@
pytest
pytest-blender
pytest-bdd
fake-bpy-module-latest
+4 -4
View File
@@ -45,10 +45,10 @@ for dep in dependencies:
subprocess.check_call(command + [dep])
try:
import pygments # noqa: F401
import pytest # noqa: F401
import pytest_bdd # noqa: F401
import pytest_blender # noqa: F401
import pygments # ruff: ignore[unused-import]
import pytest # ruff: ignore[unused-import]
import pytest_bdd # ruff: ignore[unused-import]
import pytest_blender # ruff: ignore[unused-import]
print("Test dependency installation was successful!")
except Exception as e:
+21 -24
View File
@@ -163,32 +163,29 @@ class Drawer:
# self.svg_settings.set_deflection_tolerance(0.0001)
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
self.serialiser_settings.set("svg-without-storeys", True)
self.serialiser_settings.set("svg-write-poly", True)
self.serialiser_settings.set("svg-poly", True)
self.svg_settings.set("svg-without-storeys", True)
self.svg_settings.set("svg-write-poly", True)
self.svg_settings.set("svg-poly", True)
# Objects with more than these edges are rendered as wireframe instead of HLR for optimisation
self.serialiser_settings.set("profile-threshold", 10000)
self.serialiser_settings.set("svg-xmlns", True)
self.serialiser_settings.set("svg-project", True)
self.serialiser_settings.set("auto-elevation", False)
self.serialiser_settings.set("auto-section", False)
self.serialiser_settings.set("print-space-names", False)
self.serialiser_settings.set("print-space-areas", False)
self.serialiser_settings.set("door-arcs", False)
self.serialiser_settings.set("svg-no-css", True)
self.serialiser_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
self.serialiser_settings.set("scale", "1/50")
self.serialiser_settings.set("svg-subtract-before", "always")
self.serialiser_settings.set("svg-prefilter", True) # See #3359
# self.serialiser_settings.set("svg-prefilter", False) # See #3359
self.serialiser_settings.set("svg-unify-inputs", True)
self.serialiser_settings.set("svg-segment-projection", True)
self.svg_settings.set("profile-threshold", 10000)
self.svg_settings.set("svg-xmlns", True)
self.svg_settings.set("svg-project", True)
self.svg_settings.set("auto-elevation", False)
self.svg_settings.set("auto-section", False)
self.svg_settings.set("print-space-names", False)
self.svg_settings.set("print-space-areas", False)
self.svg_settings.set("door-arcs", False)
self.svg_settings.set("svg-no-css", True)
self.svg_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
self.svg_settings.set("scale", "1/50")
self.svg_settings.set("svg-subtract-before", "always")
self.svg_settings.set("svg-prefilter", True) # See #3359
# self.svg_settings.set("svg-prefilter", False) # See #3359
self.svg_settings.set("svg-unify-inputs", True)
self.svg_settings.set("svg-segment-projection", True)
if target_view == "REFLECTED_PLAN_VIEW":
self.serialiser_settings.set("svg-mirror-y", True)
self.serialiser = ifcopenshell.geom.serializers.svg(
self.svg_buffer, self.svg_settings, self.serialiser_settings
)
self.svg_settings.set("svg-mirror-y", True)
self.serialiser = ifcopenshell.geom.serializers.svg(self.svg_buffer, self.svg_settings)
self.serialiser.setFile(ifc)
@@ -72,12 +72,12 @@ Scenario: Add classification reference - object
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
When I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
When I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
Then nothing happens
Scenario: Change classification level
@@ -88,8 +88,8 @@ Scenario: Change classification level
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
@@ -104,8 +104,8 @@ Scenario: Disable editing classification references
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
When I press "bim.disable_editing_classification_references"
@@ -119,12 +119,12 @@ Scenario: Enable editing classification reference
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
And the variable "reference" is "{ifc}.by_type('IfcClassificationReference')[0].id()"
When I press "bim.enable_editing_classification_reference(reference={reference})"
Then nothing happens
@@ -137,12 +137,12 @@ Scenario: Disable editing classification reference
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
And the variable "reference" is "{ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.enable_editing_classification_reference(reference={reference})"
When I press "bim.disable_editing_classification_reference"
@@ -156,15 +156,15 @@ Scenario: Remove classification reference - object
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
And the variable "reference" is "{ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.enable_editing_classification_reference(reference={reference})"
When I press "bim.remove_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
When I press "bim.remove_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
Then nothing happens
Scenario: Edit classification reference
@@ -175,12 +175,12 @@ Scenario: Edit classification reference
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
And the variable "reference" is "{ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.enable_editing_classification_reference(reference={reference})"
When I press "bim.edit_classification_reference"
@@ -185,6 +185,7 @@ Scenario: Update representation - updating a layered extrusion
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -213,6 +214,7 @@ Scenario: Update representation - updating a profiled extrusion
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -416,6 +418,7 @@ Scenario: Override duplicate move - copying a layered extrusion
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -447,6 +450,7 @@ Scenario: Override duplicate move - copying a profiled extrusion
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -121,6 +121,7 @@ Scenario: Assign material - material layer set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
Then the object "IfcWallType/Empty" does not have the material "Default"
@@ -134,6 +135,7 @@ Scenario: Unassign material - material layer set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
When I press "bim.unassign_material"
@@ -155,6 +157,7 @@ Scenario: Unassign material - removing inherited material
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
@@ -181,6 +184,7 @@ Scenario: Enable editing assigned material - material layer set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
When I press "bim.enable_editing_assigned_material"
@@ -200,6 +204,7 @@ Scenario: Disable editing assigned material - material layer set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -220,6 +225,7 @@ Scenario: Edit assigned material - material layer set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -235,6 +241,7 @@ Scenario: Assign material - material profile set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
Then the object "IfcWallType/Empty" does not have the material "Default"
@@ -248,6 +255,7 @@ Scenario: Unassign material - material profile set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
When I press "bim.unassign_material"
@@ -267,6 +275,7 @@ Scenario: Enable editing assigned material - material profile set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
When I press "bim.enable_editing_assigned_material"
@@ -286,6 +295,7 @@ Scenario: Disable editing assigned material - material profile set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -306,6 +316,7 @@ Scenario: Edit assigned material - material profile set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -454,6 +465,7 @@ Scenario: Add material set layer
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -477,6 +489,7 @@ Scenario: Remove material set layer
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
+139 -97
View File
@@ -314,6 +314,12 @@ Scenario: Load project elements - auto offset of cartesian points
Then the object "IfcBuildingElementProxy/NAME" is at "0,0,0"
Scenario: Load project elements - all georeferencing coordinate situations - disabled false origin mode
# D, G and J have their geometry far from their placement, so each is
# shifted onto one of its own verts to keep its precision. Which vert that
# is comes from the geometry kernel and has changed before, so these assert
# that the origin is on a vert rather than which one, and name verts rather
# than origins. In automatic mode the model origin is picked the same way
# and everything moves with it, so there they are relative to it.
Given an empty Blender session
And I press "bim.load_project(filepath='{cwd}/test/files/geolocation.ifc', is_advanced=True)"
When I set "scene.BIMProjectProperties.false_origin_mode" to "DISABLED"
@@ -326,13 +332,19 @@ Scenario: Load project elements - all georeferencing coordinate situations - dis
And the object "IfcActuator/A" is at "7,3,0"
And the object "IfcActuator/B" is at "6,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" is at "13,4,-1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "13000,4000,-1000"
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "15000,2000,1000"
And the object "IfcActuator/E" is at "6,3,0"
And the object "IfcActuator/F" is at "3,3,0"
And the object "IfcActuator/G" is at "15,6,-1"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "15000,6000,-1000"
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "17000,4000,1000"
And the object "IfcActuator/H" is at "9,2,0"
And the object "IfcActuator/I" is at "3,3,0"
And the object "IfcActuator/J" is at "11,3,-1"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "11000,3000,-1000"
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "13000,1000,1000"
And the object "IfcActuator/K" is at "10,0,0"
Scenario: Load project elements - all georeferencing coordinate situations - automatic false origin mode
@@ -342,24 +354,27 @@ Scenario: Load project elements - all georeferencing coordinate situations - aut
When I set "scene.BIMProjectProperties.distance_limit" to "5"
And I press "bim.load_project_elements"
Then "scene.BIMGeoreferenceProperties.has_blender_offset" is "True"
And "scene.BIMGeoreferenceProperties.model_origin" is "13000.0,4000.0,-1000.0"
And "scene.BIMGeoreferenceProperties.blender_offset_x" is "13000.0"
And "scene.BIMGeoreferenceProperties.blender_offset_y" is "4000.0"
And "scene.BIMGeoreferenceProperties.blender_offset_z" is "-1000.0"
And the model origin is on an object vertex
And the object "IfcSite/My Site" is at "0,0,0"
And the object "IfcBuilding/My Building" is at "0,0,0"
And the object "IfcBuildingStorey/My Storey" is at "0,0,0"
And the object "IfcActuator/A" is at "-6,-1,1"
And the object "IfcActuator/B" is at "-7,-3,1"
And the object "IfcActuator/A" is at "7,3,0" relative to the model origin at map coordinates "7000,3000,0"
And the object "IfcActuator/B" is at "6,1,0" relative to the model origin at map coordinates "6000,1000,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" is at "0,0,0"
And the object "IfcActuator/E" is at "-7,-1,1"
And the object "IfcActuator/F" is at "-10,-1,1"
And the object "IfcActuator/G" is at "2,2,0"
And the object "IfcActuator/H" is at "-4,-2,1"
And the object "IfcActuator/I" is at "-10,-1,1"
And the object "IfcActuator/J" is at "-2,-1,0"
And the object "IfcActuator/K" is at "-3,-4,1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "13,4,-1" relative to the model origin at map coordinates "13000,4000,-1000"
And the object "IfcActuator/D" has a vert at "15,2,1" relative to the model origin at map coordinates "15000,2000,1000"
And the object "IfcActuator/E" is at "6,3,0" relative to the model origin at map coordinates "6000,3000,0"
And the object "IfcActuator/F" is at "3,3,0" relative to the model origin at map coordinates "3000,3000,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "15,6,-1" relative to the model origin at map coordinates "15000,6000,-1000"
And the object "IfcActuator/G" has a vert at "17,4,1" relative to the model origin at map coordinates "17000,4000,1000"
And the object "IfcActuator/H" is at "9,2,0" relative to the model origin at map coordinates "9000,2000,0"
And the object "IfcActuator/I" is at "3,3,0" relative to the model origin at map coordinates "3000,3000,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "11,3,-1" relative to the model origin at map coordinates "11000,3000,-1000"
And the object "IfcActuator/J" has a vert at "13,1,1" relative to the model origin at map coordinates "13000,1000,1000"
And the object "IfcActuator/K" is at "10,0,0" relative to the model origin at map coordinates "10000,0,0"
Scenario: Load project elements - all georeferencing coordinate situations - manual false origin mode
Given an empty Blender session
@@ -379,23 +394,20 @@ Scenario: Load project elements - all georeferencing coordinate situations - man
And the object "IfcActuator/A" is at "-3,3,0"
And the object "IfcActuator/B" is at "-4,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" is at "3,4,-1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "3,4,-1" at map coordinates "13000,4000,-1000"
And the object "IfcActuator/D" has a vert at "5,2,1" at map coordinates "15000,2000,1000"
And the object "IfcActuator/E" is at "-4,3,0"
And the object "IfcActuator/F" is at "-7,3,0"
And the object "IfcActuator/G" is at "5,6,-1"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "5,6,-1" at map coordinates "15000,6000,-1000"
And the object "IfcActuator/G" has a vert at "7,4,1" at map coordinates "17000,4000,1000"
And the object "IfcActuator/H" is at "-1,2,0"
And the object "IfcActuator/I" is at "-7,3,0"
And the object "IfcActuator/J" is at "1,3,-1"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "1,3,-1" at map coordinates "11000,3000,-1000"
And the object "IfcActuator/J" has a vert at "3,1,1" at map coordinates "13000,1000,1000"
And the object "IfcActuator/K" is at "0,0,0"
And the object "IfcActuator/D" has a cartesian point offset of "31,4,-1"
And the object "IfcActuator/G" has a cartesian point offset of "-25,6,-1"
And the object "IfcActuator/J" has a cartesian point offset of "11,3,-1"
And the object "IfcActuator/D" has a vertex at "3,2,-1"
And the object "IfcActuator/D" has a vertex at "5,2,-1"
And the object "IfcActuator/G" has a vertex at "5,4,-1"
And the object "IfcActuator/G" has a vertex at "7,4,-1"
And the object "IfcActuator/J" has a vertex at "1,1,-1"
And the object "IfcActuator/J" has a vertex at "3,1,-1"
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - disabled false origin mode
Given an empty Blender session
@@ -410,13 +422,19 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "5.985,14.71,0"
And the object "IfcActuator/B" is at "5.5367,12.519,0"
And the object "IfcActuator/C" is at "0,10,0"
And the object "IfcActuator/D" is at "11.522,17.228,-1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "11.5218,17.2284,-1" at map coordinates "11521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "13.9712,15.8141,1" at map coordinates "13971.246,15814.136,1000"
And the object "IfcActuator/E" is at "5.0191,14.451,0"
And the object "IfcActuator/F" is at "2.1213,13.674,0"
And the object "IfcActuator/G" is at "12.936,19.678,-1"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "12.936,19.6778,-1" at map coordinates "12935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "15.3855,18.2636,1" at map coordinates "15385.465,18263.627,1000"
And the object "IfcActuator/H" is at "8.1757,14.261,0"
And the object "IfcActuator/I" is at "2.1213,13.674,0"
And the object "IfcActuator/J" is at "9.8487,15.745,-1"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "9.8487,15.7448,-1" at map coordinates "9848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "12.2982,14.3306,1" at map coordinates "12298.216,14330.573,1000"
And the object "IfcActuator/K" is at "9.6593,12.588,0"
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - automatic false origin mode
@@ -436,13 +454,19 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "7,3,0"
And the object "IfcActuator/B" is at "6,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" is at "13,4,-1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "11521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "13971.246,15814.136,1000"
And the object "IfcActuator/E" is at "6,3,0"
And the object "IfcActuator/F" is at "3,3,0"
And the object "IfcActuator/G" is at "15,6,-1"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "12935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "15385.465,18263.627,1000"
And the object "IfcActuator/H" is at "9,2,0"
And the object "IfcActuator/I" is at "3,3,0"
And the object "IfcActuator/J" is at "11,3,-1"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "9848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "12298.216,14330.573,1000"
And the object "IfcActuator/K" is at "10,0,0"
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - manual false origin mode
@@ -463,23 +487,20 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "5.985,4.71,0"
And the object "IfcActuator/B" is at "5.5367,2.519,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" is at "11.522,7.228,-1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "11.5218,7.2284,-1" at map coordinates "11521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "13.9712,5.8141,1" at map coordinates "13971.246,15814.136,1000"
And the object "IfcActuator/E" is at "5.0191,4.451,0"
And the object "IfcActuator/F" is at "2.1213,3.674,0"
And the object "IfcActuator/G" is at "12.936,9.678,-1"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "12.936,9.6778,-1" at map coordinates "12935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "15.3855,8.2636,1" at map coordinates "15385.465,18263.627,1000"
And the object "IfcActuator/H" is at "8.1757,4.261,0"
And the object "IfcActuator/I" is at "2.1213,3.674,0"
And the object "IfcActuator/J" is at "9.8487,5.745,-1"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "9.8487,5.7448,-1" at map coordinates "9848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "12.2982,4.3306,1" at map coordinates "12298.216,14330.573,1000"
And the object "IfcActuator/K" is at "9.6593,2.588,0"
And the object "IfcActuator/D" has a cartesian point offset of "31,4,-1"
And the object "IfcActuator/G" has a cartesian point offset of "-25,6,-1"
And the object "IfcActuator/J" has a cartesian point offset of "11,3,-1"
And the object "IfcActuator/D" has a vertex at "12.039,5.296,-1"
And the object "IfcActuator/D" has a vertex at "13.971,5.814,-1"
And the object "IfcActuator/G" has a vertex at "13.454,7.746,-1"
And the object "IfcActuator/G" has a vertex at "15.385,8.264,-1"
And the object "IfcActuator/J" has a vertex at "10.366,3.813,-1"
And the object "IfcActuator/J" has a vertex at "12.298,4.331,-1"
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - manual false origin mode - with custom project north
Given an empty Blender session
@@ -500,13 +521,19 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "7,3,0"
And the object "IfcActuator/B" is at "6,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" is at "13,4,-1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "11521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "13971.246,15814.136,1000"
And the object "IfcActuator/E" is at "6,3,0"
And the object "IfcActuator/F" is at "3,3,0"
And the object "IfcActuator/G" is at "15,6,-1"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "12935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "15385.465,18263.627,1000"
And the object "IfcActuator/H" is at "9,2,0"
And the object "IfcActuator/I" is at "3,3,0"
And the object "IfcActuator/J" is at "11,3,-1"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "9848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "12298.216,14330.573,1000"
And the object "IfcActuator/K" is at "10,0,0"
Scenario: Load project elements - all georeferencing coordinate situations with a map conversion - disabled false origin mode (this should be identical to the situation with no map conversion)
@@ -522,13 +549,19 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "7,3,0"
And the object "IfcActuator/B" is at "6,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" is at "13,4,-1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "28000,4000,-1000"
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "30000,2000,1000"
And the object "IfcActuator/E" is at "6,3,0"
And the object "IfcActuator/F" is at "3,3,0"
And the object "IfcActuator/G" is at "15,6,-1"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "30000,6000,-1000"
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "32000,4000,1000"
And the object "IfcActuator/H" is at "9,2,0"
And the object "IfcActuator/I" is at "3,3,0"
And the object "IfcActuator/J" is at "11,3,-1"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "26000,3000,-1000"
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "28000,1000,1000"
And the object "IfcActuator/K" is at "10,0,0"
Scenario: Load project elements - all georeferencing coordinate situations with a map conversion - automatic false origin mode (this should affect the Blender eastings and northings, which is now different to the Blender offset XYZ, but is otherwise identical to the non-map conversion variant)
@@ -538,24 +571,27 @@ Scenario: Load project elements - all georeferencing coordinate situations with
When I set "scene.BIMProjectProperties.distance_limit" to "5"
And I press "bim.load_project_elements"
Then "scene.BIMGeoreferenceProperties.has_blender_offset" is "True"
And "scene.BIMGeoreferenceProperties.model_origin" is "28000.0,4000.0,-1000.0"
And "scene.BIMGeoreferenceProperties.blender_offset_x" is "13000.0"
And "scene.BIMGeoreferenceProperties.blender_offset_y" is "4000.0"
And "scene.BIMGeoreferenceProperties.blender_offset_z" is "-1000.0"
And the model origin is on an object vertex
And the object "IfcSite/My Site" is at "0,0,0"
And the object "IfcBuilding/My Building" is at "0,0,0"
And the object "IfcBuildingStorey/My Storey" is at "0,0,0"
And the object "IfcActuator/A" is at "-6,-1,1"
And the object "IfcActuator/B" is at "-7,-3,1"
And the object "IfcActuator/A" is at "22,3,0" relative to the model origin at map coordinates "22000,3000,0"
And the object "IfcActuator/B" is at "21,1,0" relative to the model origin at map coordinates "21000,1000,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" is at "0,0,0"
And the object "IfcActuator/E" is at "-7,-1,1"
And the object "IfcActuator/F" is at "-10,-1,1"
And the object "IfcActuator/G" is at "2,2,0"
And the object "IfcActuator/H" is at "-4,-2,1"
And the object "IfcActuator/I" is at "-10,-1,1"
And the object "IfcActuator/J" is at "-2,-1,0"
And the object "IfcActuator/K" is at "-3,-4,1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "28,4,-1" relative to the model origin at map coordinates "28000,4000,-1000"
And the object "IfcActuator/D" has a vert at "30,2,1" relative to the model origin at map coordinates "30000,2000,1000"
And the object "IfcActuator/E" is at "21,3,0" relative to the model origin at map coordinates "21000,3000,0"
And the object "IfcActuator/F" is at "18,3,0" relative to the model origin at map coordinates "18000,3000,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "30,6,-1" relative to the model origin at map coordinates "30000,6000,-1000"
And the object "IfcActuator/G" has a vert at "32,4,1" relative to the model origin at map coordinates "32000,4000,1000"
And the object "IfcActuator/H" is at "24,2,0" relative to the model origin at map coordinates "24000,2000,0"
And the object "IfcActuator/I" is at "18,3,0" relative to the model origin at map coordinates "18000,3000,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "26,3,-1" relative to the model origin at map coordinates "26000,3000,-1000"
And the object "IfcActuator/J" has a vert at "28,1,1" relative to the model origin at map coordinates "28000,1000,1000"
And the object "IfcActuator/K" is at "25,0,0" relative to the model origin at map coordinates "25000,0,0"
Scenario: Load project elements - all georeferencing coordinate situations with a map conversion - manual false origin mode (this should affect the Blender eastings and northings, which is now different to the Blender offset XYZ, but is otherwise identical to the non-map conversion variant)
Given an empty Blender session
@@ -575,23 +611,20 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "-3,3,0"
And the object "IfcActuator/B" is at "-4,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" is at "3,4,-1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "3,4,-1" at map coordinates "28000,4000,-1000"
And the object "IfcActuator/D" has a vert at "5,2,1" at map coordinates "30000,2000,1000"
And the object "IfcActuator/E" is at "-4,3,0"
And the object "IfcActuator/F" is at "-7,3,0"
And the object "IfcActuator/G" is at "5,6,-1"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "5,6,-1" at map coordinates "30000,6000,-1000"
And the object "IfcActuator/G" has a vert at "7,4,1" at map coordinates "32000,4000,1000"
And the object "IfcActuator/H" is at "-1,2,0"
And the object "IfcActuator/I" is at "-7,3,0"
And the object "IfcActuator/J" is at "1,3,-1"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "1,3,-1" at map coordinates "26000,3000,-1000"
And the object "IfcActuator/J" has a vert at "3,1,1" at map coordinates "28000,1000,1000"
And the object "IfcActuator/K" is at "0,0,0"
And the object "IfcActuator/D" has a cartesian point offset of "31,4,-1"
And the object "IfcActuator/G" has a cartesian point offset of "-25,6,-1"
And the object "IfcActuator/J" has a cartesian point offset of "11,3,-1"
And the object "IfcActuator/D" has a vertex at "3,2,-1"
And the object "IfcActuator/D" has a vertex at "5,2,-1"
And the object "IfcActuator/G" has a vertex at "5,4,-1"
And the object "IfcActuator/G" has a vertex at "7,4,-1"
And the object "IfcActuator/J" has a vertex at "1,1,-1"
And the object "IfcActuator/J" has a vertex at "3,1,-1"
Scenario: Load project elements - all georeferencing coordinate situations with map conversion and an offset site - disabled false origin mode
Given an empty Blender session
@@ -606,13 +639,19 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "5.985,14.71,0"
And the object "IfcActuator/B" is at "5.5367,12.519,0"
And the object "IfcActuator/C" is at "0,10,0"
And the object "IfcActuator/D" is at "11.522,17.228,-1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "11.5218,17.2284,-1" at map coordinates "26521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "13.9712,15.8141,1" at map coordinates "28971.246,15814.136,1000"
And the object "IfcActuator/E" is at "5.0191,14.451,0"
And the object "IfcActuator/F" is at "2.1213,13.674,0"
And the object "IfcActuator/G" is at "12.936,19.678,-1"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "12.936,19.6778,-1" at map coordinates "27935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "15.3855,18.2636,1" at map coordinates "30385.465,18263.627,1000"
And the object "IfcActuator/H" is at "8.1757,14.261,0"
And the object "IfcActuator/I" is at "2.1213,13.674,0"
And the object "IfcActuator/J" is at "9.8487,15.745,-1"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "9.8487,15.7448,-1" at map coordinates "24848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "12.2982,14.3306,1" at map coordinates "27298.216,14330.573,1000"
And the object "IfcActuator/K" is at "9.6593,12.588,0"
Scenario: Load project elements - all georeferencing coordinate situations with map conversion and an offset site - automatic false origin mode
@@ -632,13 +671,19 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "7,3,0"
And the object "IfcActuator/B" is at "6,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" is at "13,4,-1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "26521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "28971.246,15814.136,1000"
And the object "IfcActuator/E" is at "6,3,0"
And the object "IfcActuator/F" is at "3,3,0"
And the object "IfcActuator/G" is at "15,6,-1"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "27935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "30385.465,18263.627,1000"
And the object "IfcActuator/H" is at "9,2,0"
And the object "IfcActuator/I" is at "3,3,0"
And the object "IfcActuator/J" is at "11,3,-1"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "24848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "27298.216,14330.573,1000"
And the object "IfcActuator/K" is at "10,0,0"
Scenario: Load project elements - all georeferencing coordinate situations with map conversion and an offset site - manual false origin mode
@@ -659,23 +704,20 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "5.985,4.71,0"
And the object "IfcActuator/B" is at "5.5367,2.519,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" is at "11.522,7.228,-1"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "11.5218,7.2284,-1" at map coordinates "26521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "13.9712,5.8141,1" at map coordinates "28971.246,15814.136,1000"
And the object "IfcActuator/E" is at "5.0191,4.451,0"
And the object "IfcActuator/F" is at "2.1213,3.674,0"
And the object "IfcActuator/G" is at "12.936,9.678,-1"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "12.936,9.6778,-1" at map coordinates "27935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "15.3855,8.2636,1" at map coordinates "30385.465,18263.627,1000"
And the object "IfcActuator/H" is at "8.1757,4.261,0"
And the object "IfcActuator/I" is at "2.1213,3.674,0"
And the object "IfcActuator/J" is at "9.8487,5.745,-1"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "9.8487,5.7448,-1" at map coordinates "24848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "12.2982,4.3306,1" at map coordinates "27298.216,14330.573,1000"
And the object "IfcActuator/K" is at "9.6593,2.588,0"
And the object "IfcActuator/D" has a cartesian point offset of "31,4,-1"
And the object "IfcActuator/G" has a cartesian point offset of "-25,6,-1"
And the object "IfcActuator/J" has a cartesian point offset of "11,3,-1"
And the object "IfcActuator/D" has a vertex at "12.039,5.296,-1"
And the object "IfcActuator/D" has a vertex at "13.971,5.814,-1"
And the object "IfcActuator/G" has a vertex at "13.454,7.746,-1"
And the object "IfcActuator/G" has a vertex at "15.385,8.264,-1"
And the object "IfcActuator/J" has a vertex at "10.366,3.813,-1"
And the object "IfcActuator/J" has a vertex at "12.298,4.331,-1"
Scenario: Link IFC - from an empty IFC project
Given an empty IFC project
+4
View File
@@ -81,6 +81,7 @@ Scenario: Assign type - assign to a type with a material layer set, which automa
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
When the variable "type" is "{ifc}.by_type('IfcWallType')[0].id()"
@@ -102,6 +103,7 @@ Scenario: Assign type - assign to a type with a material layer set, which automa
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
When the variable "type" is "{ifc}.by_type('IfcWallType')[0].id()"
@@ -125,6 +127,7 @@ Scenario: Assign type - assign to a different type with a LAYER2 material layer
And I press "bim.assign_class"
And the variable "type" is "{ifc}.by_type('IfcWallType')[-1].id()"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I add an empty
@@ -180,6 +183,7 @@ Scenario: Assign type - assign to a type with a material profile set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -45,14 +45,14 @@ def test_text_formatter_defaults_to_none():
def test_text_formatter_field_stores_callable():
formatter = lambda props, value: f"{value:.2f}m" # noqa: E731
formatter = lambda props, value: f"{value:.2f}m"
config = DimensionGizmoConfig(attr_name="length", axis=(1, 0, 0), text_formatter=formatter)
assert config.text_formatter is not None
assert callable(config.text_formatter)
def test_text_formatter_receives_props_and_value():
formatter = lambda props, value: f"{props.label}={value}" # noqa: E731
formatter = lambda props, value: f"{props.label}={value}"
config = DimensionGizmoConfig(attr_name="length", axis=(1, 0, 0), text_formatter=formatter)
props = SimpleNamespace(label="L")
assert config.text_formatter(props, 3.14) == "L=3.14"
@@ -104,7 +104,7 @@ class TestParametricGizmoPollsHideDuringTransformModal:
continue
try:
result = poll(bpy.context)
except Exception as exc: # noqa: BLE001
except Exception as exc:
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
continue
if result:
@@ -98,7 +98,7 @@ class TestWallGizmoGroupsHideDuringPreview:
continue
try:
result = poll(bpy.context)
except Exception as exc: # noqa: BLE001
except Exception as exc:
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
continue
if result:
@@ -119,7 +119,7 @@ class TestWallGizmoGroupsHideOnArrayChildSelection:
for name, cls in groups:
try:
result = cls.poll(bpy.context)
except Exception as exc: # noqa: BLE001
except Exception as exc:
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
continue
if result:
@@ -159,7 +159,7 @@ class TestWallOperatorsRejectArrayChildSelection:
for name, cls in ops:
try:
result = cls.poll(bpy.context)
except Exception as exc: # noqa: BLE001
except Exception as exc:
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
continue
if result:
+107
View File
@@ -36,6 +36,7 @@ import bpy
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import numpy as np
import pytest
from mathutils import Vector
@@ -1000,6 +1001,7 @@ def i_click_button_and_expect_error_error_msg(button, error_msg):
@given(parsers.parse('I evaluate expression "{expression}"'))
@when(parsers.parse('I evaluate expression "{expression}"'))
@then(parsers.parse('I evaluate expression "{expression}"'))
def i_evaluate_expression(expression):
expression = replace_variables(expression)
exec(expression)
@@ -1680,6 +1682,111 @@ def the_object_name_has_a_vertex_at_location(name, location):
assert is_pass, f"No verts found at {location}: {verts}"
def get_model_origin() -> Vector:
"""Where the model was shifted to, in Blender units.
Geometry far from the origin is moved next to it so it keeps its precision,
and the shift is recorded as the model origin. Which vert of which object it
lands on is not something to depend on, so anything measured from it stays
put even when that choice changes.
"""
props = bpy.context.scene.BIMGeoreferenceProperties
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(an_ifc_file_exists())
return Vector([float(co) for co in props.model_origin.split(",")]) * unit_scale
def get_world_verts(obj: bpy.types.Object) -> list[Vector]:
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh) and len(mesh.vertices), f"Object {obj.name} has no mesh"
return [obj.matrix_world @ v.co for v in mesh.vertices]
def assert_vert_at_map_coordinates(obj: bpy.types.Object, vert: Vector, coordinates: str) -> None:
# Same conversion as the georeferencing calculator, which works in project
# units rather than Blender ones.
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(an_ifc_file_exists())
enh = Vector(tool.Georeference.xyz2enh(tuple(co / unit_scale for co in vert)))
expected = Vector([float(co) for co in coordinates.split(",")])
assert (enh - expected).length < 0.05, f"Vert {vert} is at map coordinates {enh[:]} instead of {coordinates}"
@then(
parsers.parse(
'the object "{name}" is at "{location}" relative to the model origin at map coordinates "{coordinates}"'
)
)
def the_object_name_is_at_location_relative_to_the_model_origin_at_map_coordinates(name, location, coordinates):
"""For objects with no geometry to name a vert on.
The Blender location is only meaningful next to the origin everything was
shifted by, since the two move together, but the map coordinates hold still
either way.
"""
obj = the_object_name_exists(name)
obj_location = obj.location + get_model_origin()
assert (
obj_location - Vector([float(co) for co in location.split(",")])
).length < 0.05, f"Object is at {obj_location} relative to the model origin instead of {location}"
assert_vert_at_map_coordinates(obj, obj.matrix_world.translation, coordinates)
@then(parsers.parse('the object "{name}" has a vert at "{location}" at map coordinates "{coordinates}"'))
def the_object_name_has_a_vert_at_location_at_map_coordinates(name, location, coordinates):
"""Check where a vert sits in Blender and where it is in the world.
Both matter: the Blender location is what the user sees, and checking only
the map coordinates would pass just as happily if the georeferencing maths
or the offsets it reads were wrong, since the same maths produces both.
"""
obj = the_object_name_exists(name)
target = Vector([float(co) for co in location.split(",")])
verts = get_world_verts(obj)
vert = next((v for v in verts if (v - target).length < 0.001), None)
assert vert is not None, f"No vert found at {location}: {verts}"
assert_vert_at_map_coordinates(obj, vert, coordinates)
@then(
parsers.parse(
'the object "{name}" has a vert at "{location}" relative to the model origin at map coordinates "{coordinates}"'
)
)
def the_object_name_has_a_vert_at_location_relative_to_the_model_origin_at_map_coordinates(name, location, coordinates):
"""As above, for when the whole model has been shifted onto the origin.
Blender locations are then only meaningful relative to that origin, since
everything moves together with it.
"""
obj = the_object_name_exists(name)
target = Vector([float(co) for co in location.split(",")]) - get_model_origin()
verts = get_world_verts(obj)
vert = next((v for v in verts if (v - target).length < 0.001), None)
assert vert is not None, f"No vert found at {location} relative to the model origin: {verts}"
assert_vert_at_map_coordinates(obj, vert, coordinates)
@then(parsers.parse('the object "{name}" has its origin on a vertex'))
def the_object_name_has_its_origin_on_a_vertex(name):
"""Far away geometry is shifted onto one of its own verts, which keeps the
origin on the geometry and the local coordinates small enough to keep their
precision. Which vert that is does not matter."""
obj = the_object_name_exists(name)
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh) and len(mesh.vertices), f"Object {obj.name} has no mesh"
nearest = min(v.co.length for v in mesh.vertices)
assert nearest < 0.001, f"Object origin is {nearest} away from its nearest vert"
@then("the model origin is on an object vertex")
def the_model_origin_is_on_an_object_vertex():
for obj in bpy.data.objects:
if not isinstance(obj.data, bpy.types.Mesh):
continue
if any(v.length < 0.001 for v in get_world_verts(obj)):
return
assert False, "No object has a vert at the model origin"
@then(parsers.parse('the object "{name}" has no scale'))
def the_object_name_has_no_scale(name):
assert the_object_name_exists(name).scale == Vector(
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.ruff]
extend = "../pyproject.toml"
lint.ignore = [
"F401", # unused imports
"unused-import", # unused imports
]
+3 -3
View File
@@ -168,7 +168,7 @@ ifcopenshell_deploy_qt_runtime(BonsaiViewer)
# them explicitly. (In a static build these are absent from lib/
# and the glob just no-ops, so this rule is safe in both modes.)
#
# 2. Plug-ins (ifcopenshell.*.dylib, no `lib` prefix) dlopen-only
# 2. Plug-ins (ifcopenshell_*.dylib, no `lib` prefix) dlopen-only
# deps the plug-in loader resolves at runtime. macdeployqt has
# no way to know about these.
#
@@ -177,7 +177,7 @@ ifcopenshell_deploy_qt_runtime(BonsaiViewer)
# inside the bundle), so plug-ins and core libs both find each other
# on the first probe.
#
# The geometry-writer filter drops ifcopenshell.geometry.writer.*.dylib
# The geometry-writer filter drops ifcopenshell_geometry_writer_*.dylib
# (the per-schema OBJ / glTF / DAE / STP / IGS / SVG / TTL export
# converters heavy, viewer-irrelevant). Mirrors the Rocky workflow's
# filter in `stage_runtime_payload` (see 27249770e).
@@ -195,7 +195,7 @@ if(APPLE)
install(CODE [[
set(_fw "${CMAKE_INSTALL_PREFIX}/BonsaiViewer.app/Contents/Frameworks")
file(GLOB _ifc_dylibs "${CMAKE_INSTALL_PREFIX}/lib/*.dylib")
list(FILTER _ifc_dylibs EXCLUDE REGEX "ifcopenshell\\.geometry\\.writer\\.")
list(FILTER _ifc_dylibs EXCLUDE REGEX "ifcopenshell_geometry_writer_")
if(_ifc_dylibs)
message(STATUS "Staging IfcOpenShell dylibs (linked core + plug-ins) into BonsaiViewer.app/Contents/Frameworks")
file(COPY ${_ifc_dylibs} DESTINATION "${_fw}")

Some files were not shown because too many files have changed in this diff Show More