Compare commits

...

99 Commits

Author SHA1 Message Date
Ryan Schultz 0844550d06 feat(selector): add variable substitution and arithmetic to format()
- Add {{variable}} syntax for element property references
- Add arithmetic operators: +, -, *, / with proper precedence
- Extend format() with optional element parameter
- Variables work with all format functions (imperial_length, round, etc.)
- Maintains full backward compatibility

Example: format("imperial_length({{z}} / 2, 4)", element)
2025-11-24 11:25:33 -06:00
Andrej730 1d0b4675c0 Ruff - fix usage of deprecated Tuple, List 2025-11-24 11:01:26 +05:00
Andrej730 497ebc55e7 black . 2025-11-24 11:01:26 +05:00
Ryan Schultz 97a501036b Have dimensions like 4' - 1/2" read like 4' - 0 1/2", as is more the convention. 2025-11-23 18:22:40 -06:00
Gorgious 592c51b526 Add interactive gizmo system for editing parmeterized door
Implement visual parameter manipulation in 3D viewport with reusable
gizmo components. Features snap-to-mesh (Ctrl), precision mode (Shift),
and real-time feedback. Includes cone gizmos for dimensions,
arc gizmo for swing direction, and icon gizmos for edit controls. Gizmos are globally are individually toggleable in the addon preferences.

May be expanded upon later to add gizmos to other paramaterized elements (window, railing, roof, stairs, array, ...).

Unfortunately gizmos can't tap into the builtin snap system or bonsai snap system which is built for modal operators so gizmos implement yet another custom rather naive snap system.
2025-11-23 20:16:27 +01:00
Gorgious 3dd7290ee1 Display default parameters for parametric elements under "Model" in the preferences 2025-11-23 15:43:59 +01:00
Ryan Schultz d7ac5f2441 Fix #7397 - don't delete aggregate if relatingobject is a IfcSpatialElement 2025-11-23 08:09:13 -06:00
Thomas Krijnen 461404cd45 Don't terminate parsing on invalid entity names 2025-11-23 11:16:47 +01:00
Ryan Schultz bfd5b1bede add to https://github.com/IfcOpenShell/IfcOpenShell/commit/57bc709b6fef6381bbb2810da15e568290c4a77a: Fix elevation display by converting from project units to meters on import
When importing spatial decomposition, get_storey_elevation() returns values in the project's unit system (e.g., feet), but format_distance() expects values in meters. Added unit_scale conversion (elevation * unit_scale) before formatting to ensure elevations display correctly regardless of project units.
2025-11-22 16:53:28 -06:00
Ryan Schultz 0d0b6cc6b2 Additional fixes to https://github.com/IfcOpenShell/IfcOpenShell/commit/57bc709b6fef6381bbb2810da15e568290c4a77a 2025-11-22 15:40:47 -06:00
Ryan Schultz 57bc709b6f Fix imperial unit conversion for plan and section level decorators
Level annotations were incorrectly converting feet to meters and back,
causing values like 3ft to display as 9'10". Updated format_value() to
accept in_unit_length parameter and pass it to format_distance(), since
Blender's Z coordinates are already in project units.

- Add in_unit_length parameter to BaseDecorator.format_value()
- Set in_unit_length=True in PlanLevelDecorator.draw_labels()
- Set in_unit_length=True in SectionLevelDecorator.draw_labels()
2025-11-22 14:39:13 -06:00
Ryan Schultz b7b6e4aadf Consolidate format_distance functions and fix imperial unit formatting
Replaced duplicate format_distance in unit.py with comprehensive MeasureIt-ARCH version from helper.py. Fixed critical floating point errors by changing inPerFoot from 11.9999 to 12.0, eliminating spurious fractions on whole numbers. Implemented auto-precision detection to simplify fractions (64/256 → 1/4) while preserving precise values like 3/256 when needed. Enhanced imperial parsing to support flexible input formats (optional symbols, mixed fractions, leading decimals) and fixed suppress_zero_inches functionality in dimension annotations.
2025-11-22 13:49:30 -06:00
Gorgious 9c39ac0a48 You can now edit default parameters when generating a new parametric geometry for Door, Window, Railing, Roof and Stair types.
UI is in the addon preferences.
2025-11-22 18:12:50 +01:00
Gorgious a77f1b9fd1 Fix bug where one could setup negative lengths in the parametric door properties that whould throw an error when validating the geometry 2025-11-22 15:28:51 +01:00
Gorgious 63921e3fd8 Improve formatting for imperial units for the elevation of spatial elements elevation in the spatial decomposition panel (Re #7373) 2025-11-22 13:32:19 +01:00
Massimo Fabbro 27d0b04db5 black 2025-11-22 09:02:53 +01:00
Massimo Fabbro 87283874a1 Add simple examples for ifc5d import from csv 2025-11-22 09:02:53 +01:00
Gorgious 584d37de50 Fix #6048 (and #7373 ?) Adding a distance with a unit for an IfcSpatialElement in the Spatial Decomposition list doesn't save value
Support imperial units
Spatial element empty is moved in the 3d viewport accordingly
2025-11-22 01:16:39 +01:00
Gorgious ca2058d56b Fix #4974 - remap parent relationships when duplicating objects 2025-11-22 00:30:01 +01:00
Gorgious 481676e51b fix - recalculate normals for wall-mounted handrail railings
Fixes inverted normals on SweptDiskSolid end caps by recalculating
face normals after IFC geometry is loaded into Blender.

Fixes #6483
2025-11-22 00:04:57 +01:00
Gorgious56 fe502577c9 Merge pull request #7400 from IfcOpenShell/0.8.0
fix - apply object scale before using it to void an element #6378
2025-11-21 23:56:10 +01:00
Gorgious 65e8ca17d2 fix - apply object scale before using it to void an element #6378 2025-11-21 23:54:20 +01:00
Gorgious56 6bd7f02208 Merge pull request #7399 from IfcOpenShell/main
fix - handle float64 checksums from files saved in previous Blender v…
2025-11-21 23:28:28 +01:00
Gorgious 452d0f6023 fix - handle float64 checksums from files saved in previous Blender versions
Files saved in previous Blender versions stored position checksums as float64
(24 bytes for location, 72 bytes for rotation), but the current code only
expected float32 format. This caused a shape mismatch error (6,) vs (3,) when
comparing checksums during save operations.

Added byte size detection in is_moved() methods to automatically handle both
old float64 and new float32 checksum formats, ensuring backward compatibility
with files from previous versions.

Fixes ValueError when saving IFC files loaded from older Blender versions.
2025-11-21 23:23:57 +01:00
Andrej730 07005a9d3b build-deps - switch to freetype github archives for stability #7390 2025-11-22 01:02:41 +05:00
Andrej730 450ce689ba build-deps - fix set resetting ERRORLEVEL in commands logged in 52d3bae54 #7390 2025-11-22 01:02:41 +05:00
Andrej730 7c210721e6 build-deps - add pushd/popd for consistency 2025-11-22 01:02:16 +05:00
Andrej730 7e48d712a2 cmake - fix error building with GLTF_SUPPORT=OFF (487a4f3, #7390) 2025-11-21 18:28:06 +05:00
Andrej730 9eedbc6f72 FindOpenCASCADE - fix linking issue with non-configs #7390
Unexpected consequence of #7365 - previously `OPENCASCADE_LIBRARIES` supposed to be a list of libraries filepaths, but after refactor `OpenCASCADE_LIBRARIES` end up being both list of library names and list of filepaths.
And since link directories for the names was not provided, it failed to find the libraries to link.
If using OpenCASCADEconfig it wouldn't fail as cmake would evaluate library names as imported target names.

To fix it and simplify it even further, in case of manually provided paths, it now creates `OpenCASCADE_INTERFACE` target and stores it in `OpenCASCADE_LIBRARIES` - target will automatically have all the includes, library names and library dirs.
2025-11-21 18:28:06 +05:00
Andrej730 68b021b0e6 build-deps - fix hdf5 warnings in logs (similar to 890665b77) 2025-11-21 18:28:06 +05:00
Andrej730 a559b72e18 build-deps - fix boost project cache reset targeting wrong path
Noticed by another "The system cannot find the path specified." in logs 😅
2025-11-21 18:28:06 +05:00
Andrej730 32496c40b3 build-deps - fix missing popd for mpfr dir leading to issues installing hdf5 #7390 2025-11-21 18:28:05 +05:00
Andrej730 1b249b1013 build-deps - fix very very old bug with GitCloneAndCheckoutRevision not always checking out revision
Since this if-block happens in parentheses, then all `%VAR%` are expanded at the start and then `%ERRORLEVEL%` and `%RET%` values have no relation to the actual result of `git clone`. So if user were (un)lucky, then `%RET%` would be not yet set to "0" (by the previous function call), then it would skip checking out revision.

`GitCloneOrPullRepository` had similar issue, but it was never used anywhere.
2025-11-21 18:28:05 +05:00
Andrej730 d3fcf39c76 Windows build scripts - ECHO_ON env variable for debugging 2025-11-21 18:28:05 +05:00
Andrej730 692ef5ecea cmake - fix error finding Eigen::Eigen target #7390
`EIgen::Eigen` target used in #7340 for linking exists only if there's a cmake config, while there are cases when there's just include directory. Added same name interface target to mimic config.
2025-11-20 20:12:48 +05:00
Andrej730 f56a97512a Rename IFC_GEOM_EXPORTS replaced in #7340 2025-11-20 20:12:48 +05:00
Andrej730 4f5ebef5da cmake - use HDF5_INCLUDE_DIR instead HDF5_INCLUDE_DIRS
As this is the variable defined by `FindHDF5`.
2025-11-20 20:12:48 +05:00
Andrej730 57a724e80f FindCGAL.cmake 2025-11-20 20:12:48 +05:00
Andrej730 1b8652a789 FindOpenCOLLADA.cmake 2025-11-20 20:12:48 +05:00
Andrej730 c1aa702896 FindHDF5 - describe used variables, initialize from env 2025-11-20 20:12:47 +05:00
Andrej730 5b43d2b75e HDF5Config -> FindHDF5
See the reasoning in https://github.com/IfcOpenShell/IfcOpenShell/pull/7193#issuecomment-3552830928
2025-11-20 20:12:47 +05:00
Andrej730 3330e79556 FindLibXml2 - initialize from env variables 2025-11-20 20:12:47 +05:00
Andrej730 88d09c83b7 FindLibXml2 - add_debug_variants 2025-11-20 20:12:47 +05:00
Andrej730 5e6b96ee99 cmake - remove wasm_sysroot as superseded by e833d0c 2025-11-20 20:12:47 +05:00
Thomas Krijnen bd07078def Remove spurious cd 2025-11-20 16:12:26 +01:00
Andrej730 1d01d1f184 build-all.py - postpone 4396126 2025-11-20 16:58:04 +05:00
Dion Moult 3cee0811d4 Update VERSION 2025-11-20 22:06:24 +11:00
Dion Moult a46228a57e Hotfix to generate IOS docs until we properly package bsdd 2025-11-20 21:16:57 +11:00
Andrej730 158fe92644 HDF5Config.cmake - use CMAKE_SOURCE_DIR instead PROJECT_SOURCE_DIR
It doesn't really matter in this case - it matters really only for FindLibXml2, because `PROJECT_SOURCE_DIR` point to svgfill, when it's config executed, instead of `CMAKE_SOURCE_DIR` - so `CMAKE_SOURCE_DIR ` is never removed from the path, leading to cyclic calls. Adding for HDF5 too just for consistency.
2025-11-19 19:41:30 +05:00
Andrej730 12166ddbba HDF5Config.cmake - restore patched module path at the end 2025-11-19 19:41:30 +05:00
Andrej730 36a0c22217 FindUSD.cmake (untested) 2025-11-19 19:41:30 +05:00
Andrej730 59cc9d4a23 FindLibXml2.cmake 2025-11-19 19:41:29 +05:00
Andrej730 f3d918f76c FindOpenCASCADE - describe used variables at the top, move env vars 2025-11-19 19:41:29 +05:00
Esteban Dugueperoux 613015e37e cmake: Externalize OpenCascade config (#7365)
Co-authored-by: Andrej <azhilenkov@gmail.com>
2025-11-19 18:02:07 +05:00
Esteban DUGUEPEROUX be0eea1bed cmake: Have a dedicated CMakeLists.txt for IfcConvert & co 2025-11-19 13:28:45 +01:00
dependabot[bot] 37bbc0b693 Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:11:39 +01:00
dependabot[bot] 30093499fa Bump aws-actions/configure-aws-credentials from 4 to 5
Bumps [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) from 4 to 5.
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/v4...v5)

---
updated-dependencies:
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:08:39 +01:00
dependabot[bot] 8e95bf73f1 Bump actions/upload-pages-artifact from 3 to 4
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:08:09 +01:00
dependabot[bot] 05f7aac853 Bump docker/setup-buildx-action from 1 to 3
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 1 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v1...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:08:02 +01:00
dependabot[bot] b135dd6994 Bump actions/setup-python from 2 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:07:34 +01:00
dependabot[bot] a5e7d7c4cc Bump docker/login-action from 1 to 3
Bumps [docker/login-action](https://github.com/docker/login-action) from 1 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:07:26 +01:00
dependabot[bot] 9680f6b29c Bump docker/build-push-action from 2 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 2 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v2...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:05:03 +01:00
dependabot[bot] bc8dd3a3ab Bump mamba-org/setup-micromamba from 1 to 2
Bumps [mamba-org/setup-micromamba](https://github.com/mamba-org/setup-micromamba) from 1 to 2.
- [Release notes](https://github.com/mamba-org/setup-micromamba/releases)
- [Commits](https://github.com/mamba-org/setup-micromamba/compare/v1...v2)

---
updated-dependencies:
- dependency-name: mamba-org/setup-micromamba
  dependency-version: '2'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:04:39 +01:00
dependabot[bot] 9caf47b41f Bump docker/setup-qemu-action from 1 to 3
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 1 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v1...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:04:14 +01:00
dependabot[bot] 5e1787b146 Bump actions/download-artifact from 4.1.7 to 6.0.0
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.7 to 6.0.0.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4.1.7...v6.0.0)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:03:52 +01:00
dependabot[bot] 07147f4b7f Bump actions/checkout from 2 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 5.
- [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/v2...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:02:55 +01:00
dependabot[bot] 4dfe15bd88 Merge pull request #7270 from IfcOpenShell/dependabot/npm_and_yarn/src/ifctester/webapp/vite-6.4.1 2025-11-19 17:00:32 +05:00
Andrej730 5896630630 Add dependabot to track github actions versions 2025-11-19 16:58:22 +05:00
Andrej730 48b1493c5c build_win - use official ccache-action as PR got merged 2025-11-19 16:58:22 +05:00
Andrej730 ee75ea1182 black . 2025-11-19 16:58:22 +05:00
Esteban DUGUEPEROUX 1876a788a9 cmake: Externalize cmake config for Serializers 2025-11-19 11:24:15 +01:00
Esteban DUGUEPEROUX 9d28628157 cmake: Have a dedicated CMakeLists.txt for kernels 2025-11-19 11:23:04 +01:00
Esteban DUGUEPEROUX 9b257b0a10 Remove support of cityjson for ifcconvert binary 2025-11-19 11:19:58 +01:00
Thomas Krijnen db7900027e fix stub 2025-11-19 10:43:52 +01:00
Andrej730 2af7470950 run-cmake.bat - use boost installation path
Now there's no need for BOOST_ROOT and BOOST_LIBRARYDIR anymore.
2025-11-18 19:59:34 +05:00
Andrej730 e1c3c44fb3 IfcGeomServer cmake - remove headers from source files 2025-11-18 19:59:34 +05:00
Andrej730 e7c422469a IfcGeomServer cmake - remove not required chrono component #7337
Noticed Linux build failing now
2025-11-18 19:59:34 +05:00
Andrej730 52d3bae543 build-deps - add some logs to make errors easier to narrow donw 2025-11-18 19:59:34 +05:00
Andrej730 b130925c5f build-deps - fix missing cd before trying to extract boost archive
Not sure when it was introduced, but it was still using previous dependency path and leading to confusing error:
```
Boost 1.86.0 already downloaded. Skipping.
Extracting Boost 1.86.0 into L:\Projects\Github\IfcOpenShell\_deps from boost-1.86.0-b2-nodocs.7z
An error occurred
```
2025-11-18 19:59:34 +05:00
Andrej730 890665b77b build-deps - fix reappearing "The system cannot find the file specified." error in logs
because of the failing rename
2025-11-18 19:59:34 +05:00
Andrej730 99c95bc92d black . 2025-11-18 19:59:34 +05:00
Bruno Perdigão 8647f9c357 Polyline tool - avoids rounding angle when distance is too big. 2025-11-17 20:38:34 -03:00
Bruno Perdigão 300855276f See #7046. Add Numpy version of a few Blender fuctions to improve precision. The snapping system was having a poor precision with large numbers. Some functions were rewritten to use Numpy instead of Mathutils. This seems to solve the issue, but further adjustments might be necessary
Note: rv3d.perspective_matrix should be avoided in these context. Use a numpy version of np.array(window_matrix) @ np.array(view_matrix). See https://github.com/IfcOpenShell/IfcOpenShell/issues/7046

Note2: region_2d_to_origin_3d_np clamp should be used for the snap to work in orthographic view.
2025-11-17 20:38:34 -03:00
Bruno Perdigão 1a8831012b Fix snapping for blender curve objects 2025-11-17 20:38:34 -03:00
Thomas Krijnen 1bc4e82b7d reorder 2025-11-17 19:15:20 +01:00
Thomas Krijnen fc197c1d32 anon namespace 2025-11-17 19:09:31 +01:00
Thomas Krijnen 33cfa9e2b1 function3 2025-11-17 18:57:22 +01:00
Thomas Krijnen 9e9d664511 .glb --separate-z-up-node option 2025-11-17 18:26:00 +01:00
Ryan Schultz 1b9acadd81 Disable text decorations when not in camera view 2025-11-17 09:01:19 -06:00
Andrej730 c5f6d47c44 build-deps - include swig version to dependency name 2025-11-17 19:29:07 +05:00
Andrej730 d19b6d72ef build-all.py - include swig-version to dependency name
Otherwise it doesn't update and keeps using older cached version.
2025-11-17 19:28:10 +05:00
Andrej730 919231b096 build-all.py - remove outdated comment 2025-11-17 19:28:10 +05:00
Andrej730 cead5cc75d ci - add swig build to fix failing stub test
Older swig versions have some redundant symbols, using the one we use in other build scripts.
2025-11-17 19:28:09 +05:00
Andrej730 9396cb226f ci - add opencollada to fix failing stub test 2025-11-17 19:28:09 +05:00
Andrej730 56a63cab6e build-deps - sync swig version with build-all.py 2025-11-17 19:28:09 +05:00
Andrej730 449d13bd5f Fix stub missing construct_iterator 2025-11-17 19:26:14 +05:00
Thomas Krijnen 487a4f345d Creoox json serializer 2025-11-17 14:06:39 +01:00
Thomas Krijnen d6daa64c48 Support --element-hierarchy in .glb output 2025-11-17 14:05:59 +01:00
Thomas Krijnen bf0aa247a0 IfcConvert: only conditionally bypass properties 2025-11-17 13:50:50 +01:00
130 changed files with 4563 additions and 1086 deletions
+8
View File
@@ -0,0 +1,8 @@
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
+4 -4
View File
@@ -21,12 +21,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -81,7 +81,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: build-logs-osx-${{ matrix.arch }}
path: |
@@ -139,7 +139,7 @@ jobs:
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+4 -4
View File
@@ -9,13 +9,13 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
submodules: recursive
path: IfcOpenShell
- name: Checkout Build Repository
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
repository: IfcOpenShell/build-outputs
path: ifcopenshell_build
@@ -42,7 +42,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: build-logs-pyodide
path: |
@@ -77,7 +77,7 @@ jobs:
git push || echo "Push failed"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+6 -6
View File
@@ -29,12 +29,12 @@ jobs:
aws --version
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -48,8 +48,8 @@ jobs:
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
- name: ccache
# Latest release (1.2.19) doesn't support Rocky, so using a specific commit.
uses: hendrikmuhs/ccache-action@eca11c308176d48942455b9e5b1b70ff6950a778
# TODO: Use tag after 1.2.20 releases.
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
@@ -61,7 +61,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: build-logs-rocky
path: |
@@ -118,7 +118,7 @@ jobs:
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+6 -6
View File
@@ -29,12 +29,12 @@ jobs:
aws --version
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -48,8 +48,8 @@ jobs:
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
- name: ccache
# Latest release (1.2.19) doesn't support Rocky, so using a specific commit.
uses: hendrikmuhs/ccache-action@eca11c308176d48942455b9e5b1b70ff6950a778
# TODO: Use tag after 1.2.20 releases.
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
@@ -61,7 +61,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: build-logs-rocky-arm64
path: |
@@ -118,7 +118,7 @@ jobs:
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+5 -5
View File
@@ -12,12 +12,12 @@ jobs:
arch: ['x64']
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
repository: IfcOpenShell/build-outputs
path: _deps-vs2022-x64-installed
@@ -37,8 +37,8 @@ jobs:
}
- name: ccache
# Use fork to resolve cache misses / duplicated cache entries on Windows.
uses: Andrej730/ccache-action@main
# TODO: Use tag after 1.2.20 releases.
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
with:
key: win-${{ matrix.arch }}
# Windows ccache needs ~1GB
@@ -73,7 +73,7 @@ jobs:
git push || echo "Push failed"
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+2 -2
View File
@@ -19,8 +19,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2 # https://github.com/actions/checkout
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5 # https://github.com/actions/checkout
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+3 -3
View File
@@ -9,15 +9,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Action - checkout repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v5
- name: Action - install python
uses: actions/setup-python@v5.3.0
uses: actions/setup-python@v6
with:
python-version: "3.9"
- name: Action - install python
uses: actions/setup-python@v5.3.0
uses: actions/setup-python@v6
with:
python-version: "3.11"
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-tags: true
fetch-depth: 0
+3 -3
View File
@@ -54,8 +54,8 @@ jobs:
short_name: macosm1,
}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
@@ -93,7 +93,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout bonsai_unstable_repo repository
uses: actions/checkout@v2
uses: actions/checkout@v5
with:
repository: IfcOpenShell/bonsai_unstable_repo
token: ${{ secrets.IFCOPENBOT_TOKEN }}
+2 -2
View File
@@ -43,8 +43,8 @@ jobs:
short_name: macosm1,
}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -37,8 +37,8 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
@@ -24,7 +24,7 @@ jobs:
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- uses: mamba-org/setup-micromamba@v1 # https://github.com/mamba-org/setup-micromamba
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
with:
environment-name: test-env
create-args: >-
@@ -21,7 +21,7 @@ jobs:
date: ${{ steps.date.outputs.date }}
verdate: ${{ steps.verdate.outputs.verdate }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set env
run: echo ok go
@@ -76,7 +76,7 @@ jobs:
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
fi
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
+8 -8
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-22.04
needs: activate
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
with:
submodules: recursive
@@ -76,7 +76,7 @@ jobs:
make package
working-directory: build
- name: Upload
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
# Artifact name
name: ifcos-artifacts
@@ -89,31 +89,31 @@ jobs:
name: Docker Build, Tag, Push
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
with:
lfs: true
- name: Download
uses: actions/download-artifact@v4.1.7
uses: actions/download-artifact@v6.0.0
with:
# Artifact name
name: ifcos-artifacts
path: artifacts/
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v3
-
name: Login to Dockerhub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: aecgeeks
password: ${{ secrets.DOCKER_HUB_TOKEN }}
-
name: Build container image
uses: docker/build-push-action@v2
uses: docker/build-push-action@v6
with:
context: artifacts
repository: aecgeeks/ifcopenshell
@@ -47,10 +47,10 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+2 -2
View File
@@ -38,10 +38,10 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -25,8 +25,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
+2 -2
View File
@@ -19,8 +19,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
+2 -2
View File
@@ -10,9 +10,9 @@ jobs:
publish_website:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
- name: Checkout ifctester_org_static_html
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
repository: IfcOpenShell/ifctester_org_static_html
token: ${{ secrets.IFCOPENBOT_TOKEN }}
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+42 -5
View File
@@ -35,12 +35,12 @@ jobs:
runs-on: ubuntu-22.04
needs: activate
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: 3.11
@@ -64,13 +64,16 @@ jobs:
libboost-regex-dev \
libboost-system-dev \
libboost-thread-dev \
swig libpcre3-dev libxml2-dev \
libpcre3-dev libxml2-dev \
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 \
libhdf5-dev libcgal-dev libeigen3-dev
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
# TODO: temporarily pointing to 1.2.19 to get notified by dependabot when 1.2.20 is released
# to update hardcoded references to commits in some other workflows.
# Then we can switch back to 1.2 in all actions.
uses: hendrikmuhs/ccache-action@v1.2.19
with:
key: ubuntu-22.04-${{ runner.arch }}
@@ -97,6 +100,41 @@ jobs:
..
sudo make -j$(nproc) install
# OpenCOLLADA is ancient, but we still have it in the main build.
# So adding it to CI to catch any breakages.
- name: build OpenCOLLADA
run: |
git clone https://github.com/KhronosGroup/OpenCOLLADA
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
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_FLAGS_INIT="-fPIC"
sudo make -j$(nproc) install
# Ubuntu has `swig` package, but we build it to match the version we use in the main build.
# To avoid failing tests when checking stub generation.
- name: build swig
run: |
# Remove default swig to avoid conflicts.
sudo apt remove --purge swig swig4.0
sudo apt-get install -y libpcre2-dev bison
git clone https://github.com/swig/swig
cd swig
git checkout v4.1.0
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
sudo make -j$(nproc) install
- name: Build ifcopenshell
run: |
echo $Python3_ROOT_DIR
@@ -114,7 +152,6 @@ jobs:
-DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DPYTHON_EXECUTABLE:FILEPATH=${{ env.pythonLocation }}/bin/python \
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
-DCOLLADA_SUPPORT=Off \
-DUSE_MMAP=On \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
-DGLTF_SUPPORT=On \
@@ -11,10 +11,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v6
with:
python-version: '3.x'
+2 -2
View File
@@ -9,10 +9,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v6
with:
python-version: '3.x'
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout (recursive)
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
submodules: recursive
fetch-depth: 0
@@ -34,7 +34,7 @@ jobs:
uses: actions/configure-pages@v5
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v4
with:
path: src/pyodide/demo-app/
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
with:
submodules: recursive
- name: Install C++ dependencies
+1
View File
@@ -17,6 +17,7 @@
# General Python residue
__pycache__
*.py.bak
venv
# Visual Studio Code files
.vscode
-3
View File
@@ -11,9 +11,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 "src/ifcconvert/cityjson"]
path = src/ifcconvert/cityjson
url = https://github.com/IfcOpenShell/ifc-to-cityjson
[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
+1 -1
View File
@@ -1 +1 @@
0.8.4
0.8.5
+20 -538
View File
@@ -79,7 +79,6 @@ option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
option(WITH_PROJ "Enable output of Earth-Centered Earth-Fixed glTF output using the PROJ library" OFF)
option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
option(CITYJSON_SUPPORT "Build IfcConvert with CityJSON support (requires CityJSON library)." OFF)
option(WITH_RELATIONSHIP_VALIDATION "Build IfcConvert with option to validate geometrical relationships." OFF)
option(WITH_ROCKSDB "Support a RocksDB key-value store as a file backend in IfcOpenShell" OFF)
option(WITH_ZSTD "Use Zstd compression in RocksDB writes" OFF)
@@ -199,30 +198,13 @@ if(BUILD_SHARED_LIBS)
set(IFCOPENSHELL_LIBRARY_DIR "${LIBDIR}")
endif()
UNIFY_ENVVARS_AND_CACHE(OCC_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OCC_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(BOOST_ROOT)
UNIFY_ENVVARS_AND_CACHE(BOOST_LIBRARYDIR)
UNIFY_ENVVARS_AND_CACHE(EIGEN_DIR)
if(NOT MINIMAL_BUILD)
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(LIBXML2_LIBRARIES)
UNIFY_ENVVARS_AND_CACHE(PCRE_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(PYTHON_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(PYTHON_LIBRARY)
UNIFY_ENVVARS_AND_CACHE(PYTHON_EXECUTABLE)
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
UNIFY_ENVVARS_AND_CACHE(CGAL_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(CGAL_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_LIBRARY_DIR)
endif()
# Get a list of all OPTION flags from the CMakeLists.txt
@@ -233,53 +215,13 @@ foreach(option_flag IN LISTS option_flags)
convert_env_var_to_bool("${option_flag}")
endforeach()
set(CMAKE_FIND_ROOT_PATH_BACKUP "${CMAKE_FIND_ROOT_PATH}")
macro(clear_wasm_sysroot)
if(WASM_BUILD)
# when using the nix/build-all.py build script we should not
# look into the sysroot for most of the dependencies but rather
# in the designated build/ folder created by the script.
set(CMAKE_FIND_ROOT_PATH "")
endif()
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
endmacro()
macro(restore_wasm_sysroot)
if(WASM_BUILD)
# reset to use sysroot
set(CMAKE_FIND_ROOT_PATH "${CMAKE_FIND_ROOT_PATH_BACKUP}")
endif()
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endmacro()
if(WITH_CGAL)
if(NOT CGAL_INCLUDE_DIR)
# CGAL is not respecting default Boost_USE_STATIC_LIBS value
# and sometiems it's getting in the way.
if(NOT DEFINED Boost_USE_STATIC_LIBS)
set(CGAL_Boost_USE_STATIC_LIBS OFF)
else()
set(CGAL_Boost_USE_STATIC_LIBS "${Boost_USE_STATIC_LIBS}")
endif()
find_package(CGAL REQUIRED)
if(NOT CGAL_DIR)
message(
FATAL_ERROR
"CGAL_SUPPORT enabled, but CGAL_INCLUDE_DIR wasn't provided and CGAL package couldn't be found."
)
endif()
message(STATUS "CGAL: found config at '${CGAL_DIR}'.")
if(TARGET CGAL::CGAL)
link_libraries(CGAL::CGAL)
endif()
add_definitions(-DIFOPSH_WITH_CGAL)
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_CGAL)
if(CITYJSON_SUPPORT)
add_definitions(-DIFOPSH_WITH_CITYJSON)
endif()
list(APPEND GEOMETRY_KERNELS cgal)
endif()
@@ -289,7 +231,7 @@ if(WITH_OPENCASCADE)
list(APPEND GEOMETRY_KERNELS opencascade)
endif()
if(GLTF_SUPPORT OR CITYJSON_SUPPORT)
if(GLTF_SUPPORT)
UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR)
if(NOT JSON_INCLUDE_DIR)
find_package(nlohmann_json CONFIG)
@@ -301,9 +243,7 @@ if(GLTF_SUPPORT OR CITYJSON_SUPPORT)
endif()
if(NOT nlohmann_json_DIR)
clear_wasm_sysroot()
find_path(json_header_path "nlohmann/json.hpp" HINTS ${JSON_INCLUDE_DIR})
restore_wasm_sysroot()
set(JSON_INCLUDE_DIR ${json_header_path})
if(json_header_path)
@@ -319,49 +259,7 @@ endif()
# Add USD support to serializers
if(USD_SUPPORT)
UNIFY_ENVVARS_AND_CACHE(USD_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(USD_LIBRARY_DIR)
if("${USD_INCLUDE_DIR}" STREQUAL "")
find_path(USD_INCLUDE_DIR pxr.h
PATHS
/usr/include/pxr
/usr/local/include/pxr
REQUIRED
)
if(USD_INCLUDE_DIR)
message(STATUS "Found USD include files in: ${USD_INCLUDE_DIR}")
else()
message(FATAL_ERROR "Unable to find USD include directory, specify USD_INCLUDE_DIR manually.")
endif()
else()
set(USD_INCLUDE_DIR ${USD_INCLUDE_DIR} CACHE FILEPATH "USD header files")
message(STATUS "Looking for USD include files in: ${USD_INCLUDE_DIR}")
endif()
set(USD_LIBRARIES
usd_usd
usd_usdGeom
usd_usdShade
usd_usdLux
usd_vt
usd_sdf
usd_tf
usd_gf
)
find_library(USD_LIBRARY
NAMES ${USD_LIBRARIES}
PATHS ${USD_LIBRARY_DIR})
if(USD_LIBRARY)
message(STATUS "USD libraries ${USD_LIBRARIES} found in: ${USD_LIBRARY_DIR}")
link_directories(${USD_LIBRARY_DIR})
else()
message(FATAL_ERROR "Unable to find USD libraries in: ${USD_LIBRARY_DIR}")
endif()
add_definitions(-DWITH_USD)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_USD)
find_package(USD REQUIRED)
endif(USD_SUPPORT)
if (WITH_ROCKSDB)
@@ -443,44 +341,15 @@ if(USE_MMAP)
add_definitions(-DUSE_MMAP)
endif()
clear_wasm_sysroot()
find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS})
restore_wasm_sysroot()
message(STATUS "Boost include files found in ${Boost_INCLUDE_DIRS}")
message(STATUS "Boost libraries found in ${Boost_LIBRARY_DIRS}")
if(NOT MINIMAL_BUILD)
# libxml2 is required for IFCXML (optional) and SVGFILL (mandatory)
clear_wasm_sysroot()
if(IFCXML_SUPPORT)
if((NOT LIBXML2_INCLUDE_DIR AND NOT LIBXML2_LIBRARIES))
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04)
# CONFIG is provided using root path, so no need to clear sysroot here.
restore_wasm_sysroot()
find_package(LibXml2 QUIET CONFIG)
clear_wasm_sysroot()
if(NOT LibXml2_FOUND)
# Fallback to CMake's builtin FindLibXml2 module (works on Ubuntu)
find_package(LibXml2 REQUIRED)
else()
message(STATUS "Found LibXml2 config: ${LibXml2_DIR}")
endif()
if(TARGET LibXml2::LibXml2)
# Config mode already gives us the target
set(LIBXML2_LIBRARIES LibXml2::LibXml2)
get_target_property(LIBXML2_INCLUDE_DIR LibXml2::LibXml2 INTERFACE_INCLUDE_DIRECTORIES)
else()
# Module mode (Ubuntu)
set(LIBXML2_LIBRARIES ${LibXml2_LIBRARIES})
set(LIBXML2_INCLUDE_DIR ${LibXml2_INCLUDE_DIRS})
endif()
else()
find_package(LibXml2 REQUIRED)
endif()
find_package(LibXml2 REQUIRED)
endif()
restore_wasm_sysroot()
endif()
if(IFCXML_SUPPORT)
@@ -489,248 +358,15 @@ if(IFCXML_SUPPORT)
endif()
if(BUILD_IFCGEOM)
if(MSVC AND NOT LibXml2_DIR)
add_debug_variants(LIBXML2_LIBRARIES "${LIBXML2_LIBRARIES}" d)
endif()
# Open CASCADE
if(WITH_OPENCASCADE)
if(OCC_INCLUDE_DIR)
set(OCC_INCLUDE_DIR ${OCC_INCLUDE_DIR} CACHE FILEPATH "Open CASCADE header files")
message(STATUS "Looking for Open CASCADE include files in: ${OCC_INCLUDE_DIR}")
endif()
if(OCC_LIBRARY_DIR)
set(OCC_LIBRARY_DIR ${OCC_LIBRARY_DIR} CACHE FILEPATH "Open CASCADE library files")
message(STATUS "Looking for Open CASCADE library files in: ${OCC_LIBRARY_DIR}")
endif()
# No specific paths specified, try to find package.
if(OCC_INCLUDE_DIR AND OCC_LIBRARY_DIR)
message(
STATUS
"Using provided OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') "
"and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}')."
)
# Parse OCC_VERSION_STRING.
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAJOR
REGEX "#define OCC_VERSION_MAJOR.*"
)
string(REGEX MATCH "[0-9]+" OCC_MAJOR ${OCC_MAJOR})
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MINOR
REGEX "#define OCC_VERSION_MINOR.*"
)
string(REGEX MATCH "[0-9]+" OCC_MINOR ${OCC_MINOR})
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAINT
REGEX "#define OCC_VERSION_MAINTENANCE.*"
)
string(REGEX MATCH "[0-9]+" OCC_MAINT ${OCC_MAINT})
set(OCC_VERSION_STRING "${OCC_MAJOR}.${OCC_MINOR}.${OCC_MAINT}")
elseif(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR)
# OCE is not supported for find_package, because it's using a different name (`oce`)
# and also has an odd directory structure (install/lib/oce-0.18/*.cmake).
find_package(OpenCASCADE CONFIG REQUIRED)
set(OCC_INCLUDE_DIR ${OpenCASCADE_INCLUDE_DIR})
# Do not use OpenCASCADE_LIBRARY_DIR for OCC_LIBRARY_DIR - check target property explicitly.
# On Windows there is a case with OpenCASCADE_LIBRARY_DIR points to `lib` folder,
# while TKernel is actually in `libi`.
get_target_property(TKERNEL_LIB_PATH TKernel LOCATION)
get_filename_component(OCC_LIBRARY_DIR "${TKERNEL_LIB_PATH}" DIRECTORY)
set(OCC_VERSION_STRING ${OpenCASCADE_VERSION})
message(
STATUS
"Found Open CASCADE package at '${OpenCASCADE_DIR}', "
"deducing from it OCC_INCLUDE_DIR: '${OCC_INCLUDE_DIR}' "
"and OCC_LIBRARY_DIR: '${OCC_LIBRARY_DIR}'."
)
else()
message(
FATAL_ERROR
"Couldn't find Open CASCADE installation. "
"Either both OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}') "
"must be specified or OpenCASCADE package should be discoverable. "
"If you're using OCE, then providing a package is not available "
"and you need to provide OCE_INCLUDE_DIR and OCE_LIBRARY_DIR directly."
)
endif()
set(
OPENCASCADE_LIBRARY_NAMES
TKernel TKMath TKBRep TKGeomBase TKGeomAlgo TKG3d TKG2d TKShHealing TKTopAlgo TKMesh TKPrim TKBool TKBO
TKFillet TKXSBase TKOffset TKHLR
# @todo investigate the exact conditions when this is necessary
TKBin
)
if(OCC_VERSION_STRING VERSION_LESS 7.8.0)
list(APPEND OPENCASCADE_LIBRARY_NAMES TKIGES TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP)
else(OCC_VERSION_STRING VERSION_LESS 7.8.0)
list(APPEND OPENCASCADE_LIBRARY_NAMES TKDESTEP TKDEIGES)
endif(OCC_VERSION_STRING VERSION_LESS 7.8.0)
clear_wasm_sysroot()
find_library(libTKernel NAMES TKernel TKerneld PATHS ${OCC_LIBRARY_DIR} NO_DEFAULT_PATH)
restore_wasm_sysroot()
if(libTKernel)
message(STATUS "Required Open Cascade Library files found")
else()
message(
FATAL_ERROR
"Unable to find Open Cascade library files in OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}'), aborting"
)
endif()
# Use the found libTKernel as a template for all other OCC libraries
# TODO Extract this into macro/function
foreach(lib ${OPENCASCADE_LIBRARY_NAMES})
# Make sure we'll handle the Windows/MSVC debug postfix convention too.
string(REPLACE TKerneld "${lib}" lib_path "${libTKernel}")
string(REPLACE TKernel "${lib}" lib_path "${lib_path}")
list(APPEND OPENCASCADE_LIBRARIES "${lib_path}")
endforeach()
if(MSVC)
add_definitions(-DHAVE_NO_DLL)
add_debug_variants(OPENCASCADE_LIBRARIES "${OPENCASCADE_LIBRARIES}" d)
endif()
if(WIN32)
# OCC might require linking to Winsock depending on the version and build configuration
list(APPEND OPENCASCADE_LIBRARIES ws2_32.lib)
endif()
# Make sure cross-referenced symbols between static OCC libraries get
# resolved. Also add thread and rt libraries.
get_filename_component(libTKernelExt ${libTKernel} EXT)
if("${libTKernelExt}" STREQUAL ".a")
set(OCCT_STATIC ON)
endif()
if(OCCT_STATIC)
find_package(Threads)
if(WASM_BUILD)
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
# OPENCASCADE_LIBRARIES repeated N times below in order to fix cyclic dependencies
# tfk: --start-group ... --end-group didn't work on the apple linker when last tested
if(APPLE)
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
set(OPENCASCADE_LIBRARIES -Wl,--start-group ${OPENCASCADE_LIBRARIES} -Wl,--end-group ${CMAKE_THREAD_LIBS_INIT})
endif()
endif()
if(NOT APPLE AND NOT WIN32)
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} "rt")
endif()
if(NOT WIN32)
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} "dl")
endif()
endif()
find_package(OpenCASCADE REQUIRED)
endif(WITH_OPENCASCADE)
endif(BUILD_IFCGEOM)
if(COLLADA_SUPPORT AND (NOT OPENCOLLADA_INCLUDE_DIR AND NOT OPENCOLLADA_LIBRARY_DIR))
# If package is found, automatically sets
# OPENCOLLADA_INCLUDE_DIRS and OPENCOLLADA_LIBRARIES (list of targets, not paths).
find_package(OpenCOLLADA CONFIG)
if(OpenCOLLADA_DIR)
message(STATUS "Found OpenCOLLADA: '${OpenCOLLADA_DIR}'.")
set(OPENCOLLADA_FOUND TRUE)
else()
message(STATUS "OpenCOLLADA package not found, falling back to manual search.")
endif()
if(COLLADA_SUPPORT)
find_package(OpenCOLLADA REQUIRED)
endif()
if(COLLADA_SUPPORT AND NOT OpenCOLLADA_DIR)
# Find OpenCOLLADA
if("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
message(STATUS "No OpenCOLLADA include directory specified")
set(OPENCOLLADA_INCLUDE_DIR "/usr/include/opencollada" CACHE FILEPATH "OpenCOLLADA header files")
else()
set(OPENCOLLADA_INCLUDE_DIR "${OPENCOLLADA_INCLUDE_DIR}" CACHE FILEPATH "OpenCOLLADA header files")
endif()
if("${OPENCOLLADA_LIBRARY_DIR}" STREQUAL "")
message(STATUS "No OpenCOLLADA library directory specified")
find_library(OPENCOLLADA_FRAMEWORK_LIB NAMES OpenCOLLADAFramework
PATHS /usr/lib64/opencollada /usr/lib/opencollada /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib)
get_filename_component(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_FRAMEWORK_LIB} PATH)
endif()
find_library(OpenCOLLADAFramework NAMES OpenCOLLADAFramework OpenCOLLADAFrameworkd PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
if(OpenCOLLADAFramework)
message(STATUS "OpenCOLLADA library files found")
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA libraries. "
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
endif()
set(OPENCOLLADA_LIBRARY_DIR "${OPENCOLLADA_LIBRARY_DIR}" CACHE FILEPATH "OpenCOLLADA library files")
set(OPENCOLLADA_INCLUDE_DIRS "${OPENCOLLADA_INCLUDE_DIR}/COLLADABaseUtils" "${OPENCOLLADA_INCLUDE_DIR}/COLLADAStreamWriter")
find_file(COLLADASWStreamWriter_h "COLLADASWStreamWriter.h" ${OPENCOLLADA_INCLUDE_DIRS})
if(COLLADASWStreamWriter_h)
message(STATUS "OpenCOLLADA header files found")
set(OPENCOLLADA_FOUND TRUE)
set(OPENCOLLADA_LIBRARY_NAMES
GeneratedSaxParser MathMLSolver OpenCOLLADABaseUtils OpenCOLLADAFramework OpenCOLLADASaxFrameworkLoader
OpenCOLLADAStreamWriter UTF buffer ftoa
)
# Use the found OpenCOLLADAFramework as a template for all other OpenCOLLADA libraries
foreach(lib ${OPENCOLLADA_LIBRARY_NAMES})
# Make sure we'll handle the Windows/MSVC debug postfix convention too.
string(REPLACE OpenCOLLADAFrameworkd "${lib}" lib_path "${OpenCOLLADAFramework}")
string(REPLACE OpenCOLLADAFramework "${lib}" lib_path "${lib_path}")
list(APPEND OPENCOLLADA_LIBRARIES "${lib_path}")
endforeach()
if("${PCRE_LIBRARY_DIR}" STREQUAL "")
if(WIN32)
find_library(pcre_library NAMES pcre pcred PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
else()
find_library(pcre_library NAMES pcre PATHS ${OPENCOLLADA_LIBRARY_DIR})
endif()
get_filename_component(PCRE_LIBRARY_DIR ${pcre_library} PATH)
else()
find_library(pcre_library NAMES pcre pcred PATHS ${PCRE_LIBRARY_DIR} NO_DEFAULT_PATH)
endif()
if(pcre_library)
set(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_LIBRARY_DIR} ${PCRE_LIBRARY_DIR})
if(MSVC)
# Add release lib regardless whether release or debug found. Debug version will be appended below.
list(APPEND OPENCOLLADA_LIBRARIES "${PCRE_LIBRARY_DIR}/pcre.lib")
else()
list(APPEND OPENCOLLADA_LIBRARIES "${pcre_library}")
endif()
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find PCRE. "
"Disable COLLADA_SUPPORT or fix PCRE_LIBRARY_DIR path to proceed.")
endif()
if(MSVC)
add_debug_variants(OPENCOLLADA_LIBRARIES "${OPENCOLLADA_LIBRARIES}" d)
endif()
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA headers. "
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
endif()
endif(COLLADA_SUPPORT AND NOT OpenCOLLADA_DIR)
if(COLLADA_SUPPORT AND OPENCOLLADA_FOUND)
add_definitions(-DWITH_OPENCOLLADA)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_OPENCOLLADA)
endif()
if(HDF5_SUPPORT)
find_package(HDF5 REQUIRED COMPONENTS C CXX)
@@ -850,25 +486,12 @@ else()
endif(MSVC)
# Ensure other dependencies are provided.
if(NOT EXISTS "${EIGEN_DIR}")
find_package(Eigen3 CONFIG)
if(Eigen3_DIR)
message(STATUS "Eigen3: found config at '${Eigen3_DIR}'.")
else()
message(
FATAL_ERROR
"EIGEN_DIR is not provided or provided folder doesn't exist (current value: '${EIGEN_DIR}'). "
"Also couldn't find Eigen3 as a package."
)
endif()
link_libraries(Eigen3::Eigen)
endif()
find_package(Eigen3 REQUIRED)
link_libraries(Eigen3::Eigen)
include_directories(${INCLUDE_DIRECTORIES} ${OCC_INCLUDE_DIR} ${OPENCOLLADA_INCLUDE_DIRS}
${Boost_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIR} ${JSON_INCLUDE_DIR} ${HDF5_INCLUDE_DIRS}
${EIGEN_DIR} ${CGAL_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${MPFR_INCLUDE_DIR} ${USD_INCLUDE_DIR}
include_directories(${INCLUDE_DIRECTORIES} ${OPENCOLLADA_INCLUDE_DIRS}
${Boost_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIR} ${JSON_INCLUDE_DIR} ${HDF5_INCLUDE_DIR}
${CGAL_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${MPFR_INCLUDE_DIR} ${USD_INCLUDE_DIR}
${TBB_INCLUDE_DIR}
)
@@ -968,18 +591,13 @@ if(BUILD_IFCGEOM)
endif()
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
foreach(schema ${SCHEMA_VERSIONS})
set(SERIALIZER_SCHEMA_LIBRARIES ${SERIALIZER_SCHEMA_LIBRARIES} Serializers_ifc${schema})
endforeach()
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} Serializers ${SERIALIZER_SCHEMA_LIBRARIES})
if(WITH_OPENCASCADE)
if(WITH_OPENCASCADE)
foreach(schema ${SCHEMA_VERSIONS})
set(GEOM_SERIALIZER_SCHEMA_LIBRARIES ${GEOM_SERIALIZER_SCHEMA_LIBRARIES} GeometrySerializers_ifc${schema})
add_library(geometry_serializer_ifc${schema} STATIC ../src/ifcgeom/Serialization/schema/Serialization.cpp)
set_target_properties(geometry_serializer_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOMSERIALIZATION_EXPORTS -DIfcSchema=Ifc${schema}")
target_link_libraries(geometry_serializer_ifc${schema} ${OPENCASCADE_LIBRARIES})
target_link_libraries(geometry_serializer_ifc${schema} ${OpenCASCADE_LIBRARIES})
list(APPEND geometry_serializer_libraries geometry_serializer_ifc${schema})
endforeach()
@@ -996,10 +614,8 @@ if(BUILD_IFCGEOM)
set(CGAL_LIBRARIES CGAL::CGAL)
message(STATUS "Using found CGAL package at '${CGAL_DIR}'")
elseif(WITH_CGAL AND NOT CGAL_DIR)
clear_wasm_sysroot()
find_library(libGMP NAMES gmp mpir PATHS ${GMP_LIBRARY_DIR} NO_DEFAULT_PATH)
find_library(libMPFR NAMES mpfr PATHS ${MPFR_LIBRARY_DIR} NO_DEFAULT_PATH)
restore_wasm_sysroot()
if(NOT libGMP)
message(FATAL_ERROR "Unable to find GMP library files, aborting")
endif()
@@ -1011,33 +627,8 @@ if(BUILD_IFCGEOM)
list(APPEND CGAL_LIBRARIES "${libGMP}")
endif()
foreach(kernel ${GEOMETRY_KERNELS})
string(TOUPPER ${kernel} KERNEL_UPPER)
file(GLOB IFCGEOM_H_FILES ../src/ifcgeom/kernels/${kernel}/*.h)
file(GLOB IFCGEOM_CPP_FILES ../src/ifcgeom/kernels/${kernel}/*.cpp)
set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES})
add_library(geometry_kernel_${kernel} ${IFCGEOM_FILES} ../src/ifcgeom/kernels/ifc_geomlibrary_api.h)
set_property(TARGET geometry_kernel_${kernel} APPEND PROPERTY COMPILE_FLAGS "-DIFC_GEOMLIBRARY_EXPORTS")
# needed?
# if(NOT WASM_BUILD)
# endif()
target_link_libraries(geometry_kernel_${kernel} ${${KERNEL_UPPER}_LIBRARIES} IfcGeom IfcParse)
list(APPEND kernel_libraries geometry_kernel_${kernel})
if(${kernel} STREQUAL "cgal")
set_property(TARGET geometry_kernel_${kernel} APPEND_STRING PROPERTY COMPILE_FLAGS " -DCGAL_HAS_THREADS")
add_library(geometry_kernel_${kernel}_simple ${IFCGEOM_FILES})
set_target_properties(geometry_kernel_${kernel}_simple PROPERTIES COMPILE_FLAGS "-DIFC_GEOMLIBRARY_EXPORTS -DIFOPSH_SIMPLE_KERNEL -DCGAL_HAS_THREADS")
# needed?
# if(NOT WASM_BUILD)
# endif()
target_link_libraries(geometry_kernel_${kernel}_simple ${${KERNEL_UPPER}_LIBRARIES} IfcGeom IfcParse)
list(APPEND kernel_libraries geometry_kernel_${kernel}_simple)
endif()
endforeach()
add_subdirectory(../src/ifcgeom ifcgeom)
# IfcGeom
foreach(schema ${SCHEMA_VERSIONS})
file(GLOB IFCGEOM_I_FILES ../src/ifcgeom/mapping/*.i)
@@ -1066,110 +657,17 @@ if(BUILD_IFCGEOM)
find_package(Threads)
endif()
target_link_libraries(IfcGeom IfcParse ${mapping_libraries} ${CMAKE_THREAD_LIBS_INIT})
target_link_libraries(IfcGeom IfcParse ${mapping_libraries} ${CMAKE_THREAD_LIBS_INIT} ${OpenCASCADE_LIBRARIES})
endif(BUILD_IFCGEOM)
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
# Serializers
file(GLOB SERIALIZERS_H_FILES ../src/serializers/*.h)
file(GLOB SERIALIZERS_CPP_FILES ../src/serializers/*.cpp)
set(SERIALIZERS_FILES ${SERIALIZERS_H_FILES} ${SERIALIZERS_CPP_FILES})
file(GLOB SERIALIZERS_S_H_FILES ../src/serializers/schema_dependent/*.h)
file(GLOB SERIALIZERS_S_CPP_FILES ../src/serializers/schema_dependent/*.cpp)
set(SERIALIZERS_S_FILES ${SERIALIZERS_S_H_FILES} ${SERIALIZERS_S_CPP_FILES})
foreach(schema ${SCHEMA_VERSIONS})
add_library(Serializers_ifc${schema} STATIC ${SERIALIZERS_S_FILES})
set_target_properties(Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DSERIALIZERS_EXPORTS -DIfcSchema=Ifc${schema}")
if(WASM_BUILD)
target_link_libraries(Serializers_ifc${schema} ${HDF5_LIBRARIES})
else()
target_link_libraries(Serializers_ifc${schema} IfcGeom ${OPENCASCADE_LIBRARIES} ${HDF5_LIBRARIES})
endif()
endforeach()
add_library(Serializers ${SERIALIZERS_FILES})
set_target_properties(Serializers PROPERTIES COMPILE_FLAGS "-DSERIALIZERS_EXPORTS" VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
if(WITH_PROJ)
target_compile_definitions(Serializers PRIVATE "WITH_PROJ")
if (PROJ_STATIC)
target_compile_definitions(Serializers PRIVATE "PROJ_DLL=")
endif()
target_include_directories(Serializers PRIVATE ${PROJ_INCLUDE_DIR} ${SQLITE_INCLUDE_DIR})
target_link_libraries(Serializers ${PROJ_LIBRARIES})
endif()
target_link_libraries(Serializers ${SERIALIZER_SCHEMA_LIBRARIES} ${OPENCOLLADA_LIBRARIES} ${USD_LIBRARIES} IfcGeom ${OPENCASCADE_LIBRARIES} ${kernel_libraries} IfcParse)
add_subdirectory(../src/serializers serializers)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} ${SERIALIZER_SCHEMA_LIBRARIES})
endif(BUILD_CONVERT OR BUILD_IFCPYTHON)
if(BUILD_CONVERT)
if(WITH_CGAL AND CITYJSON_SUPPORT)
message(STATUS "Building CityJSON support")
set(CITYJSON_CONVERT_FILES
../src/ifcconvert/cityjson/geobim.cpp
../src/ifcconvert/cityjson/global_execution_context.cpp
../src/ifcconvert/cityjson/opening_collector.cpp
../src/ifcconvert/cityjson/processing.cpp
../src/ifcconvert/cityjson/radius_comparison.cpp
../src/ifcconvert/cityjson/radius_execution_context.cpp
../src/ifcconvert/cityjson/settings.cpp
../src/ifcconvert/cityjson/writer.cpp
)
add_library(cityjson_converter ${CITYJSON_CONVERT_FILES})
target_include_directories(cityjson_converter PRIVATE ../src)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} cityjson_converter)
install(TARGETS cityjson_converter
ARCHIVE DESTINATION ${LIBDIR}
LIBRARY DESTINATION ${LIBDIR}
)
add_executable(cityjson_converter_exe ${CITYJSON_CONVERT_FILES})
set_target_properties(cityjson_converter_exe PROPERTIES COMPILE_FLAGS "-DCITYJSON_EXECUTABLE")
target_include_directories(cityjson_converter_exe PRIVATE ../src)
target_link_libraries(cityjson_converter_exe ${IFCOPENSHELL_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${Boost_LIBRARIES} ${HDF5_LIBRARIES} ${USD_LIBRARIES})
install(TARGETS cityjson_converter_exe
RUNTIME DESTINATION ${BINDIR}
)
endif()
# IfcConvert
if (WITH_RELATIONSHIP_VALIDATION)
file(GLOB IFCCONVERT_CPP_FILES ../src/ifcconvert/*.cpp)
file(GLOB IFCCONVERT_H_FILES ../src/ifcconvert/*.h)
else()
file(GLOB IFCCONVERT_CPP_FILES ../src/ifcconvert/IfcConvert.cpp)
file(GLOB IFCCONVERT_H_FILES)
endif()
set(IFCCONVERT_FILES ${IFCCONVERT_CPP_FILES} ${IFCCONVERT_H_FILES})
add_executable(IfcConvert ${IFCCONVERT_FILES})
target_link_libraries(IfcConvert IfcGeom IfcParse Serializers ${kernel_libraries} ${OPENCASCADE_LIBRARIES} ${Boost_LIBRARIES} ${HDF5_LIBRARIES} ${USD_LIBRARIES})
if (WITH_RELATIONSHIP_VALIDATION)
set_property(TARGET IfcConvert APPEND_STRING PROPERTY COMPILE_FLAGS " -DWITH_RELATIONSHIP_VALIDATION")
endif()
if(WITH_CGAL AND CITYJSON_SUPPORT)
set_property(TARGET IfcConvert APPEND_STRING PROPERTY COMPILE_FLAGS " -DIFOPSH_WITH_CITYJSON")
endif()
if((NOT WIN32) AND BUILD_SHARED_LIBS)
# Only set RPATHs when building shared libraries (i.e. IfcParse and
# IfcGeom are dynamically linked). Not necessarily a perfect solution
# but probably a good indication of whether RPATHs are necessary.
SET_INSTALL_RPATHS(IfcConvert "${IFCOPENSHELL_LIBRARY_DIR};${OCC_LIBRARY_DIR};${Boost_LIBRARY_DIRS};${OPENCOLLADA_LIBRARY_DIR}")
endif()
install(TARGETS IfcConvert
ARCHIVE DESTINATION ${LIBDIR}
LIBRARY DESTINATION ${LIBDIR}
RUNTIME DESTINATION ${BINDIR}
)
add_subdirectory(../src/ifcconvert ifcconvert)
endif(BUILD_CONVERT)
# IfcGeomServer
@@ -1289,22 +787,6 @@ if(BUILD_IFCGEOM)
)
endif(BUILD_IFCGEOM)
if(BUILD_CONVERT)
install(TARGETS Serializers ${SERIALIZER_SCHEMA_LIBRARIES}
ARCHIVE DESTINATION ${LIBDIR}
LIBRARY DESTINATION ${LIBDIR}
RUNTIME DESTINATION ${BINDIR}
)
install(FILES ${SERIALIZERS_H_FILES}
DESTINATION ${INCLUDEDIR}/serializers/
)
install(FILES ${SERIALIZERS_S_H_FILES}
DESTINATION ${INCLUDEDIR}/serializers/schema_dependent
)
endif(BUILD_CONVERT)
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
if(WITH_OPENCASCADE)
install(TARGETS geometry_serializer ${geometry_serializer_libraries}
-1
View File
@@ -17,7 +17,6 @@
"IFCXML_SUPPORT": "ON",
"HDF5_SUPPORT": "ON",
"SCHEMA_VERSIONS": "4x3_add2",
"CITYJSON_SUPPORT": "OFF",
"CMAKE_GENERATOR_PLATFORM": "",
"CMAKE_GENERATOR_TOOLSET": ""
}
+42
View File
@@ -0,0 +1,42 @@
#
# Input variables:
# - `CGAL_INCLUDE_DIR`
# - `CGAL_LIBRARY_DIR`
# - `GMP_INCLUDE_DIR`
# - `GMP_LIBRARY_DIR`
# - `MPFR_INCLUDE_DIR`
# - `MPFR_LIBRARY_DIR`
# If input variables are not specified, try to find HDF5 config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `CGAL_INCLUDE_DIR`
#
UNIFY_ENVVARS_AND_CACHE(CGAL_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(CGAL_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_LIBRARY_DIR)
if(NOT CGAL_INCLUDE_DIR)
# CGAL is not respecting default Boost_USE_STATIC_LIBS value
# and sometiems it's getting in the way.
if(NOT DEFINED Boost_USE_STATIC_LIBS)
set(CGAL_Boost_USE_STATIC_LIBS OFF)
else()
set(CGAL_Boost_USE_STATIC_LIBS "${Boost_USE_STATIC_LIBS}")
endif()
find_package(CGAL CONFIG REQUIRED)
if(NOT CGAL_DIR)
message(
FATAL_ERROR
"CGAL_SUPPORT enabled, but CGAL_INCLUDE_DIR wasn't provided and CGAL package couldn't be found."
)
endif()
message(STATUS "CGAL: found config at '${CGAL_DIR}'.")
endif()
add_definitions(-DIFOPSH_WITH_CGAL)
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_CGAL)
+27
View File
@@ -0,0 +1,27 @@
#
# Input variables:
# - `EIGEN_DIR`
# If input variables are not specified, try to find Eigen3 config.
# Input variables could also be provided as environment variables.
#
# Output targets:
# - `Eigen3::Eigen`
UNIFY_ENVVARS_AND_CACHE(EIGEN_DIR)
if(EXISTS "${EIGEN_DIR}")
# Mimic Eigen3Config.cmake target.
add_library(Eigen3::Eigen INTERFACE IMPORTED)
target_include_directories(Eigen3::Eigen INTERFACE "${EIGEN_DIR}")
else()
find_package(Eigen3 CONFIG)
if(Eigen3_DIR)
message(STATUS "Eigen3: found config at '${Eigen3_DIR}'.")
else()
message(
FATAL_ERROR
"EIGEN_DIR is not provided or provided folder doesn't exist (current value: '${EIGEN_DIR}'). "
"Also couldn't find Eigen3 as a package."
)
endif()
endif()
@@ -1,5 +1,23 @@
#
# Input variables:
# - `HDF5_INCLUDE_DIR`
# - `HDF5_LIBRARY_DIR`
# - `HDF5_LIBRARIES`
# If input variables are not specified, try to find HDF5 config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `HDF5_INCLUDE_DIR`
# - `HDF5_LIBRARY_DIR`
# - `HDF5_LIBRARIES`
#
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
# To avoid cyclic calls to this file
list(REMOVE_ITEM CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
if("${HDF5_INCLUDE_DIR}" STREQUAL "")
message(STATUS "No HDF5 include directory specified")
@@ -86,3 +104,6 @@ if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
endif()
endif()
endif()
# Restore module path.
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
+49
View File
@@ -0,0 +1,49 @@
#
# Input variables:
# - `LIBXML2_INCLUDE_DIR`
# - `LIBXML2_LIBRARIES`
# If input variables are not specified, try to find LibXml2 config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `LIBXML2_INCLUDE_DIR`
# - `LIBXML2_LIBRARIES`
#
# To avoid cyclic calls to this file
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(LIBXML2_LIBRARIES)
if((NOT LIBXML2_INCLUDE_DIR AND NOT LIBXML2_LIBRARIES))
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04)
# CONFIG is provided using root path, so no need to clear sysroot here.
find_package(LibXml2 QUIET CONFIG)
if(NOT LibXml2_FOUND)
# Fallback to CMake's builtin FindLibXml2 module (works on Ubuntu)
find_package(LibXml2 REQUIRED)
else()
message(STATUS "Found LibXml2 config: ${LibXml2_DIR}")
endif()
if(TARGET LibXml2::LibXml2)
# Config mode already gives us the target
set(LIBXML2_LIBRARIES LibXml2::LibXml2)
get_target_property(LIBXML2_INCLUDE_DIR LibXml2::LibXml2 INTERFACE_INCLUDE_DIRECTORIES)
else()
# Module mode (Ubuntu)
set(LIBXML2_LIBRARIES ${LibXml2_LIBRARIES})
set(LIBXML2_INCLUDE_DIR ${LibXml2_INCLUDE_DIRS})
endif()
else()
find_package(LibXml2 REQUIRED)
endif()
if(MSVC AND NOT LibXml2_DIR)
add_debug_variants(LIBXML2_LIBRARIES "${LIBXML2_LIBRARIES}" d)
endif()
# Restore module path.
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
+138
View File
@@ -0,0 +1,138 @@
#
# Input variables:
# - `OCC_INCLUDE_DIR`
# - `OCC_LIBRARY_DIR`
# If input variables are not specified, try to find OpenCASCADE config.
# Input variables could also be provided as environment variables.
#
# Output variables
# - `OpenCASCADE_LIBRARIES`
#
UNIFY_ENVVARS_AND_CACHE(OCC_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OCC_LIBRARY_DIR)
if(OCC_INCLUDE_DIR)
set(OCC_INCLUDE_DIR ${OCC_INCLUDE_DIR} CACHE FILEPATH "Open CASCADE header files")
message(STATUS "Looking for Open CASCADE include files in: ${OCC_INCLUDE_DIR}")
endif()
if(OCC_LIBRARY_DIR)
set(OCC_LIBRARY_DIR ${OCC_LIBRARY_DIR} CACHE FILEPATH "Open CASCADE library files")
message(STATUS "Looking for Open CASCADE library files in: ${OCC_LIBRARY_DIR}")
endif()
if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR)
# OCE is not supported for find_package, because it's using a different name (`oce`)
# and also has an odd directory structure (install/lib/oce-0.18/*.cmake).
# find_package creates variables:
# - `OpenCASCADE_INCLUDE_DIR`
# - `OpenCASCADE_LIBRARIES`
find_package(OpenCASCADE CONFIG REQUIRED)
return()
endif()
# No specific paths specified, try to find package.
if(OCC_INCLUDE_DIR AND OCC_LIBRARY_DIR)
message(
STATUS
"Using provided OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') "
"and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}')."
)
# Parse OCC_VERSION_STRING.
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAJOR
REGEX "#define OCC_VERSION_MAJOR.*"
)
string(REGEX MATCH "[0-9]+" OCC_MAJOR ${OCC_MAJOR})
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MINOR
REGEX "#define OCC_VERSION_MINOR.*"
)
string(REGEX MATCH "[0-9]+" OCC_MINOR ${OCC_MINOR})
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAINT
REGEX "#define OCC_VERSION_MAINTENANCE.*"
)
string(REGEX MATCH "[0-9]+" OCC_MAINT ${OCC_MAINT})
set(OCC_VERSION_STRING "${OCC_MAJOR}.${OCC_MINOR}.${OCC_MAINT}")
else()
message(
FATAL_ERROR
"Couldn't find Open CASCADE installation. "
"Either both OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}') "
"must be specified or OpenCASCADE package should be discoverable. "
"If you're using OCE, then providing a package is not available "
"and you need to provide OCE_INCLUDE_DIR and OCE_LIBRARY_DIR directly."
)
endif()
set(
OpenCASCADE_LIBRARIES
TKernel TKMath TKBRep TKGeomBase TKGeomAlgo TKG3d TKG2d TKShHealing TKTopAlgo TKMesh TKPrim TKBool TKBO
TKFillet TKXSBase TKOffset TKHLR
# @todo investigate the exact conditions when this is necessary
TKBin
)
if(OCC_VERSION_STRING VERSION_LESS 7.8.0)
list(APPEND OpenCASCADE_LIBRARIES TKIGES TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP)
else(OCC_VERSION_STRING VERSION_LESS 7.8.0)
list(APPEND OpenCASCADE_LIBRARIES TKDESTEP TKDEIGES)
endif(OCC_VERSION_STRING VERSION_LESS 7.8.0)
find_library(libTKernel NAMES TKernel TKerneld PATHS ${OCC_LIBRARY_DIR} NO_DEFAULT_PATH)
if(libTKernel)
message(STATUS "Required Open Cascade Library files found")
else()
message(
FATAL_ERROR
"Unable to find Open Cascade library files in OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}'), aborting"
)
endif()
if(MSVC)
add_definitions(-DHAVE_NO_DLL)
add_debug_variants(OpenCASCADE_LIBRARIES "${OpenCASCADE_LIBRARIES}" d)
endif()
if(WIN32)
# OCC might require linking to Winsock depending on the version and build configuration
list(APPEND OpenCASCADE_LIBRARIES ws2_32.lib)
endif()
# Make sure cross-referenced symbols between static OCC libraries get
# resolved. Also add thread and rt libraries.
get_filename_component(libTKernelExt ${libTKernel} EXT)
if("${libTKernelExt}" STREQUAL ".a")
set(OCCT_STATIC ON)
endif()
if(OCCT_STATIC)
find_package(Threads)
if(WASM_BUILD)
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
# OpenCASCADE_LIBRARIES repeated N times below in order to fix cyclic dependencies
# tfk: --start-group ... --end-group didn't work on the apple linker when last tested
if(APPLE)
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
set(OpenCASCADE_LIBRARIES -Wl,--start-group ${OpenCASCADE_LIBRARIES} -Wl,--end-group ${CMAKE_THREAD_LIBS_INIT})
endif()
endif()
if(NOT APPLE AND NOT WIN32)
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} "rt")
endif()
if(NOT WIN32)
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} "dl")
endif()
endif()
add_library(OpenCASCADE_INTERFACE INTERFACE IMPORTED)
target_include_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_INCLUDE_DIR}")
target_link_libraries(OpenCASCADE_INTERFACE INTERFACE ${OpenCASCADE_LIBRARIES})
target_link_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_LIBRARY_DIR}")
set(OpenCASCADE_LIBRARIES OpenCASCADE_INTERFACE)
+118
View File
@@ -0,0 +1,118 @@
#
# Input variables:
# - `OPENCOLLADA_INCLUDE_DIR`
# - `OPENCOLLADA_LIBRARY_DIR`
# - `PCRE_LIBRARY_DIR`
# If input variables are not specified, try to find OpenCOLLADA config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `OPENCOLLADA_INCLUDE_DIR`
# - `OPENCOLLADA_LIBRARY_DIR`
# - `OPENCOLLADA_LIBRARIES`
#
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(PCRE_LIBRARY_DIR)
if(NOT OPENCOLLADA_INCLUDE_DIR AND NOT OPENCOLLADA_LIBRARY_DIR)
# If package is found, automatically sets
# OPENCOLLADA_INCLUDE_DIRS and OPENCOLLADA_LIBRARIES (list of targets, not paths).
find_package(OpenCOLLADA CONFIG)
if(OpenCOLLADA_DIR)
message(STATUS "Found OpenCOLLADA: '${OpenCOLLADA_DIR}'.")
set(OPENCOLLADA_FOUND TRUE)
else()
message(STATUS "OpenCOLLADA package not found, falling back to manual search.")
endif()
endif()
if(NOT OpenCOLLADA_DIR)
# Find OpenCOLLADA
if("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
message(STATUS "No OpenCOLLADA include directory specified")
set(OPENCOLLADA_INCLUDE_DIR "/usr/include/opencollada" CACHE FILEPATH "OpenCOLLADA header files")
else()
set(OPENCOLLADA_INCLUDE_DIR "${OPENCOLLADA_INCLUDE_DIR}" CACHE FILEPATH "OpenCOLLADA header files")
endif()
if("${OPENCOLLADA_LIBRARY_DIR}" STREQUAL "")
message(STATUS "No OpenCOLLADA library directory specified")
find_library(OPENCOLLADA_FRAMEWORK_LIB NAMES OpenCOLLADAFramework
PATHS /usr/lib64/opencollada /usr/lib/opencollada /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib)
get_filename_component(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_FRAMEWORK_LIB} PATH)
endif()
find_library(OpenCOLLADAFramework NAMES OpenCOLLADAFramework OpenCOLLADAFrameworkd PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
if(OpenCOLLADAFramework)
message(STATUS "OpenCOLLADA library files found")
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA libraries. "
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
endif()
set(OPENCOLLADA_LIBRARY_DIR "${OPENCOLLADA_LIBRARY_DIR}" CACHE FILEPATH "OpenCOLLADA library files")
set(OPENCOLLADA_INCLUDE_DIRS "${OPENCOLLADA_INCLUDE_DIR}/COLLADABaseUtils" "${OPENCOLLADA_INCLUDE_DIR}/COLLADAStreamWriter")
find_file(COLLADASWStreamWriter_h "COLLADASWStreamWriter.h" ${OPENCOLLADA_INCLUDE_DIRS})
if(COLLADASWStreamWriter_h)
message(STATUS "OpenCOLLADA header files found")
set(OPENCOLLADA_FOUND TRUE)
set(OPENCOLLADA_LIBRARY_NAMES
GeneratedSaxParser MathMLSolver OpenCOLLADABaseUtils OpenCOLLADAFramework OpenCOLLADASaxFrameworkLoader
OpenCOLLADAStreamWriter UTF buffer ftoa
)
# Use the found OpenCOLLADAFramework as a template for all other OpenCOLLADA libraries
foreach(lib ${OPENCOLLADA_LIBRARY_NAMES})
# Make sure we'll handle the Windows/MSVC debug postfix convention too.
string(REPLACE OpenCOLLADAFrameworkd "${lib}" lib_path "${OpenCOLLADAFramework}")
string(REPLACE OpenCOLLADAFramework "${lib}" lib_path "${lib_path}")
list(APPEND OPENCOLLADA_LIBRARIES "${lib_path}")
endforeach()
if("${PCRE_LIBRARY_DIR}" STREQUAL "")
if(WIN32)
find_library(pcre_library NAMES pcre pcred PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
else()
find_library(pcre_library NAMES pcre PATHS ${OPENCOLLADA_LIBRARY_DIR})
endif()
get_filename_component(PCRE_LIBRARY_DIR ${pcre_library} PATH)
else()
find_library(pcre_library NAMES pcre pcred PATHS ${PCRE_LIBRARY_DIR} NO_DEFAULT_PATH)
endif()
if(pcre_library)
set(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_LIBRARY_DIR} ${PCRE_LIBRARY_DIR})
if(MSVC)
# Add release lib regardless whether release or debug found. Debug version will be appended below.
list(APPEND OPENCOLLADA_LIBRARIES "${PCRE_LIBRARY_DIR}/pcre.lib")
else()
list(APPEND OPENCOLLADA_LIBRARIES "${pcre_library}")
endif()
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find PCRE. "
"Disable COLLADA_SUPPORT or fix PCRE_LIBRARY_DIR path to proceed.")
endif()
if(MSVC)
add_debug_variants(OPENCOLLADA_LIBRARIES "${OPENCOLLADA_LIBRARIES}" d)
endif()
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA headers. "
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
endif()
endif(NOT OpenCOLLADA_DIR)
if(OPENCOLLADA_FOUND)
add_definitions(-DWITH_OPENCOLLADA)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_OPENCOLLADA)
endif()
+54
View File
@@ -0,0 +1,54 @@
#
# Input variables:
# - `USD_INCLUDE_DIR`
# - `USD_LIBRARY_DIR`
# Input variables could also be provided as environment variables.
# TODO: Try to find USD config if varibales are not provided.
# TODO: does usd have a config file?
#
# Output variables:
# - `USD_LIBRARIES`
UNIFY_ENVVARS_AND_CACHE(USD_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(USD_LIBRARY_DIR)
if("${USD_INCLUDE_DIR}" STREQUAL "")
find_path(USD_INCLUDE_DIR pxr.h
PATHS
/usr/include/pxr
/usr/local/include/pxr
REQUIRED
)
if(USD_INCLUDE_DIR)
message(STATUS "Found USD include files in: ${USD_INCLUDE_DIR}")
else()
message(FATAL_ERROR "Unable to find USD include directory, specify USD_INCLUDE_DIR manually.")
endif()
else()
set(USD_INCLUDE_DIR ${USD_INCLUDE_DIR} CACHE FILEPATH "USD header files")
message(STATUS "Looking for USD include files in: ${USD_INCLUDE_DIR}")
endif()
set(USD_LIBRARIES
usd_usd
usd_usdGeom
usd_usdShade
usd_usdLux
usd_vt
usd_sdf
usd_tf
usd_gf
)
find_library(USD_LIBRARY
NAMES ${USD_LIBRARIES}
PATHS ${USD_LIBRARY_DIR})
if(USD_LIBRARY)
message(STATUS "USD libraries ${USD_LIBRARIES} found in: ${USD_LIBRARY_DIR}")
link_directories(${USD_LIBRARY_DIR})
else()
message(FATAL_ERROR "Unable to find USD libraries in: ${USD_LIBRARY_DIR}")
endif()
add_definitions(-DWITH_USD)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_USD)
-1
View File
@@ -42,7 +42,6 @@ cmake -G "Ninja" ^
-D Boost_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
-D Boost_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
-D Boost_USE_STATIC_LIBS:BOOL=OFF ^
-D CITYJSON_SUPPORT:BOOL=OFF ^
../cmake
if errorlevel 1 exit 1
-1
View File
@@ -42,7 +42,6 @@ cmake ${CMAKE_ARGS} -G Ninja \
-DBUILD_IFCGEOM:BOOL=ON \
-DBUILD_GEOMSERVER:BOOL=OFF \
-DBOOST_USE_STATIC_LIBS:BOOL=OFF \
-DCITYJSON_SUPPORT:BOOL=OFF \
./cmake
ninja
+8 -8
View File
@@ -243,7 +243,9 @@ if WASM:
# Pyodide still in transition from `FLAGS` to `FLAGS_INIT`.
# `FLAGS_INIT` allow us to provide flags using environment variables
# and providing `FLAGS` directly would break pyodide toolchain.
WASM_CMAKE_IS_USING_INIT_VARS = get_pyodide_build_version() >= (0, 30, 8)
# NOTE: changes in pyodide-build currently got postponed:
# https://github.com/pyodide/pyodide-build/pull/249
WASM_CMAKE_IS_USING_INIT_VARS = get_pyodide_build_version() >= (99, 0, 0)
# pyodide provide empty `CXXFLAGS`, leading to issues using C++ files compiled with `-fexceptions`
# which is used by OCCT.
@@ -941,11 +943,9 @@ if "pcre2" in targets:
download_name=f"pcre2-{PCRE2_VERSION}.tar.bz2",
)
# An issue exists with swig-1.3 and python >= 3.2
# Therefore, build a recent copy from source
if "swig" in targets:
build_dependency(
name="swig",
name=f"swig-{SWIG_VERSION}",
mode="autoconf",
build_tool_args=["--disable-ccache", f"--with-pcre2-prefix={DEPS_DIR}/install/pcre2-{PCRE2_VERSION}"],
download_url="https://github.com/swig/swig.git",
@@ -1431,6 +1431,9 @@ if "rocksdb" in targets:
]
)
if "swig" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/swig-{SWIG_VERSION}")
if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets)):
logger.info("\rConfiguring executables...")
@@ -1483,9 +1486,6 @@ if "IfcOpenShell-Python" in targets:
if os.path.exists(cache_path):
os.remove(cache_path)
prefix_paths: list[str] = []
if "swig" in targets:
prefix_paths.append(f"{DEPS_DIR}/install/swig")
if python_path:
# We couldn't just prefix PATH and have to provide all variables explicitly,
# see ifcwrap/cmake for the details.
@@ -1502,7 +1502,7 @@ if "IfcOpenShell-Python" in targets:
run_cmake(
"",
cmake_args
+ get_cmake_args_prefix_path(prefix_paths)
+ get_cmake_args_prefix_path()
+ [
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
# Needed because pyodide is expecting setup.py to be in the root.
-2
View File
@@ -9,7 +9,6 @@ extend-exclude = '''
|src/ifcopenshell-python/ifcopenshell/mvd/*
|src/ifcopenshell-python/ifcopenshell/simple_spf/*
|src/ifc2ca/templates/*
|src/ifcconvert/cityjson/*
|src/svgfill
|src/exterior-shell-extractor
'''
@@ -25,7 +24,6 @@ reportUnnecessaryTypeIgnoreComment = true
[tool.ruff]
exclude = [
# Submodules.
"src/ifcconvert/cityjson",
"src/ifcopenshell-python/ifcopenshell/express",
"src/ifcopenshell-python/ifcopenshell/mvd",
"src/ifcopenshell-python/ifcopenshell/simple_spf",
+40 -9
View File
@@ -21,7 +21,7 @@ import bpy
import bpy.utils.previews
import importlib
from bpy_extras.io_utils import ImportHelper, ExportHelper
from . import handler, ui, prop, operator
from . import handler, ui, prop, operator, gizmo
from typing import Union
from collections.abc import Callable
@@ -148,7 +148,9 @@ classes = [
ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic,
ui.DocPreferences,
ui.BIM_ADDON_preferences,
ui.GizmoPreferencesDoor, # Register before GizmoPreferences
ui.GizmoPreferences,
# ui.DefaultParameters and ui.BIM_ADDON_preferences are registered separately after modules (see late_classes below)
# Tabs panel
ui.BIM_PT_tabs,
# Project overview
@@ -202,6 +204,14 @@ classes = [
ui.BIM_PT_section_with_cappings,
ui.BIM_PT_decorators_overlay,
ui.BIM_PT_snappping,
# Gizmos
gizmo.GizmoArrow,
gizmo.GizmoCone,
gizmo.GizmoLock,
gizmo.GizmoArc,
gizmo.GizmoPen,
gizmo.GizmoValidate,
gizmo.GizmoCancel,
]
for mod in modules.values():
@@ -225,14 +235,33 @@ def on_register(scene):
is_registering = False
def register():
for cls in classes:
# Classes that need to be registered after modules (due to cross-module dependencies)
late_classes = (
ui.DefaultParameters, # Register before BIM_ADDON_preferences
ui.BIM_ADDON_preferences,
)
def register_classes(classes_to_register):
for cls in classes_to_register:
# Prevent crashes in Blender 4.4.0, see #6420.
if issubclass(cls, (ImportHelper, ExportHelper)):
assert getattr(cls, "bl_description", "") or cls.__doc__, cls
bpy.utils.register_class(cls)
def unregister_classes(classes_to_unregister):
for cls in reversed(classes_to_unregister):
if getattr(cls, "is_registered", None) is None:
bpy.utils.unregister_class(cls)
elif cls.is_registered:
bpy.utils.unregister_class(cls)
def register():
register_classes(classes)
bpy.app.handlers.depsgraph_update_post.append(on_register)
bpy.app.handlers.undo_post.append(handler.undo_post)
bpy.app.handlers.redo_post.append(handler.redo_post)
@@ -256,6 +285,9 @@ def register():
for mod in modules.values():
mod.register()
# Delay registering classes that depend on module classes
register_classes(late_classes)
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
@@ -287,11 +319,7 @@ def unregister():
bpy.utils.previews.remove(icons)
for cls in reversed(classes):
if getattr(cls, "is_registered", None) is None:
bpy.utils.unregister_class(cls)
elif cls.is_registered:
bpy.utils.unregister_class(cls)
unregister_classes(classes)
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
@@ -306,6 +334,9 @@ def unregister():
bpy.types.UI_MT_button_context_menu.remove(ui.draw_custom_context_menu)
bpy.types.STATUSBAR_HT_header.remove(ui.draw_statusbar)
# Unregister late classes before modules
unregister_classes(late_classes)
for mod in reversed(list(modules.values())):
mod.unregister()
+805
View File
@@ -0,0 +1,805 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
"""
Shared gizmo components for reuse across modules.
"""
import bpy
import math
from typing import Optional
from mathutils import Vector
from mathutils.bvhtree import BVHTree
from mathutils.geometry import intersect_line_line
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
import gpu
from gpu_extras.batch import batch_for_shader
SNAP_CIRCLE_SEGMENTS = 16
SNAP_CIRCLE_RADIUS = 0.1
SNAP_CIRCLE_COLOR = (1.0, 0.5, 0.0, 1.0)
SNAP_LINE_WIDTH = 3.0
SNAP_MAX_RADIUS = 10.0
ARROW_SHAFT_LENGTH = 0.8
ARROW_HEAD_LENGTH = 0.2
ARROW_WIDTH = 0.015
ARROW_HEAD_WIDTH_MULTIPLIER = 10
ARROW_CIRCLE_SEGMENTS = 8
CONE_LENGTH = 1.0
CONE_RADIUS = 0.35
CONE_SEGMENTS = 16
ARC_SEGMENTS = 24
ARC_LINE_WIDTH = 0.015
PRECISION_MODE_MULTIPLIER = 0.1
class SnapManager:
"""Manages snap point visualization and mesh snapping."""
def __init__(self):
self._snap_point: Optional[tuple[float, float, float]] = None
self._draw_handler = None
self._shader = None
def set_snap_point(self, point: Optional[tuple[float, float, float]]) -> None:
"""Set snap point and register draw handler if needed."""
self._snap_point = point
if self._draw_handler is None and point is not None:
self._draw_handler = bpy.types.SpaceView3D.draw_handler_add(self._draw, (), "WINDOW", "POST_VIEW")
self._redraw_viewport()
def clear(self) -> None:
"""Clear snap point and unregister handler."""
self._snap_point = None
if self._draw_handler is not None:
bpy.types.SpaceView3D.draw_handler_remove(self._draw_handler, "WINDOW")
self._draw_handler = None
self._redraw_viewport()
def _draw(self) -> None:
"""Draw snap point circles."""
if self._snap_point is None:
return
if self._shader is None:
self._shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self._shader.bind()
self._shader.uniform_float("color", SNAP_CIRCLE_COLOR)
gpu.state.line_width_set(SNAP_LINE_WIDTH)
for plane in ("XY", "XZ", "YZ"):
vertices = generate_circle_vertices(self._snap_point, SNAP_CIRCLE_RADIUS, SNAP_CIRCLE_SEGMENTS, plane)
batch = batch_for_shader(self._shader, "LINE_STRIP", {"pos": vertices})
batch.draw(self._shader)
gpu.state.line_width_set(1.0)
@staticmethod
def _redraw_viewport() -> None:
"""Force 3D viewport redraw."""
for area in bpy.context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
@staticmethod
def snap_to_mesh(
location: Vector, context: bpy.types.Context, axis_vector: Vector, active_obj: bpy.types.Object
) -> Vector:
"""Snap a location to the nearest mesh element if snapping is enabled.
Args:
location: The world space location to snap
context: The Blender context
axis_vector: The axis direction of the gizmo
active_obj: The active object to exclude from snapping
Returns:
The snapped location or original location if snapping is disabled
"""
tool_settings = context.scene.tool_settings
if not tool_settings.use_snap:
return location
snap_elements = tool_settings.snap_elements_base
mesh_objects = [
obj for obj in context.visible_objects if obj.type == "MESH" and obj != active_obj and obj.visible_get()
]
if not mesh_objects:
return location
nearby_objects = []
for obj in mesh_objects:
bbox_corners = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
for corner in bbox_corners:
if (corner - location).length <= SNAP_MAX_RADIUS:
nearby_objects.append(obj)
break
if not nearby_objects:
return location
closest_point = None
closest_distance = float("inf")
for obj in nearby_objects:
if not obj.data.vertices:
continue
depsgraph = context.evaluated_depsgraph_get()
obj_eval = obj.evaluated_get(depsgraph)
mesh = obj_eval.to_mesh()
if not mesh.vertices:
obj_eval.to_mesh_clear()
continue
world_vertices = [obj.matrix_world @ v.co for v in mesh.vertices]
if "VERTEX" in snap_elements:
for v_co in world_vertices:
dist = (v_co - location).length
if dist < closest_distance:
closest_distance = dist
closest_point = v_co
if "EDGE" in snap_elements:
for edge in mesh.edges:
v1 = world_vertices[edge.vertices[0]]
v2 = world_vertices[edge.vertices[1]]
edge_vec = v2 - v1
edge_len_sq = edge_vec.length_squared
if edge_len_sq > 0:
t = max(0, min(1, (location - v1).dot(edge_vec) / edge_len_sq))
closest_on_edge = v1 + t * edge_vec
dist = (closest_on_edge - location).length
if dist < closest_distance:
closest_distance = dist
closest_point = closest_on_edge
if "FACE" in snap_elements:
bvh = BVHTree.FromPolygons(world_vertices, [p.vertices for p in mesh.polygons])
nearest_loc, normal, index, dist = bvh.find_nearest(location)
if nearest_loc and dist < closest_distance:
closest_distance = dist
closest_point = nearest_loc
obj_eval.to_mesh_clear()
if closest_point and closest_distance < SNAP_MAX_RADIUS:
return closest_point
return location
_snap_manager = SnapManager()
def set_snap_point(point: Optional[tuple[float, float, float]]) -> None:
_snap_manager.set_snap_point(point)
def clear_snap_point() -> None:
_snap_manager.clear()
def snap_to_mesh(
location: Vector, context: bpy.types.Context, axis_vector: Vector, active_obj: bpy.types.Object
) -> Vector:
return _snap_manager.snap_to_mesh(location, context, axis_vector, active_obj)
def generate_circle_vertices(
center: tuple[float, float, float], radius: float, segments: int, plane: str = "XY"
) -> list[tuple[float, float, float]]:
"""Generate circle vertices in specified plane.
Args:
center: (x, y, z) tuple for circle center
radius: Circle radius
segments: Number of segments
plane: 'XY', 'XZ', or 'YZ'
Returns:
List of (x, y, z) vertices
"""
vertices = []
for i in range(segments + 1):
angle = (2 * math.pi * i) / segments
cos_a = radius * math.cos(angle)
sin_a = radius * math.sin(angle)
if plane == "XY":
vertices.append((center[0] + cos_a, center[1] + sin_a, center[2]))
elif plane == "XZ":
vertices.append((center[0] + cos_a, center[1], center[2] + sin_a))
else:
vertices.append((center[0], center[1] + cos_a, center[2] + sin_a))
return vertices
def create_quarter_circle_arc(
radius: float = 1.0, segments: int = ARC_SEGMENTS, direction: str = "LEFT", line_width: float = ARC_LINE_WIDTH
) -> tuple[tuple[float, float, float], ...]:
"""Create a quarter circle arc with cross-section thickness for visibility from all angles.
Args:
radius: Radius of the arc
segments: Number of segments for smoothness
direction: 'LEFT' for counterclockwise (0 to 90°), 'RIGHT' for clockwise (0 to -90°)
line_width: Width of the arc line in both perpendicular directions
Returns:
Tuple of arc triangles for drawing geometry visible from all angles
"""
half_width = line_width / 2
arc_points = []
if direction == "LEFT":
for i in range(segments + 1):
angle = (math.pi / 2) * (i / segments)
x = radius * math.cos(angle)
y = radius * math.sin(angle)
arc_points.append((x, y))
else:
for i in range(segments + 1):
angle = (math.pi / 2) * (i / segments)
x = -radius * math.cos(angle)
y = radius * math.sin(angle)
arc_points.append((x, y))
arc_triangles = []
for i in range(len(arc_points) - 1):
x1, y1 = arc_points[i]
x2, y2 = arc_points[i + 1]
dx, dy = x2 - x1, y2 - y1
length = (dx**2 + dy**2) ** 0.5
if length > 0:
px, py = -dy / length * half_width, dx / length * half_width
arc_triangles.extend(
[
(x1 + px, y1 + py, 0.0),
(x1 - px, y1 - py, 0.0),
(x2 + px, y2 + py, 0.0),
]
)
arc_triangles.extend(
[
(x2 + px, y2 + py, 0.0),
(x1 - px, y1 - py, 0.0),
(x2 - px, y2 - py, 0.0),
]
)
arc_triangles.extend(
[
(x1, y1, -half_width),
(x2, y2, -half_width),
(x1, y1, +half_width),
]
)
arc_triangles.extend(
[
(x1, y1, +half_width),
(x2, y2, -half_width),
(x2, y2, +half_width),
]
)
return tuple(arc_triangles)
class GizmoMovable(bpy.types.Gizmo):
"""Base class for movable gizmos with snapping and precision controls.
This class provides common functionality for gizmos that can be dragged
to modify values, including:
- Snap to mesh support (toggle with Ctrl)
- Precision mode (hold Shift for 10x slower movement)
- Header text with value and modifier hints
- Consistent invoke/exit/modal behavior
"""
__slots__ = (
"custom_shape",
"init_value",
"move_get_cb",
"move_set_cb",
"axis",
"local_axis",
"start_location",
"active_obj",
"initial_snap_state",
)
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set:
self.init_value = self.move_get_cb() if self.move_get_cb else 0.0
self.start_location = self.matrix_basis.translation.copy()
self.active_obj = context.active_object
self.initial_snap_state = context.scene.tool_settings.use_snap
return {"RUNNING_MODAL"}
def exit(self, context: bpy.types.Context, cancel: bool) -> None:
context.area.header_text_set(None)
if cancel and self.move_set_cb:
self.move_set_cb(self.init_value)
if hasattr(self, "initial_snap_state"):
context.scene.tool_settings.use_snap = self.initial_snap_state
clear_snap_point()
def get_axis_direction(self) -> Vector:
return self.axis
def modal(self, context: bpy.types.Context, event: bpy.types.Event, tweak) -> set:
region = context.region
rv3d = context.region_data
tool_settings = context.scene.tool_settings
tool_settings.use_snap = not self.initial_snap_state if event.ctrl else self.initial_snap_state
current_coord = (event.mouse_region_x, event.mouse_region_y)
view_origin = region_2d_to_origin_3d(region, rv3d, current_coord)
view_direction = region_2d_to_vector_3d(region, rv3d, current_coord)
axis_direction = self.get_axis_direction()
result = intersect_line_line(
view_origin,
view_origin + view_direction * 1000,
self.start_location,
self.start_location + axis_direction * 1000,
)
current_3d = result[1] if result else self.start_location
delta = (current_3d - self.start_location).dot(axis_direction)
if tool_settings.use_snap and self.active_obj:
snapped_pos = snap_to_mesh(current_3d, context, axis_direction, self.active_obj)
if snapped_pos != current_3d:
delta = (snapped_pos - self.start_location).dot(axis_direction)
set_snap_point(snapped_pos)
else:
clear_snap_point()
else:
clear_snap_point()
if event.shift:
delta *= PRECISION_MODE_MULTIPLIER
if self.move_set_cb:
self.move_set_cb(self.init_value + delta)
self._update_header(context, self.init_value + delta, tool_settings.use_snap, event.shift)
return {"RUNNING_MODAL"}
def _update_header(self, context: bpy.types.Context, value: float, is_snapping: bool, is_precision: bool) -> None:
header_text = f"Value: {value:.3f}m"
hints = []
if is_snapping:
hints.append("Snapping: ON")
if is_precision:
hints.append("Precision Mode (0.1x)")
hints.extend(["Ctrl: Toggle Snap", "Shift: Precision"])
if hints:
header_text += " | " + " | ".join(hints)
context.area.header_text_set(header_text)
class GizmoLock(bpy.types.Gizmo):
"""Reusable lock icon gizmo that switches between closed and open states."""
bl_idname = "VIEW3D_GT_lock"
__slots__ = (
"custom_shape_closed",
"custom_shape_open",
"prop_path",
)
tris_closed = (
(0.16803650558, 0.18791499734, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(0.20165449381, 0.13603900373, 0.0),
(-0.11167433113, 0.13603900373, 0.0),
(0.20165449381, -0.44701099396, 0.0),
(0.20165449381, -0.44701099396, 0.0),
(-0.11167433113, 0.13603900373, 0.0),
(-0.11167433113, -0.44701099396, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(-0.39353451133, 0.18791499734, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.39353451133, 0.18791499734, 0.0),
(-0.39353451133, 0.30746498704, 0.0),
(-0.44701099396, 0.18791499734, 0.0),
(-0.44701099396, -0.04477182776, 0.0),
(-0.39353451133, 0.18791499734, 0.0),
(-0.39353451133, 0.18791499734, 0.0),
(-0.44701099396, -0.04477182776, 0.0),
(-0.39353451133, -0.04477182776, 0.0),
)
tris_open = (
(0.16803650558, 0.18791499734, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(0.20165449381, 0.13603900373, 0.0),
(-0.11167433113, 0.13603900373, 0.0),
(0.20165449381, -0.44701099396, 0.0),
(0.20165449381, -0.44701099396, 0.0),
(-0.11167433113, 0.13603900373, 0.0),
(-0.11167433113, -0.44701099396, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(0.16803650558, 0.70610702038, 0.0),
(0.16803650558, 0.70610702038, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.07805634290, 0.58656096458, 0.0),
(-0.11167433113, 0.44701099396, 0.0),
(-0.11167433113, 0.73432201147, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.11167433113, 0.73432201147, 0.0),
(-0.07805634290, 0.73432201147, 0.0),
)
def get_custom_shape(self, context: bpy.types.Context):
obj = context.active_object
if not obj:
return self.custom_shape_closed
try:
is_open = obj.path_resolve(self.prop_path)
return self.custom_shape_open if is_open else self.custom_shape_closed
except:
return self.custom_shape_closed
def setup(self) -> None:
self.custom_shape_closed = self.new_custom_shape("TRIS", self.tris_closed)
self.custom_shape_open = self.new_custom_shape("TRIS", self.tris_open)
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self.get_custom_shape(context))
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self.get_custom_shape(context), select_id=select_id)
class GizmoArc(bpy.types.Gizmo):
"""Reusable quarter circle arc gizmo."""
bl_idname = "VIEW3D_GT_arc"
__slots__ = (
"custom_shape_left",
"custom_shape_right",
"prop_path",
)
def setup(self) -> None:
"""Create quarter circle shapes for both LEFT and RIGHT directions."""
arc_left = create_quarter_circle_arc(radius=1.0, direction="LEFT")
arc_right = create_quarter_circle_arc(radius=1.0, direction="RIGHT")
self.custom_shape_left = self.new_custom_shape(type="TRIS", verts=arc_left)
self.custom_shape_right = self.new_custom_shape(type="TRIS", verts=arc_right)
def _get_shape_for_direction(self, context: bpy.types.Context):
obj = context.active_object
if not obj:
return self.custom_shape_left
try:
direction_value = obj.path_resolve(self.prop_path)
if "RIGHT" in str(direction_value):
return self.custom_shape_right
except:
pass
return self.custom_shape_left
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self._get_shape_for_direction(context))
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self._get_shape_for_direction(context), select_id=select_id)
class GizmoPen(bpy.types.Gizmo):
"""Reusable pen/edit icon gizmo for entering edit mode."""
bl_idname = "VIEW3D_GT_pen"
__slots__ = ("custom_shape",)
tris = (
(-0.07595771551132202, -0.2948460578918457, 0.0),
(0.16886109113693237, 0.23203276097774506, 0.0),
(0.062240585684776306, 0.28157487511634827, 0.0),
(0.07201281189918518, 0.30260589718818665, 0.0),
(0.21042980253696442, 0.321493536233902, 0.0),
(0.17863331735134125, 0.25306373834609985, 0.0),
(0.062240585684776306, 0.28157487511634827, 0.0),
(-0.1825782209634781, -0.2453039139509201, 0.0),
(-0.07595771551132202, -0.2948460578918457, 0.0),
(-0.1825782209634781, -0.2453039139509201, 0.0),
(-0.19114767014980316, -0.4032624065876007, 0.0),
(-0.07595771551132202, -0.2948460578918457, 0.0),
(0.07201281189918518, 0.30260589718818665, 0.0),
(0.10380929708480835, 0.371035635471344, 0.0),
(0.21042980253696442, 0.321493536233902, 0.0),
)
def setup(self) -> None:
self.custom_shape = self.new_custom_shape("TRIS", self.tris)
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self.custom_shape)
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self.custom_shape, select_id=select_id)
class GizmoValidate(bpy.types.Gizmo):
"""Reusable validate/checkmark icon gizmo for confirming edits."""
bl_idname = "VIEW3D_GT_validate"
__slots__ = ("custom_shape",)
tris = (
(0.36775994300842285, 0.205583393573761, 0.0),
(0.030080009251832962, -0.1881658434867859, 0.0),
(0.030080009251832962, -0.3380376696586609, 0.0),
(-0.22017201781272888, -0.16090886294841766, 0.0),
(0.030080009251832962, -0.1881658434867859, 0.0),
(-0.22017201781272888, -0.011037036776542664, 0.0),
(0.36775994300842285, 0.205583393573761, 0.0),
(0.36775994300842285, 0.355455219745636, 0.0),
(0.030080009251832962, -0.1881658434867859, 0.0),
(-0.22017201781272888, -0.16090886294841766, 0.0),
(0.030080009251832962, -0.3380376696586609, 0.0),
(0.030080009251832962, -0.1881658434867859, 0.0),
)
def setup(self) -> None:
self.custom_shape = self.new_custom_shape("TRIS", self.tris)
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self.custom_shape)
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self.custom_shape, select_id=select_id)
class GizmoCancel(bpy.types.Gizmo):
"""Reusable cancel/X icon gizmo for canceling edits."""
bl_idname = "VIEW3D_GT_cancel"
__slots__ = ("custom_shape",)
tris = (
(0.0, 0.048707593232393265, 0.0),
(0.0, -0.048707593232393265, 0.0),
(0.048707593232393265, 0.0, 0.0),
(0.0, 0.048707593232393265, 0.0),
(-0.21918421983718872, 0.2678918242454529, 0.0),
(-0.048707593232393265, 0.0, 0.0),
(-0.21918421983718872, 0.2678918242454529, 0.0),
(-0.2678918242454529, 0.21918421983718872, 0.0),
(-0.048707593232393265, 0.0, 0.0),
(-0.048707593232393265, 0.0, 0.0),
(-0.2678918242454529, -0.21918421983718872, 0.0),
(-0.21918421983718872, -0.2678918242454529, 0.0),
(0.2678918242454529, 0.21918421983718872, 0.0),
(0.21918421983718872, 0.2678918242454529, 0.0),
(0.0, 0.048707593232393265, 0.0),
(0.21918421983718872, -0.2678918242454529, 0.0),
(0.2678918242454529, -0.21918421983718872, 0.0),
(0.048707593232393265, 0.0, 0.0),
(0.048707593232393265, 0.0, 0.0),
(0.2678918242454529, 0.21918421983718872, 0.0),
(0.0, 0.048707593232393265, 0.0),
(0.0, 0.048707593232393265, 0.0),
(-0.048707593232393265, 0.0, 0.0),
(0.0, -0.048707593232393265, 0.0),
(-0.048707593232393265, 0.0, 0.0),
(-0.21918421983718872, -0.2678918242454529, 0.0),
(0.0, -0.048707593232393265, 0.0),
(0.0, -0.048707593232393265, 0.0),
(0.21918421983718872, -0.2678918242454529, 0.0),
(0.048707593232393265, 0.0, 0.0),
)
def setup(self) -> None:
self.custom_shape = self.new_custom_shape("TRIS", self.tris)
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self.custom_shape)
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self.custom_shape, select_id=select_id)
class GizmoArrow(GizmoMovable):
"""Arrow gizmo for directional value editing."""
bl_idname = "BIM_GT_gizmo_arrow"
bl_target_properties = ({"id": "offset", "type": "FLOAT", "array_length": 1},)
def _get_arrow_triangles(self) -> tuple[tuple[float, float, float], ...]:
"""Generate arrow geometry along +X axis."""
triangles = []
triangles.extend(
[
(0, -ARROW_WIDTH, 0),
(ARROW_SHAFT_LENGTH, -ARROW_WIDTH, 0),
(0, +ARROW_WIDTH, 0),
]
)
triangles.extend(
[
(0, +ARROW_WIDTH, 0),
(ARROW_SHAFT_LENGTH, -ARROW_WIDTH, 0),
(ARROW_SHAFT_LENGTH, +ARROW_WIDTH, 0),
]
)
triangles.extend(
[
(0, 0, -ARROW_WIDTH),
(ARROW_SHAFT_LENGTH, 0, -ARROW_WIDTH),
(0, 0, +ARROW_WIDTH),
]
)
triangles.extend(
[
(0, 0, +ARROW_WIDTH),
(ARROW_SHAFT_LENGTH, 0, -ARROW_WIDTH),
(ARROW_SHAFT_LENGTH, 0, +ARROW_WIDTH),
]
)
head_width = ARROW_WIDTH * ARROW_HEAD_WIDTH_MULTIPLIER
triangles.extend(
[
(ARROW_SHAFT_LENGTH, -head_width, 0),
(ARROW_SHAFT_LENGTH + ARROW_HEAD_LENGTH, 0, 0),
(ARROW_SHAFT_LENGTH, +head_width, 0),
]
)
triangles.extend(
[
(ARROW_SHAFT_LENGTH, 0, -head_width),
(ARROW_SHAFT_LENGTH + ARROW_HEAD_LENGTH, 0, 0),
(ARROW_SHAFT_LENGTH, 0, +head_width),
]
)
for i in range(ARROW_CIRCLE_SEGMENTS):
angle1 = (2 * math.pi * i) / ARROW_CIRCLE_SEGMENTS
angle2 = (2 * math.pi * (i + 1)) / ARROW_CIRCLE_SEGMENTS
y1 = head_width * math.cos(angle1)
z1 = head_width * math.sin(angle1)
y2 = head_width * math.cos(angle2)
z2 = head_width * math.sin(angle2)
triangles.extend(
[
(ARROW_SHAFT_LENGTH, 0, 0),
(ARROW_SHAFT_LENGTH, y1, z1),
(ARROW_SHAFT_LENGTH, y2, z2),
]
)
return tuple(triangles)
def setup(self) -> None:
self.custom_shape = self.new_custom_shape("TRIS", self._get_arrow_triangles())
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self.custom_shape)
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self.custom_shape, select_id=select_id)
class GizmoCone(GizmoMovable):
"""Cone gizmo for directional value editing with local axis support."""
bl_idname = "BIM_GT_gizmo_cone"
bl_target_properties = ({"id": "offset", "type": "FLOAT", "array_length": 1},)
def get_axis_direction(self) -> Vector:
if hasattr(self, "local_axis") and self.active_obj:
obj_rotation = self.active_obj.matrix_world.to_3x3()
axis_direction = obj_rotation @ self.local_axis
axis_direction.normalize()
return axis_direction
return self.axis
def _get_cone_triangles(self) -> tuple[tuple[float, float, float], ...]:
"""Generate cone geometry along +X axis."""
triangles = []
cone_tip_x = CONE_LENGTH
for i in range(CONE_SEGMENTS):
angle1 = (2 * math.pi * i) / CONE_SEGMENTS
angle2 = (2 * math.pi * (i + 1)) / CONE_SEGMENTS
y1 = CONE_RADIUS * math.cos(angle1)
z1 = CONE_RADIUS * math.sin(angle1)
y2 = CONE_RADIUS * math.cos(angle2)
z2 = CONE_RADIUS * math.sin(angle2)
triangles.extend(
[
(cone_tip_x, 0, 0),
(0, y1, z1),
(0, y2, z2),
]
)
triangles.extend(
[
(0, 0, 0),
(0, y2, z2),
(0, y1, z1),
]
)
return tuple(triangles)
def setup(self) -> None:
self.custom_shape = self.new_custom_shape("TRIS", self._get_cone_triangles())
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self.custom_shape)
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self.custom_shape, select_id=select_id)
@@ -127,6 +127,10 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
# Second pass: delete aggregates that now have no parts
deleted_aggregates = []
for aggregate in aggregates_to_check:
# Skip spatial elements - they should not be deleted even if empty
if aggregate.is_a("IfcSpatialElement"):
continue
related_objects = ifcopenshell.util.element.get_parts(aggregate)
if len(related_objects) == 0:
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
@@ -489,7 +489,7 @@ class BaseDecorator:
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
@cache
def format_value(self, context, value, custom_unit=None):
def format_value(self, context, value, suppress_zero_inches=False, custom_unit=None, in_unit_length=False):
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
precision = drawing_pset_data.get("MetricPrecision", None)
if not precision:
@@ -500,8 +500,9 @@ class BaseDecorator:
value,
precision=precision,
decimal_places=decimal_places,
suppress_zero_inches=True,
suppress_zero_inches=suppress_zero_inches,
custom_unit=custom_unit,
in_unit_length=in_unit_length,
)
def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
@@ -743,7 +744,12 @@ class DimensionDecorator(BaseDecorator):
if not show_description_only:
length = (v1 - v0).length
text = self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
text = self.format_value(
context,
length,
suppress_zero_inches=dimension_data["suppress_zero_inches"],
custom_unit=dimension_data["custom_unit"],
)
if isinstance(self, DiameterDecorator):
text = "D" + text
text = text_prefix + text + text_suffix
@@ -1198,7 +1204,11 @@ class PlanLevelDecorator(BaseDecorator):
abs_z = verts[0].z
z = helper.get_relative_z(obj, element, abs_z)
rl = self.format_value(context, z)
# Z is in meters (Blender world space), convert to project units (feet)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
z = z / unit_scale # Convert meters to feet
rl = self.format_value(context, z, in_unit_length=True)
text = "{}{}".format("" if z < 0 else "+", rl)
return text
@@ -1271,7 +1281,11 @@ class SectionLevelDecorator(BaseDecorator):
def get_text():
z = verts[0].z
rl = self.format_value(context, z)
# Z is in meters (Blender world space), convert to project units (feet)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
z = z / unit_scale # Convert meters to feet
rl = self.format_value(context, z, in_unit_length=True)
text = "RL {}{}".format("" if z < 0 else "+", rl)
return text
@@ -1739,13 +1753,27 @@ class CutDecorator:
self.cache_camera_matrix()
return True # Let's be conservative
obj = bpy.context.scene.camera
loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum), dtype=np.float32)
loc_real = np.array(obj.matrix_world.translation).flatten()
# Handle both old float64 and new float32 checksums for version compatibility
loc_checksum_bytes = eval(DecoratorData.camera_location_checksum)
if len(loc_checksum_bytes) == 24: # Old format: 3 * 8 bytes (float64)
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float64).astype(np.float32)
else: # New format: 3 * 4 bytes (float32)
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float32)
loc_real = np.array(obj.matrix_world.translation, dtype=np.float32).flatten()
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
self.cache_camera_matrix()
return True
rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum), dtype=np.float32).reshape(3, 3)
rot_real = np.array(obj.matrix_world.to_3x3())
# Handle both old float64 and new float32 checksums for version compatibility
rot_checksum_bytes = eval(DecoratorData.camera_rotation_checksum)
if len(rot_checksum_bytes) == 72: # Old format: 9 * 8 bytes (float64)
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float64).astype(np.float32).reshape(3, 3)
else: # New format: 9 * 4 bytes (float32)
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float32).reshape(3, 3)
rot_real = np.array(obj.matrix_world.to_3x3(), dtype=np.float32)
rot_dot = np.dot(rot_check, rot_real.T)
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
if angle_rad > 0.0017453292519943296: # 0.1 degrees
@@ -2015,6 +2043,10 @@ class DecorationsHandler:
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
return # Don't draw decorations in local view
# disable decorations when not in camera view
if context.region_data.view_perspective != "CAMERA":
return
if not DrawingsData.is_loaded:
DrawingsData.load()
+89 -20
View File
@@ -122,6 +122,45 @@ class BoundingBox:
return retVal
def find_best_precision(fractional_inches, max_precision=256, base_tolerance=0.0001):
"""
Find the simplest fraction denominator that accurately represents the value.
Uses a sliding tolerance - smaller denominators get more tolerance.
:param fractional_inches: The fractional part of inches (0.0 to 1.0)
:param max_precision: Maximum denominator to try (256 for imperial)
:param base_tolerance: Base tolerance in inches (very strict)
:return: Best denominator (power of 2: 2, 4, 8, 16, 32, 64, 128, 256)
"""
if abs(fractional_inches) < 0.00001:
return 1 # No fraction needed
# Try denominators from coarsest to finest: 2, 4, 8, 16, 32, 64, 128, 256
for exp in range(1, 9): # 2^1 to 2^8
denom = 2**exp
if denom > max_precision:
break
# Find nearest numerator for this denominator
numer = round(fractional_inches * denom)
# Skip if numerator is 0
if numer == 0:
continue
# Check if this fraction is close enough
fraction_value = numer / denom
error = abs(fraction_value - fractional_inches)
# Very strict tolerance - only simplify if it's a near-perfect match
# This keeps 3/256 as 3/256, but allows floating point errors to be cleaned up
if error < base_tolerance:
return denom
# If no simpler fraction works, use max precision
return max_precision
# This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py
# MeasureIt-ARCH is GPL-v3
def format_distance(
@@ -187,14 +226,18 @@ def format_distance(
if unit_length == "INCHES":
toInches = 1
if unit_length == "FEET":
toInches = 11.9999
toInches = 12
else:
toInches = 39.3700787401574887
inPerFoot = 11.9999
inPerFoot = 12.0
if isArea:
toInches = 1550
inPerFoot = 143.999
inPerFoot = 144
decInches = value * toInches
decFeet = decInches / 12
if not precision:
precision = 256
elif precision == "1":
@@ -203,8 +246,6 @@ def format_distance(
precision = int(precision.split("/")[1])
base = int(precision)
decInches = value * toInches
decFeet = decInches / 12
# Separate ft and inches
# Unless Inches are the specified Length Unit or unit_fraction is False
@@ -220,20 +261,33 @@ def format_distance(
# Separate Fractional Inches
decInches = abs(decInches) # ignore the sign for inches
inches = math.floor(decInches) # remove decimal
if inches != 0:
frac = round(base * (decInches - inches))
# Clean up floating point errors
fractional_inches = decInches - inches
tolerance = 0.01 # About 1/100 of an inch
# If the fractional part is very small, treat it as zero
if fractional_inches < tolerance:
frac = 0
# If very close to the next whole inch, round up
elif fractional_inches > (1.0 - tolerance):
frac = 0
inches += 1
else:
frac = round(base * (decInches))
# Calculate fraction normally
if inches != 0:
frac = round(base * fractional_inches)
else:
frac = round(base * fractional_inches)
# Set proper numerator and denominator
if frac != base:
numcycles = int(math.log2(base))
for i in range(numcycles):
if frac % 2 == 0:
frac = int(frac / 2)
base = int(base / 2)
else:
break
# Simplify using GCD
from math import gcd
divisor = gcd(int(frac), int(base))
frac = int(frac / divisor)
base = int(base / divisor)
else:
frac = 0
inches += 1
@@ -252,31 +306,46 @@ def format_distance(
frac = None
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches or (inches == 0 and frac)
tx_dist = ""
if feet:
tx_dist += str(feet) + "'"
if not feet and not add_inches:
tx_dist += str(feet) + "'"
if feet and add_inches:
# Add "0' - " when we have inches but no feet
# But only add " - " separator if we actually have inches to show
if not feet and add_inches:
tx_dist += "0' - "
elif feet and add_inches:
tx_dist += " - "
if not feet and value < 0:
tx_dist += "-"
if add_inches:
if feet == 0 and inches == 0:
pass
if feet == 0 and inches == 0 and not frac:
# Special case: exactly zero, show "0"
tx_dist += "0"
elif feet == 0 and inches == 0:
pass # Has fraction, will be added below
else:
tx_dist += str(inches)
if add_inches and frac:
if feet == 0 and inches == 0:
pass
pass # Has fraction, will be added below
else:
tx_dist += " "
if frac:
tx_dist += str(frac) + "/" + str(base)
if add_inches or frac:
tx_dist += '"'
# Only add inch symbol if we actually added inch content
if inches > 0 or frac > 0 or feet == 0:
tx_dist += '"'
if precision == "12" and unit_system == "IMPERIAL":
tx_dist = str(round(decFeet)) + "'"
if tx_dist == '"':
tx_dist = "0' - 0\""
else:
fmt = "%1.3f"
sq_feet = round(value * toInches / inPerFoot, 4)
@@ -975,6 +975,10 @@ class OverrideDelete(bpy.types.Operator):
except:
continue
# Skip spatial elements - they should not be deleted even if empty
if aggregate.is_a("IfcSpatialElement"):
continue
related_objects = ifcopenshell.util.element.get_parts(aggregate)
if len(related_objects) == 0:
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
@@ -178,6 +178,8 @@ classes = (
door.FinishEditingDoor,
door.EnableEditingDoor,
door.RemoveDoor,
door.ToggleDoorSwing,
door.GizmoDoorEdition,
railing.BIM_OT_add_railing,
railing.CopyRailingParameters,
railing.AddRailing,
+334 -5
View File
@@ -34,6 +34,7 @@ import bonsai.core.geometry as core
import bonsai.core.root
from bonsai.bim.module.model.window import create_bm_window, create_bm_box
import math
from mathutils import Vector, Matrix
from typing import Union, Any, Optional
@@ -278,7 +279,7 @@ def create_bm_door_lining(
new_faces = [bm.faces.new([new_verts[vi] for vi in face[1]]) for face in faces]
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
extrusion_vector = Vector((0, 1, 0)) * depth
extrusion_vector = V_(0, 1, 0) * depth
translate_verts = [v for v in extruded["geom"] if isinstance(v, bmesh.types.BMVert)]
bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts)
@@ -291,6 +292,8 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
obj = context.active_object
assert obj
props = tool.Model.get_door_props(obj)
if not props.is_editing:
return
overall_width = props.overall_width
overall_height = props.overall_height
@@ -408,7 +411,10 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
if door_swing_type == "LEFT":
bm_mirror(
bm, door_handle_verts, mirror_axes=V_(1, 0, 0), mirror_point=panel_position + V_(panel_size.x / 2, 0, 0)
bm,
door_handle_verts,
mirror_axes=V_(1, 0, 0),
mirror_point=panel_position + V_(panel_size.x / 2, 0, 0),
)
door_handle_mirrored_verts = bm_mirror(
@@ -482,10 +488,10 @@ class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
return tool.Ifc.get() and context.mode == "OBJECT"
def _execute(self, context):
def _execute(self, context: bpy.types.Context) -> set[str]:
ifc_file = tool.Ifc.get()
if not ifc_file:
self.report({"ERROR"}, "You need to start IFC project first to create a door.")
@@ -528,6 +534,8 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
assert element
props = tool.Model.get_door_props(obj)
tool.Blender.get_addon_preferences().default_parameters.door.copy_to(props)
door_data = props.get_general_kwargs(convert_to_project_units=True)
lining_props = props.get_lining_kwargs(convert_to_project_units=True)
panel_props = props.get_panel_kwargs(convert_to_project_units=True)
@@ -546,7 +554,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
)
update_door_modifier_representation(obj)
def _execute(self, context):
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
@@ -557,6 +565,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_door"
bl_label = "Cancel Editing Door on Selected Objects"
bl_description = "Cancel editing and revert door parameters to their previous values"
bl_options = {"REGISTER", "UNDO"}
def cancel_editing_door_on_object(self, obj: bpy.types.Object) -> None:
@@ -591,6 +600,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_door"
bl_label = "Finish Editing Door on Selected Objects"
bl_description = "Apply changes and finish editing door parameters"
bl_options = {"REGISTER", "UNDO"}
def finish_editing_door_on_object(self, obj):
@@ -627,6 +637,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_door"
bl_label = "Enable Editing Door on Selected Objects"
bl_description = "Enter edit mode to modify door parameters interactively"
bl_options = {"REGISTER", "UNDO"}
def edit_door_on_obj(self, obj):
@@ -670,3 +681,321 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
for obj in tool.Blender.get_selected_objects():
self.remove_door_on_object(obj)
return {"FINISHED"}
class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator):
"""Toggle door swing direction between LEFT and RIGHT"""
bl_idname = "bim.toggle_door_swing"
bl_label = "Toggle Door Swing"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = tool.Blender.get_active_object()
if not obj:
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element or not tool.Blender.Modifier.is_door(element):
return {"CANCELLED"}
props = tool.Model.get_door_props(obj)
current_type = props.door_type
if "LEFT" in current_type:
props.door_type = current_type.replace("LEFT", "RIGHT")
elif "RIGHT" in current_type:
props.door_type = current_type.replace("RIGHT", "LEFT")
return {"FINISHED"}
class GizmoDoorEdition(bpy.types.GizmoGroup):
bl_idname = "OBJECT_GGT_bim_door_edition"
bl_label = "Door Editing Gizmo"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
COLOR_RED = (1.0, 0.2, 0.2)
COLOR_GREEN = (0.1, 0.8, 0.1)
COLOR_BLUE = (0.2, 0.2, 1.0)
ARROW_SCALE = 0.25
@classmethod
def get_arrow_color_from_axis(cls, axis):
"""Get arrow color based on axis direction (X=red, Y=green, Z=blue)."""
if axis[0] != 0:
return cls.COLOR_RED
elif axis[1] != 0:
return cls.COLOR_GREEN
else:
return cls.COLOR_BLUE
gizmo_props = [
{"attr_name": "overall_height", "axis": (0, 0, 1)},
{"attr_name": "overall_width", "axis": (1, 0, 0)},
{"attr_name": "threshold_thickness", "axis": (0, 0, 1)},
{"attr_name": "threshold_depth", "axis": (0, 1, 0)},
{"attr_name": "lining_depth", "axis": (0, 1, 0)},
{"attr_name": "lining_thickness", "axis": (1, 0, 0)},
{"attr_name": "transom_offset", "axis": (0, 0, 1)},
{"attr_name": "transom_thickness", "axis": (0, 0, 1)},
]
@classmethod
def poll(cls, context):
"""Show gizmo only when a single door object is selected and active."""
prefs = tool.Blender.get_addon_preferences()
if not prefs.gizmos.draw_gizmos_in_3d_viewport:
return False
obj = tool.Blender.get_active_object(is_selected=True)
if not obj:
return False
if len(tool.Blender.get_selected_objects()) != 1:
return False
element = tool.Ifc.get_entity(obj)
if not element or not tool.Blender.Modifier.is_door(element):
return False
return True
def get_axis_rotation_matrix(self, axis):
"""Get rotation matrix to align arrow (default +X direction) with the given axis.
Args:
axis: Tuple of (x, y, z) representing the target axis direction
Returns:
Rotation matrix to align arrow with the axis
"""
axis_vec = V_(*axis).normalized()
default_dir = V_(1, 0, 0)
return default_dir.rotation_difference(axis_vec).to_matrix().to_4x4()
def get_gizmo_matrix_overall_height(self, props):
translation = Matrix.Translation(V_(props.overall_width - 0.05, 0.0, props.overall_height))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_overall_width(self, props):
translation = Matrix.Translation(V_(props.overall_width, 0.0, props.overall_height - 0.05))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_threshold_thickness(self, props):
translation = Matrix.Translation(
V_(
props.overall_width / 2,
props.threshold_offset + props.threshold_depth / 2,
props.threshold_thickness,
)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_threshold_depth(self, props):
translation = Matrix.Translation(
V_(props.overall_width / 2, props.threshold_depth, props.threshold_thickness / 2)
)
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_lining_depth(self, props):
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_depth + props.lining_offset, props.overall_height)
)
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_lining_thickness(self, props):
translation = Matrix.Translation(
V_(props.lining_thickness, props.lining_depth / 2 + props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_transom_offset(self, props):
translation = Matrix.Translation(V_(props.overall_width / 2, 0.0, props.transom_offset))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_transom_thickness(self, props):
translation = Matrix.Translation(
V_(props.overall_width / 2, 0.0, props.transom_offset + props.transom_thickness)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def setup(self, context):
prefs = tool.Blender.get_addon_preferences()
default_color = prefs.decorations_colour[:3]
highlight_color = prefs.decorator_color_selected[:3]
inactive_color = prefs.decorator_color_background[:3]
special_color = prefs.decorator_color_special[:3]
def add_gizmo_prop(prop_config):
attr_name = prop_config["attr_name"]
gizmo = self.gizmos.new("BIM_GT_gizmo_cone")
def move_get():
obj = bpy.context.active_object
if not obj:
return 0.0
props = tool.Model.get_door_props(obj)
return getattr(props, attr_name)
def move_set(value):
obj = bpy.context.active_object
if not obj:
return
props = tool.Model.get_door_props(obj)
setattr(props, attr_name, max(0.0, value))
# Set callbacks on the gizmo instance
gizmo.move_get_cb = move_get
gizmo.move_set_cb = move_set
gizmo.axis = V_(*prop_config["axis"])
gizmo.local_axis = V_(*prop_config["axis"])
gizmo.color = self.get_arrow_color_from_axis(prop_config["axis"])
gizmo.color_highlight = highlight_color
gizmo.alpha = 0.99
gizmo.use_draw_modal = True
gizmo.use_draw_scale = True
setattr(self, f"gizmo_{attr_name}", gizmo)
for prop_config in self.gizmo_props:
add_gizmo_prop(prop_config)
self.gizmo_door_type = self.gizmos.new("VIEW3D_GT_arc")
self.gizmo_door_type.use_draw_scale = False
self.gizmo_door_type.color = special_color
self.gizmo_door_type.alpha = 0.5
self.gizmo_door_type.color_highlight = highlight_color
self.gizmo_door_type.prop_path = "BIMDoorProperties.door_type"
self.gizmo_door_type.target_set_operator("bim.toggle_door_swing")
# Flip arc gizmo - mirrors the swing arc along X axis, flips door when clicked
self.gizmo_flip_arc = self.gizmos.new("VIEW3D_GT_arc")
self.gizmo_flip_arc.use_draw_scale = False
self.gizmo_flip_arc.color = inactive_color
self.gizmo_flip_arc.alpha = 0.5
self.gizmo_flip_arc.color_highlight = highlight_color
self.gizmo_flip_arc.prop_path = "BIMDoorProperties.door_type"
op = self.gizmo_flip_arc.target_set_operator("bim.flip_object")
op.flip_local_axes = "XY"
self.pen_gizmo = self.gizmos.new("VIEW3D_GT_pen")
self.pen_gizmo.use_draw_scale = False
self.pen_gizmo.color = default_color
self.pen_gizmo.color_highlight = highlight_color
self.pen_gizmo.alpha = 0.8
self.pen_gizmo.target_set_operator("bim.enable_editing_door")
self.validate_gizmo = self.gizmos.new("VIEW3D_GT_validate")
self.validate_gizmo.use_draw_scale = False
self.validate_gizmo.color = self.COLOR_GREEN
self.validate_gizmo.color_highlight = highlight_color
self.validate_gizmo.alpha = 0.8
self.validate_gizmo.target_set_operator("bim.finish_editing_door")
self.cancel_gizmo = self.gizmos.new("VIEW3D_GT_cancel")
self.cancel_gizmo.use_draw_scale = False
self.cancel_gizmo.color = self.COLOR_RED
self.cancel_gizmo.color_highlight = highlight_color
self.cancel_gizmo.alpha = 0.8
self.cancel_gizmo.target_set_operator("bim.cancel_editing_door")
def refresh(self, context):
obj = context.active_object
if not obj:
return
props = tool.Model.get_door_props(obj)
mw = obj.matrix_world
self.update_arrows(mw, props)
self.update_swing_gizmo(mw, props)
self.update_editing_gizmos(mw, props)
def update_arrows(self, mw, props):
"""Update arrow gizmos position and visibility based on editing state."""
prefs = tool.Blender.get_addon_preferences()
door_gizmo_prefs = prefs.gizmos.door
for prop_config in self.gizmo_props:
attr_name = prop_config["attr_name"]
gizmo = getattr(self, f"gizmo_{attr_name}", None)
if gizmo is None:
continue
if not getattr(door_gizmo_prefs, attr_name, True):
gizmo.hide = True
continue
if (
not props.is_editing
or (attr_name == "threshold_depth" and props.threshold_thickness == 0.0)
or (attr_name == "transom_offset" and props.transom_thickness == 0.0)
):
gizmo.hide = True
continue
# Update position and show arrow when editing
gizmo.hide = False
matrix_method = getattr(self, f"get_gizmo_matrix_{attr_name}", None)
if matrix_method:
gizmo.matrix_basis = mw @ matrix_method(props)
gizmo.matrix_offset = Matrix.Scale(self.ARROW_SCALE, 4)
def update_swing_gizmo(self, mw, props):
"""Update swing gizmo position and color based on editing state."""
prefs = tool.Blender.get_addon_preferences()
door_gizmo_prefs = prefs.gizmos.door
self.gizmo_door_type.hide = not props.is_editing or not door_gizmo_prefs.swing_arc
self.gizmo_flip_arc.hide = not props.is_editing or not door_gizmo_prefs.flip_arc
# If both are hidden, no need to calculate transforms
if self.gizmo_door_type.hide and self.gizmo_flip_arc.hide:
return
# Calculate base swing transformation (common to both arcs)
swing_x_offset = props.overall_width if "RIGHT" in props.door_type else 0.0
base_swing_transform = Matrix.Translation(V_(swing_x_offset, 0, 0)) @ Matrix.Scale(props.overall_width, 4)
if not self.gizmo_door_type.hide:
self.gizmo_door_type.matrix_basis = mw @ base_swing_transform
self.gizmo_door_type.color = prefs.decorations_colour[:3]
if not self.gizmo_flip_arc.hide:
mirror_x = Matrix.Scale(-1, 4, (0, 1, 0))
self.gizmo_flip_arc.matrix_basis = mw @ base_swing_transform @ mirror_x
def update_editing_gizmos(self, mw, props):
"""Update editing control gizmos (pen/validate/cancel) visibility and position."""
icon_z = props.overall_height + 0.5
local_transform = (
Matrix.Translation(V_(0, 0.0, icon_z))
@ Matrix.Rotation(math.radians(90), 4, (1.0, 0.0, 0.0))
@ Matrix.Scale(0.5, 4)
)
icon_matrix_base = mw @ local_transform
if props.is_editing:
self.pen_gizmo.hide = True
self.validate_gizmo.hide = False
self.validate_gizmo.matrix_basis = icon_matrix_base
self.cancel_gizmo.hide = False
cancel_local = Matrix.Translation(V_(0.5, 0.0, 0.0)) @ local_transform
self.cancel_gizmo.matrix_basis = mw @ cancel_local
else:
self.pen_gizmo.hide = False
self.pen_gizmo.matrix_basis = icon_matrix_base
self.validate_gizmo.hide = True
self.cancel_gizmo.hide = True
+94 -9
View File
@@ -645,6 +645,15 @@ class BIMStairProperties(PropertyGroup):
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
def copy_to(self, target_props: "BIMStairProperties") -> None:
"""Copy preset values to target stair properties."""
target_props.custom_tread_lock = self.custom_tread_lock
for prop_name, prop_value in self.get_props_kwargs().items():
# Skip custom_first_last_tread_run if it contains None values (when lock is enabled)
if prop_name == "custom_first_last_tread_run" and None in prop_value:
continue
setattr(target_props, prop_name, prop_value)
class BIMSverchokProperties(PropertyGroup):
node_group: bpy.props.PointerProperty(name="Node Group", type=NodeTree)
@@ -863,6 +872,14 @@ class BIMWindowProperties(PropertyGroup):
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
def copy_to(self, target_props: "BIMWindowProperties") -> None:
"""Copy preset values to target window properties, excluding materials."""
exclude_props = {"lining_material", "framing_material", "glazing_material"}
for kwargs_dict in (self.get_general_kwargs(), self.get_lining_kwargs(), self.get_panel_kwargs()):
for prop_name, prop_value in kwargs_dict.items():
if prop_name not in exclude_props:
setattr(target_props, prop_name, prop_value)
DoorType = Literal[
"SINGLE_SWING_LEFT",
@@ -892,13 +909,35 @@ class BIMDoorProperties(PropertyGroup):
default="SINGLE_SWING_LEFT",
update=update_door,
)
overall_height: bpy.props.FloatProperty(name="Overall Height", default=2.0, subtype="DISTANCE", update=update_door)
overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.9, subtype="DISTANCE", update=update_door)
overall_height: bpy.props.FloatProperty(
name="Overall Height",
default=2.0,
min=0,
subtype="DISTANCE",
update=update_door,
)
overall_width: bpy.props.FloatProperty(
name="Overall Width",
default=0.9,
min=0,
subtype="DISTANCE",
update=update_door,
)
# lining properties
lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050, subtype="DISTANCE", update=update_door)
lining_depth: bpy.props.FloatProperty(
name="Lining Depth",
default=0.050,
min=0.001,
subtype="DISTANCE",
update=update_door,
)
lining_thickness: bpy.props.FloatProperty(
name="Lining Thickness", default=0.050, subtype="DISTANCE", update=update_door
name="Lining Thickness",
default=0.050,
min=0.001,
subtype="DISTANCE",
update=update_door,
)
lining_offset: bpy.props.FloatProperty(
name="Lining Offset",
@@ -920,6 +959,7 @@ class BIMDoorProperties(PropertyGroup):
name="Transom Thickness",
description="Set values > 0 to add a transom.\n" "`0.050 mm` is good as default value",
default=0.000,
min=0,
subtype="DISTANCE",
update=update_door,
)
@@ -927,6 +967,7 @@ class BIMDoorProperties(PropertyGroup):
name="Transom Offset",
description="Distance from the bottom door opening to the beginning of the transom (unlike windows)",
default=1.525,
min=0,
subtype="DISTANCE",
update=update_door,
)
@@ -935,20 +976,32 @@ class BIMDoorProperties(PropertyGroup):
name="Casing Thickness",
description="Set values > 0 and LiningOffset = 0 to add a casing.",
default=0.075,
min=0,
subtype="DISTANCE",
update=update_door,
)
casing_depth: bpy.props.FloatProperty(
name="Casing Depth",
default=0.005,
min=0.001,
subtype="DISTANCE",
update=update_door,
)
casing_depth: bpy.props.FloatProperty(name="Casing Depth", default=0.005, subtype="DISTANCE", update=update_door)
threshold_thickness: bpy.props.FloatProperty(
name="Threshold Thickness",
description="Set values > 0 to add a threshold.",
default=0.025,
min=0,
subtype="DISTANCE",
update=update_door,
)
threshold_depth: bpy.props.FloatProperty(
name="Threshold Depth", default=0.1, subtype="DISTANCE", update=update_door
name="Threshold Depth",
default=0.1,
min=0.001,
subtype="DISTANCE",
update=update_door,
)
threshold_offset: bpy.props.FloatProperty(
name="Threshold Offset",
@@ -959,7 +1012,13 @@ class BIMDoorProperties(PropertyGroup):
)
# panel properties
panel_depth: bpy.props.FloatProperty(name="Panel Depth", default=0.035, subtype="DISTANCE", update=update_door)
panel_depth: bpy.props.FloatProperty(
name="Panel Depth",
default=0.035,
min=0,
subtype="DISTANCE",
update=update_door,
)
panel_width_ratio: bpy.props.FloatProperty(
name="Panel Width Ratio",
description="Width of this panel, given as ratio " "relative to the total clear opening width of the door",
@@ -969,10 +1028,18 @@ class BIMDoorProperties(PropertyGroup):
update=update_door,
)
frame_thickness: bpy.props.FloatProperty(
name="Window Frame Thickness", default=0.035, subtype="DISTANCE", update=update_door
name="Window Frame Thickness",
default=0.035,
min=0,
subtype="DISTANCE",
update=update_door,
)
frame_depth: bpy.props.FloatProperty(
name="Window Frame Depth", default=0.035, subtype="DISTANCE", update=update_door
name="Window Frame Depth",
default=0.035,
min=0,
subtype="DISTANCE",
update=update_door,
)
# Material properties
@@ -1080,6 +1147,14 @@ class BIMDoorProperties(PropertyGroup):
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
def copy_to(self, target_props: "BIMDoorProperties") -> None:
"""Copy properties to target door properties, excluding materials."""
exclude_props = {"lining_material", "framing_material", "glazing_material"}
for kwargs_dict in (self.get_general_kwargs(), self.get_lining_kwargs(), self.get_panel_kwargs()):
for prop_name, prop_value in kwargs_dict.items():
if prop_name not in exclude_props:
setattr(target_props, prop_name, prop_value)
RailingType = Literal["FRAMELESS_PANEL", "WALL_MOUNTED_HANDRAIL"]
CapType = Literal["TO_END_POST_AND_FLOOR", "TO_END_POST", "TO_FLOOR", "TO_WALL", "180", "NONE"]
@@ -1179,6 +1254,11 @@ class BIMRailingProperties(PropertyGroup):
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
def copy_to(self, target_props: "BIMRailingProperties") -> None:
"""Copy preset values to target railing properties."""
for prop_name, prop_value in self.get_general_kwargs().items():
setattr(target_props, prop_name, prop_value)
def to_angle(percentage: float) -> float:
return math.atan(percentage / 100)
@@ -1281,6 +1361,11 @@ class BIMRoofProperties(PropertyGroup):
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
def copy_to(self, target_props: "BIMRoofProperties") -> None:
"""Copy preset values to target roof properties."""
for prop_name, prop_value in self.get_general_kwargs().items():
setattr(target_props, prop_name, prop_value)
class SnapMousePoint(PropertyGroup):
x: bpy.props.FloatProperty(name="X")
@@ -106,6 +106,13 @@ def update_railing_modifier_ifc_data(context: bpy.types.Context) -> None:
model_representation = ifcopenshell.api.geometry.add_railing_representation(ifc_file, **representation_data)
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
# recalculate normals to ensure correct shading
mesh = obj.data
if isinstance(mesh, bpy.types.Mesh):
bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=False)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
tool.Blender.apply_bmesh(mesh, bm)
elif props.railing_type == "FRAMELESS_PANEL":
tool.Model.add_body_representation(obj)
@@ -335,6 +342,8 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Model.get_railing_props(obj)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
tool.Blender.get_addon_preferences().default_parameters.railing.copy_to(props)
railing_data = props.get_general_kwargs(convert_to_project_units=True)
path_data = get_path_data(obj)
@@ -570,6 +570,8 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Model.get_roof_props(obj)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
tool.Blender.get_addon_preferences().default_parameters.roof.copy_to(props)
# rejecting original roof shape to be safe
# taking into account only it's bounding box dimensions
if obj.dimensions.x == 0 or obj.dimensions.y == 0:
@@ -188,6 +188,8 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Model.get_stair_props(obj)
ifc_file = tool.Ifc.get()
tool.Blender.get_addon_preferences().default_parameters.stair.copy_to(props)
# Use the special method that includes custom_tread_lock for IFC storage
stair_data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
+125 -93
View File
@@ -305,48 +305,9 @@ class BIM_PT_stair(bpy.types.Panel):
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
row = self.layout.row(align=True)
for prop_name in props.get_props_kwargs():
# Skip custom_tread_lock as it's handled with custom_first_last_tread_run
if prop_name == "custom_tread_lock":
continue
prop_value = getattr(props, prop_name)
draw_stair_properties(self.layout, props)
# Special handling for custom_first_last_tread_run
if prop_name == "custom_first_last_tread_run":
# Draw the lock toggle
row_lock = self.layout.row(align=True)
lock_text = (
"Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads"
)
row_lock.prop(
props,
"custom_tread_lock",
text=lock_text,
icon="LOCKED" if props.custom_tread_lock else "UNLOCKED",
)
# Only show the custom values input if unlocked
if not props.custom_tread_lock:
prop_readable_name = props.bl_rna.properties[prop_name].name
self.layout.label(text=f"{prop_readable_name}:")
self.layout.prop(props, prop_name, text="")
elif isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
prop_readable_name = props.bl_rna.properties[prop_name].name
self.layout.label(text=f"{prop_readable_name}:")
self.layout.prop(props, prop_name, text="")
else:
self.layout.prop(props, prop_name)
if prop_name == "height": # Weak but we just want to insert this inside props drawing
row_length = self.layout.row(align=True)
row_length.prop(props, "total_length_target")
row_length.prop(
props,
"total_length_lock",
text="",
icon="LOCKED" if props.total_length_lock else "UNLOCKED",
)
regenerate_stair_mesh(obj)
else:
calculated_params = StairData.data["calculated_params"]
@@ -440,39 +401,11 @@ class BIM_PT_window(bpy.types.Panel):
row.label(text="Window parameters", icon="OUTLINER_OB_LATTICE")
if props.is_editing:
number_of_panels, panels_data = props.window_types_panels[props.window_type]
row = self.layout.row(align=True)
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_window", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
for prop in general_props:
self.layout.prop(props, prop)
lining_props = props.get_lining_kwargs()
self.layout.label(text="Lining properties")
for prop in lining_props:
self.layout.prop(props, prop)
panel_props = props.get_panel_kwargs()
self.layout.label(text="Panel properties")
panel_box = self.layout.box()
row = panel_box.row()
cols = [row.column(align=True) for i in range(number_of_panels + 1)]
cols[0].label(text="")
for panel_i in range(number_of_panels):
r = cols[panel_i + 1].row()
r.alignment = "CENTER"
r.label(text=f"#{panel_i}")
r = cols[panel_i + 1].row()
for prop in panel_props:
cols[0].label(text=f"{props.bl_rna.properties[prop].name}")
for panel_i in range(number_of_panels):
cols[panel_i + 1].prop(props, prop, index=panel_i, text="")
draw_window_properties(self.layout, props)
self.layout.use_property_split = True
self.layout.label(text="Material Properties")
@@ -560,19 +493,7 @@ class BIM_PT_door(bpy.types.Panel):
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_door", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
for prop in general_props:
self.layout.prop(props, prop)
lining_props = props.get_lining_kwargs()
self.layout.label(text="Lining Properties")
for prop in lining_props:
self.layout.prop(props, prop)
panel_props = props.get_panel_kwargs()
self.layout.label(text="Panel Properties")
for prop in panel_props:
self.layout.prop(props, prop)
draw_door_properties(self.layout, props)
self.layout.use_property_split = True
self.layout.label(text="Material Properties")
@@ -643,14 +564,7 @@ class BIM_PT_railing(bpy.types.Panel):
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_railing", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
for prop in general_props:
if prop == "support_spacing" and props.use_manual_supports:
row = self.layout.row()
row.prop(props, prop)
row.active = False
continue
self.layout.prop(props, prop)
draw_railing_properties(self.layout, props)
update_railing_modifier_bmesh(context)
@@ -708,9 +622,7 @@ class BIM_PT_roof(bpy.types.Panel):
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_roof", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
for prop in general_props:
self.layout.prop(props, prop)
draw_roof_properties(self.layout, props)
update_roof_modifier_bmesh(obj)
elif props.is_editing_path:
@@ -776,6 +688,126 @@ class BIM_PT_external_parametric_geometry(bpy.types.Panel):
row.prop(input, "default_value", text=input.name)
def draw_door_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
"""Draw door properties UI (shared between properties panel and preferences)."""
# General properties
general_props = props.get_general_kwargs()
for prop in general_props:
layout.prop(props, prop)
# Lining properties
layout.label(text="Lining Properties")
lining_props = props.get_lining_kwargs()
for prop in lining_props:
layout.prop(props, prop)
# Panel properties
layout.label(text="Panel Properties")
panel_props = props.get_panel_kwargs()
for prop in panel_props:
layout.prop(props, prop)
def draw_window_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
"""Draw window properties UI (shared between properties panel and preferences)."""
number_of_panels, panels_data = props.window_types_panels[props.window_type]
# General and lining properties
general_props = props.get_general_kwargs()
for prop in general_props:
layout.prop(props, prop)
layout.label(text="Lining Properties")
lining_props = props.get_lining_kwargs()
for prop in lining_props:
layout.prop(props, prop)
# Panel properties (special layout for multiple panels)
panel_props = props.get_panel_kwargs()
layout.label(text="Panel Properties")
panel_box = layout.box()
row = panel_box.row()
cols = [row.column(align=True) for i in range(number_of_panels + 1)]
cols[0].label(text="")
for panel_i in range(number_of_panels):
r = cols[panel_i + 1].row()
r.alignment = "CENTER"
r.label(text=f"#{panel_i}")
r = cols[panel_i + 1].row()
for prop in panel_props:
cols[0].label(text=f"{props.bl_rna.properties[prop].name}")
for panel_i in range(number_of_panels):
cols[panel_i + 1].prop(props, prop, index=panel_i, text="")
def draw_railing_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
"""Draw railing properties UI (shared between properties panel and preferences)."""
general_props = props.get_general_kwargs()
for prop in general_props:
if prop == "support_spacing" and props.use_manual_supports:
row = layout.row()
row.prop(props, prop)
row.active = False
continue
layout.prop(props, prop)
def draw_roof_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
"""Draw roof properties UI (shared between properties panel and preferences)."""
# General properties
general_props = props.get_general_kwargs()
for prop in general_props:
layout.prop(props, prop)
def draw_stair_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
"""Draw stair properties UI (shared between properties panel and preferences)."""
for prop_name in props.get_props_kwargs():
# Skip custom_tread_lock as it's handled with custom_first_last_tread_run
if prop_name == "custom_tread_lock":
continue
prop_value = getattr(props, prop_name)
# Special handling for custom_first_last_tread_run
if prop_name == "custom_first_last_tread_run":
# Draw the lock toggle
row_lock = layout.row(align=True)
lock_text = "Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads"
row_lock.prop(
props,
"custom_tread_lock",
text=lock_text,
icon="LOCKED" if props.custom_tread_lock else "UNLOCKED",
)
# Only show the custom values input if unlocked
if not props.custom_tread_lock:
prop_readable_name = props.bl_rna.properties[prop_name].name
layout.label(text=f"{prop_readable_name}:")
layout.prop(props, prop_name, text="")
elif isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
prop_readable_name = props.bl_rna.properties[prop_name].name
layout.label(text=f"{prop_readable_name}:")
layout.prop(props, prop_name, text="")
else:
layout.prop(props, prop_name)
if prop_name == "height": # Weak but we just want to insert this inside props drawing
row_length = layout.row(align=True)
row_length.prop(props, "total_length_target")
row_length.prop(
props,
"total_length_lock",
text="",
icon="LOCKED" if props.total_length_lock else "UNLOCKED",
)
def add_menu(self: bpy.types.Menu, context: bpy.types.Context) -> None:
self.layout.operator_context = "INVOKE_REGION_WIN"
self.layout.operator("bim.add_element", icon_value=bonsai.bim.icons["IFC"].icon_id, text="IFC Element")
@@ -258,6 +258,9 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None:
obj = context.active_object
assert obj
props = tool.Model.get_window_props(obj)
if not props.is_editing:
return
panel_schema = DEFAULT_PANEL_SCHEMAS[props.window_type]
accumulated_height = [0] * len(panel_schema[0])
built_panels = []
@@ -451,6 +454,8 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
assert element
props = tool.Model.get_window_props(obj)
tool.Blender.get_addon_preferences().default_parameters.window.copy_to(props)
window_data = props.get_general_kwargs(convert_to_project_units=True)
lining_props = props.get_lining_kwargs(convert_to_project_units=True)
panel_props = props.get_panel_kwargs(convert_to_project_units=True)
+38 -9
View File
@@ -36,6 +36,7 @@ import bonsai.core.geometry
import ifcopenshell
import ifcopenshell.api.attribute
import ifcopenshell.util.unit
from bonsai.tool.unit import parse_distance_string
from typing import TYPE_CHECKING, Union, Literal
@@ -48,18 +49,46 @@ def get_subelement_class(
def update_elevation(self: "BIMContainer", context: bpy.types.Context) -> None:
try:
elevation = float(self.elevation)
if self.elevation != str(elevation):
self.elevation = str(elevation)
return
except:
elevation = 0
# Try to parse the input string with unit support first
is_valid, parsed_elevation = parse_distance_string(self.elevation, use_project_unit=True)
if is_valid:
elevation = parsed_elevation
# Format the string if it needs normalization:
# - Plain numbers (no units): "4" → "4' - 0""
# - Missing symbols: "5'3" → "5' - 3""
# - Inconsistent formatting: "5' 3 3/256" → "5' - 3 3/256""
input_str = self.elevation.strip()
# Check if input needs formatting (has feet/inch symbols or is plain number)
needs_formatting = (
input_str.replace(".", "").replace("-", "").replace(" ", "").replace("/", "").isdigit() # Plain number
or "'" in input_str # Has feet symbol
or '"' in input_str # Has inch symbol
or " " in input_str.replace(" - ", "") # Has spaces (but not the formatted " - ")
)
# Only format if the current string doesn't match our standard format
if needs_formatting:
formatted = tool.Unit.format_distance(elevation)
if self.elevation != formatted:
self.elevation = formatted
return
else:
# Fall back to direct float conversion for backward compatibility
try:
elevation = float(self.elevation)
except Exception as e:
print(f"Elevation parsing failed for '{self.elevation}': {e}")
elevation = 0
# Update the object's position in the 3D scene
if ifc_definition_id := self.ifc_definition_id:
entity = tool.Ifc.get().by_id(ifc_definition_id)
if obj := tool.Ifc.get_object(entity):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
obj.matrix_world[2][3] = elevation * unit_scale
# elevation is already in meters, set it directly
obj.matrix_world[2][3] = elevation
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj)
+1 -1
View File
@@ -277,7 +277,7 @@ class BIM_UL_containers_manager(UIList):
if item:
row = layout.row(align=True)
icon = self.icon_by_class.get(item.ifc_class, "META_PLANE")
split = row.split(factor=0.85)
split = row.split(factor=0.8)
if item.long_name:
split2 = split.split(factor=0.7)
row = split2.row(align=True)
@@ -102,6 +102,19 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
element_had_openings = tool.Geometry.has_openings(voided_element)
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body")
if not element2:
# Apply scale transformation for vanilla Blender objects before creating IFC representation
# This ensures the opening geometry matches the visual scale of the object
if tool.Geometry.is_scaled(obj2):
if obj2.data:
# Make mesh data single-user if it's shared
if obj2.data.users > 1:
obj2.data = obj2.data.copy()
context_override = {}
context_override["object"] = context_override["active_object"] = obj2
context_override["selected_objects"] = context_override["selected_editable_objects"] = [obj2]
with bpy.context.temp_override(**context_override):
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
element2 = bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
+129
View File
@@ -38,6 +38,20 @@ from ifcopenshell.util.file import IfcHeaderExtractor
from bonsai.bim.prop import Attribute
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty
from bonsai.bim.module.pset.prop import IfcProperty
from bonsai.bim.module.model.prop import (
BIMDoorProperties,
BIMWindowProperties,
BIMRailingProperties,
BIMRoofProperties,
BIMStairProperties,
)
from bonsai.bim.module.model.ui import (
draw_door_properties,
draw_window_properties,
draw_railing_properties,
draw_roof_properties,
draw_stair_properties,
)
from typing import Optional, TYPE_CHECKING, Literal
from natsort import natsorted
import textwrap
@@ -228,6 +242,48 @@ class BIM_UL_generic(bpy.types.UIList):
layout.label(text="", translate=False)
class GizmoPreferencesDoor(bpy.types.PropertyGroup):
"""Property group for door gizmo visibility settings."""
overall_height: BoolProperty(name="Overall Height", default=True)
overall_width: BoolProperty(name="Overall Width", default=True)
threshold_thickness: BoolProperty(name="Threshold Thickness", default=True)
threshold_depth: BoolProperty(name="Threshold Depth", default=True)
lining_depth: BoolProperty(name="Lining Depth", default=True)
lining_thickness: BoolProperty(name="Lining Thickness", default=True)
transom_offset: BoolProperty(name="Transom Offset", default=True)
transom_thickness: BoolProperty(name="Transom Thickness", default=True)
swing_arc: BoolProperty(name="Swing Arc", default=True, description="Show door swing direction arc")
flip_arc: BoolProperty(name="Flip Arc", default=True, description="Show flip door orientation arc")
if TYPE_CHECKING:
overall_height: bool
overall_width: bool
threshold_thickness: bool
threshold_depth: bool
lining_depth: bool
lining_thickness: bool
transom_offset: bool
transom_thickness: bool
swing_arc: bool
flip_arc: bool
class GizmoPreferences(bpy.types.PropertyGroup):
"""Property group for all gizmo visibility settings."""
draw_gizmos_in_3d_viewport: BoolProperty(
name="Draw Gizmos In 3D Viewport",
default=True,
description="Show interactive gizmos in the 3D viewport for parametric elements",
)
door: bpy.props.PointerProperty(type=GizmoPreferencesDoor)
if TYPE_CHECKING:
draw_gizmos_in_3d_viewport: bool
door: GizmoPreferencesDoor
class DocPreferences(bpy.types.PropertyGroup):
sheets_dir: StringProperty(
default=os.path.join("sheets") + os.path.sep,
@@ -320,6 +376,14 @@ class DocPreferences(bpy.types.PropertyGroup):
classes_no_cut: str
class DefaultParameters(bpy.types.PropertyGroup):
door: bpy.props.PointerProperty(type=BIMDoorProperties)
window: bpy.props.PointerProperty(type=BIMWindowProperties)
railing: bpy.props.PointerProperty(type=BIMRailingProperties)
roof: bpy.props.PointerProperty(type=BIMRoofProperties)
stair: bpy.props.PointerProperty(type=BIMStairProperties)
class BIM_ADDON_preferences(bpy.types.AddonPreferences):
bl_idname = tool.Blender.get_blender_addon_package_name()
svg2pdf_command: StringProperty(
@@ -493,6 +557,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
name="Occurrence Name Function",
description="Code that will be evaluated to generate occurrence name for CUSTOM occurrence name style",
)
gizmos: bpy.props.PointerProperty(type=GizmoPreferences)
def update_data_dir(self, context: bpy.types.Context) -> None:
import bonsai.bim.schema
@@ -523,6 +588,11 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
subtype="DIR_PATH",
)
doc: bpy.props.PointerProperty(type=DocPreferences)
default_parameters: bpy.props.PointerProperty(
type=DefaultParameters,
name="Default Parameters",
description="Default parameters for BIM elements",
)
if TYPE_CHECKING:
svg2pdf_command: str
@@ -553,10 +623,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
should_always_cache: bool
occurrence_name_style: Literal["CLASS", "TYPE", "CUSTOM"]
occurrence_name_function: str
gizmos: GizmoPreferences
data_dir: str
cache_dir: str
pset_dir: str
doc: DocPreferences
default_parameters: DefaultParameters
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
@@ -599,10 +671,34 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.prop(self, "should_use_snap")
layout.prop(self, "should_play_chaching_sound")
layout.prop(self.gizmos, "draw_gizmos_in_3d_viewport")
if self.gizmos.draw_gizmos_in_3d_viewport:
bonsai.bim.helper.draw_expandable_panel(
layout,
context,
"Gizmos Parameters",
self.draw_gizmo_parameters,
)
def draw_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
box = layout.box()
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Door", self.draw_door_gizmo_parameters)
def draw_door_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
door_gizmos = self.gizmos.door
for prop in door_gizmos.__annotations__:
layout.prop(door_gizmos, prop)
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "occurrence_name_style")
if self.occurrence_name_style == "CUSTOM":
layout.prop(self, "occurrence_name_function")
bonsai.bim.helper.draw_expandable_panel(
layout,
context,
"Default Parameters",
self.draw_default_parameters,
)
def draw_directories(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "data_dir")
@@ -639,6 +735,39 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.row().prop(self, "decorator_color_error")
layout.row().prop(self, "decorator_color_background")
def draw_default_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
box = layout.box()
bonsai.bim.helper.draw_expandable_panel(
box,
context,
"Door",
lambda _layout, _context: draw_door_properties(_layout, self.default_parameters.door),
)
bonsai.bim.helper.draw_expandable_panel(
box,
context,
"Window",
lambda _layout, _context: draw_window_properties(_layout, self.default_parameters.window),
)
bonsai.bim.helper.draw_expandable_panel(
box,
context,
"Railing",
lambda _layout, _context: draw_railing_properties(_layout, self.default_parameters.railing),
)
bonsai.bim.helper.draw_expandable_panel(
box,
context,
"Roof",
lambda _layout, _context: draw_roof_properties(_layout, self.default_parameters.roof),
)
bonsai.bim.helper.draw_expandable_panel(
box,
context,
"Stair",
lambda _layout, _context: draw_stair_properties(_layout, self.default_parameters.stair),
)
def draw_other_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "opening_focus_opacity")
layout.prop(self, "should_disable_undo_on_save")
+208 -20
View File
@@ -33,6 +33,7 @@ import sys
import bpy
import math
import bmesh
import numpy as np
import mathutils.geometry
from mathutils import Vector, Matrix, geometry
import itertools
@@ -188,6 +189,33 @@ class Cad:
"""
return geometry.intersect_line_plane(v1, v2, plane_co, plane_no)
def intersect_edge_plane_v2(v1, v2, plane_co, plane_no, eps=1e-9):
"""
Numpy version of intersect_edge_plane
> takes an edges as two vector, and a plane as origin point and normal
< return the intersection point or None
"""
# References: https://rosettacode.org/wiki/Find_the_intersection_of_a_line_with_a_plane#Python,
# https://stackoverflow.com/a/18543221
p0 = np.array((v1.x, v1.y, v1.z), dtype=np.float64)
p1 = np.array((v2.x, v2.y, v2.z), dtype=np.float64)
pc = np.array((plane_co.x, plane_co.y, plane_co.z), dtype=np.float64)
n = np.array((plane_no.x, plane_no.y, plane_no.z), dtype=np.float64)
u = p1 - p0
dot = np.dot(u, n)
if abs(dot) < eps:
# Line is parallel to plane (no intersection or lies in plane)
return None
w = pc - p0
fac = np.dot(w, n) / dot
p = p0 + fac * u
return Vector(p)
@classmethod
def intersect_edges(
cls, edge1: tuple[Vector, Vector], edge2: tuple[Vector, Vector]
@@ -215,47 +243,50 @@ class Cad:
def intersect_edges_v2(cls, edge1, edge2):
"""
Calculate the closest points on two line segments.
Note: This function doesn't use intersect_line_line
Note: This function doesn't use intersect_line_line and uses Numpy for calculations
> edge1: tuple of two vectors (v1, v2) representing the first segment
> edge2: tuple of two vectors (v3, v4) representing the second segment
< returns: tuple of two vectors (C1, C2) or (None, None) if lines are parallel
"""
# This function seems to work better then intersect_line_line
# This function seems to work better than intersect_line_line
# in orthogonal view
# https://en.wikipedia.org/wiki/Skew_lines#Nearest_points
is_2d = False
# Starting and ending points
P1, P1_end = edge1
P2, P2_end = edge2
if len(P1) == 2:
p1, p1_end = edge1
p2, p2_end = edge2
if len(p1) == 2:
is_2d = True
P1, P1_end = P1.to_3d(), P1_end.to_3d()
P2, P2_end = P2.to_3d(), P2_end.to_3d()
p1, p1_end = p1.to_3d(), p1_end.to_3d()
p2, p2_end = p2.to_3d(), p2_end.to_3d()
p1 = np.array((p1.x, p1.y, p1.z), dtype=np.float64)
p1_end = np.array((p1_end.x, p1_end.y, p1_end.z), dtype=np.float64)
p2 = np.array((p2.x, p2.y, p2.z), dtype=np.float64)
p2_end = np.array((p2_end.x, p2_end.y, p2_end.z), dtype=np.float64)
# Directions
d1 = (P1_end - P1).normalized()
d2 = (P2_end - P2).normalized()
d1 = p1_end - p1
d2 = p2_end - p2
d1 /= np.linalg.norm(d1) or 1 # equivalent of Vector.normalized() or Vector / Vector.length
d2 /= np.linalg.norm(d2) or 1
n = d1.cross(d2)
n = np.cross(d1, d2)
# if n is zero, lines are parallel
if abs(n.length) < 1e-6:
if abs(np.linalg.norm(n)) < 1e-6:
return None, None
n2 = d2.cross(n)
C1 = P1 + ((P2 - P1).dot(n2) / (d1.dot(n2))) * d1
n1 = d1.cross(n)
C2 = P2 + ((P1 - P2).dot(n1) / (d2.dot(n1))) * d2
n2 = np.cross(d2, n)
c1 = p1 + (np.dot((p2 - p1), n2) / (np.dot(d1, n2))) * d1
n1 = np.cross(d1, n)
c2 = p2 + (np.dot((p1 - p2), n1) / (np.dot(d2, n1))) * d2
if is_2d:
return C1.to_2d(), C2.to_2d()
return Vector(c1[:2].copy()), Vector(c2[:2].copy())
else:
return C1, C2
return Vector(c1), Vector(c2)
@classmethod
def get_intersection(cls, edge1, edge2):
@@ -807,3 +838,160 @@ class Cad:
v1 = v2
return new_verts
@classmethod
def region_2d_to_vector_3d_np(cls, region: bpy.types.Region, rv3d: bpy.types.RegionView3d, coord: Vector) -> Vector:
"""
Numpy version of view3d_utils.region_2d_to_vector_3d
Return a direction vector from the viewport at the specific 2d region
coordinate.
> region: region of the 3D viewport, typically bpy.context.region.
> rv3d: 3D region data, typically bpy.context.space_data.region_3d.
> coord: 2d coordinates relative to the region:
(event.mouse_region_x, event.mouse_region_y) for example.
< returns a normalized 3d vector.
"""
view_m = np.array(rv3d.view_matrix)
window_m = np.array(rv3d.window_matrix)
viewinv = np.linalg.inv(view_m)
if rv3d.is_perspective:
# For better precision with large numbers, avoid using rv3d.perspective_matrix. See https://github.com/IfcOpenShell/IfcOpenShell/issues/7046
# Calculate it with view_matrix and window_matrix as numpy arrays.
pers_m = window_m @ view_m
persinv = np.linalg.inv(pers_m)
out = np.array(
[(2.0 * coord[0] / region.width) - 1.0, (2.0 * coord[1] / region.height) - 1.0, -0.5, 1.0],
dtype=np.float64,
)
w = out[:3].dot(persinv[3, :3]) + persinv[3, 3]
viewinv_translation = viewinv[:3, 3]
view_vector = (persinv.dot(out)[:3] / w) - viewinv_translation
else:
view_vector = -viewinv[:3, 2].copy() # -Z column
view_vector /= np.linalg.norm(view_vector) or 1 # equivalent to Vector.normalized()
return Vector(view_vector)
@classmethod
def region_2d_to_location_3d_np(
cls, region: bpy.types.Region, rv3d: bpy.types.RegionView3d, coord: Vector, depth_location: Vector
) -> Vector:
"""
Numpy version of view3d_utils.region_2d_to_location_3d
Return a 3d location from the region relative 2d coords, aligned with
*depth_location*.
> region: region of the 3D viewport, typically bpy.context.region.
> rv3d: 3D region data, typically bpy.context.space_data.region_3d.
> coord: 2d coordinates relative to the region:
(event.mouse_region_x, event.mouse_region_y) for example.
< returns a normalized 3d vector.
"""
coord_vec = cls.region_2d_to_vector_3d_np(region, rv3d, coord)
depth_location = np.array([depth_location[0], depth_location[1], depth_location[2]], dtype=np.float64)
origin_start = cls.region_2d_to_origin_3d_np(region, rv3d, coord)
origin_end = origin_start + coord_vec
if rv3d.is_perspective:
viewinv = np.linalg.inv(rv3d.view_matrix)
view_vec = viewinv[:3, 2].copy()
return cls.intersect_edge_plane_v2(
Vector(origin_start), Vector(origin_end), Vector(depth_location), Vector(view_vec)
)
else:
return cls.point_on_edge(
Vector(depth_location),
(Vector(origin_start), Vector(origin_end)),
)
@classmethod
def region_2d_to_origin_3d_np(
cls, region: bpy.types.Region, rv3d: bpy.types.RegionView3d, coord: Vector, *, clamp: float = None
) -> Vector:
"""
Numpy version of view3d_utils.region_2d_to_origin_3d
Return the 3d view origin from the region relative 2d coords.
.. note::
Orthographic views have a less obvious origin,
the far clip is used to define the viewport near/far extents.
Since far clip can be a very large value,
the result may give with numeric precision issues.
To avoid this problem, you can optionally clamp the far clip to a
smaller value based on the data you're operating on.
> region: region of the 3D viewport, typically bpy.context.region.
> rv3d: 3D region data, typically bpy.context.space_data.region_3d.
> coord: 2d coordinates relative to the region:
(event.mouse_region_x, event.mouse_region_y) for example.
> clamp: clamp: Clamp the maximum far-clip value used.
(negative value will move the offset away from the view_location)
< returns the origin of the viewpoint in 3d space.
"""
view_m = np.array(rv3d.view_matrix)
window_m = np.array(rv3d.window_matrix)
viewinv = np.linalg.inv(view_m)
if rv3d.is_perspective:
origin_start = viewinv[:3, 3].copy()
else:
pers_m = window_m @ view_m # See https://github.com/IfcOpenShell/IfcOpenShell/issues/7046
persinv = np.linalg.inv(pers_m)
dx = (2.0 * coord[0] / region.width) - 1.0
dy = (2.0 * coord[1] / region.height) - 1.0
origin_start = (persinv[:3, 0] * dx) + (persinv[:3, 1] * dy) + persinv[:3, 3]
if clamp != 0.0:
if rv3d.view_perspective != "CAMERA":
origin_offset = persinv[:3, 2].copy() # column 2
if clamp is not None:
c = float(clamp)
if c < 0.0:
origin_offset = -origin_offset
c = -c
length = np.linalg.norm(origin_offset)
if length > c and length > 0.0:
origin_offset = (origin_offset / length) * c
origin_start = origin_start - origin_offset
return Vector(origin_start)
@classmethod
def location_3d_to_region_2d_np(
cls, region: bpy.types.Region, rv3d: bpy.types.RegionView3d, coord: Vector, *, default=None
) -> Vector:
"""
Numpy version of view3d_utils.location_3d_to_region_2d
Return the *region* relative 2d location of a 3d position.
> region: region of the 3D viewport, typically bpy.context.region.
> rv3d: 3D region data, typically bpy.context.space_data.region_3d.
> coord: 2d coordinates relative to the region:
(event.mouse_region_x, event.mouse_region_y) for example.
< returns a 2d location.
"""
pt = np.array((coord[0], coord[1], coord[2], 1.0), dtype=np.float64)
view_m = np.array(rv3d.view_matrix)
window_m = np.array(rv3d.window_matrix)
pers_m = window_m @ view_m
prj = pers_m.dot(pt) # 4-vector
w = prj[3]
if w > 0.0:
width_half = region.width / 2.0
height_half = region.height / 2.0
x = width_half + width_half * (prj[0] / w)
y = height_half + height_half * (prj[1] / w)
return Vector((float(x), float(y)))
return default
+6 -1
View File
@@ -1625,7 +1625,12 @@ class Drawing(bonsai.core.tool.Drawing):
xmin, xmax, ymin, ymax = helper.ortho_view_frame(camera.data)[:4]
rl = ifcopenshell.util.placement.get_local_placement(storey.ObjectPlacement)[2][3]
y = (camera.matrix_world.inverted() @ Vector((0.0, 0.0, rl))).y
# Convert RL from project units (feet) to meters for Blender world space
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
rl_meters = rl * unit_scale
y = (camera.matrix_world.inverted() @ Vector((0.0, 0.0, rl_meters))).y
if y < ymin or y > ymax:
return
+15
View File
@@ -2096,6 +2096,7 @@ class Geometry(bonsai.core.tool.Geometry):
decomposition_relationships = tool.Root.get_decomposition_relationships(objects_to_duplicate)
connection_relationships = tool.Root.get_connection_relationships(objects_to_duplicate)
old_to_new: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = {}
old_obj_name_to_new_obj_name: dict[str, str] = {}
for obj in objects_to_duplicate:
element = tool.Ifc.get_entity(obj)
@@ -2147,6 +2148,7 @@ class Geometry(bonsai.core.tool.Geometry):
collection.objects.link(new_obj)
obj.select_set(False)
new_obj.select_set(True)
old_obj_name_to_new_obj_name[obj.name] = new_obj.name
if not element:
continue
@@ -2179,6 +2181,19 @@ class Geometry(bonsai.core.tool.Geometry):
if new.is_a("IfcRelSpaceBoundary"):
tool.Boundary.decorate_boundary(new_obj)
# Remap Blender parent relationships for duplicated objects
for old_obj_name, new_obj_name in old_obj_name_to_new_obj_name.items():
new_obj = bpy.data.objects.get(new_obj_name)
if new_obj and new_obj.parent and new_obj.parent.name in old_obj_name_to_new_obj_name:
# Store world matrix before reparenting to preserve transform
world_matrix = new_obj.matrix_world.copy()
new_parent_name = old_obj_name_to_new_obj_name[new_obj.parent.name]
new_parent = bpy.data.objects.get(new_parent_name)
if new_parent:
new_obj.parent = new_parent
# Restore world transform by setting matrix_world
new_obj.matrix_world = world_matrix
# Recreate aggregate relationship
for old in old_to_new.keys():
if old.is_a("IfcElementAssembly"):
+18 -4
View File
@@ -110,12 +110,26 @@ class Ifc(bonsai.core.tool.Ifc):
oprops = tool.Blender.get_object_bim_props(obj)
if not oprops.location_checksum:
return True # Let's be conservative
loc_check = np.frombuffer(eval(oprops.location_checksum), dtype=np.float32)
loc_real = np.array(obj.matrix_world.translation).flatten()
# Handle both old float64 and new float32 checksums for version compatibility
loc_checksum_bytes = eval(oprops.location_checksum)
if len(loc_checksum_bytes) == 24: # Old format: 3 * 8 bytes (float64)
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float64).astype(np.float32)
else: # New format: 3 * 4 bytes (float32)
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float32)
loc_real = np.array(obj.matrix_world.translation, dtype=np.float32).flatten()
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
return True
rot_check = np.frombuffer(eval(oprops.rotation_checksum), dtype=np.float32).reshape(3, 3)
rot_real = np.array(obj.matrix_world.to_3x3())
# Handle both old float64 and new float32 checksums for version compatibility
rot_checksum_bytes = eval(oprops.rotation_checksum)
if len(rot_checksum_bytes) == 72: # Old format: 9 * 8 bytes (float64)
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float64).astype(np.float32).reshape(3, 3)
else: # New format: 9 * 4 bytes (float32)
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float32).reshape(3, 3)
rot_real = np.array(obj.matrix_world.to_3x3(), dtype=np.float32)
rot_dot = np.dot(rot_check, rot_real.T)
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
if angle_rad > 0.0017453292519943296: # 0.1 degrees
+4 -3
View File
@@ -172,23 +172,24 @@ class Polyline(bonsai.core.tool.Polyline):
angle = tool.Cad.angle_3_vectors(
second_to_last_point, last_point, mouse_vector, new_angle=None, degrees=True
)
angle_round_threshold = 1000 # Avoids rounding when distance is too big
# Round angle to the nearest 0.05
angle = round(angle / 0.05) * 0.05
angle = round(angle / 0.05) * 0.05 if distance < angle_round_threshold else angle
orientation_angle = tool.Cad.angle_3_vectors(
world_second_to_last_point, last_point, mouse_vector, new_angle=None, degrees=True
)
# Round angle to the nearest 0.05
orientation_angle = round(orientation_angle / 0.05) * 0.05
orientation_angle = round(orientation_angle / 0.05) * 0.05 if distance < angle_round_threshold else angle
if distance == 0:
angle = 0
orientation_angle = 0
if input_ui:
if should_round:
angle = 5 * round(angle / 5)
angle = 5 * round(angle / 5) if distance < angle_round_threshold else angle
factor = tool.Snap.get_increment_snap_value(context)
distance = factor * round(distance / factor)
input_ui.set_value("X", mouse_vector.x)
+13 -8
View File
@@ -93,7 +93,7 @@ class Raycast(bonsai.core.tool.Raycast):
return None
for v in bbox:
coord_2d = view3d_utils.location_3d_to_region_2d(context.region, context.space_data.region_3d, v)
coord_2d = tool.Cad.location_3d_to_region_2d_np(context.region, context.space_data.region_3d, v)
if coord_2d is not None:
transposed_bbox.append(coord_2d)
@@ -180,8 +180,11 @@ class Raycast(bonsai.core.tool.Raycast):
if not mouse_pos:
mouse_pos = event.mouse_region_x, event.mouse_region_y
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, mouse_pos)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, mouse_pos)
view_vector = tool.Cad.region_2d_to_vector_3d_np(region, rv3d, mouse_pos)
ray_origin = tool.Cad.region_2d_to_origin_3d_np(
region, rv3d, mouse_pos, clamp=10
) # TODO clamp is hardcoded but might be necessary to adapt
ray_target = ray_origin + view_vector
ray_direction = ray_target - ray_origin
@@ -247,7 +250,7 @@ class Raycast(bonsai.core.tool.Raycast):
snap_threshold = cls.calculate_snap_threshold(rv3d.view_distance)
try:
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
loc = tool.Cad.region_2d_to_location_3d_np(region, rv3d, mouse_pos, ray_direction)
except:
loc = Vector((0, 0, 0))
@@ -267,7 +270,9 @@ class Raycast(bonsai.core.tool.Raycast):
points.append(snap_point)
return points
if obj and obj.type == "CURVE":
mw = obj.matrix_world.copy()
obj = bpy.data.objects.new("new_object", obj.to_mesh().copy())
obj.matrix_world = mw @ obj.matrix_world
if not custom_bmesh:
bm = bmesh.new()
@@ -340,7 +345,7 @@ class Raycast(bonsai.core.tool.Raycast):
snap_threshold = cls.calculate_snap_threshold(rv3d.view_distance)
try:
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
loc = tool.Cad.region_2d_to_location_3d_np(region, rv3d, mouse_pos, ray_direction)
except:
loc = Vector((0, 0, 0))
@@ -399,8 +404,8 @@ class Raycast(bonsai.core.tool.Raycast):
default_container_elevation = 0.0
intersection = Vector((0, 0, default_container_elevation))
try:
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
intersection = mathutils.geometry.intersect_line_plane(ray_target, loc, plane_origin, plane_normal)
loc = tool.Cad.region_2d_to_location_3d_np(region, rv3d, mouse_pos, ray_direction)
intersection = tool.Cad.intersect_edge_plane_v2(ray_target, loc, plane_origin, plane_normal)
except:
intersection = Vector((0, 0, default_container_elevation))
@@ -418,7 +423,7 @@ class Raycast(bonsai.core.tool.Raycast):
snap_threshold = cls.calculate_snap_threshold(rv3d.view_distance)
try:
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
loc = tool.Cad.region_2d_to_location_3d_np(region, rv3d, mouse_pos, ray_direction)
except:
loc = Vector((0, 0, 0))
+3 -1
View File
@@ -496,7 +496,9 @@ class Spatial(bonsai.core.tool.Spatial):
new.long_name = element.LongName or ""
if not element.is_a("IfcProject"):
elevation = ifcopenshell.util.placement.get_storey_elevation(element)
new["elevation"] = tool.Unit.format_value(elevation)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
elevation_in_meters = elevation * unit_scale
new.elevation = tool.Unit.format_distance(elevation_in_meters)
new.is_expanded = element.id() not in cls.contracted_containers
new.level_index = level_index
children = ifcopenshell.util.element.get_parts(element)
+259
View File
@@ -24,15 +24,274 @@ import ifcopenshell
import bonsai.bim.helper
import bonsai.core.tool
import bonsai.tool as tool
from lark import Lark, Transformer
from typing import Union, Literal, Any, TYPE_CHECKING, assert_never
if TYPE_CHECKING:
from bonsai.bim.module.unit.prop import BIMUnitProperties
def parse_distance_string(input_string: str, use_project_unit: bool = True) -> tuple[bool, float]:
"""
Parse a distance string with optional unit suffixes and convert to meters.
This function parses distance inputs with units (e.g., "5m", "10ft", "3.5cm")
and converts them to meters (SI units) for use in IFC models.
Supports:
- Metric units: mm, cm, dm, m
- Imperial units: ft/feet ('), in/inches (")
- Arithmetic expressions: +, -, *, /
- Fractions for imperial units (e.g., 1/2")
- Formula mode: values starting with "="
:param input_string: The string to parse (e.g., "5m", "10ft", "10'6\"", "3.5cm", "12in")
:param use_project_unit: If True, uses project unit scale; if False, uses Blender unit scale
:return: Tuple (is_valid, value_in_meters) where is_valid indicates successful parsing
and value_in_meters is the converted value in meters
Examples:
>>> parse_distance_string("5m")
(True, 5.0)
>>> parse_distance_string("30cm")
(True, 0.3)
>>> parse_distance_string("10ft")
(True, 3.048)
>>> parse_distance_string("12in")
(True, 0.3048)
>>> parse_distance_string("5'6\"")
(True, 1.6764)
>>> parse_distance_string("invalid")
(False, 0.0)
"""
grammar_imperial = r"""
start: (FORMULA dim expr) | dim
dim: imperial
FORMULA: "="
imperial: feet_inches | feet_only | inches_only | plain_number
feet_only: NUMBER (FEET_SYM | FEET_TEXT)
inches_only: inch_value (INCH_SYM | INCH_TEXT)
feet_inches: NUMBER (FEET_SYM | FEET_TEXT) DASH? inch_value (INCH_SYM | INCH_TEXT)?
plain_number: NUMBER
inch_value: NUMBER fraction | fraction | NUMBER
fraction: NUMBER "/" NUMBER
expr: (ADD | SUB) dim | (MUL | DIV) NUMBER
NUMBER: /-?(?:\d+\.?\d*|\.\d+)/
FEET_SYM: "'"
FEET_TEXT: "ft"
INCH_SYM: "\""
INCH_TEXT: "in"
DASH: "-"
ADD: "+"
SUB: "-"
MUL: "*"
DIV: "/"
%ignore " "
"""
grammar_metric = r"""
start: FORMULA? dim expr?
dim: metric
FORMULA: "="
metric: NUMBER (MM | CM | DM | M | DEG)?
expr: (ADD | SUB | MUL | DIV) dim
NUMBER: /-?(?:\d+\.?\d*|\.\d+)/
MM: "mm"
CM: "cm"
DM: "dm"
M: "m"
DEG: "°"
ADD: "+"
SUB: "-"
MUL: "*"
DIV: "/"
%ignore " "
"""
class InputTransform(Transformer):
def NUMBER(self, n):
return float(n)
def fraction(self, numbers):
return numbers[0] / numbers[1]
def inch_value(self, args):
# Can be: NUMBER fraction, fraction, or NUMBER
if len(args) == 2:
# NUMBER fraction (e.g., "9 1/64")
return args[0] + args[1]
else:
# Just fraction or just NUMBER
return args[0]
def plain_number(self, args):
# A plain number in imperial context is assumed to be feet
feet = args[0]
# Convert feet to meters (1 foot = 0.3048 meters)
return feet * 0.3048
def feet_only(self, args):
# args[0] is the number of feet, args[1] is the unit token (we can ignore it)
feet = args[0]
# Convert feet to meters (1 foot = 0.3048 meters)
return feet * 0.3048
def inches_only(self, args):
# args[0] is the inch_value, args[1] is the unit token
inches = args[0]
# Convert inches to meters (1 inch = 0.0254 meters)
return inches * 0.0254
def feet_inches(self, args):
# Extract feet and inches values
feet = args[0]
# Find the inch_value (it's a number, not a token)
inches = None
for arg in args[1:]:
if isinstance(arg, (int, float)):
inches = arg
break
if inches is None:
inches = 0
# Convert to meters
total_meters = (feet * 0.3048) + (inches * 0.0254)
return total_meters
def imperial(self, args):
# Just return the value from the sub-rule (feet_only, inches_only, or feet_inches)
return args[0]
def metric(self, args):
# args[0] is the NUMBER, args[1] if present is the unit
value = args[0]
if len(args) > 1:
unit = str(args[1])
# Convert to meters based on unit
if unit == "mm":
value = value / 1000.0
elif unit == "cm":
value = value / 100.0
elif unit == "dm":
value = value / 10.0
elif unit == "m":
value = value # already in meters
elif unit == "°":
value = value # degrees, pass through
# If no unit specified, assume it's already in the project's unit system
return value
def dim(self, args):
return args[0]
def expr(self, args):
op = args[0]
value = float(args[1])
if op == "+":
return lambda x: x + value
elif op == "-":
return lambda x: x - value
elif op == "*":
return lambda x: x * value
elif op == "/":
return lambda x: x / value
def FORMULA(self, args):
return args[0]
def start(self, args):
i = 0
if args[0] == "=":
i += 1
else:
if len(args) > 1:
raise ValueError("Invalid input.")
dimension = args[i]
if len(args) > i + 1:
expression = args[i + 1]
return expression(dimension)
else:
return dimension
try:
# Determine unit scale
if use_project_unit and tool.Ifc.get():
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
else:
unit_scale = tool.Blender.get_unit_scale()
# Try to parse with the project's default grammar first
if bpy.context.scene.unit_settings.system == "IMPERIAL":
primary_parser = Lark(grammar_imperial)
fallback_parser = Lark(grammar_metric)
else:
primary_parser = Lark(grammar_metric)
fallback_parser = Lark(grammar_imperial)
# Try parsing with primary grammar
parse_tree = None
try:
parse_tree = primary_parser.parse(input_string)
except Exception as e:
# If primary fails, try fallback grammar (allows metric in imperial projects and vice versa)
try:
parse_tree = fallback_parser.parse(input_string)
except Exception as e2:
pass
if parse_tree is None:
return False, 0.0
# Transform the parse tree to get the numeric result
transformer = InputTransform()
result = transformer.transform(parse_tree)
result = round(result, 6)
return True, result
except Exception as e:
return False, 0.0
class Unit(bonsai.core.tool.Unit):
UNIT_TYPE = Literal["LENGTHUNIT", "AREAUNIT", "VOLUMEUNIT"]
@staticmethod
def format_distance(meters: float, use_imperial: bool = None, **kwargs) -> str:
"""
Format a distance value in meters to a string in the project's unit system.
:param meters: The distance value in meters
:param use_imperial: If True, format as imperial; if False, format as metric; if None, auto-detect from scene
:param kwargs: Additional arguments to pass to the underlying format_distance function
(hide_units, precision, decimal_places, etc.)
:return: Formatted string with units
"""
# Import the comprehensive format_distance from the helper module
from bonsai.bim.module.drawing import helper
# The comprehensive function expects value in scene units, not meters
# So we pass meters directly since it handles unit conversion internally
return helper.format_distance(
meters,
hide_units=kwargs.get("hide_units", False),
precision=kwargs.get("precision"),
decimal_places=kwargs.get("decimal_places"),
**kwargs,
)
@classmethod
def get_unit_props(cls) -> BIMUnitProperties:
return bpy.context.scene.BIMUnitProperties
-1
View File
@@ -1,6 +1,5 @@
from __future__ import annotations
from pydantic import BaseModel
from .type_hints import *
from typing import Optional, Literal
from datetime import datetime
+1 -3
View File
@@ -1,6 +1,4 @@
from typing import Literal, Annotated
from datetime import datetime
from pydantic import BeforeValidator
from typing import Literal
COUNTRY_CODE = Literal[
+2 -2
View File
@@ -26,11 +26,11 @@ target_compile_features(IfcParseExamples PUBLIC cxx_std_17)
if (WITH_OPENCASCADE)
ADD_EXECUTABLE(IfcOpenHouse IfcOpenHouse.cpp)
TARGET_LINK_LIBRARIES(IfcOpenHouse ${IFCOPENSHELL_LIBRARIES} ${OPENCASCADE_LIBRARIES})
TARGET_LINK_LIBRARIES(IfcOpenHouse ${IFCOPENSHELL_LIBRARIES} ${OpenCASCADE_LIBRARIES})
set_target_properties(IfcOpenHouse PROPERTIES FOLDER Examples)
ADD_EXECUTABLE(IfcAdvancedHouse IfcAdvancedHouse.cpp)
TARGET_LINK_LIBRARIES(IfcAdvancedHouse ${IFCOPENSHELL_LIBRARIES} ${OPENCASCADE_LIBRARIES})
TARGET_LINK_LIBRARIES(IfcAdvancedHouse ${IFCOPENSHELL_LIBRARIES} ${OpenCASCADE_LIBRARIES})
set_target_properties(IfcAdvancedHouse PROPERTIES FOLDER Examples)
endif()
+10
View File
@@ -0,0 +1,10 @@
Index,Identification,Name,Unit,Value,Quantity
1,E.01,Walls,m3,,
2,E.01.01,Ground floor walls,m3,100,42
2,E.01.02,First floor walls,m3,200,35
1,A.02,Paintings,m2,,
2,A.03,Paintings with water,m2,,
3,B.05,White paintings,m2,25,45
3,B.06,Colored paintings,m2,32,33
2,C-01,Paintings with machine,m2,17,133
2,C-02,Decorated paintings,m2,40,8
1 Index Identification Name Unit Value Quantity
2 1 E.01 Walls m3
3 2 E.01.01 Ground floor walls m3 100 42
4 2 E.01.02 First floor walls m3 200 35
5 1 A.02 Paintings m2
6 2 A.03 Paintings with water m2
7 3 B.05 White paintings m2 25 45
8 3 B.06 Colored paintings m2 32 33
9 2 C-01 Paintings with machine m2 17 133
10 2 C-02 Decorated paintings m2 40 8
+7
View File
@@ -0,0 +1,7 @@
Index,Identification,Name,Unit,Value,Quantity
1,A,Group A,,,
2,A.02,Paintings,m2,20,1
2,A.03,Paintings with water,m2,23,1
1,C,Group C,,,
2,C-01,Paintings with machine,m2,32,1
2,C-02,Decorated paintings,m2,40,2
1 Index Identification Name Unit Value Quantity
2 1 A Group A
3 2 A.02 Paintings m2 20 1
4 2 A.03 Paintings with water m2 23 1
5 1 C Group C
6 2 C-01 Paintings with machine m2 32 1
7 2 C-02 Decorated paintings m2 40 2
+10
View File
@@ -0,0 +1,10 @@
Index,Identification,Name,Unit,Value,Quantity,Query,Property
1,E.01,Walls,m3,,,,
2,E.01.01,Ground floor walls,m3,100,,"IfcWall, location=""Ground Floor""",GrossVolume
2,E.01.02,First floor walls,m3,200,,"IfcWall, location=""First Floor""",GrossVolume
1,A.02,Paintings,m2,,,,
2,A.03,Paintings with water,m2,,,,
3,B.05,White paintings,m2,25,45,,
3,B.06,Colored paintings,m2,32,33,,
2,C-01,Paintings with machine,m2,17,133,,
2,C-02,Decorated paintings,m2,40,8,,
1 Index Identification Name Unit Value Quantity Query Property
2 1 E.01 Walls m3
3 2 E.01.01 Ground floor walls m3 100 IfcWall, location="Ground Floor" GrossVolume
4 2 E.01.02 First floor walls m3 200 IfcWall, location="First Floor" GrossVolume
5 1 A.02 Paintings m2
6 2 A.03 Paintings with water m2
7 3 B.05 White paintings m2 25 45
8 3 B.06 Colored paintings m2 32 33
9 2 C-01 Paintings with machine m2 17 133
10 2 C-02 Decorated paintings m2 40 8
+10
View File
@@ -0,0 +1,10 @@
Index,Identification,Name,Unit,Value,Quantity,Description
1,E.01,Walls,m3,,,
2,E.01.01,Ground floor walls,m3,100,42,"Semi-solid blocks of plain-faced common brick, with an apparent density (excluding holes) of 800 kg/m³; minor drilling 45%; apparent thermal conductivity 0.21 W/mK; characteristic mechanical strength parallel to the holes greater than or equal to 10 N/mm2, perpendicular to the holes greater than or equal to 2N/mm2"
2,E.01.02,First floor walls,m3,200,35,"Semi-solid blocks of plain-faced common brick, with an apparent density (excluding holes) of 800 kg/m³; minor drilling 45%; apparent thermal conductivity 0.21 W/mK; characteristic mechanical strength parallel to the holes greater than or equal to 10 N/mm2, perpendicular to the holes greater than or equal to 2N/mm2"
1,A.02,Painting,m2,,,"Painting with washable water-based wall paint for indoor/outdoor. The price includes and compensates the costs for the supply of paint, any scaffolding up to a maximum height of 4 m from the support surface, the costs for the protection of furniture, fixed systems or the protection of floors, the cleaning of the surfaces to be treated through the use of rags or net purposes in order to remove residues that can be easily removed. The cost of occasional and partial grouting of surfaces, in order to eliminate any small scratches, including sanding of the grouted parts, is also to be considered included and compensated. For 2 coats with brush or roller."
2,A.03,Washable painting,m2,,,"Supply and installation of washable tempera paint for interiors and exteriors. The price includes and compensates for the costs of supplying the paint, any scaffolding up to a maximum height of 4 meters from the support surface, the costs of protecting furnishings, fixed installations, or floors, and cleaning the surfaces to be treated using rags or clean brushes to remove easily removable residues. On previously prepared plaster. Apply two coats with a brush or roller. (Tempera colors from the color chart)."
3,B.05,White paintings,m2,25,45,
3,B.06,Colored paintings,m2,32,33,
2,C-01,External painting,m2,17,133,"Painting with plastic coating. The price includes and compensates for the costs of supplying the paint, any scaffolding up to a maximum height of 4 meters from the support surface, the costs of protecting furnishings, fixed systems, or floors, and cleaning the surfaces to be treated using rags or clean brushes to remove easily removable residues. On already prepared plaster. For 2 coats (interior textured finish)."
2,C-02,Decorated paintings,m2,40,8,
1 Index Identification Name Unit Value Quantity Description
2 1 E.01 Walls m3
3 2 E.01.01 Ground floor walls m3 100 42 Semi-solid blocks of plain-faced common brick, with an apparent density (excluding holes) of 800 kg/m³; minor drilling 45%; apparent thermal conductivity 0.21 W/mK; characteristic mechanical strength parallel to the holes greater than or equal to 10 N/mm2, perpendicular to the holes greater than or equal to 2N/mm2
4 2 E.01.02 First floor walls m3 200 35 Semi-solid blocks of plain-faced common brick, with an apparent density (excluding holes) of 800 kg/m³; minor drilling 45%; apparent thermal conductivity 0.21 W/mK; characteristic mechanical strength parallel to the holes greater than or equal to 10 N/mm2, perpendicular to the holes greater than or equal to 2N/mm2
5 1 A.02 Painting m2 Painting with washable water-based wall paint for indoor/outdoor. The price includes and compensates the costs for the supply of paint, any scaffolding up to a maximum height of 4 m from the support surface, the costs for the protection of furniture, fixed systems or the protection of floors, the cleaning of the surfaces to be treated through the use of rags or net purposes in order to remove residues that can be easily removed. The cost of occasional and partial grouting of surfaces, in order to eliminate any small scratches, including sanding of the grouted parts, is also to be considered included and compensated. For 2 coats with brush or roller.
6 2 A.03 Washable painting m2 Supply and installation of washable tempera paint for interiors and exteriors. The price includes and compensates for the costs of supplying the paint, any scaffolding up to a maximum height of 4 meters from the support surface, the costs of protecting furnishings, fixed installations, or floors, and cleaning the surfaces to be treated using rags or clean brushes to remove easily removable residues. On previously prepared plaster. Apply two coats with a brush or roller. (Tempera colors from the color chart).
7 3 B.05 White paintings m2 25 45
8 3 B.06 Colored paintings m2 32 33
9 2 C-01 External painting m2 17 133 Painting with plastic coating. The price includes and compensates for the costs of supplying the paint, any scaffolding up to a maximum height of 4 meters from the support surface, the costs of protecting furnishings, fixed systems, or floors, and cleaning the surfaces to be treated using rags or clean brushes to remove easily removable residues. On already prepared plaster. For 2 coats (interior textured finish).
10 2 C-02 Decorated paintings m2 40 8
+7
View File
@@ -0,0 +1,7 @@
Index,Identification,Name,Unit,Value,Quantity,Description
1,A,Group A,,,,
2,A.02,Paintings,m2,20,1,Paint made by the best painter in the world
2,A.03,Paintings with water,m2,23,1,
1,C,Group C,,,,
2,C-01,Paintings with machine,m2,32,1,Best painting in the world painted with the best painted machine accordingly with ISO9001
2,C-02,Decorated paintings,m2,40,2,
1 Index Identification Name Unit Value Quantity Description
2 1 A Group A
3 2 A.02 Paintings m2 20 1 Paint made by the best painter in the world
4 2 A.03 Paintings with water m2 23 1
5 1 C Group C
6 2 C-01 Paintings with machine m2 32 1 Best painting in the world painted with the best painted machine accordingly with ISO9001
7 2 C-02 Decorated paintings m2 40 2
+10
View File
@@ -0,0 +1,10 @@
Index,Identification,Name,Unit,Material,Labor,Quantity
1,E.01,Walls,m3,,,
2,E.01.01,Ground floor walls,m3,55,45,42
2,E.01.02,First floor walls,m3,120,80,35
1,A.02,Painting,m2,,,
2,A.03,Washable painting,m2,,,
3,B.05,White paintings,m2,,,45
3,B.06,Colored paintings,m2,,,33
2,C-01,External painting,m2,,,133
2,C-02,Decorated paintings,m2,,,8
1 Index Identification Name Unit Material Labor Quantity
2 1 E.01 Walls m3
3 2 E.01.01 Ground floor walls m3 55 45 42
4 2 E.01.02 First floor walls m3 120 80 35
5 1 A.02 Painting m2
6 2 A.03 Washable painting m2
7 3 B.05 White paintings m2 45
8 3 B.06 Colored paintings m2 33
9 2 C-01 External painting m2 133
10 2 C-02 Decorated paintings m2 8
+10
View File
@@ -0,0 +1,10 @@
Index,Identification,Name,Unit,Value,Quantity,RateSchedule,RateID
1,E.01,Walls,m3,,,,
2,E.01.01,Ground floor walls,m3,100,42,,
2,E.01.02,First floor walls,m3,200,35,,
1,A.02,Paintings,m2,,,,
2,A.03,Paintings with water,m2,,,,
3,B.05,White paintings,m2,,45,Ex2 - SoR,A.03
3,B.06,Colored paintings,m2,32,33,,
2,C-01,Paintings with machine,m2,17,133,,
2,C-02,Decorated paintings,m2,40,8,,
1 Index Identification Name Unit Value Quantity RateSchedule RateID
2 1 E.01 Walls m3
3 2 E.01.01 Ground floor walls m3 100 42
4 2 E.01.02 First floor walls m3 200 35
5 1 A.02 Paintings m2
6 2 A.03 Paintings with water m2
7 3 B.05 White paintings m2 45 Ex2 - SoR A.03
8 3 B.06 Colored paintings m2 32 33
9 2 C-01 Paintings with machine m2 17 133
10 2 C-02 Decorated paintings m2 40 8
+8 -1
View File
@@ -32,6 +32,13 @@ csv2ifc.execute()
```
See example files as a CSV file format reference:
- Ex1 - BoQ without query.csv (a simple Bill of Quantities)
- Ex2 - SoR.csv (a simple Schedule of Rates)
- Ex3 - BoQ with query.csv (a simple BoQ with the query function)
- Ex4 - BoQ with description.csv (a simple BoQ with description column)
- Ex5 - SoR_with_description.csv (a simple SoR with description column)
- Ex6 - BoQ with categories.csv (a simple BoQ with categories columns)
- Ex7 - BoQ with Rates.csv (a simple BoQ that connect to an existing SoR. It needs an already loaded SoR.)
- `sample_cost_schedule_house_FR.csv` / `.ods`
- `schedule.csv`, `rates.csv` (schedule of rates example)
@@ -124,4 +131,4 @@ path = "directory/cost_schedule"
writer = Ifc5DOdsWriter(file=file, output=path)
writer.write()
```
```
+17
View File
@@ -0,0 +1,17 @@
# IfcConvert
if(WITH_RELATIONSHIP_VALIDATION)
file(GLOB IFCCONVERT_CPP_FILES *.cpp)
file(GLOB IFCCONVERT_H_FILES *.h)
else()
file(GLOB IFCCONVERT_CPP_FILES IfcConvert.cpp)
file(GLOB IFCCONVERT_H_FILES)
endif()
set(IFCCONVERT_FILES ${IFCCONVERT_CPP_FILES} ${IFCCONVERT_H_FILES})
add_executable(IfcConvert ${IFCCONVERT_FILES})
target_link_libraries(IfcConvert IfcGeom IfcParse Serializers ${OpenCASCADE_LIBRARIES} ${Boost_LIBRARIES} ${HDF5_LIBRARIES} ${USD_LIBRARIES})
if(WITH_RELATIONSHIP_VALIDATION)
set_property(TARGET IfcConvert APPEND_STRING PROPERTY COMPILE_FLAGS " -DWITH_RELATIONSHIP_VALIDATION")
endif()
install(TARGETS IfcConvert)
+29 -100
View File
@@ -41,6 +41,7 @@
#include "../serializers/USDSerializer.h"
#include "../serializers/TtlWktSerializer.h"
#include "../serializers/RocksDbSerializer.h"
#include "../serializers/JsonSerializer.h"
#include "../ifcgeom/IfcGeomFilter.h"
#include "../ifcgeom/Iterator.h"
@@ -49,10 +50,6 @@
#include "../ifcparse/utils.h"
#ifdef IFOPSH_WITH_CITYJSON
#include "./cityjson/geobim.h"
#endif
#ifdef IFOPSH_WITH_OPENCASCADE
#include <Standard_Version.hxx>
@@ -128,13 +125,13 @@ void print_usage(bool suggest_help = true)
<< " .stp STEP Standard for the Exchange of Product Data\n"
<< " .igs IGES Initial Graphics Exchange Specification\n"
<< " .xml XML Property definitions and decomposition tree\n"
<< " .rdb RocksDB RocksDB Key-Value store serialization of IFC data\n"
#ifdef WITH_GLTF
<< " .json JSON Property definitions and decomposition tree in xeokit json format\n"
#endif
<< " .rdb RocksDB RocksDB Key-Value store serialization of IFC data\n"
<< " .svg SVG Scalable Vector Graphics (2D floor plan)\n"
#ifdef WITH_HDF5
<< " .h5 HDF Hierarchical Data Format storing positions, normals and indices\n"
#endif
#ifdef IFOPSH_WITH_CITYJSON
<< " .cityjson City JSON format for geospatial data\n"
#endif
<< " .ttl TTL/WKT RDF Turtle with Well-Known-Text geometry\n"
<< " .ifc IFC-SPF Industry Foundation Classes\n"
@@ -654,9 +651,9 @@ int main(int argc, char** argv) {
CACHE = IfcUtil::path::from_utf8(".cache"),
HDF = IfcUtil::path::from_utf8(".h5"),
XML = IfcUtil::path::from_utf8(".xml"),
// @todo this is just temporary as it doesn't make sense to require an extension for a DB
JSON = IfcUtil::path::from_utf8(".json"),
// @todo this is just temporary as it doesn't make sense to require an extension for a DB
RDB = IfcUtil::path::from_utf8(".rdb"),
CITY_JSON = IfcUtil::path::from_utf8(".cityjson"),
IFC = IfcUtil::path::from_utf8(".ifc"),
USD = IfcUtil::path::from_utf8(".usd"),
USDA = IfcUtil::path::from_utf8(".usda"),
@@ -665,15 +662,23 @@ int main(int argc, char** argv) {
// @todo clean up serializer selection
// @todo detect program options that conflict with the chosen serializer
if (output_extension == XML) {
if (output_extension == XML || output_extension == JSON) {
int exit_code = EXIT_FAILURE;
try {
if (init_input_file(IfcUtil::path::to_utf8(input_filename), ifc_file, no_progress || quiet, mmap)) {
time_t start, end;
time(&start);
XmlSerializer s(ifc_file, IfcUtil::path::to_utf8(output_temp_filename));
Logger::Status("Writing XML output...");
s.finalize();
if (output_extension == XML) {
XmlSerializer s(ifc_file, IfcUtil::path::to_utf8(output_temp_filename));
Logger::Status("Writing XML output...");
s.finalize();
} else {
#ifdef WITH_GLTF
JsonSerializer s(ifc_file, IfcUtil::path::to_utf8(output_temp_filename), JsonSerializer::JSON_DIALECT_CREOOX);
Logger::Status("Writing JSON output...");
s.finalize();
#endif
}
time(&end);
Logger::Status("Done! Conversion took " + format_duration(start, end));
@@ -742,84 +747,6 @@ int main(int argc, char** argv) {
return exit_code;
}
#endif
#ifdef IFOPSH_WITH_CITYJSON
else if (output_extension == CITY_JSON || (output_extension == OBJ || output_extension == DAE || output_extension == GLB) && vmap.count("exterior-only") && exterior_only_algo != "none") {
// none, convex-decomposition, minkowski-triangles or halfspace-snapping
boost::to_lower(exterior_only_algo);
if (exterior_only_algo == "halfspace-snapping") {
cerr_ << "[Error] halfspace-snapping not implemented yet" << std::endl;
print_usage();
return EXIT_FAILURE;
} else if (exterior_only_algo == "minkowski-triangles") {
//
} else if (exterior_only_algo == "convex-decomposition") {
//
} else if (exterior_only_algo == "none") {
//
} else {
cerr_ << "[Error] --exterior-only should be convex-decomposition|minkowski-triangles|halfspace-snapping" << std::endl;
print_usage();
return EXIT_FAILURE;
}
geobim_settings settings;
settings.input_filenames = { IfcUtil::path::to_utf8(input_filename) };
settings.file = { new IfcParse::IfcFile(IfcUtil::path::to_utf8(input_filename)) };
/*
// No longer set, because we pass to real serializers now, awaiting a proper iterator adaptor
if (output_extension == OBJ) {
settings.obj_output_filename = IfcUtil::path::to_utf8(output_filename);
}
*/
if (output_extension == CITY_JSON) {
// we don't have a cityjson serializer though
settings.cityjson_output_filename = IfcUtil::path::to_utf8(output_filename);
}
// @todo
settings.radii = { "0.05" };
settings.apply_openings = false;
settings.apply_openings_posthoc = true;
settings.debug = false;
settings.exact_segmentation = true;
settings.minkowski_triangles = exterior_only_algo == "minkowski-triangles";
settings.no_erosion = false;
settings.spherical_padding = false;
if (num_threads != 1) {
settings.threads = num_threads;
}
settings.settings.get<ifcopenshell::geometry::settings::UseWorldCoords>().value = false;
settings.settings.get<ifcopenshell::geometry::settings::WeldVertices>().value = false;
settings.settings.get<ifcopenshell::geometry::settings::ReorientShells>().value = true;
settings.settings.get<ifcopenshell::geometry::settings::ConvertBackUnits>().value = true;
settings.settings.get<ifcopenshell::geometry::settings::IteratorOutput>().value = ifcopenshell::geometry::settings::NATIVE;
settings.settings.get<ifcopenshell::geometry::settings::DisableOpeningSubtractions>().value = !settings.apply_openings;
if (include_filter.type != geom_filter::UNUSED) {
settings.entity_names = include_filter.values;
settings.entity_names_included = true;
} else if (exclude_filter.type != geom_filter::UNUSED) {
settings.entity_names = exclude_filter.values;
settings.entity_names_included = false;
} else {
settings.entity_names = { { "IfcSpace", "IfcOpeningElement" } };
settings.entity_names_included = false;
}
elems_from_adaptor.emplace();
perform(settings, *elems_from_adaptor);
if (output_extension == CITY_JSON) {
return 0;
}
// else ... continue on to serialize elems_from_adaptor
}
#endif
/// @todo Clean up this filter code further.
std::vector<geom_filter> used_filters;
@@ -867,8 +794,8 @@ int main(int argc, char** argv) {
geometry_settings.get<ifcopenshell::geometry::settings::NoParallelMapping>().value = true;
}
if (geometry_settings.get<ifcopenshell::geometry::settings::UseElementHierarchy>().get() && output_extension != DAE && output_extension != USD && output_extension != USDA && output_extension != USDC) {
cerr_ << "[Error] --use-element-hierarchy can be used only with .dae or .usd output.\n";
if (geometry_settings.get<ifcopenshell::geometry::settings::UseElementHierarchy>().get() && output_extension != DAE && output_extension != USD && output_extension != USDA && output_extension != USDC && output_extension != GLB) {
cerr_ << "[Error] --use-element-hierarchy can be used only with .dae or .usd or .glb output.\n";
/// @todo Lots of duplicate error-and-exit code.
write_log(!quiet);
print_usage();
@@ -1359,12 +1286,14 @@ bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file,
requires_init = true;
}
ifc_file->bypass_type("IfcRelDefinesByProperties");
ifc_file->bypass_type("IfcPropertySetDefinition");
ifc_file->bypass_type("IfcProperty");
ifc_file->bypass_type("IfcMaterialProperties");
ifc_file->bypass_type("IfcProfileProperties");
ifc_file->bypass_type("IfcPhysicalQuantity");
if (bypass_properties) {
ifc_file->bypass_type("IfcRelDefinesByProperties");
ifc_file->bypass_type("IfcPropertySetDefinition");
ifc_file->bypass_type("IfcProperty");
ifc_file->bypass_type("IfcMaterialProperties");
ifc_file->bypass_type("IfcProfileProperties");
ifc_file->bypass_type("IfcPhysicalQuantity");
}
#ifdef USE_MMAP
if (mmap) {
+3
View File
@@ -0,0 +1,3 @@
add_subdirectory(kernels)
set(kernel_libraries ${kernel_libraries} PARENT_SCOPE)
+1 -1
View File
@@ -647,7 +647,7 @@ namespace ifcopenshell {
};
class Settings : public SettingsContainer<
std::tuple<MesherLinearDeflection, MesherAngularDeflection, ReorientShells, LengthUnit, PlaneUnit, Precision, OutputDimensionality, LayersetFirst, DisableBooleanResult, NoWireIntersectionCheck, NoWireIntersectionTolerance, PrecisionFactor, DebugBooleanOperations, BooleanAttempt2d, SurfaceColour, WeldVertices, UseWorldCoords, UnifyShapes, UseMaterialNames, ConvertBackUnits, ContextIds, ContextTypes, ContextIdentifiers, IteratorOutput, DisableOpeningSubtractions, ApplyDefaultMaterials, DontEmitNormals, GenerateUvs, ApplyLayerSets, UseElementHierarchy, ValidateQuantities, EdgeArrows, BuildingLocalPlacement, SiteLocalPlacement, ForceSpaceTransparency, CircleSegments, CgalSmoothAngleDegrees, KeepBoundingBoxes, ComputeCurvature, FunctionStepType, FunctionStepParam, NoParallelMapping, PermissiveShapeReuse, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges, OcctNoCleanTriangulation, CacheShapes, DeferProcessingFirstElement, MaxOffset, MaxOffsetDeviation, ApplyOffset>
std::tuple<MesherLinearDeflection, MesherAngularDeflection, ReorientShells, LengthUnit, PlaneUnit, Precision, OutputDimensionality, LayersetFirst, DisableBooleanResult, NoWireIntersectionCheck, NoWireIntersectionTolerance, PrecisionFactor, DebugBooleanOperations, BooleanAttempt2d, SurfaceColour, WeldVertices, UseWorldCoords, UnifyShapes, UseMaterialNames, ConvertBackUnits, ContextIds, ContextTypes, ContextIdentifiers, IteratorOutput, DisableOpeningSubtractions, ApplyDefaultMaterials, DontEmitNormals, GenerateUvs, ApplyLayerSets, UseElementHierarchy, ValidateQuantities, EdgeArrows, BuildingLocalPlacement, SiteLocalPlacement, ForceSpaceTransparency, CircleSegments, CgalSmoothAngleDegrees, KeepBoundingBoxes, ComputeCurvature, FunctionStepType, FunctionStepParam, NoParallelMapping, PermissiveShapeReuse, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges, OcctNoCleanTriangulation, CacheShapes, DeferProcessingFirstElement, MaxOffset, MaxOffsetDeviation, ApplyOffset>
>
{};
}
+7 -2
View File
@@ -87,12 +87,17 @@ inline namespace settings {
static constexpr const char* const description = "Use a geometrical section rather than full polyhedral output and footprint in TTL WKT";
static constexpr bool defaultvalue = false;
};
struct SeparateZUpNode : public SettingBase<SeparateZUpNode, bool> {
static constexpr const char* const name = "separate-z-up-node";
static constexpr const char* const description = "Introduce a separate Z-Up node into the GlTF hierarchy instead of multiplying the transform into the root node matrices";
static constexpr bool defaultvalue = false;
};
}
class SerializerSettings : public SettingsContainer <
// @todo should we use tuple_cat here to unify the settings into a single class?
std::tuple<UseElementNames, UseElementGuids, UseElementStepIds, UseElementTypes, UseYUp, WriteGltfEcef, FloatingPointDigits, BaseUri, WktUseSection>
>
std::tuple<UseElementNames, UseElementGuids, UseElementStepIds, UseElementTypes, UseYUp, WriteGltfEcef, FloatingPointDigits, BaseUri, WktUseSection, SeparateZUpNode>>
{};
}
+13 -7
View File
@@ -564,13 +564,19 @@ IfcGeom::Element* IfcGeom::Iterator::get()
// We need to find all the parents
while (parent_object != NULL && hasParent && parent_object->parent_id() != -1) {
// Find the next parent
try {
parent_object = get_object(parent_object->parent_id());
} catch (const std::exception& e) {
Logger::Error(e);
hasParent = false;
}
auto pid = parent_object->parent_id();
auto ifc_product = ifc_file->instance_by_id(pid)->as<IfcUtil::IfcBaseEntity>();
if (ifc_product->declaration().name() == "IfcProject") {
hasParent = false;
} else {
try {
parent_object = get_object(pid);
} catch (const std::exception& e) {
Logger::Error(e);
hasParent = false;
}
}
// Add the previously found parent to the vector
if (hasParent) parents.insert(parents.begin(), parent_object);
+42
View File
@@ -0,0 +1,42 @@
foreach(kernel ${GEOMETRY_KERNELS})
string(TOUPPER ${kernel} KERNEL_UPPER)
file(GLOB IFCGEOM_H_FILES ${kernel}/*.h)
file(GLOB IFCGEOM_CPP_FILES ${kernel}/*.cpp)
set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES})
set(KERNEL_TARGET "geometry_kernel_${kernel}")
add_library(${KERNEL_TARGET} OBJECT ${IFCGEOM_FILES})
set_property(TARGET ${KERNEL_TARGET} APPEND PROPERTY COMPILE_FLAGS "-DIFC_GEOM_EXPORTS")
list(APPEND kernel_libraries ${KERNEL_TARGET})
target_link_libraries(${KERNEL_TARGET} ${${KERNEL_UPPER}_LIBRARIES})
install(TARGETS ${KERNEL_TARGET}
ARCHIVE DESTINATION ${LIBDIR}
LIBRARY DESTINATION ${LIBDIR}
RUNTIME DESTINATION ${BINDIR}
)
if(${kernel} STREQUAL "cgal")
set_property(TARGET ${KERNEL_TARGET} APPEND_STRING PROPERTY COMPILE_FLAGS " -DCGAL_HAS_THREADS")
set(KERNEL_TARGET_SIMPLE "${KERNEL_TARGET}_simple")
add_library(${KERNEL_TARGET_SIMPLE} OBJECT ${IFCGEOM_FILES})
set_target_properties(${KERNEL_TARGET_SIMPLE} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIFOPSH_SIMPLE_KERNEL -DCGAL_HAS_THREADS")
list(APPEND kernel_libraries ${KERNEL_TARGET_SIMPLE})
target_link_libraries(${KERNEL_TARGET_SIMPLE} ${${KERNEL_UPPER}_LIBRARIES} Eigen3::Eigen)
install(TARGETS ${KERNEL_TARGET_SIMPLE}
ARCHIVE DESTINATION ${LIBDIR}
LIBRARY DESTINATION ${LIBDIR}
RUNTIME DESTINATION ${BINDIR}
)
elseif(${kernel} STREQUAL "opencascade")
target_link_libraries(${KERNEL_TARGET} ${OpenCASCADE_LIBRARIES})
endif()
install(FILES ${IFCGEOM_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcgeom/kernels/${kernel}
)
endforeach()
set(kernel_libraries ${kernel_libraries} PARENT_SCOPE)
+1 -1
View File
@@ -22,7 +22,7 @@
#ifdef IFC_SHARED_BUILD
#ifdef _WIN32
#ifdef IFC_GEOMLIBRARY_EXPORTS
#ifdef IFC_GEOM_EXPORTS
#define IFC_GEOMLIBRARY_API __declspec(dllexport)
#else
#define IFC_GEOMLIBRARY_API __declspec(dllimport)
+4 -5
View File
@@ -1,9 +1,8 @@
find_package(Boost COMPONENTS chrono REQUIRED)
find_package(Boost REQUIRED)
file(GLOB CPP_FILES *.cpp)
file(GLOB H_FILES *.h)
set(SOURCE_FILES ${CPP_FILES} ${H_FILES})
set(SOURCE_FILES ${CPP_FILES})
add_executable(IfcGeomServer ${SOURCE_FILES})
target_link_libraries(IfcGeomServer IfcGeom ${kernel_libraries} ${OPENCASCADE_LIBRARIES})
target_link_libraries(IfcGeomServer IfcGeom ${kernel_libraries} ${OpenCASCADE_LIBRARIES})
install(TARGETS IfcGeomServer)
install(TARGETS IfcGeomServer)
+3 -3
View File
@@ -35,12 +35,12 @@ if(HAS_MAX)
message(STATUS "Building IFCMax library for Autodesk 3ds Max SDK ${max_year}")
include_directories(${INCLUDE_DIRECTORIES} ${OCC_INCLUDE_DIR} ${OPENCOLLADA_INCLUDE_DIRS} ${ICU_INCLUDE_DIR}
include_directories(${INCLUDE_DIRECTORIES} ${OPENCOLLADA_INCLUDE_DIRS} ${ICU_INCLUDE_DIR}
${Boost_INCLUDE_DIRS} ${max_sdk}/include
)
# All recent versions of 3ds Max (2014 and newer) are 64-bit only so assume lib/x64 directory
link_directories(${LINK_DIRECTORIES} ${IfcOpenShell_BINARY_DIR} ${OCC_LIBRARY_DIR} ${OPENCOLLADA_LIBRARY_DIR}
link_directories(${LINK_DIRECTORIES} ${IfcOpenShell_BINARY_DIR} ${OPENCOLLADA_LIBRARY_DIR}
${ICU_LIBRARY_DIR} ${Boost_LIBRARY_DIRS} ${max_sdk}/lib/x64/Release
)
@@ -74,7 +74,7 @@ if(HAS_MAX)
tessint.lib
viewfile.lib
zlibdll.lib
${OPENCASCADE_LIBRARIES}
${OpenCASCADE_LIBRARIES}
)
set_target_properties(IfcMax_${max_year} PROPERTIES SUFFIX ".dli")
+7 -1
View File
@@ -103,7 +103,13 @@ autoapi_dirs = [
# autoapi_dirs = ['../../bcf/bcf', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester']
# These are auto-generated based on the IFC schema, so exclude them
autoapi_ignore = ["*ifcopenshell/express/rules*"]
# Temporarily ignore bsdd files until they are packaged properly in the wheel.
autoapi_ignore = [
"*ifcopenshell/express/rules*",
"*bsdd/bsdd_json.py",
"*bsdd/type_hints.py",
"*bsdd/yml_to_classes.py",
]
# Custom autoapi templates to make it easier to read our docs
autoapi_template_dir = "_autoapi_templates"

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