Compare commits

...

272 Commits

Author SHA1 Message Date
Ryan Schultz 7864657f88 Fix #6334: Preserve annotation vertex order during subdivision to prevent arrow/text flip 2025-11-30 15:53:42 -06:00
Ryan Schultz 8d617c87c8 Extend github.com/IfcOpenShell/IfcOpenShell/commit/1b2ad6c93f633df46d55f65bf97e730461fd7e6c - apply new type to all the selected objects. 2025-11-29 15:53:08 -06:00
Ryan Schultz 1b2ad6c93f Add interactive dialog for type duplication with assignment option
Add dialog to DuplicateType operator allowing users to set name, description,
and optionally assign the active object to the duplicated type. Add duplicate
button to product UI in type panel with auto-assignment enabled by default.
2025-11-29 15:13:38 -06:00
Gorgious ffe9f65b3c Refactor and improve parametric gizmo system
- You can now input values using the keyboard once you clicked on a gizmo
- Gizmos now support click > move > click  in addition to drag and drop (yay carpal tunnel !)
- Optimize snapping performance
- Consolidate door and window type in model.py
- You can now cycle through window and door types using the cycle gizmo
- Pen, validate, cancel, lock, and cycle gizmos are now billboards and follow view direction
- Draggable gizmos are now billboard 2D arrows instead of 3D cones
2025-11-28 23:22:42 +01:00
Ryan Schultz 5a8f557e7f Fix #7398: move purge funtions with other purge functions 2025-11-28 10:41:34 -06:00
Andrej730 5b56093147 black . 2025-11-28 21:03:50 +05:00
Andrej730 bcb63f450b build-deps - make rocksdb installation minimal 2025-11-28 21:03:50 +05:00
Andrej730 e4c82e2c92 FindOpenCASCADE - log found config 2025-11-28 21:03:50 +05:00
Esteban DUGUEPEROUX 8fb10d2ff3 cmake: Use OBJECT target instead of STATIC one to avoid mix static and shared targets in a same build 2025-11-28 17:03:25 +01:00
Esteban DUGUEPEROUX 37d29e5d47 cmake: Use same PRIVATE scope for Serializers target 2025-11-28 16:17:41 +01:00
Diego Mateos 7221e72cfa fix some target folders to respect parent cmake 2025-11-28 14:11:57 +01:00
Gorgious c828c1b08d Refactor parametric element gizmos
- Add gizmos for windows and stairs
- Add BaseParametricGizmoGroup mixin for doors, windows, and stairs
- Improve snapping with screen-space distance calculation
- Add GizmoPlus, GizmoMinus, GizmoCycle icon gizmos
- Add GizmoPropConfig dataclass for cleaner gizmo configuration
2025-11-27 22:56:27 +01:00
Ryan Schultz fcf5614004 Is user saving .blend file? 2025-11-27 09:50:31 -06:00
myoualid 5a4d60e08c util functions to retrieve material profiles and material layers for an element 2025-11-27 13:30:38 +01:00
Bruno Postle a675365dfd Correct fix for OCC built with VTK support 2025-11-26 22:53:38 +00:00
Bruno Postle 5ab6deb121 Revert cbd2689 Propagate CMP0167 policy 2025-11-25 23:08:52 +00:00
Bruno Postle cbd26896ba Propagate CMP0167 policy to fix cmake warnings 2025-11-25 18:45:59 +00:00
Bruno Postle ceeca752ea Fix build error from OCC with VTK support
OpenCASCADE may be built with VTK support. Try to find VTK first to avoid
CMake errors when OpenCASCADE's config references VTK targets.
2025-11-25 18:44:32 +00:00
Thomas Krijnen 9d437b5bc3 Mark simple type instance file ownership #7226 2025-11-25 09:19:28 +01: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
Andrej730 a7c8c908fe Fix left out invalid mesh data blocks and fix failing geom core test (after fcee54c) 2025-11-17 16:30:28 +05:00
Andrej730 64686dde5c Remove redundant :type from doc-strings
Args types already covered by annotations
2025-11-17 16:30:27 +05:00
Andrej730 29ab446f8c use common rst syntax in doc-strings 2025-11-17 16:30:27 +05:00
Andrej730 51e2e7cd82 Fix boundary bim test after 6c31d43 2025-11-17 16:30:27 +05:00
Andrej730 e879dac4bd Remove obsolete tests after e662308 2025-11-17 16:30:26 +05:00
Andrej730 145c629d92 Fix core geometry tests after e662308 c4d54f7 2025-11-17 16:30:26 +05:00
Andrej730 3fe9279bb4 pyproject - fix typo 2025-11-17 16:30:26 +05:00
Andrej730 428d88f01c Fix missing bsdd pydantic dependency in #7145 2025-11-17 16:30:26 +05:00
Andrej730 3986d4e85b Remove unused tool.sequence methods 2025-11-17 16:30:26 +05:00
Andrej730 02d35b6047 Fix missing ifc_file definition in 471bd34 2025-11-17 16:30:25 +05:00
Andrej730 3cc5931eae Fix missing imports and don't use api.run 2025-11-17 16:30:25 +05:00
Andrej730 db5ec77730 black format src/pyodide 2025-11-17 16:30:25 +05:00
Andrej730 bec46562f5 gitignore running_pid.json 2025-11-17 16:30:25 +05:00
Andrej730 1508c4ab05 test_feature - restore EPset_Drawing.ifc 2025-11-17 16:30:24 +05:00
Andrej730 99ae768ddf test_feature - clean up .ifc.cache. files after test was executed 2025-11-17 16:30:24 +05:00
Andrej730 fb902a04f5 test_feature - use common prompts for loading/saving temp projects 2025-11-17 16:30:24 +05:00
Andrej730 e46a99870b typing 2025-11-17 16:30:24 +05:00
Andrej 3fbba405d4 Merge pull request #7362 from sboddy/fix_7348_ci_test_normalize_path 2025-11-17 07:12:11 +01:00
Andrej730 989415f423 setup_pytest - use check_call and less repetition 2025-11-17 11:09:15 +05:00
Andrej730 7a3c7704b5 preferences descriptions - document autoreplaced strings in commands 2025-11-17 11:09:14 +05:00
Andrej730 5d7407e2b9 preferences descriptions - adjust line length 2025-11-17 11:09:08 +05:00
Andrej730 4a5f1b01a1 black . 2025-11-17 11:09:08 +05:00
Ryan Schultz a038fa62ba Use spacebar to toggle on/off direct profile edit mode. Works on IfcExtrudedSolids, LAYER2 objects(slabs), and axis-based geometry. 2025-11-16 15:00:10 -06:00
Ryan Schultz bb1127d815 prompt for Blender version 2025-11-15 19:10:15 -06:00
Ryan Schultz 40cf322fb4 fall back to a Windows system font if the font isn’t found in addon folder 2025-11-15 09:58:37 -06:00
Ryan Schultz f32ccec154 Fix #7364: If you change the width/height of a drawing camera it doesn't scale the text decoration. 2025-11-15 08:25:27 -06:00
Ryan Schultz dc0b73f46f Ensure annotation is in front of a RCP camera. 2025-11-15 08:06:06 -06:00
Ryan Schultz 36cb625c4e Fix #7353: Fix section annotations squashed in section views
Add target view detection to generate_section_reference_points() to create
appropriate geometry for each view type. Plan views get horizontal lines
(clip_segment), section/elevation views get vertical lines (elevate_segment).
2025-11-15 07:52:07 -06:00
Stephen Boddy 89314733ff blackwq 2025-11-14 15:47:50 +00:00
Stephen Boddy d4e945222d Fix #7348: test_normalize_path failure on CI/Linux. 2025-11-14 15:41:13 +00:00
Andrej730 fe80620a5d Add basic parametric objects tests #7355 2025-11-13 19:13:28 +05:00
Andrej730 166ddb4799 Fix parametric stair editing after 3617d11 #7355 2025-11-13 19:13:28 +05:00
Andrej730 8e2a310f92 stair generation - remove debug prints (b46136e) 2025-11-13 19:13:28 +05:00
Andrej730 339a0606c0 Fix failing product assignment test after 5a511ee
Moved code from `core`, added a warning so we could possibly narrow down #4014 in the future.
2025-11-13 19:13:27 +05:00
Andrej730 b6f1119eaa Fix failing product assignment test after 555621c 2025-11-13 19:13:27 +05:00
Andrej730 139a024d5f dev_environment.py - bump Blender version to 5.0 2025-11-13 19:13:27 +05:00
Andrej730 9491096485 black . 2025-11-13 19:13:27 +05:00
Thomas Krijnen a34e42a519 Handle infra sweeps of open loops #7338 2025-11-13 12:43:41 +01:00
Esteban DUGUEPEROUX 6a7ecf7f58 cmake: Have a dedicated CMakeLists.txt for IfcGeomServer 2025-11-13 10:14:59 +01:00
Richard Brice 96da78c670 Updates IfcSectionedSurface so it works with taxonomy::function_item 2025-11-12 11:37:07 -08:00
Andrej730 9adc8e484a Bonsai - bump ifcopenshell build #7330 2025-11-12 22:34:42 +05:00
kaplus e8eb5e4d0a Add continue statement to skip duplicate calculations
Skip general path to avoid duplicate calculations.
2025-11-12 13:14:38 +01:00
Andrej730 080222ffe6 black . 2025-11-12 12:56:35 +05:00
Ryan Schultz abef655482 Fix #6938 - Have parent facet select the parent aggregate as well. 2025-11-11 20:19:38 -06:00
Ryan Schultz 85b3f831dc copy query for aggregates into clipboard, to align with common UI pattern. 2025-11-11 20:17:37 -06:00
falken10vdl d8ece32d62 Add markdown-it-py and mdurl dependencies for drawing module annotations (#7346) 2025-11-11 16:33:19 -06:00
Ryan Schultz 0df7117d37 Sort annotations to ensure FILLAREA types are drawn first (at the bottom) 2025-11-11 14:26:55 -06:00
sboddy 91d67e6371 Improve Merge/Mirror error messages to reduce confusion. (#6911) 2025-11-11 13:57:52 -06:00
Ryan Schultz 87d1b0637a Close #7294: Add target-view and scale classes to SVG parent group from IFC data
Reads from EPset_Drawing.TargetView and EPset_Drawing.Scale to add
CSS classes to the linework parent group for view-specific styling.
2025-11-11 12:47:58 -06:00
Esteban Dugueperoux b8adddc648 fix test_number_formatting tests and add cases (#7332) 2025-11-11 11:25:00 -06:00
Andrej730 25d3edc537 Avoid producing test artifacts in src/bonsai 2025-11-11 19:56:57 +05:00
Andrej730 7f8cb80281 Fix TestDrawingStyles after 44cc127 2025-11-11 19:56:57 +05:00
Andrej730 a2ce2ea23a Fix TestDisableEditingText crash after 679fa6d 2025-11-11 19:56:56 +05:00
Andrej730 7f7aef0e6e Fix duplicate drawing core test (8ffa6fc) 2025-11-11 19:56:56 +05:00
Andrej730 821b5380ea Fix copy to container test (ddbe627) 2025-11-11 19:56:56 +05:00
Andrej730 e8bcbc83a4 Update operator.py 2025-11-11 19:56:56 +05:00
Andrej730 091af3c1c9 Add todo for unused expression (17d6b8a) 2025-11-11 19:56:55 +05:00
Andrej730 4cc50301dd Remove unused variable (17d6b8a) 2025-11-11 19:56:55 +05:00
Andrej730 f643a1615e Fix failing aggregate duplication test after 17d6b8a 2025-11-11 19:56:55 +05:00
Andrej730 71e78c67f8 Add visibility and lock steps to geometry.feature
Added steps to set 'is_visible' to TRUE and 'is_locked' to FALSE in the 'Spatial Decomposition' panel for the 'Override paste buffer - with active IFC data' scenario. This ensures the test covers visibility and lock state handling for IFC objects.
2025-11-11 19:56:55 +05:00
Andrej730 e7c4a19d9e bim.override_object_duplicate_move - sync poll with object.duplicate_move 2025-11-11 19:56:54 +05:00
Andrej730 6c31d43e72 Fix feature tests after a11f81f 2025-11-11 19:56:54 +05:00
Andrej730 83b56d591b Fix error activating drawing style
Example error:
```
TypeError: bpy_struct: item.attr = val: enum "Default" not found in ('city.exr', 'courtyard.exr', 'forest.exr', 'interior.exr', 'night.exr', 'studio.exr', 'sunrise.exr', 'sunset.exr')
```

There was a bug in shading_styles.json where we were using `Default` for `studio_light`, which is only available for `SOLID` shading type. Changed value to `forest.exr`, added code to handle old bugged value since users already copied and possibly modified shading_styles.json
2025-11-11 19:56:54 +05:00
Andrej730 e5b26067ab typing 2025-11-11 19:56:54 +05:00
Andrej730 a4fae94be8 test_feature - add separate non-debug option to save ifc project
Also clean up temp folder before save (met issues when tests were using not updated shading_styles.json)
2025-11-11 19:56:53 +05:00
Andrej730 af4851f03a Fixing bsdd feature tests (a929ab0) 2025-11-10 18:58:02 +05:00
Andrej730 155d4c44ae PanelSpy - fix error handling methods that have a return value
E.g. `ReferenceUI` has `self.get_object_name(context)` - since value wasn't returned, it was returning `None`, breaking the tests.
2025-11-10 18:58:02 +05:00
Andrej730 48119baf97 test_feature - non-background mode for debugging 2025-11-10 18:58:02 +05:00
Andrej730 dd1989aebb test_feature - use pformat to print props for readibility 2025-11-10 18:58:01 +05:00
Andrej730 0806430700 TemplateListSpy - add repr 2025-11-10 18:58:01 +05:00
Andrej730 2d97f6638b test_feature - add debugpy option 2025-11-10 18:58:01 +05:00
Andrej730 b10e5ad4d6 PanelSpy - target not all callables
Because sometimes we have `Data` classes as panel attributes and they're also callable.
2025-11-10 18:58:01 +05:00
Andrej730 24f991ceb1 Fix feature tests after a11f81f 2025-11-10 18:58:01 +05:00
Andrej730 ac11baa0c4 Fix the_object_name_does_not_exist not checking for errors
`assert True` was introduced in 1c1035e and original implementation with `assert False` was removed in b0d8101
2025-11-10 18:58:01 +05:00
Andrej730 1609a77b77 typing for PanelSpy related options 2025-11-10 18:58:00 +05:00
Andrej730 dd2f091a52 object.delete and outliner.delete overrides to reuse original poll methods for consistency 2025-11-10 18:58:00 +05:00
Andrej730 a7bf0e7d5e Fix remove drawing feature test (f11d79f) 2025-11-10 18:58:00 +05:00
Andrej730 19bdd3e622 i_set_the_prop_property_to_value - don't fail silently on incorrect bool values 2025-11-10 18:58:00 +05:00
Andrej730 e1bf891ec6 PanelSpy - also support draw_header for panels 2025-11-10 18:58:00 +05:00
Andrej730 4009834b97 PanelSpy - rename panel attr to blender_panel to avoid conflicts with panel method
Also added support for `panel` method.
2025-11-10 18:57:59 +05:00
Andrej730 77f5e1fbad PanelSpy - add typing 2025-11-10 18:57:59 +05:00
Andrej730 c4eb77d544 Blender 5.0 - update shadint_styles eevee name 2025-11-10 18:57:59 +05:00
Andrej730 8a4ea43239 Blender 5.0 - fix error activating drawing styles
Some props were removed in Blender 5.0
2025-11-10 18:57:59 +05:00
Thomas Krijnen f056e22f60 ruff 2025-11-10 11:59:10 +01:00
Thomas Krijnen 05e81040ce black 2025-11-10 11:53:33 +01:00
Thomas Krijnen 8d8133e780 submodule 2025-11-10 11:45:05 +01:00
Thomas Krijnen dceb35782e Add platform tag for linuxarm64 in Makefile #7325 2025-11-10 11:17:24 +01:00
Thomas Krijnen 2c5f0c6cf2 Add Linux ARM 64bit to CI workflow #7325 2025-11-10 11:16:27 +01:00
Thomas Krijnen abd667684d Supply CGAL_WITH_GMPXX to ifcopenshell configure #7330 2025-11-10 10:48:24 +01:00
Thomas Krijnen 31b5f88497 Rename WITH_GMPXX to CGAL_WITH_GMPXX in build script #7330 2025-11-10 10:33:48 +01:00
Thomas Krijnen 77a7026d33 Disable GMPXX in CGAL build #733 2025-11-10 10:15:14 +01:00
Thomas Krijnen 9c5f3f9754 Disable GMPXX in CGAL build #7330 2025-11-10 09:51:38 +01:00
Esteban DUGUEPEROUX 5d56025014 cmake: Separate ifcparse build in its own CMakeLists.txt file 2025-11-10 09:42:51 +01:00
Bruno Postle ad62c75f4f ifcclash ifccsv ifcdiff ifcpatch: README.md stubs
The pyproject.toml files were referencing these missing files
2025-11-09 20:52:12 +00:00
Ryan Schultz 7a5bd03216 Close #7220: Duplicate multiple selected Linked Aggregates at one time 2025-11-09 13:45:09 -06:00
Ryan Schultz 593e0e28a9 fix #4189: local_view turns off/on text decorations, as well as cut decorator. 2025-11-08 16:35:46 -06:00
Massimo Fabbro ed2b2de79a Add profiler in ifcopenshell util and ifc5d quantify function time
Useful for quantify function optimization
2025-11-08 20:00:35 +01:00
Ryan Schultz 5a511eecc1 Fix #4014: Unassign annotation from all products before reassignment
The edit_assigned_product() function now removes the annotation from all
existing IfcRelAssignsToProduct relationships instead of just one, preventing
annotations from being incorrectly assigned to multiple products simultaneously.

This fixes issues where leader lines wouldn't update correctly because the
annotation retained old product assignments.
2025-11-08 12:46:33 -06:00
Bruno Postle 83e0fa1287 Bump python bindings maximum python version
Python 3.14 is now a thing and the build seems ok
2025-11-08 17:38:56 +00:00
Ryan Schultz a2dbdbf8cc Fix #7274: Add unique CSS class suffix to all selectors in sheet drawings
Previously, when multiple CSS selectors were comma-separated in a rule,
only the last selector received the unique drawing ID suffix. This caused
style conflicts when multiple drawings were placed on the same sheet.

Now all selectors in comma-separated lists receive the unique suffix,
ensuring proper style isolation between drawings.

Example:
Before: .cut.Status-DEMOLISH, .cut.Option-D.d2156 { ... }
After:  .cut.Status-DEMOLISH.d2156, .cut.Option-D.d2156 { ... }
2025-11-08 10:41:43 -06:00
Ryan Schultz 3d68bbc0ca Fix #7287: to correctly position IfcAnnotation on RCP's. 2025-11-08 09:11:38 -06:00
Thomas Krijnen 40f9f261ae submodule 2025-11-07 16:22:51 +01:00
Andrej730 c08cc58904 Blender 5.0 - fix opening Web UI
In Blender 5.0 some theme attributes were removed and moved to other places and code broke.

In the commit I've also switched the code to less dynamic approach, so static type checker could caught potential errors.
2025-11-07 19:13:44 +05:00
Andrej730 ebdb122a22 Fix error removing systems 6469f79 2025-11-07 19:13:44 +05:00
Andrej730 5de2752685 light ui - use calendar for simplicity 2025-11-07 19:13:44 +05:00
Andrej730 d2245a95c7 Blender 5.0 - fix issues related to EEVEE renamed again
Ahh, it was a mistake warning about non-`_NEXT` values previously - didn't realized that this name was temporary for Blender.
2025-11-07 19:13:43 +05:00
Andrej730 47075e95bd Blender 5.0 - fix error enabling Rendered mode in Solar Analysis 2025-11-07 19:13:43 +05:00
Thomas Krijnen 9d4c980e02 black 2025-11-07 11:50:33 +01:00
Thomas Krijnen 53c1a1537d ruff 2025-11-07 11:50:18 +01:00
Thomas Krijnen 7db2f66685 WKT footprint massaging 2025-11-07 10:59:33 +01:00
Thomas Krijnen 6dd34b1fa4 Fix failing test by bumping commit ref #7193 2025-11-07 10:59:10 +01:00
Ryan Schultz 862713cda4 related to #7298 - skip material, as well. 2025-11-06 20:56:56 -06:00
Ryan Schultz 06c1668e42 Closes #7298 - skips count on csv import. 2025-11-06 13:20:55 -06:00
Thomas Krijnen 5f9f0f26eb submodule 2025-11-06 19:06:51 +01:00
Andrej730 8db22cdaf7 test_model - fix incorrect stair profile values in generation tests 2025-11-06 18:46:28 +05:00
Andrej730 3bed9262e5 Fix miscalculated stair lengths in some cases (f52aafd)
`get_active_stair_calculated_params` wasn't updated when zero tread runs support was added and it's still was relying on a convention that `0.0` custom tread run = no custom tread run.
2025-11-06 18:46:06 +05:00
Andrej730 0c171a457b stair generation - fix bug with extended top nib with zero first tread run
`s0 = V_(custom_first_last_tread_run[0] or tread_run, tread_rise) + td_vector` was assuming `0` means "no custom first tread run", not it means actually "zero first tread run", so could should be adjusted

before - https://files.catbox.moe/5ovqnh.mp4
after - https://files.catbox.moe/srorl2.mp4
2025-11-06 18:44:49 +05:00
Andrej730 b5e8dfd863 generate_stair_2d_profile - add typing 2025-11-06 18:44:49 +05:00
Thomas Krijnen c9df87fc25 submodule 2025-11-06 10:48:12 +01:00
Esteban DUGUEPEROUX 30925a4b3c fix format 2025-11-05 19:26:22 +01:00
Esteban DUGUEPEROUX ccf0efffce cmake: Simplify hdf5 linkage 2025-11-05 19:26:22 +01:00
Andrej730 169c4392a9 test_model - simplify stair generation kwargs 2025-11-05 19:30:36 +05:00
Andrej730 e543785959 Fix breaking tests for stair length calculations with custom zero tread run (f52aafd)
In both cases it was providing arguments `(0,0)`, but was assuming only one tread will be zeroed.
2025-11-05 19:30:36 +05:00
Andrej730 3617d119cb Fix breaking old stair generation tests after f52aafd
Before f52aafd default `(0,0)` meant that there are no custom tread runs, but after the change it actually means that runs for first and last treads are 0. So default value should be adjusted.
Added explicit `None` value for clarity.
2025-11-05 19:30:36 +05:00
Andrej730 5d1f28c3cb validate_object_selection - don't invalidate active objects when they're not in view layer
Since it's not illegal in Blender and can be helpful - e.g. when you were assigning class to ifc space, you would lose selection completely and would need to reselect object to continue working on it.
2025-11-05 19:19:49 +05:00
Andrej730 6f04eb12d0 black . 2025-11-05 19:18:11 +05:00
Ryan Schultz b46136e9f8 For stairs, Lock total length now takes first and last tread widths into account as well. 2025-11-05 06:55:03 -06:00
Andrej730 c3fe2d0ee3 build_win - revert schema versions limit
Committed by accident when debugging 🥲
2025-11-05 14:05:00 +05:00
Andrej730 aeed371888 ifcopenshell.file - add todo notes for temp workarounds, make workaround more explicit 2025-11-04 19:24:22 +05:00
Andrej730 a50c8869a5 bonsai - fix ifcsqlite import (546a042) 2025-11-04 19:24:21 +05:00
Andrej730 04d1fe4052 sql, stream - more clear error message when accessing wrapped_data 2025-11-04 19:24:21 +05:00
Andrej730 4e1d0bcf22 Fix failing selecting products tests (3aa5ddb07)
After 3aa5ddb07 `select_products` adds to current selection, instead of resetting it.

So couple tests started to fail since they create new objects and `view_layer` is not yet updated (previously `bpy.ops.object.select_all(action="DESELECT")` was also implicitly updating view layer).
```
FAILED test/tool/test_spatial.py::TestSelectProducts::test_select_products - AssertionError: assert bpy.data.objects['Object'] in []
FAILED test/tool/test_system.py::TestSelectSystemProducts::test_run - AssertionError: assert bpy.data.objects['Object'] in []
```
2025-11-04 19:24:21 +05:00
Andrej730 d2126e5929 Blender 5.0 - avoid using deprecated use_nodes 2025-11-04 19:24:20 +05:00
Andrej730 3bdfbebc02 Blender 5.0 - accomodate new mathutils buffer protocol type
Long story short - since 5.0 `np.array(Vector())` is now producing `np.float32` instead of `np.float64`. So we have to provide `dtype` explicitly to support both <5.0 and >= 5.0.

See https://projects.blender.org/blender/blender/issues/149283
2025-11-03 19:32:59 +05:00
Andrej730 1a9bd1891e blender 5.0 - fix moved bl_ui_utils module (bonsai is now launchable in 5.0) 2025-11-03 18:33:34 +05:00
Andrej730 1599bc14b3 Fix core/test_system.py (5de96c9) 2025-11-03 18:33:34 +05:00
Andrej730 30b74ab09c blender 5.0 - ensure the latest module is used for type checking 2025-11-03 18:33:34 +05:00
Andrej730 24bee01f01 black . 2025-11-03 18:33:34 +05:00
Ryan Schultz 7329c485ea Fix quantify() regression where by_type queries and grouped iteration could apply QTOs to all entities in the model instead of the intended element subset. 2025-10-31 10:03:52 -05:00
Andrej730 9c93b12084 Blender 5.0 - fix bpy_restrict_state error
It's probably will be very popular bug, when someone would try to migrate to Blender 5.0 using old Bonsai.

Example traceback:

```
  File "Blender\5.0\extensions\.local\lib\python3.11\site-packages\bonsai\bim\prop.py", line 34, in <module>
    import bonsai.bim.schema
  File "Blender\5.0\extensions\.local\lib\python3.11\site-packages\bonsai\bim\schema.py", line 23, in <module>
    import bpy_restrict_state
ModuleNotFoundError: No module named 'bpy_restrict_state'
```
2025-10-31 18:50:53 +05:00
Andrej730 b97c7d39ad dev_environment.py - small error message tweak 2025-10-31 18:50:52 +05:00
Andrej730 fa0c1c5d2a dev_environment.py - fix typo 2025-10-31 18:50:52 +05:00
Andrej730 e4c49ce57b Fix missing Serializers target when building just IfcGeomServer
Noticed testing command from #7277. I believe Serializers it not actually needed to build IfcGeomServer.

Example error:
```
/usr/bin/ld: cannot find -lSerializers: No such file or directory
```
2025-10-31 18:50:52 +05:00
Andrej730 3ae77c68e8 build-all.py - add VERBOSE=1 for building executables 2025-10-31 18:50:52 +05:00
Andrej730 8b1cb8e7ae black . 2025-10-31 18:50:52 +05:00
Andrej730 ce0348518d cmake - note future cmake bug fixes 2025-10-31 18:50:52 +05:00
Andrej730 a660027b93 test_package - ensure wasm package is present for active build commit 2025-10-31 16:09:22 +05:00
Thomas Krijnen 44ef409335 Don't check for drawing semantics on qto or predefpset #7304 2025-10-31 09:56:54 +01:00
Ryan Schultz 089edff1db fix for #7296: handle openings without Blender representation in get_gross_top_area (#7297) 2025-10-30 20:40:06 -05:00
Thomas Krijnen bd5a1b5bda Account 2x3/4 differences in Shading/Rendering::Transparency() 2025-10-30 18:35:20 +01:00
Andrej730 ba3517f3aa build-all.py - switch to dynamic_lookup for macos builds
1) `-undefined,suppress` is deprecated, linker is sending warnings about it and it seems to be superseded by `dynamic_lookup`
2) `-flat_namespace` is not compatible with GMP when building for intel, for some reason and results in horrible errors during linking:

Example error:
```
ld: branch8 out of range 366651 in ___gmpn_add_nc
final section layout:
    __TEXT               addr=0x00000000, size=0x02d09000, fileOffset=0x00000000
        __text           addr=0x00001400, size=0x0264a5e4, fileOffset=0x00001400
        __stubs          addr=0x0264b9e4, size=0x0000423c, fileOffset=0x0264b9e4
        __stub_helper    addr=0x0264fc20, size=0x00006c58, fileOffset=0x0264fc20
        __gcc_except_tab addr=0x02656878, size=0x002efc04, fileOffset=0x02656878
        __const          addr=0x02946480, size=0x001cdc18, fileOffset=0x02946480
        __cstring        addr=0x02b140a0, size=0x0014324b, fileOffset=0x02b140a0
        __unwind_info    addr=0x02c572ec, size=0x0008d2d0, fileOffset=0x02c572ec
        __eh_frame       addr=0x02ce45c0, size=0x00024a30, fileOffset=0x02ce45c0
    __DATA_CONST         addr=0x02d09000, size=0x0022b000, fileOffset=0x02d09000
        __got            addr=0x02d09000, size=0x00001dc8, fileOffset=0x02d09000
        __mod_init_func  addr=0x02d0adc8, size=0x00000690, fileOffset=0x02d0adc8
        __const          addr=0x02d0b460, size=0x00227fd0, fileOffset=0x02d0b460
    __DATA               addr=0x02f34000, size=0x000c3000, fileOffset=0x02f34000
        __la_symbol_ptr  addr=0x02f34000, size=0x000056a0, fileOffset=0x02f34000
        __data           addr=0x02f396a0, size=0x0005f7b8, fileOffset=0x02f396a0
        __thread_vars    addr=0x02f98e58, size=0x000006d8, fileOffset=0x02f98e58
        __thread_ptrs    addr=0x02f99530, size=0x00000178, fileOffset=0x02f99530
        __thread_data    addr=0x02f996b0, size=0x000000d8, fileOffset=0x02f996b0
        __thread_bss     addr=0x02f99790, size=0x00018850, fileOffset=0x02f99790
        __bss            addr=0x02fb1fe0, size=0x0003b0c0, fileOffset=0x00000000
        __common         addr=0x02fed0a0, size=0x00009098, fileOffset=0x00000000
    __LINKEDIT           addr=0x02ff7000, size=0x0148e000, fileOffset=0x02fb2000
clang: error: linker command failed with exit code 1 (use -v to see invocation)
```
2025-10-30 19:45:24 +05:00
Andrej730 7f144b7b95 build-all.py - remove unused wasm flags
`-Wl,-undefined,suppress` have no meaning in emscripten and they are just ignored. And emscripten is ignoring undefined symbols by default without any flags.
2025-10-30 19:45:24 +05:00
Andrej730 19ac0b6c38 build-all.py - add VERBOSE=1 to the wrapper make command 2025-10-30 19:45:24 +05:00
Andrej730 3562437293 build-all.py - fix missing mac cross compilation host args in gmp
I guess something has changed in gmp 6.3.0 and this issue become more apparent.

Example error:
```
tmp-cnd_sub_n.s:133:14: error: tmp-cnd_add_n.sbrackets expression not supported on this target:
130 stp x8, x9, [x1,#16]
             ^
:2tmp-cnd_sub_n.s: :error: 134invalid instruction mnemonic 'bic':
6 bic x7, x13, x0:
error:  invalid instruction mnemonic 'cset'^
~Lrt: cset x0, cc~

     ^~~tmp-cnd_add_n.s~:
131:2: error: invalid instruction mnemonic 'adcs'
 adcs x8, x10, x6
 ^~~~
tmp-cnd_add_n.s:132:2: error: invalid instruction mnemonic 'adcs'
 adcs x9, x11, x7
 ^~~~
tmp-cnd_add_n.s:133:14: error: brackets expression not supported on this target
 stp x8, x9, [x1,#16]
             ^
tmp-cnd_add_n.s:134:6: error: invalid instruction mnemonic 'cset'
Lrt: cset x0, cs
     ^~~~
```
2025-10-30 19:45:24 +05:00
Andrej730 e8ece71d22 black . 2025-10-30 19:45:24 +05:00
236 changed files with 9076 additions and 2343 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
@@ -34,6 +34,10 @@ jobs:
name: "Linux 64bit",
short_name: linux64
}
- {
name: "Linux ARM 64bit",
short_name: linuxarm64
}
- {
name: "MacOS Intel 64bit",
short_name: macos64
@@ -43,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
+2 -5
View File
@@ -17,6 +17,7 @@
# General Python residue
__pycache__
*.py.bak
venv
# Visual Studio Code files
.vscode
@@ -78,8 +79,6 @@ src/bonsai/bonsai/translations.py
# bonsai test temp files
src/bonsai/test/files/temp
src/bonsai/test/files/basic.ifc.cache.blend
src/bonsai/test/files/basic.ifc.cache.sqlite
# bonsai data
src/bonsai/bonsai/bim/data/build/
@@ -87,9 +86,7 @@ src/bonsai/bonsai/bim/data/gantt/index.html
src/bonsai/bonsai/bim/data/gantt/jsgantt.js
src/bonsai/bonsai/bim/data/gantt/jsgantt.css
src/bonsai/bonsai/bim/data/webui/static/js/jquery.min.js
src/bonsai/drawings
src/bonsai/layouts
src/bonsai/bonsai/bim/data/webui/running_pid.json
# ifcopenshell swig and compiled files
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
-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
+29 -706
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)
@@ -104,6 +103,9 @@ endif()
project(IfcOpenShell VERSION ${RELEASE_VERSION})
# Make sure CMake modules in this project are found first
list(PREPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
if(MINIMAL_BUILD)
message(STATUS "Setting options for minimal build")
set(BUILD_GEOMSERVER OFF)
@@ -196,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
@@ -230,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}'.")
find_package(CGAL REQUIRED)
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()
@@ -286,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)
@@ -298,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)
@@ -316,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)
@@ -440,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)
@@ -486,335 +358,18 @@ 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)
if("${HDF5_INCLUDE_DIR}" STREQUAL "")
message(STATUS "No HDF5 include directory specified")
else()
set(HDF5_INCLUDE_DIR "${HDF5_INCLUDE_DIR}" CACHE FILEPATH "HDF5 header files")
endif()
if("${HDF5_LIBRARY_DIR}" STREQUAL "")
message(STATUS "No HDF5 library directory specified")
else()
set(HDF5_LIBRARY_DIR "${HDF5_LIBRARY_DIR}" CACHE FILEPATH "HDF5 library files")
endif()
if(HDF5_LIBRARY_DIR)
# result of the HDF5 ctest package
# Find zlib using cmake find_library. How should this be implemented?
# FIND_LIBRARY(NAMES z libz libz_debug PATHS ... NO_DEFAULT_PATH)
if("$ENV{CONDA_BUILD}" STREQUAL "")
# result of the HDF5 ctest package
if(WIN32)
set(zlib_post lib)
set(lib_ext lib)
else()
set(lib_ext a)
endif()
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(debug_postfix "_debug")
endif()
set(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
)
else()
message(STATUS "Packaging hdf5 and zlib for conda distribution")
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
# macOS
set(zlib_post libz)
set(lib_ext dylib)
set(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
)
else()
# linux and windows
# Find HDF5 package
find_package(HDF5 REQUIRED COMPONENTS C CXX)
# Find ZLIB package
find_package(ZLIB REQUIRED)
# Include directories
include_directories(${HDF5_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
# Link libraries
set(HDF5_LIBRARIES ${HDF5_LIBRARIES} ${ZLIB_LIBRARIES})
message(STATUS "HDF5 libraries: ${HDF5_LIBRARIES}")
endif()
endif()
endif()
if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
# First try to find it as a config.
find_package(HDF5 CONFIG)
if(HDF5_DIR)
message(STATUS "HDF5: found config at '${HDF5_DIR}'.")
set(HDF5_LIBRARIES hdf5_cpp-static)
else()
# If it failed, still try to find as a module.
# E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake.
# Will automatically fill HDF5_LIBRARIES and HDF5_INCLUDE_DIR.
find_package(HDF5 COMPONENTS CXX)
if(NOT HDF5_INCLUDE_DIR)
message(
FATAL_ERROR
"HDF5_INCLUDE_DIR is not provided (current value: '${HDF5_INCLUDE_DIR}'). "
"HDF5_LIBRARY_DIR is not provided (current value: '${HDF5_LIBRARY_DIR}'). "
"Also could not find HDF5 package (neither module or config)."
)
endif()
endif()
endif()
find_package(HDF5 REQUIRED COMPONENTS C CXX)
add_definitions(-DWITH_HDF5)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_HDF5)
@@ -931,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}
include_directories(${INCLUDE_DIRECTORIES} ${OPENCOLLADA_INCLUDE_DIRS}
${Boost_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIR} ${JSON_INCLUDE_DIR} ${HDF5_INCLUDE_DIR}
${EIGEN_DIR} ${CGAL_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${MPFR_INCLUDE_DIR} ${USD_INCLUDE_DIR}
${CGAL_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${MPFR_INCLUDE_DIR} ${USD_INCLUDE_DIR}
${TBB_INCLUDE_DIR}
)
@@ -1038,6 +580,7 @@ if(NOT Boost_VERSION LESS 105800)
add_definitions(-DBOOST_OPTIONAL_USE_OLD_DEFINITION_OF_NONE)
endif()
add_subdirectory(../src/ifcparse ifcparse)
set(IFCOPENSHELL_LIBRARIES IfcParse)
if(BUILD_IFCGEOM)
@@ -1048,84 +591,31 @@ 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)
add_library(geometry_serializer_ifc${schema} OBJECT ../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()
add_library(geometry_serializer STATIC ../src/ifcgeom/Serialization/Serialization.h ../src/ifcgeom/Serialization/Serialization.cpp)
add_library(geometry_serializer OBJECT ../src/ifcgeom/Serialization/Serialization.h ../src/ifcgeom/Serialization/Serialization.cpp)
set_target_properties(geometry_serializer PROPERTIES COMPILE_FLAGS "-DIFC_GEOMSERIALIZATION_EXPORTS")
target_link_libraries(geometry_serializer ${geometry_serializer_libraries} IfcParse)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} geometry_serializer ${geometry_serializer_libraries})
endif()
endif()
# IfcParse
file(GLOB IFCPARSE_H_FILES_ALL ../src/ifcparse/*.h)
file(GLOB IFCPARSE_CPP_FILES_ALL ../src/ifcparse/*.cpp)
foreach(file ${IFCPARSE_H_FILES_ALL})
get_filename_component(filename "${file}" NAME)
if(NOT "${filename}" MATCHES "[0-9]")
list(APPEND IFCPARSE_H_FILES "${file}")
endif()
endforeach()
foreach(file ${IFCPARSE_CPP_FILES_ALL})
get_filename_component(filename "${file}" NAME)
if(NOT "${filename}" MATCHES "[0-9]")
list(APPEND IFCPARSE_CPP_FILES "${file}")
endif()
endforeach()
foreach(schema ${SCHEMA_VERSIONS})
list(APPEND IFCPARSE_H_FILES
../src/ifcparse/Ifc${schema}.h
../src/ifcparse/Ifc${schema}-definitions.h
)
list(APPEND IFCPARSE_CPP_FILES
../src/ifcparse/Ifc${schema}.cpp
../src/ifcparse/Ifc${schema}-schema.cpp
)
endforeach()
set(IFCPARSE_FILES ${IFCPARSE_CPP_FILES} ${IFCPARSE_H_FILES})
add_library(IfcParse ${IFCPARSE_FILES})
target_link_libraries(IfcParse ${STDCPPFS})
set_target_properties(IfcParse PROPERTIES COMPILE_FLAGS -DIFC_PARSE_EXPORTS VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
if(LibXml2_DIR)
target_compile_definitions(IfcParse PRIVATE ${LIBXML2_DEFINITIONS})
endif()
if(WASM_BUILD)
target_link_libraries(IfcParse ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
else()
target_link_libraries(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
endif()
if(BUILD_IFCGEOM)
# CGAL::CGAL target already has dependencies resolved.
if(WITH_CGAL AND CGAL_DIR)
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()
@@ -1137,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)
@@ -1171,12 +636,9 @@ if(BUILD_IFCGEOM)
file(GLOB IFCGEOM_CPP_FILES ../src/ifcgeom/mapping/*.cpp)
set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES} ${IFCGEOM_I_FILES})
add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES})
add_library(geometry_mapping_ifc${schema} OBJECT ${IFCGEOM_FILES})
set_target_properties(geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
target_link_libraries(geometry_mapping_ifc${schema} IfcParse)
if (NOT BUILD_SHARED_LIBS)
target_link_libraries(geometry_mapping_ifc${schema} IfcGeom)
endif()
list(APPEND mapping_libraries geometry_mapping_ifc${schema})
endforeach()
@@ -1192,134 +654,22 @@ 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
if(BUILD_GEOMSERVER)
if(NOT WITH_OPENCASCADE)
message(FATAL_ERROR "Open CASCADE is required to build IfcGeomServer.")
endif()
file(GLOB CPP_FILES ../src/ifcgeomserver/*.cpp)
file(GLOB H_FILES ../src/ifcgeomserver/*.h)
set(SOURCE_FILES ${CPP_FILES} ${H_FILES})
add_executable(IfcGeomServer ${SOURCE_FILES})
target_link_libraries(IfcGeomServer IfcGeom IfcParse Serializers ${kernel_libraries} ${OPENCASCADE_LIBRARIES} ${Boost_LIBRARIES})
if((NOT WIN32) AND BUILD_SHARED_LIBS)
SET_INSTALL_RPATHS(IfcGeomServer "${IFCOPENSHELL_LIBRARY_DIR};${OCC_LIBRARY_DIR};${Boost_LIBRARY_DIRS}")
endif()
install(TARGETS IfcGeomServer
ARCHIVE DESTINATION ${LIBDIR}
LIBRARY DESTINATION ${LIBDIR}
RUNTIME DESTINATION ${BINDIR}
)
add_subdirectory(../src/ifcgeomserver ifcgeomserver)
endif(BUILD_GEOMSERVER)
if(ADD_COMMIT_SHA)
@@ -1406,17 +756,6 @@ if(BUILD_QTVIEWER)
add_subdirectory(../src/qtviewer qtviewer)
endif()
# CMake installation targets
install(FILES ${IFCPARSE_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcparse
)
install(TARGETS IfcParse
ARCHIVE DESTINATION ${LIBDIR}
LIBRARY DESTINATION ${LIBDIR}
RUNTIME DESTINATION ${BINDIR}
)
if(BUILD_IFCGEOM)
# install(FILES ${IFCGEOM_H_FILES}
# DESTINATION ${INCLUDEDIR}/ifcgeom
@@ -1445,22 +784,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()
+109
View File
@@ -0,0 +1,109 @@
#
# 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 ${CMAKE_SOURCE_DIR})
if("${HDF5_INCLUDE_DIR}" STREQUAL "")
message(STATUS "No HDF5 include directory specified")
else()
set(HDF5_INCLUDE_DIR "${HDF5_INCLUDE_DIR}" CACHE FILEPATH "HDF5 header files")
endif()
if("${HDF5_LIBRARY_DIR}" STREQUAL "")
message(STATUS "No HDF5 library directory specified")
else()
set(HDF5_LIBRARY_DIR "${HDF5_LIBRARY_DIR}" CACHE FILEPATH "HDF5 library files")
endif()
if(HDF5_LIBRARY_DIR)
# result of the HDF5 ctest package
# Find zlib using cmake find_library. How should this be implemented?
# FIND_LIBRARY(NAMES z libz libz_debug PATHS ... NO_DEFAULT_PATH)
if("$ENV{CONDA_BUILD}" STREQUAL "")
# result of the HDF5 ctest package
if(WIN32)
set(zlib_post lib)
set(lib_ext lib)
else()
set(lib_ext a)
endif()
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(debug_postfix "_debug")
endif()
set(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
)
else()
message(STATUS "Packaging hdf5 and zlib for conda distribution")
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
# macOS
set(zlib_post libz)
set(lib_ext dylib)
set(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
)
else()
# linux and windows
# Find HDF5 package
find_package(HDF5 REQUIRED COMPONENTS C CXX)
# Find ZLIB package
find_package(ZLIB REQUIRED)
# Include directories
include_directories(${HDF5_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
# Link libraries
set(HDF5_LIBRARIES ${HDF5_LIBRARIES} ${ZLIB_LIBRARIES})
message(STATUS "HDF5 libraries: ${HDF5_LIBRARIES}")
endif()
endif()
endif()
if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
# First try to find it as a config.
find_package(HDF5 CONFIG)
if(HDF5_DIR)
message(STATUS "HDF5: found config at '${HDF5_DIR}'.")
set(HDF5_LIBRARIES hdf5_cpp-static)
else()
# If it failed, still try to find as a module.
# E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake.
# Will automatically fill HDF5_LIBRARIES and HDF5_INCLUDE_DIR.
find_package(HDF5 COMPONENTS CXX)
if(NOT HDF5_INCLUDE_DIR)
message(
FATAL_ERROR
"HDF5_INCLUDE_DIR is not provided (current value: '${HDF5_INCLUDE_DIR}'). "
"HDF5_LIBRARY_DIR is not provided (current value: '${HDF5_LIBRARY_DIR}'). "
"Also could not find HDF5 package (neither module or config)."
)
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})
+144
View File
@@ -0,0 +1,144 @@
#
# 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`
# OpenCASCADE may be built with VTK support. Try to find VTK first to avoid
# CMake errors when OpenCASCADE's config references VTK targets.
find_package(VTK QUIET)
find_package(OpenCASCADE CONFIG REQUIRED)
message(STATUS "Found OpenCASCADE config: ${OpenCASCADE_DIR}")
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
+16 -14
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.
@@ -382,11 +384,13 @@ if MAC_CROSS_COMPILE_INTEL:
MAC_CROSS_COMPILE_INTEL_BJAM_ARGS = ["architecture=x86"]
MAC_CROSS_COMPILE_INTEL_CXX = "clang++ -arch x86_64"
MAC_CROSS_COMPILE_INTEL_CC = "clang -arch x86_64"
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = ["--host=x86_64-apple-darwin"]
else:
MAC_CROSS_COMPILE_INTEL_ARGS = []
MAC_CROSS_COMPILE_INTEL_BJAM_ARGS = []
MAC_CROSS_COMPILE_INTEL_CXX = ""
MAC_CROSS_COMPILE_INTEL_CC = ""
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = []
OFF_ON = ["OFF", "ON"]
BUILD_STATIC = "shared" not in flags
@@ -939,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",
@@ -1195,7 +1197,9 @@ if "cgal" in targets:
OLD_CC = None
if MAC_CROSS_COMPILE_INTEL:
OLD_CC = os.environ.get("CC")
# Otherwise it's using arm64 `gcc` and fails to build gmp.
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
gmp_args.extend(MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS)
build_dependency(
name=f"gmp-{GMP_VERSION}",
@@ -1374,6 +1378,7 @@ if "cgal" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/cgal-{CGAL_VERSION}")
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/gmp-{GMP_VERSION}")
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/mpfr-{MPFR_VERSION}")
cmake_args.append(f"-DCGAL_WITH_GMPXX=Off")
if "occ" in targets and USE_OCCT:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/occt-{OCCT_VERSION}")
@@ -1426,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...")
@@ -1441,17 +1449,14 @@ if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServe
logger.info("\rBuilding executables... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}"], cwd=executables_dir)
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=executables_dir)
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=executables_dir)
if "IfcOpenShell-Python" in targets:
# On OSX the actual Python library is not linked against.
ADDITIONAL_ARGS = ""
if platform.system() == "Darwin":
ADDITIONAL_ARGS = "-Wl,-flat_namespace,-undefined,suppress"
if "wasm" in flags:
ADDITIONAL_ARGS = "-Wl,-undefined,suppress"
ADDITIONAL_ARGS = "-Wl,-undefined,dynamic_lookup"
# NOTE: We don't use `CXXFLAGS` for wrappers, so wrapper is compiled with different flags
# (e.g. ` -fdata-sections` is missing, which is set by default for executables)
@@ -1481,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.
@@ -1500,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.
@@ -1516,7 +1518,7 @@ if "IfcOpenShell-Python" in targets:
logger.info(f"\rBuilding python {python_version} wrapper... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper"], cwd=python_dir)
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper", "VERBOSE=1"], cwd=python_dir)
run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
if python_executable:
+1 -4
View File
@@ -9,10 +9,8 @@ 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
|src/pyodide
'''
[tool.pyright]
@@ -21,12 +19,11 @@ disableBytesTypePromotions = true
reportUnnecessaryTypeIgnoreComment = true
# Define here general ruff settings,
# then they will be inherited projects .toml files.
# then they will be inherited by projects' .toml files.
# This allows using assuming different Python version for different projects.
[tool.ruff]
exclude = [
# Submodules.
"src/ifcconvert/cityjson",
"src/ifcopenshell-python/ifcopenshell/express",
"src/ifcopenshell-python/ifcopenshell/mvd",
"src/ifcopenshell-python/ifcopenshell/simple_spf",
+4 -1
View File
@@ -87,7 +87,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=6924012
OLD:=e8eb5e4
.PHONY: bump
bump:
ifndef NEW
@@ -155,6 +155,9 @@ endif
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download six --dest=./wheels
# Required by drawing module
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download lxml $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
# Required by drawing module for markdown hyperlinks in annotations
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download markdown-it-py --dest=./wheels
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download mdurl --dest=./wheels
# Required by qto and drawing module
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download shapely $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
# Required by the BIM type manager thumbnail generator
+20 -1
View File
@@ -98,7 +98,7 @@ def initialize_bbim_semver():
def get_debug_info():
bbim_version = bbim_semver["version"]
return {
debug_info = {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
@@ -113,6 +113,25 @@ def get_debug_info():
"last_error": last_error,
}
# Add .blend file save information
if bpy.data.is_saved:
debug_info["blend_file_path"] = bpy.data.filepath
debug_info["blend_file_dirty"] = bpy.data.is_dirty
else:
debug_info["blend_file_path"] = "Not saved"
debug_info["blend_file_dirty"] = "N/A"
# Add IFC file information
bim_props = tool.Blender.get_bim_props()
if bim_props.ifc_file:
debug_info["ifc_file_path"] = bim_props.ifc_file
debug_info["ifc_is_dirty"] = bim_props.is_dirty
else:
debug_info["ifc_file_path"] = "No IFC loaded"
debug_info["ifc_is_dirty"] = "N/A"
return debug_info
def format_debug_info(info: dict):
last_actions = ""
+47 -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,11 @@ classes = [
ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic,
ui.DocPreferences,
ui.BIM_ADDON_preferences,
ui.GizmoPreferencesDoor, # Register before GizmoPreferences
ui.GizmoPreferencesWindow, # Register before GizmoPreferences
ui.GizmoPreferencesStair, # 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 +206,19 @@ classes = [
ui.BIM_PT_section_with_cappings,
ui.BIM_PT_decorators_overlay,
ui.BIM_PT_snappping,
# Gizmos
gizmo.BIM_OT_gizmo_value_input,
gizmo.GizmoArrow,
gizmo.GizmoArrow2D,
gizmo.GizmoCone,
gizmo.GizmoLock,
gizmo.GizmoArc,
gizmo.GizmoPen,
gizmo.GizmoValidate,
gizmo.GizmoCancel,
gizmo.GizmoPlus,
gizmo.GizmoMinus,
gizmo.GizmoCycle,
]
for mod in modules.values():
@@ -225,14 +242,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 +292,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 +326,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 +341,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()
@@ -7,12 +7,6 @@
1.0,
1.0
],
"scene.render.bake_bias": 0.0010000000474974513,
"scene.render.bake_margin": 16,
"scene.render.bake_margin_type": "ADJACENT_FACES",
"scene.render.bake_samples": 256,
"scene.render.bake_type": "NORMALS",
"scene.render.bake_user_scale": 0.0,
"scene.render.border_max_x": 1.0,
"scene.render.border_max_y": 1.0,
"scene.render.border_min_x": 0.0,
@@ -54,11 +48,6 @@
"scene.render.stamp_note_text": "",
"scene.render.threads": 20,
"scene.render.threads_mode": "AUTO",
"scene.render.use_bake_clear": true,
"scene.render.use_bake_lores_mesh": false,
"scene.render.use_bake_multires": false,
"scene.render.use_bake_selected_to_active": false,
"scene.render.use_bake_user_scale": false,
"scene.render.use_border": false,
"scene.render.use_compositing": true,
"scene.render.use_crop_to_border": false,
@@ -253,12 +242,6 @@
1.0,
1.0
],
"scene.render.bake_bias": 0.0010000000474974513,
"scene.render.bake_margin": 16,
"scene.render.bake_margin_type": "ADJACENT_FACES",
"scene.render.bake_samples": 256,
"scene.render.bake_type": "NORMALS",
"scene.render.bake_user_scale": 0.0,
"scene.render.border_max_x": 1.0,
"scene.render.border_max_y": 1.0,
"scene.render.border_min_x": 0.0,
@@ -300,11 +283,6 @@
"scene.render.stamp_note_text": "",
"scene.render.threads": 20,
"scene.render.threads_mode": "AUTO",
"scene.render.use_bake_clear": true,
"scene.render.use_bake_lores_mesh": false,
"scene.render.use_bake_multires": false,
"scene.render.use_bake_selected_to_active": false,
"scene.render.use_bake_user_scale": false,
"scene.render.use_border": false,
"scene.render.use_compositing": true,
"scene.render.use_crop_to_border": false,
@@ -377,7 +355,7 @@
1.0,
1.0
],
"scene.display.shading.studio_light": "Default",
"scene.display.shading.studio_light": "forest.exr",
"scene.display.shading.studiolight_background_alpha": 0.0,
"scene.display.shading.studiolight_background_blur": 0.0,
"scene.display.shading.studiolight_intensity": 0.0,
@@ -503,18 +481,12 @@
"scene.eevee.shadow_ray_count": 1,
"scene.eevee.shadow_step_count": 6,
"scene.eevee.shadow_resolution_scale": 1.0,
"scene.render.bake_bias": 0.0010000000474974513,
"scene.render.bake_margin": 16,
"scene.render.bake_margin_type": "ADJACENT_FACES",
"scene.render.bake_samples": 256,
"scene.render.bake_type": "NORMALS",
"scene.render.bake_user_scale": 0.0,
"scene.render.border_max_x": 1.0,
"scene.render.border_max_y": 1.0,
"scene.render.border_min_x": 0.0,
"scene.render.border_min_y": 0.0,
"scene.render.dither_intensity": 1.0,
"scene.render.engine": "BLENDER_EEVEE_NEXT",
"scene.render.engine": "BLENDER_EEVEE",
"scene.render.film_transparent": false,
"scene.render.filter_size": 1.5,
"scene.render.fps": 24,
@@ -550,11 +522,6 @@
"scene.render.stamp_note_text": "",
"scene.render.threads": 20,
"scene.render.threads_mode": "AUTO",
"scene.render.use_bake_clear": true,
"scene.render.use_bake_lores_mesh": false,
"scene.render.use_bake_multires": false,
"scene.render.use_bake_selected_to_active": false,
"scene.render.use_bake_user_scale": false,
"scene.render.use_border": false,
"scene.render.use_compositing": true,
"scene.render.use_crop_to_border": false,
@@ -627,7 +594,7 @@
0.800000011920929,
0.800000011920929
],
"scene.display.shading.studio_light": "Default",
"scene.display.shading.studio_light": "forest.exr",
"scene.display.shading.studiolight_background_alpha": 0.0,
"scene.display.shading.studiolight_background_blur": 0.0,
"scene.display.shading.studiolight_intensity": 0.0,
+1
View File
@@ -25,6 +25,7 @@ import zipfile
import tempfile
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.unit
import bonsai.tool as tool
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -28,6 +28,7 @@ import numpy as np
import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.api.pset
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.unit
@@ -1237,8 +1238,7 @@ class IfcImporter:
if not pset:
return
if "Aggregate_Index" not in pset.keys():
ifcopenshell.api.run(
"pset.edit_pset",
ifcopenshell.api.pset.edit_pset(
self.file,
pset=self.file.by_id(pset["id"]),
properties={"Aggregate_Index": aggregate_index, "Name": name},
@@ -21,6 +21,7 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.group
import ifcopenshell.api.pset
import ifcopenshell.api.root
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.aggregate as core
@@ -126,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()}"
@@ -321,6 +326,21 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
# copy selection query to clipboard
result = ""
for aggregate in all_parts:
aggregate_class = aggregate.is_a()
aggregate_name = aggregate.Name or ""
query = f'parent = "{aggregate_name}"'
if not result:
result = query
else:
result += f" + {query}"
if result:
bpy.context.window_manager.clipboard = result
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
self.one_level_deep = False # <-- forcibly reset
return {"FINISHED"}
-17
View File
@@ -109,23 +109,6 @@ class BIM_PT_debug(Panel):
row.prop(props, "display_type", text="")
row.operator("bim.override_display_type").display = props.display_type
layout.operator("bim.purge_unused_representations")
row = layout.row(align=True)
row.prop(props, "ifc_class_purge", text="")
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
if context.active_object and (data := context.active_object.data):
mprops = tool.Geometry.get_mesh_props(data)
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
row = layout.row(align=True)
row.prop(ifc_parameter, "name", text="")
row.prop(ifc_parameter, "value", text="")
row.operator("bim.update_parametric_representation", icon="FILE_REFRESH", text="").index = index
layout.label(text="Inspector:")
row = layout.row(align=True)
+2 -2
View File
@@ -235,8 +235,8 @@ class DecoratorData:
cut_cache = {}
slice_cache = {}
fill_cache = {}
camera_location_checksum = None
camera_rotation_checksum = None
camera_location_checksum = ""
camera_rotation_checksum = ""
@classmethod
def clear_cache(cls):
@@ -19,6 +19,7 @@
import gpu
import bpy
import blf
import os
import math
import bmesh
import shapely
@@ -41,7 +42,7 @@ from bonsai.bim.module.drawing.helper import format_distance
from timeit import default_timer as timer
from functools import cache
from typing import Optional, Union
from collections.abc import Iterator
from collections.abc import Generator, Iterator
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
@@ -172,15 +173,11 @@ class BaseDecorator:
def get_camera_width_mm(self):
# Horrific prototype code to ensure bgl draws at drawing scales
# https://blender.stackexchange.com/questions/16493/is-there-a-way-to-fit-the-viewport-to-the-current-field-of-view
def is_landscape(render):
return render.resolution_x > render.resolution_y
camera = bpy.context.scene.camera
render = bpy.context.scene.render
if is_landscape(render):
camera_width_model = camera.data.ortho_scale
else:
camera_width_model = camera.data.ortho_scale / render.resolution_y * render.resolution_x
# Always use the camera's ortho_scale as the width in model space
camera_width_model = camera.data.ortho_scale
scale = tool.Drawing.get_scale_ratio(tool.Drawing.get_diagram_scale(camera)["Scale"])
camera_width_mm = scale * camera_width_model
@@ -189,29 +186,26 @@ class BaseDecorator:
def camera_zoom_to_factor(self, zoom):
return math.pow(((zoom / 50) + math.sqrt(2)) / 2, 2)
def get_splines(self, obj):
def get_splines(self, obj: bpy.types.Object) -> Generator[list[Vector]]:
"""Iterates through splines
Args:
obj: Blender object with Curve data
Yields:
verts: points of each spline, world coords
:param obj: Blender object with Curve data
:yield: points of each spline, world coords
"""
assert type(obj.data) is bpy.types.Curve
for spline in obj.data.splines:
spline_points = spline.bezier_points if spline.bezier_points else spline.points
if len(spline_points) < 2:
continue
yield [obj.matrix_world @ p.co for p in spline_points]
def get_path_geom(self, obj, topo=True):
def get_path_geom(self, obj: bpy.types.Object, topo: bool = True):
"""Parses path geometry into line segments
Args:
obj: Blender object with data of type Curve
topo: bool; if types of vertices are needed
:param obj: Blender object with data of type Curve
:param topo: if types of vertices are needed
Returns:
vertices: 3-tuples of coords
:return: vertices: 3-tuples of coords
indices: 2-tuples of each segment verices' indices
topology: types of vertices
0: internal
@@ -242,11 +236,9 @@ class BaseDecorator:
def get_mesh_geom(self, obj, check_mode=True):
"""Parses mesh geometry into line segments
Args:
obj: Blender object with data of type Mesh
:param obj: Blender object with data of type Mesh
Returns:
vertices: 3-tuples of coords
:return: vertices: 3-tuples of coords
indices: 2-tuples of each segment verices' indices
"""
if check_mode and obj.data.is_editmode:
@@ -377,11 +369,9 @@ class BaseDecorator:
):
"""Draw text label
Args:
pos: bottom-center
multiline: \n characters will be interpreted as line breaks
aligned and centered at segment middle
:param pos: bottom-center
:param multiline: ``\n`` characters will be interpreted as line breaks
aligned and centered at segment middle
NOTE: `blf.draw` seems to ignore the \n character, so we have to split the text ourselves
and use the `line_no` argument of `draw_label`
@@ -499,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:
@@ -510,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:
@@ -753,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
@@ -1208,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
@@ -1281,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
@@ -1641,6 +1645,13 @@ class CutDecorator:
if not context.scene.camera:
return
# Check if any viewport is in local view - skip decorations if so
for area in context.screen.areas:
if area.type == "VIEW_3D":
space = area.spaces.active
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
return # Don't draw decorations in local view
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
self.fallback_colour = (0.3, 0.3, 0.3, 1)
@@ -1731,21 +1742,38 @@ class CutDecorator:
def cache_camera_matrix(self):
obj = bpy.context.scene.camera
DecoratorData.camera_location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
# Explicit `dtype` for Blender <5.0 compatibility.
DecoratorData.camera_location_checksum = repr(
np.array(obj.matrix_world.translation, dtype=np.float32).tobytes()
)
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes())
def is_camera_moved(self):
if not DecoratorData.camera_location_checksum:
self.cache_camera_matrix()
return True # Let's be conservative
obj = bpy.context.scene.camera
loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum))
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)).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
@@ -1974,15 +2002,51 @@ class DecorationsHandler:
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
prefs = tool.Blender.get_addon_preferences()
if drawing_font := prefs.doc.drawing_font:
# Try Blender addon font folder
drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font)
if drawing_font_path.is_file():
font_id = blf.load(drawing_font_path.__str__())
for decorator in self.decorators.values():
decorator.font_id = font_id
if not drawing_font_path.is_file():
# 2 — Fallback search: Windows font directories
# TODO - Linux
win_font_dirs = [
Path(os.environ.get("WINDIR", "C:\\Windows")) / "Fonts",
# Add any custom enterprise paths here if needed
]
found_font = None
for font_dir in win_font_dirs:
candidate = font_dir / drawing_font
if candidate.is_file():
found_font = candidate
break
if found_font:
drawing_font_path = found_font
else:
print(f"[BIM] Font '{drawing_font}' not found in addon or Windows fonts.")
return # Bail out without assigning fonts
font_id = blf.load(str(drawing_font_path))
for decorator in self.decorators.values():
decorator.font_id = font_id
def __call__(self, context):
# Check if any viewport is in local view - skip decorations if so
for area in context.screen.areas:
if area.type == "VIEW_3D":
space = area.spaces.active
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()
+100 -32
View File
@@ -18,6 +18,7 @@
import bpy
import math
import ifcopenshell.util.element
import mathutils.geometry
import ifcopenshell
import ifcopenshell.util.unit
@@ -121,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(
@@ -186,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":
@@ -202,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
@@ -219,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
@@ -251,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)
@@ -421,8 +491,8 @@ def ortho_view_frame(
Similar to `bpy.types.Camera.view_frame`
:arg camera: camera of drawing
:arg margin: margins, in scene units
:param camera: camera of drawing
:param margin: margins, in scene units
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
"""
props = tool.Drawing.get_camera_props(camera)
@@ -443,8 +513,8 @@ def almost_zero(v):
def clip_segment(bounds, segm):
"""Clipping line segment to bounds
:arg bounds: (xmin, xmax, ymin, ymax)
:arg segm: 2 vertices of the segment
:param bounds: (xmin, xmax, ymin, ymax)
:param segm: 2 vertices of the segment
:return: 2 new vertices of segment or None if segment outside the bounding box
"""
# LiangBarsky algorithm
@@ -493,8 +563,8 @@ def clip_segment(bounds, segm):
def elevate_segment(bounds, segm):
"""Elevate line xy-perpendicular segment vertically
:arg bounds: (xmin, xmax, ymin, ymax)
:arg segm: 2 vertices of the segment
:param bounds: (xmin, xmax, ymin, ymax)
:param segm: 2 vertices of the segment
:return: 2 new vertices of segment or None if segment outside the bounding box
"""
_, _, ymin, ymax, zmin, _ = bounds
@@ -544,13 +614,11 @@ def add_newline_between_words(text: str, newline_at: int) -> str:
def get_relative_z(obj: bpy.types.Object, element, abs_z: float) -> float:
"""Return relative Z of an element, accounting for its spatial container.
Args:
obj: The Blender object representing the element.
element: The IFC entity for the object.
abs_z: The absolute Z value in world coordinates.
:param obj: The Blender object representing the element.
:param element: The IFC entity for the object.
:param abs_z: The absolute Z value in world coordinates.
Returns:
Relative Z value if the element is inside a spatial container,
:return: Relative Z value if the element is inside a spatial container,
otherwise the absolute Z.
"""
z = abs_z
@@ -932,6 +932,31 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
# Add target_view and scale classes to the parent group from IFC data
if group is not None:
existing_classes = group.get("class", "").split()
# Add target_view class
if hasattr(self, "cprops") and getattr(self.cprops, "target_view", None):
target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
target_view_full_class = f"target-view-{target_view_class}"
if target_view_full_class not in existing_classes:
existing_classes.append(target_view_full_class)
# Add scale class from EPset_Drawing.Scale
drawing_pset = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing")
if drawing_pset and drawing_pset.get("Scale"):
scale_value = drawing_pset["Scale"]
# Remove "1/" prefix if it exists
if isinstance(scale_value, str) and scale_value.startswith("1/"):
scale_value = scale_value[2:]
scale_class = tool.Drawing.canonicalise_class_name(str(scale_value))
scale_full_class = f"scale-{scale_class}"
if scale_full_class not in existing_classes:
existing_classes.append(scale_full_class)
group.set("class", " ".join(existing_classes))
if self.cprops.cut_mode == "BISECT":
self.remove_cut_linework(root)
self.generate_bisect_linework(context, root)
@@ -1287,11 +1312,6 @@ class CreateDrawing(bpy.types.Operator):
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
)
# ─── Target View ───────────────────────────────────────────
if getattr(self.cprops, "target_view", None):
target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
classes.append(f"target-view-{target_view_class}")
return classes
def is_manifold(self, obj) -> bool:
@@ -2701,21 +2721,59 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
def set_raster_style(self, context: bpy.types.Context) -> None:
scene = context.scene # Do not remove. It is used in exec later
assert (space := tool.Blender.get_view3d_space()) # Do not remove. It is used in exec later
style = json.loads(self.drawing_style.raster_style)
style: dict[str, Any] = json.loads(self.drawing_style.raster_style)
VIEWPORT_SHADING_TYPE = "scene.display.shading.type"
def preprocess(path: str, value: Any) -> tuple[str, Any, bool, bool]:
warning = False
skip = False
# @25.05.12
if path == "scene.render.engine" and value == "BLENDER_EEVEE":
value = "BLENDER_EEVEE_NEXT"
BLENDER_5_REMOVED = (
"scene.render.bake_bias",
"scene.render.bake_margin",
"scene.render.bake_margin_type",
"scene.render.bake_samples",
"scene.render.bake_type",
"scene.render.bake_user_scale",
"scene.render.use_bake_clear",
"scene.render.use_bake_lores_mesh",
"scene.render.use_bake_multires",
"scene.render.use_bake_selected_to_active",
"scene.render.use_bake_user_scale",
)
# 25.11.07, Blender 5+
if path == "scene.render.engine" and value in ("BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"):
if value == "BLENDER_EEVEE_NEXT":
print(
f"Warning: Value 'BLENDER_EEVEE_NEXT' is outdated for property '{path}' "
"since Blender 5.0 and should be replaced with 'BLENDER_EEVEE' in shading_styles.json."
)
warning = True
value = tool.Blender.get_eevee_name()
elif tool.Blender.BLENDER_5 and path in BLENDER_5_REMOVED:
print(
f"Warning: Value 'BLENDER_EEVEE' is outdated for property '{path}' "
"since Blender 4.2 and should be replaced with 'BLENDER_EEVEE_NEXT' in shading_styles.json."
f"Warning: Property '{path}' is removed "
"since Blender 5.0 and should be also removed from shading_styles.json."
)
warning = True
skip = True
# @25.11.11
elif (
path == "scene.display.shading.studio_light"
and value == "Default"
and style[VIEWPORT_SHADING_TYPE] in ("RENDERED", "MATERIAL")
):
value = "forest.exr"
print(
f"Warning: Value 'Default' for property '{path}' and "
f"'{VIEWPORT_SHADING_TYPE}' = '{style[VIEWPORT_SHADING_TYPE]}' is outdated "
"and should be replaced with 'forest.exr' in shading_styles.json."
)
warning = True
# @25.05.12
if path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
elif path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
value = "THEME"
print(
f"Warning: Value 'MATERIAL' is outdated for property '{path}' "
@@ -2749,7 +2807,16 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
return path, value, warning, skip
for path, value in style.items():
paths = list(style.keys())
PRIORITY_PATHS = (
# `scene.display.shading.studio_light` values depend on `.type`
# so we got to set it first.
VIEWPORT_SHADING_TYPE,
)
paths.sort(key=lambda p: p not in PRIORITY_PATHS)
for path in paths:
value = style[path]
path, value, warning, skip = preprocess(path, value)
self.has_warnings_during_activation |= warning
@@ -3414,6 +3481,15 @@ class LoadDrawings(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Operator is not accessible through UI if IFC project is not saved,
# but adding poll check to avoid using Drawings UI in scripts with unsaved project.
if tool.Ifc.get_path():
return True
cls.poll_message_set("IFC project is not saved.")
return False
def _execute(self, context):
core.load_drawings(tool.Drawing)
@@ -544,11 +544,10 @@ class Scheduler:
"""
Adds text to svg.
Args:
p_tags: list of cell's P tags from odt file
box_alignment: alignment of text in box
wrap_text: if True, text will be wrapped to fit in cell
cell_width: width of cell, used for wrapping text
:param p_tags: list of cell's P tags from odt file
:param box_alignment: alignment of text in box
:param wrap_text: if True, text will be wrapped to fit in cell
:param cell_width: width of cell, used for wrapping text
"""
text_lines = [str(p) for p in p_tags]
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
@@ -341,17 +341,29 @@ class SheetBuilder:
assert style_data is not None
text = ""
brackets_level = 0
selector_buffer = "" # Buffer to accumulate selectors across lines
for l in style_data:
if l == "{":
if brackets_level == 0:
cur_line = text.splitlines()[-1]
text = text[: -len(cur_line)]
# Get all accumulated selector text (may span multiple lines)
# Find where the last rule ended (after last }) or start of text
last_close = text.rfind("}")
if last_close == -1:
selector_text = text
text = ""
else:
selector_text = text[last_close + 1 :]
text = text[: last_close + 1]
# Process all selectors (split by comma)
css_selectors = []
# making sure cases like "text, tspan" will be
# converted to "text.prefix, tspan.prefix"
for css_selector in cur_line.split(","):
css_selector = f"{css_selector.strip()}.{prefix}"
css_selectors.append(css_selector)
for css_selector in selector_text.split(","):
css_selector = css_selector.strip()
if css_selector: # Only process non-empty selectors
css_selector = f"{css_selector}.{prefix}"
css_selectors.append(css_selector)
text += ", ".join(css_selectors) + " "
brackets_level += 1
elif l == "}":
@@ -202,6 +202,15 @@ class SvgWriter:
) -> Self:
self.precision = precision
self.decimal_places = decimal_places
# Sort annotations to ensure FILLAREA types are drawn first (at the bottom)
def sort_key(element):
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
# Return 0 for FILLAREA to draw them first, 1 for everything else
return 0 if predefined_type == "FILL_AREA" else 1
annotations = sorted(annotations, key=sort_key)
for element in annotations:
obj = tool.Ifc.get_object(element)
if (
+1 -1
View File
@@ -116,7 +116,7 @@ class BIM_PT_camera(Panel):
# See #6686.
if (
props.has_underlay
and str(render.engine) == "BLENDER_EEVEE_NEXT"
and str(render.engine) == tool.Blender.get_eevee_name()
and ((megapixels := (render.resolution_x * render.resolution_y / 10**6)) > MEGAPIXELS_WARNING_THRESHOLD)
):
box = self.layout.box()
@@ -30,6 +30,7 @@ classes = (
operator.AddSweptAreaSolidItem,
operator.AssignRepresentationLayer,
operator.CopyRepresentation,
operator.DirectProfileEdit,
operator.DisableEditingRepresentationItemShapeAspect,
operator.DisableEditingRepresentationItemStyle,
operator.DisableEditingRepresentationItems,
@@ -160,6 +161,8 @@ def register():
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.direct_profile_edit", "SPACE", "PRESS")
addon_keymaps.append((km, kmi))
# Deletion.
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
addon_keymaps.append((km, kmi))
@@ -178,6 +181,8 @@ def register():
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
kmi.properties.should_save = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.direct_profile_edit", "SPACE", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("wm.call_menu", "P", "PRESS")
kmi.properties.name = ui.BIM_MT_hotkey_separate.bl_idname
addon_keymaps.append((km, kmi))
+438 -73
View File
@@ -42,6 +42,7 @@ import bonsai.core.geometry
import bonsai.core.geometry as core
import bonsai.core.aggregate
import bonsai.core.nest
import bonsai.core.spatial
import bonsai.core.style
import bonsai.core.root
import bonsai.core.drawing
@@ -809,9 +810,17 @@ class OverrideDelete(bpy.types.Operator):
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
# Match `object.delete` poll for consistency.
# `object.delete` poll just checks for OBJECT mode.
poll = bpy.ops.object.delete.poll()
if poll:
return True
cls.poll_message_set("Only available in OBJECT mode")
return False
def execute(self, context):
if not context.selected_objects:
return {"FINISHED"}
# Deep magick from the dawn of time
if tool.Ifc.get() is None:
bpy.ops.object.delete(use_global=self.use_global, confirm=self.confirm)
@@ -823,6 +832,8 @@ class OverrideDelete(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def invoke(self, context, event):
if not context.selected_objects:
return {"FINISHED"}
assert context.window_manager
ifc_file = tool.Ifc.get()
if ifc_file is None:
@@ -964,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()}"
@@ -1030,9 +1045,18 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context) -> bool:
return len(getattr(context, "selected_ids", [])) > 0
# Match `outliner.delete` poll for consistency.
# `outliner.delete` just checks `area.type` == `OUTLINER`.
poll = bpy.ops.outliner.delete.poll()
if poll:
return True
cls.poll_message_set("Only available from Outliner.")
return False
def execute(self, context):
if len(getattr(context, "selected_ids", [])) == 0:
return {"FINISHED"}
# In this override, we don't check self.hierarchy. This effectively
# makes Delete and Delete Hierarchy identical. This is on purpose, since
# non-hierarchical deletion may imply a whole bunch of potentially
@@ -1051,6 +1075,9 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
def invoke(self, context, event):
if len(getattr(context, "selected_ids", [])) == 0:
return {"FINISHED"}
assert context.window_manager
ifc_file = tool.Ifc.get()
if ifc_file:
@@ -1121,10 +1148,18 @@ class OverrideDuplicateMove(bpy.types.Operator):
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def poll(cls, context) -> bool:
# Match `object.duplicate_move` poll for consistency.
# `object.duplicate_move` poll checks for OBJECT mode.
poll = bpy.ops.object.duplicate_move.poll()
if poll:
return True
cls.poll_message_set("Only available in OBJECT mode")
return False
def execute(self, context):
if not context.selected_objects:
return {"FINISHED"}
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
def _execute(self, context):
@@ -1147,7 +1182,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
@staticmethod
def execute_ifc_duplicate_operator(operator: bpy.types.Operator, context: bpy.types.Context, linked: bool = False):
objects_to_remove = set()
objects_to_remove: set[bpy.types.Object] = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -1205,6 +1240,9 @@ class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
bl_options = {"REGISTER", "UNDO"}
OldToNewType = dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]
class DuplicateMoveLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.object_duplicate_move_linked_aggregate"
bl_label = "IFC Duplicate and Move Linked Aggregate"
@@ -1227,9 +1265,9 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
self.new_active_obj = None
self.group_name = "BBIM_Linked_Aggregate"
self.pset_name = "BBIM_Linked_Aggregate"
old_to_new = {}
all_old_to_new = {} # Track all duplicates created
def select_objects_and_add_data(element):
def select_objects_and_add_data(element: ifcopenshell.entity_instance) -> None:
add_linked_aggregate_group(element)
obj = tool.Ifc.get_object(element)
obj.select_set(True)
@@ -1243,8 +1281,6 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
select_objects_and_add_data(part)
else:
index = add_linked_aggregate_pset(part, index)
# index += 1
obj = tool.Ifc.get_object(part)
obj.select_set(True)
@@ -1257,20 +1293,15 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if not pset:
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name=self.pset_name)
ifcopenshell.api.pset.edit_pset(
ifc_file,
pset=pset,
properties=properties,
)
index += 1
else:
pass
return index
def add_linked_aggregate_group(element):
def add_linked_aggregate_group(element: ifcopenshell.entity_instance) -> None:
linked_aggregate_group = None
product_groups_name = [
r.RelatingGroup.Name
@@ -1283,25 +1314,21 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
linked_aggregate_group = ifcopenshell.api.group.add_group(ifc_file, name=self.group_name)
ifcopenshell.api.group.assign_group(ifc_file, products=[element], group=linked_aggregate_group)
def custom_incremental_naming_for_element_assembly(old_to_new):
def custom_incremental_naming_for_element_assembly(old_to_new: OldToNewType) -> None:
for new in old_to_new.values():
if new[0].is_a("IfcElementAssembly"):
group_elements: list[ifcopenshell.entity_instance] = next(
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
)
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
new_obj = tool.Ifc.get_object(new[0])
new_obj.name = pset["Name"] + "_" + str(pset["Aggregate_Index"])
new_obj.name = f"{pset['Name']}_{pset['Aggregate_Index']:02d}"
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
psets = [
ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate")
for p in parts
if ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate")
]
if psets:
index = max([i["Index"] for i in psets if i])
return index
else:
return 0
@@ -1315,23 +1342,28 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new)
group_elements: list[ifcopenshell.entity_instance] = next(
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
)
if linked_aggregate_group:
ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new)
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
if pset:
new_pset = ifcopenshell.api.pset.add_pset(ifc_file, product=new[0], name=self.pset_name)
if pset["Index"] == 0:
group_elements = []
if new[0].is_a("IfcElementAssembly"):
group_elements = next(
(
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
and "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
),
[],
)
properties = {
"Index": pset["Index"],
"Name": pset["Name"],
"Aggregate_Index": len(group_elements) - 1,
"Aggregate_Index": len(group_elements) - 1 if group_elements else 0,
}
else:
properties = {"Index": pset["Index"]}
@@ -1351,43 +1383,73 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
location_diff = new_obj.location - base_obj_location
new_obj.location = context.scene.cursor.location + location_diff
if len(context.selected_objects) != 1:
# Process multiple selected aggregates
selected_aggregates = []
for obj in context.selected_objects:
selected_element = tool.Ifc.get_entity(obj)
if not selected_element:
continue
# Find the aggregate element
if selected_element.is_a("IfcElementAssembly"):
selected_aggregates.append(selected_element)
elif selected_element.Decomposes:
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
selected_aggregates.append(selected_element.Decomposes[0].RelatingObject)
# Remove duplicates
selected_aggregates = list(set(selected_aggregates))
if not selected_aggregates:
self.report({"INFO"}, "No Linked Aggregates selected.")
return {"FINISHED"}
selected_obj = context.selected_objects[0]
selected_element = tool.Ifc.get_entity(selected_obj)
assert selected_element
if selected_element.is_a("IfcElementAssembly"):
pass
elif selected_element.Decomposes:
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
selected_element = selected_element.Decomposes[0].RelatingObject
selected_obj = tool.Ifc.get_object(selected_element)
else:
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
return {"FINISHED"}
select_objects_and_add_data(selected_element)
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
tool.Root.recreate_aggregate(old_to_new)
copy_linked_aggregate_data(old_to_new)
custom_incremental_naming_for_element_assembly(old_to_new)
if location_from_3d_cursor:
get_location_from_3d_cursor(old_to_new, selected_element)
# Deselect all first
bpy.ops.object.select_all(action="DESELECT")
new_aggregate = old_to_new[selected_element][0]
tool.Blender.set_active_object(tool.Ifc.get_object(new_aggregate))
# Process each selected aggregate
for aggregate in selected_aggregates:
aggregate_obj = tool.Ifc.get_object(aggregate)
# Select and prepare the aggregate for duplication
select_objects_and_add_data(aggregate)
# Duplicate the aggregate
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
all_old_to_new.update(old_to_new) # Collect all duplicates
# Recreate aggregate structure
tool.Root.recreate_aggregate(old_to_new)
# Copy linked aggregate data
copy_linked_aggregate_data(old_to_new)
# Apply custom naming
custom_incremental_naming_for_element_assembly(old_to_new)
# Apply 3D cursor location if requested
if location_from_3d_cursor:
get_location_from_3d_cursor(old_to_new, aggregate)
# Deselect for next iteration
bpy.ops.object.select_all(action="DESELECT")
# Select all newly created aggregates
for aggregate in selected_aggregates:
if aggregate in all_old_to_new:
new_aggregate = all_old_to_new[aggregate][0]
new_aggregate_obj = tool.Ifc.get_object(new_aggregate)
if new_aggregate_obj:
new_aggregate_obj.select_set(True)
# Set active object to the first new aggregate
if all_old_to_new:
first_new_aggregate = next(iter(all_old_to_new.values()))[0]
context.view_layer.objects.active = tool.Ifc.get_object(first_new_aggregate)
bonsai.bim.handler.refresh_ui_data()
return old_to_new
return all_old_to_new
class DuplicateLinkedAggregateTo3dCursor(bpy.types.Operator):
@@ -1423,7 +1485,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
self.pset_name = "BBIM_Linked_Aggregate"
refresh_start_time = time()
old_to_new = {}
original_data: dict[int, dict[int, str]] = {}
original_data: dict[int, dict[int, dict[str, Any]]] = {}
def delete_objects(element: ifcopenshell.entity_instance) -> None:
"""Remove IfcElementAssembly and it's parts."""
@@ -1469,8 +1531,8 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
)
tool.Blender.update_viewport()
def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, str]]:
group = next(
def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, dict[str, Any]]]:
group: int = next(
r.RelatingGroup
for r in getattr(element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
@@ -1478,8 +1540,8 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
).id()
original_data[group] = {}
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset["Index"]
pset: dict[str, Any] = ifcopenshell.util.element.get_pset(element, self.pset_name)
index: int = pset["Index"]
annotations = get_assignments(element)
container = ifcopenshell.util.element.get_container(element)
original_data[group][index] = {
@@ -1493,6 +1555,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
if parts:
for part in parts:
if part.is_a("IfcElementAssembly"):
# TODO: unused expression.
original_data | get_original_data(part)
else:
try:
@@ -2346,6 +2409,14 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Geometry.get_geometry_props()
item = tool.Geometry.get_active_representation(obj)
assert item
# Fix vertex order for annotation items
rep_obj = props.representation_obj
if rep_obj:
element = tool.Ifc.get_entity(rep_obj)
if element and self._is_annotation_object(element):
tool.Geometry.ensure_annotation_vertex_order(obj)
if tool.Geometry.is_meshlike_item(item):
if tool.Geometry.is_geometric_data(obj.data) and (
item.is_a("IfcVertex") or item.is_a("IfcEdge") or obj.data.polygons
@@ -2485,6 +2556,289 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
props.mode = "EDIT"
props.is_changing_mode = False
def _is_annotation_object(self, element: ifcopenshell.entity_instance) -> bool:
"""Check if element is an annotation object that needs vertex order preservation."""
if not element.is_a("IfcAnnotation"):
return False
# Object types that have semantic vertex order (arrow direction, text position)
annotation_types_with_order = {
"TEXT_LEADER",
"DIMENSION",
"RADIUS",
"DIAMETER",
"ANGLE",
"FALL",
"SLOPE_ANGLE",
"SLOPE_FRACTION",
"SLOPE_PERCENT",
"STAIR_ARROW",
"PLAN_LEVEL",
"SECTION_LEVEL",
"SECTION",
"ELEVATION",
}
return element.ObjectType in annotation_types_with_order
class DirectProfileEdit(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.direct_profile_edit"
bl_label = "IFC Direct Profile Edit"
bl_description = "Directly enter profile/axis edit mode, or exit back to object mode"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Only allow in OBJECT or EDIT modes
if context.mode not in ("OBJECT", "EDIT_MESH", "EDIT_CURVE"):
return False
# Must have an active object
if not context.active_object:
return False
# In edit mode, we're good to go (we'll exit)
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
return True
# In object mode, check if it's an editable object type
obj = context.active_object
# Only mesh and curve objects are editable
if obj.type not in ("MESH", "CURVE"):
return False
return True
def _execute(self, context):
# Ensure we're only working with the active object
# Deselect all other objects if multiple are selected
active_obj = context.active_object
if active_obj and context.mode == "OBJECT":
if len(context.selected_objects) > 1:
self.report(
{"WARNING"},
"Currently Direct Profile Edit only works on one object at a time. Operating on active object only.",
)
bpy.ops.object.select_all(action="DESELECT")
active_obj.select_set(True)
context.view_layer.objects.active = active_obj
# If we're already in edit mode, exit to object mode and SAVE changes
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
return self.handle_exit_edit_mode(context)
# We're in object mode, try to enter profile/axis edit
return self.handle_enter_edit_mode(context)
def handle_exit_edit_mode(self, context):
"""Handle exiting from edit mode and saving changes."""
obj = context.active_object
if not obj:
return {"CANCELLED"}
# Check if we're editing a representation item
if tool.Geometry.is_representation_item(obj):
return self.exit_item_edit_mode(context, obj)
# Check if we're editing an element-level profile or axis
element = tool.Ifc.get_entity(obj)
if element and tool.Geometry.has_mesh_properties(obj.data):
return self.exit_element_edit_mode(context, obj, element)
# For other edit modes, use standard operator
return bpy.ops.bim.override_mode_set_object()
def exit_item_edit_mode(self, context, obj):
"""Exit from SweptAreaSolid item editing."""
try:
item = tool.Geometry.get_active_representation(obj)
if not item or not item.is_a("IfcSweptAreaSolid"):
return bpy.ops.bim.override_mode_set_object()
# Use the standard save workflow for items
from bonsai.bim.module.model.decorator import ProfileDecorator
ProfileDecorator.uninstall()
tool.Blender.toggle_edit_mode(context)
profile = tool.Model.export_profile(obj)
if not profile:
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
tool.Blender.toggle_edit_mode(context)
ProfileDecorator.install(context)
return {"CANCELLED"}
old_profile = item.SweptArea
profile.ProfileName = old_profile.ProfileName
ifc_file = tool.Ifc.get()
for inverse in ifc_file.get_inverse(old_profile):
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
tool.Profile.replace_profile_in_profiles_ui(old_profile.id(), profile.id())
ifcopenshell.util.element.remove_deep2(ifc_file, old_profile)
props = tool.Geometry.get_geometry_props()
if props.representation_obj:
tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.disable_item_mode()
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, f"Failed to save item changes: {str(e)}")
return {"CANCELLED"}
def exit_element_edit_mode(self, context, obj, element):
"""Exit from element-level profile or axis editing."""
try:
mesh_props = tool.Geometry.get_mesh_props(obj.data)
# Check if we're editing axis
if mesh_props.subshape_type == "AXIS":
return bpy.ops.bim.edit_extrusion_axis()
# Otherwise, we're editing a profile
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion:
return bpy.ops.bim.edit_extrusion_profile()
except Exception as e:
self.report({"ERROR"}, f"Failed to save profile/axis changes: {str(e)}")
return {"CANCELLED"}
# Fallback to standard operator
return bpy.ops.bim.override_mode_set_object()
def handle_enter_edit_mode(self, context):
"""Handle entering edit mode from object mode."""
obj = context.active_object
if not obj:
return {"CANCELLED"}
# Check if object has mesh or curve data
if not hasattr(obj, "data") or not obj.data:
self.report({"INFO"}, "Object has no editable data")
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element:
self.report({"INFO"}, "Active object is not an IFC element")
return {"CANCELLED"}
# Check if this is a LAYER2 element (wall, railing, etc.)
try:
material_usage = tool.Model.get_usage_type(element)
except:
material_usage = None
if material_usage == "LAYER2":
self.report({"ERROR"}, "LAYER2 elements (walls, railings, etc.) cannot use profile editing.")
return {"CANCELLED"}
# Try to get representation
try:
representation = tool.Geometry.get_active_representation(obj)
except:
representation = None
if not representation:
self.report({"INFO"}, "Object has no active representation")
return {"CANCELLED"}
# Check for PROFILE usage (beams, columns, members) - should edit axis
if material_usage == "PROFILE":
try:
return bpy.ops.bim.enable_editing_extrusion_axis()
except Exception as e:
self.report({"ERROR"}, f"Failed to enable axis editing: {str(e)}")
return {"CANCELLED"}
# Check if this is an element with an extrusion profile (LAYER3, etc)
try:
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion and not tool.Geometry.is_representation_item(obj):
return bpy.ops.bim.enable_editing_extrusion_profile()
except Exception as e:
self.report({"ERROR"}, f"Failed to enable profile editing: {str(e)}")
return {"CANCELLED"}
# Otherwise, try to enter item mode and edit SweptAreaSolid
return self.enter_item_edit_mode(context, obj)
def enter_item_edit_mode(self, context, obj):
"""Enter item mode for SweptAreaSolid editing."""
try:
props = tool.Geometry.get_geometry_props()
if not props.representation_obj:
bpy.ops.bim.import_representation_items()
props = tool.Geometry.get_geometry_props()
# Find the SweptAreaSolid item
swept_solid_obj = None
for item_obj_data in props.item_objs:
item_obj = item_obj_data.obj
if not item_obj:
continue
try:
item = tool.Geometry.get_active_representation(item_obj)
if item and item.is_a("IfcSweptAreaSolid"):
swept_solid_obj = item_obj
break
except:
continue
if not swept_solid_obj:
self.report({"INFO"}, "No editable SweptAreaSolid item found")
tool.Geometry.disable_item_mode()
return {"CANCELLED"}
# Select and activate the swept solid object
bpy.ops.object.select_all(action="DESELECT")
swept_solid_obj.select_set(True)
context.view_layer.objects.active = swept_solid_obj
# Import the profile to prepare for editing
item = tool.Geometry.get_active_representation(swept_solid_obj)
if item and item.is_a("IfcSweptAreaSolid"):
profile = item.SweptArea
res = tool.Model.import_profile(profile, obj=swept_solid_obj)
if res is None:
self.report({"ERROR"}, "Couldn't import profile for editing")
tool.Geometry.disable_item_mode()
return {"CANCELLED"}
tool.Ifc.link(item, swept_solid_obj.data)
tool.Blender.toggle_edit_mode(context)
from bonsai.bim.module.model.decorator import ProfileDecorator
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, f"Failed to enter item edit mode: {str(e)}")
try:
tool.Geometry.disable_item_mode()
except:
pass
return {"CANCELLED"}
return {"CANCELLED"}
class FlipObject(bpy.types.Operator):
bl_idname = "bim.flip_object"
@@ -3053,6 +3407,17 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
tool.Root.reload_item_decorator()
element = tool.Ifc.get_entity(obj)
if element and element.is_a("IfcAnnotation") and element.ObjectType in {
"TEXT_LEADER", "DIMENSION", "RADIUS", "DIAMETER", "ANGLE",
"FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT",
"STAIR_ARROW", "PLAN_LEVEL", "SECTION_LEVEL", "SECTION", "ELEVATION"
}:
for item_obj_data in props.item_objs:
item_obj = item_obj_data.obj
if item_obj and isinstance(item_obj.data, bpy.types.Mesh) and item_obj.data.vertices:
item_obj.data["bonsai_first_vert_co"] = item_obj.data.vertices[0].co[:]
class UpdateItemAttributes(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_item_attributes"
+5 -4
View File
@@ -96,11 +96,12 @@ def update_shadow_mode(self: "BIMSolarProperties", context: bpy.types.Context) -
sun_props = tool.Blender.get_sun_props()
assert sun_props
if sun_props.sun_object is None:
bpy.ops.object.light_add(type="SUN", radius=1, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1))
bpy.ops.object.move_to_collection(collection_index=0)
sun_props.sun_object = bpy.context.active_object
light = bpy.data.lights.new(name="Sun", type="SUN")
sun = bpy.data.objects.new("Sun", light)
context.scene.collection.objects.link(sun)
sun_props.sun_object = sun
update_sun_path(self)
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
context.scene.render.engine = tool.Blender.get_eevee_name()
assert context.scene.display
assert context.scene.display.shading
context.scene.display.shading.light = "FLAT"
+2 -18
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import calendar
import bpy
import bonsai.tool as tool
from typing import TYPE_CHECKING
@@ -192,24 +193,7 @@ class BIM_PT_solar(bpy.types.Panel):
row = self.layout.row()
row.prop(props, "year")
row = self.layout.row(align=True)
row.prop(
props,
"month",
text={
1: "January",
2: "February",
3: "March",
4: "April",
5: "May",
6: "June",
7: "July",
8: "August",
9: "September",
10: "October",
11: "November",
12: "December",
}[props.month],
)
row.prop(props, "month", text=calendar.month_name[props.month])
row.prop(props, "day")
row = self.layout.row(align=True)
@@ -161,6 +161,10 @@ classes = (
stair.FinishEditingStair,
stair.EnableEditingStair,
stair.RemoveStair,
stair.ToggleStairTotalLengthLock,
stair.AdjustStairTreads,
stair.CycleStairType,
stair.GizmoStairEdition,
sverchok_modifier.CreateNewSverchokGraph,
sverchok_modifier.UpdateDataFromSverchok,
sverchok_modifier.DeleteSverchokGraph,
@@ -172,12 +176,17 @@ classes = (
window.FinishEditingWindow,
window.EnableEditingWindow,
window.RemoveWindow,
window.CycleWindowType,
window.GizmoWindowEdition,
door.BIM_OT_add_door,
door.AddDoor,
door.CancelEditingDoor,
door.FinishEditingDoor,
door.EnableEditingDoor,
door.RemoveDoor,
door.ToggleDoorSwing,
door.CycleDoorType,
door.GizmoDoorEdition,
railing.BIM_OT_add_railing,
railing.CopyRailingParameters,
railing.AddRailing,
+309 -9
View File
@@ -33,13 +33,15 @@ import bonsai.core.geometry
import bonsai.core.geometry as core
import bonsai.core.root
from bonsai.bim.module.model.window import create_bm_window, create_bm_box
from bonsai.bim import gizmo
from bonsai.bim.gizmo import GizmoPropConfig
from mathutils import Vector, Matrix
from typing import Union, Any, Optional
import json
import collections
import collections.abc
from typing import get_args
V_ = tool.Blender.V_
@@ -192,8 +194,8 @@ def bm_mirror(
def create_bm_extruded_profile(
bm: bmesh.types.BMesh,
points: list[Vector],
edges: Optional[list[tuple[int, int]]] = None,
faces: Optional[list[list[int]]] = None,
edges: list[tuple[int, int]] | None = None,
faces: list[list[int]] | None = None,
position: Vector = V_(0, 0, 0).freeze(),
magnitude: float = 1.0,
extrusion_vector: Vector = V_(0, 0, 1).freeze(),
@@ -278,7 +280,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 +293,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 +412,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(
@@ -467,6 +474,7 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
lining_offset_verts = lining_verts + door_verts + window_lining_verts + frame_verts + glass_verts
bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=lining_offset_verts)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
if bpy.context.active_object.mode == "EDIT":
bmesh.update_edit_mesh(obj.data)
@@ -478,14 +486,14 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_door"
bl_label = "Door"
bl_label = "Add Door"
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 +536,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 +556,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 +567,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 +602,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 +639,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 +683,290 @@ 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 and optionally flip door geometry.
Shift+Click (when flip_geometry=True): Flip geometry only without changing door direction"""
bl_idname = "bim.toggle_door_swing"
bl_label = "Toggle Door Swing"
bl_options = {"REGISTER", "UNDO"}
flip_geometry: bpy.props.BoolProperty(name="Flip Geometry", default=False)
flip_local_axes: bpy.props.EnumProperty(
name="Flip Local Axes", items=(("XY", "XY", ""), ("YZ", "YZ", ""), ("XZ", "XZ", "")), default="XY"
)
skip_direction_change: bpy.props.BoolProperty(
name="Skip Direction Change", default=False, options={"HIDDEN", "SKIP_SAVE"}
)
def invoke(self, context, event):
self.skip_direction_change = event.shift
return self.execute(context)
def _toggle_swing_direction(self, obj: bpy.types.Object) -> bool:
"""Toggle door swing direction between LEFT and RIGHT."""
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")
return True
elif "RIGHT" in current_type:
props.door_type = current_type.replace("RIGHT", "LEFT")
return True
return False
def _execute(self, context):
obj = tool.Blender.get_active_object()
if not obj:
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element:
return {"CANCELLED"}
is_door = tool.Blender.Modifier.is_door(element)
if self.flip_geometry:
tool.Geometry.flip_object(obj, self.flip_local_axes)
if not self.skip_direction_change and is_door:
self._toggle_swing_direction(obj)
elif is_door:
self._toggle_swing_direction(obj)
else:
return {"CANCELLED"}
return {"FINISHED"}
class CycleDoorType(bpy.types.Operator, tool.Ifc.Operator):
"""Cycle through available door types. Shift+click to cycle in reverse."""
bl_idname = "bim.cycle_door_type"
bl_label = "Cycle Door Type"
bl_options = {"REGISTER", "UNDO"}
reverse: bpy.props.BoolProperty(name="Reverse", default=False, options={"HIDDEN", "SKIP_SAVE"})
def invoke(self, context, event):
self.reverse = event.shift
return self.execute(context)
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)
door_types = get_args(tool.Model.DoorType)
current_index = door_types.index(props.door_type) if props.door_type in door_types else 0
direction = -1 if self.reverse else 1
next_index = (current_index + direction) % len(door_types)
props.door_type = door_types[next_index]
return {"FINISHED"}
class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
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"}
enable_editing_operator = "bim.enable_editing_door"
finish_editing_operator = "bim.finish_editing_door"
cancel_editing_operator = "bim.cancel_editing_door"
cycle_type_operator = "bim.cycle_door_type"
gizmo_props = [
GizmoPropConfig("overall_height", (0, 0, 1)),
GizmoPropConfig("overall_width", (1, 0, 0)),
GizmoPropConfig("threshold_thickness", (0, 0, 1)),
GizmoPropConfig("threshold_depth", (0, 1, 0)),
GizmoPropConfig("lining_depth", (0, 1, 0)),
GizmoPropConfig("lining_thickness", (-1, 0, 0)),
GizmoPropConfig("transom_offset", (0, 0, 1)),
GizmoPropConfig("transom_thickness", (0, 0, 1)),
GizmoPropConfig("casing_thickness", (-1, 0, 0)),
GizmoPropConfig("casing_depth", (0, 1, 0)),
]
@classmethod
def is_element_type(cls, element) -> bool:
return tool.Blender.Modifier.is_door(element)
def get_props(self, obj):
return tool.Model.get_door_props(obj)
def get_gizmo_prefs(self):
prefs = tool.Blender.get_addon_preferences()
return prefs.gizmos.door
def should_hide_gizmo(self, attr_name, props):
"""Door-specific visibility rules for gizmos."""
if not props.is_editing:
return True
if attr_name == "threshold_depth" and props.threshold_thickness == 0.0:
return True
if attr_name == "transom_offset" and props.transom_thickness == 0.0:
return True
if attr_name == "casing_thickness" and props.lining_offset != 0.0:
return True
if attr_name == "casing_depth" and (props.lining_offset != 0.0 or props.casing_thickness == 0.0):
return True
return False
def get_gizmo_matrix_overall_height(self, props):
translation = Matrix.Translation(V_(props.overall_width - 0.05, props.lining_offset, 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, props.lining_offset, 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.lining_offset,
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.lining_offset, 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.overall_width - props.lining_thickness, props.lining_depth / 2, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_transom_offset(self, props):
# Position at the bottom of the transom (transom extends upward from offset)
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.transom_offset)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_transom_thickness(self, props):
# Position at the top of the transom (offset + thickness)
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.transom_offset + props.transom_thickness)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
@staticmethod
def _get_casing_gizmo_base_position(props) -> tuple[float, float, float]:
"""Get common base position for casing gizmos."""
x = -props.casing_thickness + props.lining_thickness
y_base = props.lining_depth + props.lining_offset
z = props.overall_height / 2
return x, y_base, z
def get_gizmo_matrix_casing_thickness(self, props):
x, y_base, z = self._get_casing_gizmo_base_position(props)
translation = Matrix.Translation(V_(x, y_base + props.casing_depth / 2, z))
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_casing_depth(self, props):
x, y_base, z = self._get_casing_gizmo_base_position(props)
translation = Matrix.Translation(V_(x, y_base + props.casing_depth, z))
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def setup(self, context):
# Use base class methods for common gizmos
self.setup_property_gizmos(context)
self.setup_editing_gizmos(context)
# Door-specific: swing arc gizmos
prefs = tool.Blender.get_addon_preferences()
highlight_color = prefs.decorator_color_selected[:3]
inactive_color = prefs.decorator_color_background[:3]
special_color = prefs.decorator_color_special[:3]
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"
op = self.gizmo_door_type.target_set_operator("bim.toggle_door_swing")
op.flip_geometry = False
# Flip arc gizmo - mirrors the swing arc along X axis, flips door and toggles direction 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.toggle_door_swing")
op.flip_geometry = True
op.flip_local_axes = "XY"
def refresh(self, context):
obj = context.active_object
if not obj:
return
props = self.get_props(obj)
mw = obj.matrix_world
self.update_property_gizmos(mw, props)
self.update_swing_gizmos(mw, props)
self.update_editing_gizmos(context, mw, props)
def update_swing_gizmos(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, props.lining_offset, 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 the flip arc along Y axis to show the opposite swing direction
mirror_y = Matrix.Scale(-1, 4, (0, 1, 0))
self.gizmo_flip_arc.matrix_basis = mw @ base_swing_transform @ mirror_y
+241 -53
View File
@@ -217,7 +217,12 @@ class BIMModelProperties(PropertyGroup):
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1, update=update_type_page)
x_angle: bpy.props.FloatProperty(
name="X Angle", default=0, subtype="ANGLE", min=math.radians(-180), max=math.radians(180), update=update_x_angle
name="X Angle",
default=0,
subtype="ANGLE",
min=math.radians(-180),
max=math.radians(180),
update=update_x_angle,
)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
@@ -348,24 +353,136 @@ class BIMArrayProperties(PropertyGroup):
def update_total_length_target(self: "BIMStairProperties", context: bpy.types.Context) -> None:
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
"""Update tread_run when total_length_target changes"""
# Calculate available length for default treads
available_length = self.total_length_target
n_default_treads = self.number_of_treads + 1 # number of risers
# Subtract custom first tread if not locked and not zero
if not self.custom_tread_lock and self.custom_first_last_tread_run[0] != 0:
available_length -= self.custom_first_last_tread_run[0]
n_default_treads -= 1
# Subtract custom last tread if not locked and not zero
if not self.custom_tread_lock and self.custom_first_last_tread_run[1] != 0:
available_length -= self.custom_first_last_tread_run[1]
n_default_treads -= 1
# Calculate tread_run for remaining treads
if n_default_treads > 0:
self["tread_run"] = available_length / n_default_treads
else:
# All treads are custom, just use target length
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
def update_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None:
"""Update either number_of_treads or total_length_target when tread_run changes"""
if self.total_length_lock:
self["number_of_treads"] = int((self.total_length_target / self.tread_run) - 1)
# Calculate how much length custom treads take up
custom_length = 0
n_custom_treads = 0
if not self.custom_tread_lock:
if self.custom_first_last_tread_run[0] != 0:
custom_length += self.custom_first_last_tread_run[0]
n_custom_treads += 1
if self.custom_first_last_tread_run[1] != 0:
custom_length += self.custom_first_last_tread_run[1]
n_custom_treads += 1
# Calculate how many default treads fit in remaining space
available_length = self.total_length_target - custom_length
if self.tread_run > 0:
n_default_treads = available_length / self.tread_run
total_treads = n_default_treads + n_custom_treads
# number_of_treads = number_of_risers - 1
self["number_of_treads"] = int(total_treads - 1)
else:
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
# Calculate total length from current settings
n_default_treads = self.number_of_treads + 1
total_length = 0
if not self.custom_tread_lock:
if self.custom_first_last_tread_run[0] != 0:
total_length += self.custom_first_last_tread_run[0]
n_default_treads -= 1
if self.custom_first_last_tread_run[1] != 0:
total_length += self.custom_first_last_tread_run[1]
n_default_treads -= 1
total_length += n_default_treads * self.tread_run
self["total_length_target"] = total_length
def update_number_of_treads(self: "BIMStairProperties", context: bpy.types.Context) -> None:
"""Update either tread_run or total_length_target when number_of_treads changes"""
if self.total_length_lock:
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
# Calculate available length for default treads
available_length = self.total_length_target
n_default_treads = self.number_of_treads + 1
if not self.custom_tread_lock:
if self.custom_first_last_tread_run[0] != 0:
available_length -= self.custom_first_last_tread_run[0]
n_default_treads -= 1
if self.custom_first_last_tread_run[1] != 0:
available_length -= self.custom_first_last_tread_run[1]
n_default_treads -= 1
if n_default_treads > 0:
self["tread_run"] = available_length / n_default_treads
else:
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
else:
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
# Calculate total length from current settings
n_default_treads = self.number_of_treads + 1
total_length = 0
if not self.custom_tread_lock:
if self.custom_first_last_tread_run[0] != 0:
total_length += self.custom_first_last_tread_run[0]
n_default_treads -= 1
if self.custom_first_last_tread_run[1] != 0:
total_length += self.custom_first_last_tread_run[1]
n_default_treads -= 1
total_length += n_default_treads * self.tread_run
self["total_length_target"] = total_length
StairType = Literal["CONCRETE", "WOOD/STEEL", "GENERIC"]
def update_custom_first_last_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None:
"""Update tread_run or total_length when custom treads change"""
if self.total_length_lock:
# Recalculate tread_run to maintain total length
available_length = self.total_length_target
n_default_treads = self.number_of_treads + 1
if not self.custom_tread_lock:
if self.custom_first_last_tread_run[0] != 0:
available_length -= self.custom_first_last_tread_run[0]
n_default_treads -= 1
if self.custom_first_last_tread_run[1] != 0:
available_length -= self.custom_first_last_tread_run[1]
n_default_treads -= 1
if n_default_treads > 0:
self["tread_run"] = available_length / n_default_treads
else:
# Recalculate total length
n_default_treads = self.number_of_treads + 1
total_length = 0
if not self.custom_tread_lock:
if self.custom_first_last_tread_run[0] != 0:
total_length += self.custom_first_last_tread_run[0]
n_default_treads -= 1
if self.custom_first_last_tread_run[1] != 0:
total_length += self.custom_first_last_tread_run[1]
n_default_treads -= 1
total_length += n_default_treads * self.tread_run
self["total_length_target"] = total_length
class BIMStairProperties(PropertyGroup):
@@ -408,7 +525,7 @@ class BIMStairProperties(PropertyGroup):
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True)
stair_type: bpy.props.EnumProperty(
name="Stair Type",
items=[(i, i.replace("/", " / ").title(), "") for i in get_args(StairType)],
items=[(i, i.replace("/", " / ").title(), "") for i in get_args(tool.Model.StairType)],
default="CONCRETE",
update=validate_nosing_value,
)
@@ -425,6 +542,7 @@ class BIMStairProperties(PropertyGroup):
min=0,
unit="LENGTH",
size=2,
update=update_custom_first_last_tread_run, # Added update callback
)
nosing_length: bpy.props.FloatProperty(
name="Nosing Length",
@@ -492,16 +610,18 @@ class BIMStairProperties(PropertyGroup):
}
stair_kwargs.update(generic_props)
non_si_units_props = self.non_si_units_props
# If locked, use tread_run for both first and last treads
if self.custom_tread_lock:
stair_kwargs["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
non_si_units_props += ("custom_first_last_tread_run",)
stair_kwargs["custom_first_last_tread_run"] = (None, None)
else:
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
if not convert_to_project_units:
return stair_kwargs
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, non_si_units_props)
return stair_kwargs
def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None):
@@ -513,18 +633,32 @@ class BIMStairProperties(PropertyGroup):
def set_props_kwargs_from_ifc_data(self, kwargs):
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
tread_run = kwargs.get("tread_run", 0.3)
# Determine lock state based on whether custom treads match tread_run
# If custom_tread_lock wasn't saved (old files), infer it from the data
if "custom_tread_lock" not in kwargs:
custom_treads = kwargs.get("custom_first_last_tread_run", (0.0, 0.0))
tread_run = kwargs.get("tread_run", 0.3)
# Lock is off if either custom tread differs from tread_run and is not 0
kwargs["custom_tread_lock"] = not any(ct != 0.0 and ct != tread_run for ct in custom_treads)
kwargs["custom_tread_lock"] = all(ct not in (0.0, tread_run) for ct in custom_treads)
if "custom_first_last_tread_run" in kwargs:
custom_treads = kwargs["custom_first_last_tread_run"]
custom_treads = [tread_run if v is None else v for v in custom_treads]
kwargs["custom_first_last_tread_run"] = custom_treads
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)
@@ -537,19 +671,6 @@ def window_type_prop_update(self, context):
update_window(self, context)
WindowType = Literal[
"SINGLE_PANEL",
"DOUBLE_PANEL_HORIZONTAL",
"DOUBLE_PANEL_VERTICAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_HORIZONTAL",
"TRIPLE_PANEL_VERTICAL",
]
# default prop values are in mm and converted later
class BIMWindowProperties(PropertyGroup):
non_si_units_props = (
@@ -578,7 +699,7 @@ class BIMWindowProperties(PropertyGroup):
is_editing: bpy.props.BoolProperty(default=False)
window_type: bpy.props.EnumProperty(
name="Window Type",
items=[(i, i, "") for i in get_args(WindowType)],
items=[(i, i, "") for i in get_args(tool.Model.WindowType)],
default="SINGLE_PANEL",
update=window_type_prop_update,
)
@@ -588,9 +709,11 @@ class BIMWindowProperties(PropertyGroup):
overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.6, subtype="DISTANCE", update=update_window)
# lining properties
lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050, subtype="DISTANCE", update=update_window)
lining_depth: bpy.props.FloatProperty(
name="Lining Depth", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
)
lining_thickness: bpy.props.FloatProperty(
name="Lining Thickness", default=0.050, subtype="DISTANCE", update=update_window
name="Lining Thickness", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
)
lining_offset: bpy.props.FloatProperty(
name="Lining Offset", default=0.050, subtype="DISTANCE", update=update_window
@@ -619,12 +742,13 @@ class BIMWindowProperties(PropertyGroup):
update=update_window,
)
transom_thickness: bpy.props.FloatProperty(
name="Transom Thickness", default=0.050, subtype="DISTANCE", update=update_window
name="Transom Thickness", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
)
first_transom_offset: bpy.props.FloatProperty(
name="First Transom Offset",
description="Distance from the first lining to the first transom center",
default=0.3,
min=0.001,
subtype="DISTANCE",
update=update_window,
)
@@ -651,7 +775,7 @@ class BIMWindowProperties(PropertyGroup):
if TYPE_CHECKING:
is_editing: bool
window_type: WindowType
window_type: tool.Model.WindowType
overall_height: float
overall_width: float
lining_depth: float
@@ -685,7 +809,7 @@ class BIMWindowProperties(PropertyGroup):
return tool.Model.convert_data_to_project_units(kwargs, ["window_type"])
def get_lining_kwargs(
self, window_type: Optional[WindowType] = None, convert_to_project_units: bool = False
self, window_type: Optional[tool.Model.WindowType] = None, convert_to_project_units: bool = False
) -> dict[str, Any]:
if not window_type:
window_type = self.window_type
@@ -743,17 +867,13 @@ class BIMWindowProperties(PropertyGroup):
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
DoorType = Literal[
"SINGLE_SWING_LEFT",
"SINGLE_SWING_RIGHT",
"DOUBLE_SWING_LEFT",
"DOUBLE_SWING_RIGHT",
"DOUBLE_DOOR_SINGLE_SWING",
"SLIDING_TO_LEFT",
"SLIDING_TO_RIGHT",
"DOUBLE_DOOR_SLIDING",
]
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)
class BIMDoorProperties(PropertyGroup):
@@ -768,17 +888,39 @@ class BIMDoorProperties(PropertyGroup):
is_editing: bpy.props.BoolProperty(default=False)
door_type: bpy.props.EnumProperty(
name="Door Operation Type",
items=tuple((i, i, "") for i in get_args(DoorType)),
items=tuple((i, i, "") for i in get_args(tool.Model.DoorType)),
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",
@@ -800,6 +942,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,
)
@@ -807,6 +950,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,
)
@@ -815,20 +959,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",
@@ -839,7 +995,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",
@@ -849,10 +1011,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
@@ -862,7 +1032,7 @@ class BIMDoorProperties(PropertyGroup):
if TYPE_CHECKING:
is_editing: bool
door_type: DoorType
door_type: tool.Model.DoorType
overall_height: float
overall_width: float
@@ -960,6 +1130,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"]
@@ -1059,6 +1237,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)
@@ -1161,6 +1344,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:
+393 -2
View File
@@ -19,6 +19,7 @@
import bpy
import json
import bmesh
import math
import ifcopenshell
import ifcopenshell.api.pset
import ifcopenshell.util.element
@@ -26,11 +27,14 @@ import ifcopenshell.util.representation
import ifcopenshell.util.unit
import bonsai.core.root
import bonsai.tool as tool
from mathutils import Vector
from bonsai.bim import gizmo
from bonsai.bim.gizmo import GizmoPropConfig
from mathutils import Vector, Matrix
from bmesh.types import BMVert
from bpy.types import Operator
from bpy.props import FloatProperty, IntProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
from typing import get_args
def regenerate_stair_mesh(obj: bpy.types.Object) -> None:
@@ -131,7 +135,7 @@ def update_ifc_stair_props(obj: bpy.types.Object) -> None:
class BIM_OT_add_stair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_stair"
bl_label = "Stair"
bl_label = "Add Stair"
bl_options = {"REGISTER", "UNDO"}
@classmethod
@@ -188,6 +192,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")
@@ -291,3 +297,388 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
class ToggleStairTotalLengthLock(bpy.types.Operator):
"""Toggle the total length lock for stair editing"""
bl_idname = "bim.toggle_stair_total_length_lock"
bl_label = "Toggle Stair Total Length Lock"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Model.get_stair_props(obj)
props.total_length_lock = not props.total_length_lock
return {"FINISHED"}
class AdjustStairTreads(bpy.types.Operator):
"""Adjust the number of treads"""
bl_idname = "bim.adjust_stair_treads"
bl_label = "Adjust Stair Treads"
bl_options = {"REGISTER", "UNDO"}
increment: IntProperty(name="Increment", default=1)
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Model.get_stair_props(obj)
new_value = props.number_of_treads + self.increment
if new_value >= 1:
props.number_of_treads = new_value
return {"FINISHED"}
class CycleStairType(bpy.types.Operator):
"""Cycle through stair types. Shift+click to cycle in reverse."""
bl_idname = "bim.cycle_stair_type"
bl_label = "Cycle Stair Type"
bl_options = {"REGISTER", "UNDO"}
reverse: bpy.props.BoolProperty(name="Reverse", default=False, options={"HIDDEN", "SKIP_SAVE"})
def invoke(self, context, event):
self.reverse = event.shift
return self.execute(context)
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Model.get_stair_props(obj)
stair_types = get_args(tool.Model.StairType)
current_idx = stair_types.index(props.stair_type) if props.stair_type in stair_types else 0
direction = -1 if self.reverse else 1
props.stair_type = stair_types[(current_idx + direction) % len(stair_types)]
return {"FINISHED"}
class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
bl_idname = "OBJECT_GGT_bim_stair_edition"
bl_label = "Stair Editing Gizmo"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
# Gizmo layout offsets (meters)
GIZMO_X_OFFSET = 0.5 # Horizontal offset from stair end
GIZMO_PLUS_X_OFFSET = 0.25 # Additional X offset for plus button
GIZMO_MINUS_X_OFFSET = 0.5 # Additional X offset for minus button
GIZMO_BUTTON_Z_OFFSET = 0.15 # Vertical offset for +/- buttons above lock
GIZMO_CYCLE_Z_OFFSET = 0.5 # Vertical offset for cycle gizmo above lock
enable_editing_operator = "bim.enable_editing_stair"
finish_editing_operator = "bim.finish_editing_stair"
cancel_editing_operator = "bim.cancel_editing_stair"
gizmo_props = [
GizmoPropConfig("width", (0, 1, 0)),
GizmoPropConfig("height", (0, 0, 1)),
GizmoPropConfig("tread_run", (1, 0, 0)),
GizmoPropConfig("tread_depth", (0, 0, -1)),
GizmoPropConfig("nosing_length", (-1, 0, 0)),
GizmoPropConfig("nosing_depth", (0, 0, -1)),
GizmoPropConfig("total_length_target", (1, 0, 0)),
GizmoPropConfig("base_slab_depth", (0, 0, -1)),
GizmoPropConfig("top_slab_depth", (0, 0, -1)),
]
@classmethod
def is_element_type(cls, element) -> bool:
return tool.Blender.Modifier.is_stair(element)
def get_props(self, obj):
return tool.Model.get_stair_props(obj)
def get_gizmo_prefs(self):
prefs = tool.Blender.get_addon_preferences()
return prefs.gizmos.stair
def should_hide_gizmo(self, attr_name, props):
"""Stair-specific visibility rules for gizmos."""
if not props.is_editing:
return True
# Hide concrete-specific gizmos for non-concrete stairs
if attr_name in ("base_slab_depth", "top_slab_depth") and props.stair_type != "CONCRETE":
return True
# Hide tread_depth for generic stairs (has no tread geometry)
if attr_name == "tread_depth" and props.stair_type == "GENERIC":
return True
# Hide nosing_depth when nosing_length is 0 or for Wood/Steel stair types
if attr_name == "nosing_depth" and (props.nosing_length == 0.0 or props.stair_type == "WOOD/STEEL"):
return True
return False
@staticmethod
def _get_stair_total_run(props) -> float:
"""Calculate the total horizontal run of the stair."""
return props.tread_run * props.number_of_treads
@staticmethod
def _get_first_riser_height(props) -> float:
"""Calculate the height of the first riser."""
return props.height / (props.number_of_treads + 1)
def get_gizmo_matrix_width(self, props):
translation = Matrix.Translation(Vector((0, props.width, 0)))
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_width_top(self, props):
"""Position for the secondary width gizmo at the top of the stairs."""
total_run = self._get_stair_total_run(props)
translation = Matrix.Translation(Vector((total_run, props.width, props.height)))
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_height(self, props):
total_run = self._get_stair_total_run(props)
translation = Matrix.Translation(Vector((total_run, props.width / 2, props.height)))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_tread_run(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(Vector((props.tread_run, props.width / 2, riser_height)))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_tread_depth(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(
Vector((props.tread_run / 2, props.width / 2, riser_height - props.tread_depth))
)
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def get_gizmo_matrix_nosing_length(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(Vector((-props.nosing_length, props.width / 2, riser_height)))
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_nosing_depth(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(
Vector((-props.nosing_length, props.width / 2, riser_height - props.nosing_depth))
)
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def get_gizmo_matrix_total_length_target(self, props):
translation = Matrix.Translation(Vector((props.total_length_target, props.width / 2, props.height)))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_base_slab_depth(self, props):
translation = Matrix.Translation(Vector((0, props.width / 2, -props.base_slab_depth)))
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def get_gizmo_matrix_top_slab_depth(self, props):
total_run = self._get_stair_total_run(props)
translation = Matrix.Translation(Vector((total_run, props.width / 2, props.height - props.top_slab_depth)))
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def setup(self, context):
self.setup_property_gizmos(context)
self.setup_editing_gizmos(context)
# Stair-specific gizmos
prefs = tool.Blender.get_addon_preferences()
highlight_color = prefs.decorator_color_selected[:3]
# Secondary width gizmo at the top of the stairs (linked to same width property)
self.gizmo_width_top = self.gizmos.new("BIM_GT_gizmo_arrow_2d")
def make_width_get():
def move_get():
obj = bpy.context.active_object
if not obj:
return 0.0
props = self.get_props(obj)
return props.width
return move_get
def make_width_set():
def move_set(value):
obj = bpy.context.active_object
if not obj:
return
props = self.get_props(obj)
props.width = max(0.0, value)
return move_set
self.gizmo_width_top.move_get_cb = make_width_get()
self.gizmo_width_top.move_set_cb = make_width_set()
self.gizmo_width_top.axis = Vector((0, 1, 0))
self.gizmo_width_top.local_axis = Vector((0, 1, 0))
self.gizmo_width_top.invert_delta = False
self.gizmo_width_top.delta_scale = 1.0
self.gizmo_width_top.prop_name = "Width"
self.gizmo_width_top.gizmo_group = self
self.gizmo_width_top.color = self.COLOR_GREEN
self.gizmo_width_top.color_highlight = highlight_color
self.gizmo_width_top.alpha = 0.99
self.gizmo_width_top.use_draw_modal = True
self.gizmo_width_top.use_draw_scale = True
# Total length lock gizmo
self.lock_gizmo = self.gizmos.new("VIEW3D_GT_lock")
self.lock_gizmo.use_draw_scale = False
self.lock_gizmo.color = self.COLOR_BLUE
self.lock_gizmo.color_highlight = highlight_color
self.lock_gizmo.alpha = 0.8
self.lock_gizmo.prop_path = "BIMStairProperties.total_length_lock"
self.lock_gizmo.target_set_operator("bim.toggle_stair_total_length_lock")
# Plus gizmo for increasing treads
self.plus_gizmo = self.gizmos.new("VIEW3D_GT_plus")
self.plus_gizmo.use_draw_scale = False
self.plus_gizmo.color = self.COLOR_GREEN
self.plus_gizmo.color_highlight = highlight_color
self.plus_gizmo.alpha = 0.8
op = self.plus_gizmo.target_set_operator("bim.adjust_stair_treads")
op.increment = 1
# Minus gizmo for decreasing treads
self.minus_gizmo = self.gizmos.new("VIEW3D_GT_minus")
self.minus_gizmo.use_draw_scale = False
self.minus_gizmo.color = self.COLOR_RED
self.minus_gizmo.color_highlight = highlight_color
self.minus_gizmo.alpha = 0.8
op = self.minus_gizmo.target_set_operator("bim.adjust_stair_treads")
op.increment = -1
# Cycle gizmo for stair type
default_color = prefs.decorations_colour[:3]
self.cycle_gizmo = self.gizmos.new("VIEW3D_GT_cycle")
self.cycle_gizmo.use_draw_scale = False
self.cycle_gizmo.color = default_color
self.cycle_gizmo.color_highlight = highlight_color
self.cycle_gizmo.alpha = 0.8
self.cycle_gizmo.target_set_operator("bim.cycle_stair_type")
def refresh(self, context):
obj = context.active_object
if not obj:
return
props = self.get_props(obj)
mw = obj.matrix_world
billboard_rot = gizmo.get_billboard_rotation(context)
self.update_property_gizmos(mw, props)
self.update_editing_gizmos(context, mw, props)
self.update_width_top_gizmo(mw, props)
self.update_lock_gizmo(mw, props, billboard_rot)
self.update_tread_count_gizmos(mw, props, billboard_rot)
self.update_cycle_gizmo(mw, props, billboard_rot)
def update_width_top_gizmo(self, mw, props):
"""Update the secondary width gizmo at the top of the stairs."""
gizmo_prefs = self.get_gizmo_prefs()
# Use same visibility as the main width gizmo
self.gizmo_width_top.hide = not props.is_editing or not getattr(gizmo_prefs, "width", True)
if self.gizmo_width_top.hide:
return
self.gizmo_width_top.matrix_basis = mw @ self.get_gizmo_matrix_width_top(props)
self.gizmo_width_top.matrix_offset = Matrix.Scale(self.ARROW_SCALE, 4)
def update_lock_gizmo(self, mw, props, billboard_rot):
gizmo_prefs = self.get_gizmo_prefs()
self.lock_gizmo.hide = not props.is_editing or not gizmo_prefs.lock
if self.lock_gizmo.hide:
return
self.lock_gizmo.color = self.COLOR_RED if props.total_length_lock else self.COLOR_GREEN
total_run = self._get_stair_total_run(props)
lock_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
local_transform = (
Matrix.Translation(Vector((lock_x, props.width / 2, props.height)))
@ billboard_rot
@ Matrix.Scale(0.2, 4)
)
self.lock_gizmo.matrix_basis = mw @ local_transform
def update_tread_count_gizmos(self, mw, props, billboard_rot):
gizmo_prefs = self.get_gizmo_prefs()
self.plus_gizmo.hide = not props.is_editing or not gizmo_prefs.plus
self.minus_gizmo.hide = not props.is_editing or props.number_of_treads <= 1 or not gizmo_prefs.minus
if self.plus_gizmo.hide and self.minus_gizmo.hide:
return
total_run = self._get_stair_total_run(props)
base_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
if not self.plus_gizmo.hide:
plus_local_transform = (
Matrix.Translation(Vector((
base_x + self.GIZMO_PLUS_X_OFFSET,
props.width / 2,
props.height + self.GIZMO_BUTTON_Z_OFFSET
)))
@ billboard_rot
@ Matrix.Scale(0.2, 4)
)
self.plus_gizmo.matrix_basis = mw @ plus_local_transform
if not self.minus_gizmo.hide:
minus_local_transform = (
Matrix.Translation(Vector((
base_x + self.GIZMO_MINUS_X_OFFSET,
props.width / 2,
props.height + self.GIZMO_BUTTON_Z_OFFSET
)))
@ billboard_rot
@ Matrix.Scale(0.2, 4)
)
self.minus_gizmo.matrix_basis = mw @ minus_local_transform
def update_cycle_gizmo(self, mw, props, billboard_rot):
gizmo_prefs = self.get_gizmo_prefs()
self.cycle_gizmo.hide = not props.is_editing or not gizmo_prefs.cycle
if self.cycle_gizmo.hide:
return
total_run = self._get_stair_total_run(props)
cycle_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
local_transform = (
Matrix.Translation(Vector((
cycle_x,
props.width / 2,
props.height + self.GIZMO_CYCLE_Z_OFFSET
)))
@ billboard_rot
@ Matrix.Scale(0.2, 4)
)
self.cycle_gizmo.matrix_basis = mw @ local_transform
def draw_prepare(self, context):
"""Called before drawing - updates gizmos to face camera."""
# Call base class implementation
super().draw_prepare(context)
# Also update the secondary width gizmo to face camera
if hasattr(self, "gizmo_width_top") and not self.gizmo_width_top.hide:
self.gizmo_width_top.draw_prepare(context)
+132 -96
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bl_ui_utils.layout
import bonsai.bim
import bonsai.tool as tool
from bpy.types import Panel, Menu
@@ -36,7 +35,12 @@ from bonsai.bim.module.model.stair import regenerate_stair_mesh
from bonsai.bim.module.model.railing import update_railing_modifier_bmesh
from bonsai.bim.module.model.roof import update_roof_modifier_bmesh
from collections.abc import Iterable
from typing import Any
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING or bpy.app.version >= (5, 0, 0):
import _bl_ui_utils.layout as bl_ui_utils_layout
else:
import bl_ui_utils.layout as bl_ui_utils_layout
class BIM_MT_type_manager_menu(bpy.types.Menu):
@@ -56,7 +60,7 @@ class BIM_MT_type_menu(bpy.types.Menu):
def draw(self, context):
props = tool.Model.get_model_props()
layout = self.layout
with bl_ui_utils.layout.operator_context(layout, "INVOKE_REGION_WIN"):
with bl_ui_utils_layout.operator_context(layout, "INVOKE_REGION_WIN"):
op = layout.operator("bim.rename_type", icon="GREASEPENCIL", text="Rename Type")
op.element = props.menu_relating_type_id
op = layout.operator("bim.select_type", icon="OBJECT_DATA")
@@ -301,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"]
@@ -436,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")
@@ -556,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")
@@ -639,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)
@@ -704,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:
@@ -772,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")
+239 -5
View File
@@ -28,6 +28,8 @@ import ifcopenshell.api.pset
import bonsai.tool as tool
import bonsai.core.root
import bonsai.core.geometry
from bonsai.bim import gizmo
from bonsai.bim.gizmo import GizmoPropConfig
from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS
import ifcopenshell.api
import ifcopenshell.api.material
@@ -36,8 +38,8 @@ import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
from bmesh.types import BMVert
from mathutils import Vector
from typing import Optional, Union
from mathutils import Vector, Matrix
from typing import get_args
V_ = tool.Blender.V_
@@ -139,7 +141,7 @@ def update_window_modifier_representation(context: bpy.types.Context) -> None:
def create_bm_window_frame(
bm: bmesh.types.BMesh, size: Vector, thickness: Union[float, list[float]], position: Vector = V_(0, 0, 0).freeze()
bm: bmesh.types.BMesh, size: Vector, thickness: float | list[float], position: Vector = V_(0, 0, 0).freeze()
) -> list[bmesh.types.BMVert]:
"""`thickness` of the profile is defined as list in the following order:
`(LEFT, TOP, RIGHT, BOTTOM)`
@@ -226,7 +228,7 @@ def create_bm_window(
frame_thickness: float,
glass_thickness: float,
position: Vector,
x_offsets: Optional[list] = None,
x_offsets: list | None = None,
) -> tuple[list[bmesh.types.BMVert], list[bmesh.types.BMVert], list[bmesh.types.BMVert]]:
"""`lining_thickness` and `x_offsets` are expected to be defined as a list,
similarly to `create_bm_window_frame` `thickness` argument"""
@@ -258,6 +260,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 = []
@@ -388,6 +393,7 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None:
bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=bm.verts)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
if bpy.context.active_object.mode == "EDIT":
bmesh.update_edit_mesh(obj.data)
@@ -399,7 +405,7 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None:
class BIM_OT_add_window(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_window"
bl_label = "Window"
bl_label = "Add Window"
bl_options = {"REGISTER", "UNDO"}
@classmethod
@@ -451,6 +457,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)
@@ -570,3 +578,229 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
class CycleWindowType(bpy.types.Operator, tool.Ifc.Operator):
"""Cycle through available window types."""
bl_idname = "bim.cycle_window_type"
bl_label = "Cycle Window Type"
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_window(element):
return {"CANCELLED"}
props = tool.Model.get_window_props(obj)
window_types = list(get_args(tool.Model.WindowType))
current_index = window_types.index(props.window_type) if props.window_type in window_types else 0
next_index = (current_index + 1) % len(window_types)
props.window_type = window_types[next_index]
return {"FINISHED"}
class GizmoWindowEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
bl_idname = "OBJECT_GGT_bim_window_edition"
bl_label = "Window Editing Gizmo"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
enable_editing_operator = "bim.enable_editing_window"
finish_editing_operator = "bim.finish_editing_window"
cancel_editing_operator = "bim.cancel_editing_window"
cycle_type_operator = "bim.cycle_window_type"
gizmo_props = [
GizmoPropConfig("overall_height", (0, 0, 1)),
GizmoPropConfig("overall_width", (1, 0, 0)),
GizmoPropConfig("lining_depth", (0, 1, 0)),
GizmoPropConfig("lining_thickness", (1, 0, 0)),
GizmoPropConfig("lining_to_panel_offset_x", (1, 0, 0)),
GizmoPropConfig("lining_to_panel_offset_y", (0, 1, 0)),
GizmoPropConfig("mullion_thickness", (1, 0, 0), delta_scale=2.0),
GizmoPropConfig("first_mullion_offset", (1, 0, 0)),
GizmoPropConfig("second_mullion_offset", (1, 0, 0)),
GizmoPropConfig("transom_thickness", (0, 0, 1), delta_scale=2.0),
GizmoPropConfig("first_transom_offset", (0, 0, 1)),
GizmoPropConfig("second_transom_offset", (0, 0, 1)),
]
@classmethod
def is_element_type(cls, element) -> bool:
return tool.Blender.Modifier.is_window(element)
def get_props(self, obj):
return tool.Model.get_window_props(obj)
def get_gizmo_prefs(self):
prefs = tool.Blender.get_addon_preferences()
return prefs.gizmos.window
def should_hide_gizmo(self, attr_name, props):
"""Window-specific visibility rules for gizmos."""
if not props.is_editing:
return True
has_mullion = self._has_mullion(props)
has_second_mullion = self._has_second_mullion(props)
has_transom = self._has_transom(props)
has_second_transom = self._has_second_transom(props)
if attr_name == "mullion_thickness" and not has_mullion:
return True
if attr_name == "first_mullion_offset" and not has_mullion:
return True
if attr_name == "second_mullion_offset" and not has_second_mullion:
return True
if attr_name == "transom_thickness" and not has_transom:
return True
if attr_name == "first_transom_offset" and not has_transom:
return True
if attr_name == "second_transom_offset" and not has_second_transom:
return True
return False
def get_gizmo_matrix_overall_height(self, props):
translation = Matrix.Translation(V_(props.overall_width - 0.05, props.lining_offset, 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, props.lining_offset, props.overall_height - 0.05))
rotation = self.get_axis_rotation_matrix((1, 0, 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
@staticmethod
def _get_lining_to_panel_offset_y_full(props) -> float:
"""Get the full Y offset for lining-to-panel positioning."""
return (props.lining_depth - props.frame_depth[0]) + props.lining_to_panel_offset_y
def get_gizmo_matrix_lining_to_panel_offset_x(self, props):
y_full = self._get_lining_to_panel_offset_y_full(props)
translation = Matrix.Translation(
V_(props.lining_to_panel_offset_x, y_full + props.lining_offset, props.lining_thickness)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_lining_to_panel_offset_y(self, props):
y_full = self._get_lining_to_panel_offset_y_full(props)
translation = Matrix.Translation(
V_(
props.lining_to_panel_offset_x,
y_full + props.frame_depth[0] + props.lining_offset,
props.lining_thickness,
)
)
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_mullion_thickness(self, props):
# Position at the right edge of the mullion (offset + thickness/2) so gizmo movement matches property 1:1
translation = Matrix.Translation(
V_(props.first_mullion_offset + props.mullion_thickness / 2, props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_first_mullion_offset(self, props):
# Position at the left edge of the mullion (offset - thickness/2)
translation = Matrix.Translation(
V_(props.first_mullion_offset - props.mullion_thickness / 2, props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_second_mullion_offset(self, props):
translation = Matrix.Translation(V_(props.second_mullion_offset, props.lining_offset, props.overall_height / 2))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_transom_thickness(self, props):
# Position at the top edge of the transom (offset + thickness/2) so gizmo movement matches property 1:1
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.first_transom_offset + props.transom_thickness / 2)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_first_transom_offset(self, props):
# Position at the bottom edge of the transom (offset - thickness/2)
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.first_transom_offset - props.transom_thickness / 2)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_second_transom_offset(self, props):
translation = Matrix.Translation(V_(props.overall_width / 2, props.lining_offset, props.second_transom_offset))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def _has_mullion(self, props):
"""Check if the window type uses mullions (vertical dividers)."""
window_type = props.window_type
return window_type in (
"DOUBLE_PANEL_VERTICAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_VERTICAL",
)
def _has_second_mullion(self, props):
"""Check if the window type uses a second mullion."""
return props.window_type == "TRIPLE_PANEL_VERTICAL"
def _has_transom(self, props):
"""Check if the window type uses transoms (horizontal dividers)."""
window_type = props.window_type
return window_type in (
"DOUBLE_PANEL_HORIZONTAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_HORIZONTAL",
)
def _has_second_transom(self, props):
"""Check if the window type uses a second transom."""
return props.window_type == "TRIPLE_PANEL_HORIZONTAL"
def setup(self, context):
# Use base class methods for common gizmos
self.setup_property_gizmos(context)
self.setup_editing_gizmos(context)
def refresh(self, context):
obj = context.active_object
if not obj:
return
props = self.get_props(obj)
mw = obj.matrix_world
self.update_property_gizmos(mw, props)
self.update_editing_gizmos(context, mw, props)
@@ -1322,6 +1322,20 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "LAYER2":
if len(bpy.context.selected_objects) != 2:
self.report(
{"ERROR"},
"Exactly two LAYER2 items (walls, railings, etc) must be selected to perform a merge.",
)
return
for item in bpy.context.selected_objects:
element = tool.Ifc.get_entity(item)
if tool.Model.get_usage_type(element) != "LAYER2":
self.report(
{"ERROR"},
"Both selected items must be LAYER2 (walls, railings, etc) to perform a merge.",
)
return
bpy.ops.bim.merge_wall()
else:
if len(bpy.context.selected_objects) == 1:
@@ -635,6 +635,7 @@ class BIM_PT_purge(Panel):
bl_parent_id = "BIM_PT_tab_quality_control"
def draw(self, context):
props = tool.Debug.get_debug_props()
layout = self.layout
layout.operator("bim.purge_unused_objects", text="Purge Unused Profiles").object_type = "PROFILE"
layout.operator("bim.purge_unused_objects", text="Purge Unused Types").object_type = "TYPE"
@@ -655,3 +656,23 @@ class BIM_PT_purge(Panel):
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type
merge_op = row.operator("bim.merge_identical_objects", text="Merge Identical")
merge_op.object_type = object_type
layout.operator("bim.purge_unused_representations")
row = layout.row(align=True)
row.prop(props, "ifc_class_purge", text="")
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
if context.active_object and (data := context.active_object.data):
mprops = tool.Geometry.get_mesh_props(data)
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
row = layout.row(align=True)
row.prop(ifc_parameter, "name", text="")
row.prop(ifc_parameter, "value", text="")
row.operator("bim.update_parametric_representation", icon="FILE_REFRESH", text="").index = index
+17 -2
View File
@@ -859,8 +859,23 @@ def get_gross_top_area(obj: bpy.types.Object, angle: float = 45) -> float:
entity = ifc.by_guid(opening_id)
open_obj = tool.Ifc.get_object(entity)
assert isinstance(open_obj, bpy.types.Object)
opening_area += get_net_top_area(open_obj, angle=angle)
# Check if the opening has a Blender object representation
if open_obj is None:
# If no Blender object exists, create a temporary mesh from the IFC geometry
try:
mesh = get_gross_element_mesh(entity)
temp_obj = bpy.data.objects.new("TempOpening", mesh)
opening_area += get_net_top_area(temp_obj, angle=angle)
delete_obj(temp_obj)
delete_mesh(mesh)
except Exception as e:
# If we can't create geometry, skip this opening
print(f"Warning: Could not calculate opening area for {opening_id}: {e}")
continue
else:
# Opening has a Blender representation, use it directly
opening_area += get_net_top_area(open_obj, angle=angle)
else:
continue
+5 -2
View File
@@ -22,6 +22,7 @@ import ifcopenshell.api
import bonsai.tool as tool
import bonsai.core.qto as core
from bonsai.bim.module.qto import helper
from ifcopenshell.util.profiler import Profiler
class CalculateCircleRadius(bpy.types.Operator):
@@ -148,7 +149,8 @@ class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator):
}
ifc_file = tool.Ifc.get()
results = ifc5d.qto.quantify(ifc_file, elements, rules)
with Profiler("Quantify function time:"):
results = ifc5d.qto.quantify(ifc_file, elements, rules)
ifc5d.qto.edit_qtos(ifc_file, results)
not_quantified_elements = elements - set(results.keys())
@@ -190,7 +192,8 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
) -> set[ifcopenshell.entity_instance]:
rules = ifc5d.qto.rules[rule]
ifc_file = tool.Ifc.get()
results = ifc5d.qto.quantify(ifc_file, elements, rules)
with Profiler("Quantify function time:"):
results = ifc5d.qto.quantify(ifc_file, elements, rules)
ifc5d.qto.edit_qtos(ifc_file, results)
not_quantified_elements = elements - set(results.keys())
return not_quantified_elements
@@ -21,6 +21,7 @@ import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.spatial as core
import bonsai.bim.handler
from typing import TYPE_CHECKING
class ReferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
@@ -233,7 +234,7 @@ class RemoveContainer(bpy.types.Operator, tool.Ifc.Operator):
class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator):
"""
Copies selected 3D elements in the viewport to the selected spatial containers
Copies selected 3D elements in the viewport to the container selected in Spatial Manager.
Example: bulk copy a wall to multiple storeys
@@ -242,9 +243,13 @@ class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator):
Copying containers to other containers currently is not supported."""
bl_idname = "bim.copy_to_container"
bl_label = "Copy To Container"
bl_label = "Copy to Container"
bl_options = {"REGISTER", "UNDO"}
container: bpy.props.IntProperty()
container: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
container: int
def _execute(self, context):
objs = tool.Spatial.get_selected_objects_without_containers()
+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)
@@ -455,7 +455,7 @@ class ActivateExternalStyle(bpy.types.Operator):
return {"CANCELLED"}
self.copy_material_attributes(db["data_block"], material)
if material.use_nodes:
if tool.Style.get_use_nodes(material):
tool.Blender.copy_node_graph(material, db["data_block"])
bpy.data.materials.remove(db["data_block"])
return {"FINISHED"}
+4 -3
View File
@@ -99,7 +99,8 @@ class BIMSystemProperties(PropertyGroup):
systems: CollectionProperty(name="Systems", type=System)
expanded_groups_json: StringProperty(name="Expanded Systems JSON", default="[]")
"""See `expanded_groups_json`. We name it "groups" for compatibility with Group UI code."""
active_system_index: IntProperty(name="Active System Index")
# Named not as `active_system_index` to match `BIMGroupProperties`.
active_group_index: IntProperty(name="Active System Index")
active_system_id: IntProperty(name="Active System Id")
edited_system_id: IntProperty(name="Edited System Id")
system_class: EnumProperty(items=get_system_class, name="Class")
@@ -113,7 +114,7 @@ class BIMSystemProperties(PropertyGroup):
is_adding: bool
systems: bpy.types.bpy_prop_collection_idprop[System]
expanded_groups_json: str
active_system_index: int
active_group_index: int
active_system_id: int
edited_system_id: int
system_class: str
@@ -121,7 +122,7 @@ class BIMSystemProperties(PropertyGroup):
@property
def active_system_ui_item(self) -> Union[System, None]:
return tool.Blender.get_active_uilist_element(self.systems, self.active_system_index)
return tool.Blender.get_active_uilist_element(self.systems, self.active_group_index)
def update_active_zone_index(self: "BIMZoneProperties", context: object) -> None:
+1 -1
View File
@@ -114,7 +114,7 @@ class BIM_PT_systems(Panel):
self.props,
"systems",
self.props,
"active_system_index",
"active_group_index",
)
if self.props.edited_system_id:
+41 -1
View File
@@ -322,6 +322,15 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Duplicate Type"
bl_options = {"REGISTER", "UNDO"}
element: bpy.props.IntProperty()
name: bpy.props.StringProperty(name="Name")
description: bpy.props.StringProperty(name="Description")
assign_selected_objects: bpy.props.BoolProperty(default=False)
if TYPE_CHECKING:
element: int
name: str
description: str
assign_selected_objects: bool
def _execute(self, context):
element = tool.Ifc.get().by_id(self.element)
@@ -332,8 +341,25 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
if obj.data:
new_obj.data = obj.data.copy()
new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
new.Name += " Copy"
# Apply the name and description from the dialog
new.Name = self.name
if self.description:
new.Description = self.description
bpy.ops.bim.load_type_thumbnails()
# Assign selected objects to the new type if requested
if self.assign_selected_objects:
selected_objects = tool.Blender.get_selected_objects()
prefs = tool.Blender.get_addon_preferences()
for selected_obj in selected_objects:
selected_element = tool.Ifc.get_entity(selected_obj)
if selected_element and selected_element.is_a("IfcObject"):
core.assign_type(tool.Ifc, tool.Type, element=selected_element, type=new)
if prefs.occurrence_name_style == "TYPE":
selected_obj.name = tool.Model.generate_occurrence_name(new, selected_element.is_a())
if obj in context.selectable_objects:
tool.Blender.select_and_activate_single_object(context, new_obj)
else:
@@ -347,3 +373,17 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
props.ifc_class = new.is_a()
props.relating_type_id = str(tool.Blender.get_ifc_definition_id(new_obj))
return {"FINISHED"}
def invoke(self, context, event):
element = tool.Ifc.get().by_id(self.element)
self.name = (element.Name or "Unnamed") + " Copy"
self.description = element.Description or ""
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
self.layout.prop(self, "name")
self.layout.prop(self, "description")
selected_objects = tool.Blender.get_selected_objects()
ifc_objects = [obj for obj in selected_objects if tool.Ifc.get_entity(obj) and tool.Ifc.get_entity(obj).is_a("IfcObject")]
if ifc_objects:
self.layout.prop(self, "assign_selected_objects", text=f"Assign {len(ifc_objects)} Selected Object(s) to New Type")
+4
View File
@@ -96,6 +96,10 @@ class BIM_PT_type(Panel):
op = row.operator("bim.select_type", icon="OBJECT_DATA", text="")
op.relating_type = 0 # will only select the relating types of only the selected objects
row.operator("bim.select_similar_type", icon="RESTRICT_SELECT_OFF", text="")
# Add duplicate button here with assign_selected_objects enabled
op = row.operator("bim.duplicate_type", icon="DUPLICATE", text="")
op.element = TypeData.data["relating_type"]["id"]
op.assign_selected_objects = True
row.operator("bim.enable_editing_type", icon="GREASEPENCIL", text="")
row.operator("bim.unassign_type", icon="X", text="")
else:
@@ -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,
+2 -2
View File
@@ -786,7 +786,7 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
material = bpy.data.materials.get("Section Override")
if not material:
material = bpy.data.materials.new("Section Override")
material.use_nodes = True
tool.Style.set_use_nodes(material, True)
props = tool.Blender.get_bim_props()
if props.should_section_selected_objects:
@@ -809,7 +809,7 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
def override_materials(self):
override = bpy.data.node_groups.get("Section Override")
for material in bpy.data.materials:
material.use_nodes = True
tool.Style.set_use_nodes(material, True)
if material.node_tree.nodes.get("Section Override"):
continue
# In EEVEE rendering engine, `blend_mode` is deprecated and replaced by `surface_render_method`
+6 -1
View File
@@ -20,7 +20,12 @@ import ifcopenshell
import ifcopenshell.util.pset
import bonsai.tool as tool
import bpy
import bpy_restrict_state
from typing import TYPE_CHECKING
if TYPE_CHECKING or bpy.app.version >= (5, 0, 0):
import _bpy_restrict_state as bpy_restrict_state
else:
import bpy_restrict_state
class IfcSchema:
+276 -10
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,120 @@ 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)
casing_thickness: BoolProperty(name="Casing Thickness", default=True)
casing_depth: BoolProperty(name="Casing Depth", 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
casing_thickness: bool
casing_depth: bool
swing_arc: bool
flip_arc: bool
class GizmoPreferencesWindow(bpy.types.PropertyGroup):
"""Property group for window gizmo visibility settings."""
overall_height: BoolProperty(name="Overall Height", default=True)
overall_width: BoolProperty(name="Overall Width", default=True)
lining_depth: BoolProperty(name="Lining Depth", default=True)
lining_thickness: BoolProperty(name="Lining Thickness", default=True)
lining_to_panel_offset_x: BoolProperty(name="Lining to Panel Offset X", default=True)
lining_to_panel_offset_y: BoolProperty(name="Lining to Panel Offset Y", default=True)
mullion_thickness: BoolProperty(name="Mullion Thickness", default=True)
first_mullion_offset: BoolProperty(name="First Mullion Offset", default=True)
second_mullion_offset: BoolProperty(name="Second Mullion Offset", default=True)
transom_thickness: BoolProperty(name="Transom Thickness", default=True)
first_transom_offset: BoolProperty(name="First Transom Offset", default=True)
second_transom_offset: BoolProperty(name="Second Transom Offset", default=True)
if TYPE_CHECKING:
overall_height: bool
overall_width: bool
lining_depth: bool
lining_thickness: bool
lining_to_panel_offset_x: bool
lining_to_panel_offset_y: bool
mullion_thickness: bool
first_mullion_offset: bool
second_mullion_offset: bool
transom_thickness: bool
first_transom_offset: bool
second_transom_offset: bool
class GizmoPreferencesStair(bpy.types.PropertyGroup):
"""Property group for stair gizmo visibility settings."""
width: BoolProperty(name="Width", default=True)
height: BoolProperty(name="Height", default=True)
tread_run: BoolProperty(name="Tread Run", default=True)
tread_depth: BoolProperty(name="Tread Depth", default=True)
nosing_length: BoolProperty(name="Nosing Length", default=True)
nosing_depth: BoolProperty(name="Nosing Depth", default=True)
total_length_target: BoolProperty(name="Total Length Target", default=True)
base_slab_depth: BoolProperty(name="Base Slab Depth", default=True)
top_slab_depth: BoolProperty(name="Top Slab Depth", default=True)
lock: BoolProperty(name="Total Length Lock", default=True)
plus: BoolProperty(name="Add Tread (+)", default=True)
minus: BoolProperty(name="Remove Tread (-)", default=True)
cycle: BoolProperty(name="Cycle Stair Type", default=True)
if TYPE_CHECKING:
width: bool
height: bool
tread_run: bool
tread_depth: bool
nosing_length: bool
nosing_depth: bool
total_length_target: bool
base_slab_depth: bool
top_slab_depth: bool
lock: bool
plus: bool
minus: bool
cycle: 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)
window: bpy.props.PointerProperty(type=GizmoPreferencesWindow)
stair: bpy.props.PointerProperty(type=GizmoPreferencesStair)
if TYPE_CHECKING:
draw_gizmos_in_3d_viewport: bool
door: GizmoPreferencesDoor
window: GizmoPreferencesWindow
stair: GizmoPreferencesStair
class DocPreferences(bpy.types.PropertyGroup):
sheets_dir: StringProperty(
default=os.path.join("sheets") + os.path.sep,
@@ -320,29 +448,63 @@ 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(
name="SVG to PDF Command",
description='print sheet to pdf together with svg. Leave blank svg is just created, E.g. [["path to application eg. /Applications/Inkscape.app/Contents/MacOS/inkscape", "svg", "-o", "pdf"]]',
description=(
"print sheet to pdf together with svg. Leave blank svg is just created, "
'E.g. [["path to application eg. /Applications/Inkscape.app/Contents/MacOS/inkscape", "svg", "-o", "pdf"]]'
'\n `"svg"`, `"pdf"` will be replaced automatically.'
),
)
svg2dxf_command: StringProperty(
name="SVG to DXF Command",
description='E.g. [["inkscape", "svg", "-o", "eps"], ["pstoedit", "-dt", "-f", "dxf:-polyaslines -mm", "eps", "dxf", "-psarg", "-dNOSAFER"]]',
description=(
'E.g. `[["inkscape", "svg", "-o", "eps"], '
'["pstoedit", "-dt", "-f", "dxf:-polyaslines -mm", "eps", "dxf", "-psarg", "-dNOSAFER"]]`'
'\n `"svg"`, `"eps"`, `"dxf"` will be replaced automatically.'
),
)
svg_command: StringProperty(
name="SVG Command",
description='Software to open generated drawing and sheets. Leave blank system default for .svg is used E.g. [["path to application eg. /Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "path"]]',
description=(
"Software to open generated drawing and sheets. Leave blank system default for .svg is used "
'E.g. [["path to application eg. /Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "path"]]'
'\n `"path"` will be replaced with the path to the generated .svg file.'
),
)
layout_svg_command: StringProperty(
name="Layout SVG Command",
description='Software to open layouts, before generated. Leave blank system default for .svg is used E.g. [["path to application eg. /Applications/Inkscape.app/Contents/MacOS/inkscape", "path"]]',
description=(
"Software to open layouts, before generated. Leave blank system default for .svg is used. "
'E.g. `[["path to application eg. /Applications/Inkscape.app/Contents/MacOS/inkscape", "path"]]`'
'\n `"path"` will be replaced with the path to the generated .svg file.'
),
)
pdf_command: StringProperty(
name="PDF Command",
description='Software to open .pdf, leave blank uses system default. E.g. [["path to application eg. /Applications/Inkscape.app/Contents/MacOS/inkscape", "path"]]',
description=(
"Software to open .pdf, leave blank uses system default. "
'E.g. `[["path to application eg. /Applications/Inkscape.app/Contents/MacOS/inkscape", "path"]]`'
'\n `"path"` will be replaced with the path to the generated .pdf file.'
),
)
spreadsheet_command: StringProperty(
name="Spreadsheet Command",
description=(
'E.g. [["libreoffice", "path"]]'
'\n `"path"` will be replaced with the path to the generated spreadsheet file.'
),
)
spreadsheet_command: StringProperty(name="Spreadsheet Command", description='E.g. [["libreoffice", "path"]]')
should_hide_empty_props: BoolProperty(
name="Hide Empty Properties",
default=True,
@@ -351,12 +513,18 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
should_setup_workspace: BoolProperty(
name="Setup Workspace Layout for BIM",
default=True,
description="If enabled, this will add a default workspace dedicated to working with BIM models.\nIt is recommended to keep this `Enabled`",
description=(
"If enabled, this will add a default workspace dedicated to working with BIM models.\n"
"It is recommended to keep this `Enabled`"
),
)
activate_workspace: BoolProperty(
name="Activate BIM Workspace on Startup",
default=True,
description="If enabled, this will automatically activate the BIM workspace when opening a project.\nIt is recommended to keep this `Enabled`",
description=(
"If enabled, this will automatically activate the BIM workspace when opening a project.\n"
"It is recommended to keep this `Enabled`"
),
)
should_setup_toolbar: BoolProperty(
name="Always Show Toolbar In 3D Viewport",
@@ -366,7 +534,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
should_use_snap: BoolProperty(
name="Enable Snapping on Startup",
default=True,
description="If enabled, snapping will be enabled on new sessions.\nIt is recommended to keep this `Enabled`",
description=(
"If enabled, snapping will be enabled on new sessions.\n" "It is recommended to keep this `Enabled`"
),
)
should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False)
tmp_dir: StringProperty(
@@ -428,7 +598,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
max=100,
subtype="PERCENTAGE",
name="Non-Openings Opacity",
description="When modifying openings, other elements of the model will display with some transparency.\n0 is fully transparent and 100 is fully opaque",
description=(
"When modifying openings, other elements of the model will display with some transparency.\n"
"0 is fully transparent and 100 is fully opaque"
),
)
bsdd_load_preview_dictionaries: BoolProperty(
@@ -456,6 +629,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
@@ -486,6 +660,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
@@ -498,6 +677,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
should_setup_workspace: bool
activate_workspace: bool
should_setup_toolbar: bool
should_use_snap: bool
should_play_chaching_sound: bool
tmp_dir: str
decorations_colour: tuple[float, float, float, float]
@@ -515,10 +695,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
@@ -561,10 +743,61 @@ 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:
layout.label(text="Toggle visibility of gizmos in editing mode")
box = layout.box()
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Door", self.draw_door_gizmo_parameters)
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Window", self.draw_window_gizmo_parameters)
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Stair", self.draw_stair_gizmo_parameters)
def draw_door_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
from bonsai.bim.module.model.door import GizmoDoorEdition
door_gizmos = self.gizmos.door
gizmo_prop_names = {p.attr_name for p in GizmoDoorEdition.gizmo_props}
gizmo_prop_names.update(("swing_arc", "flip_arc"))
for prop in door_gizmos.__annotations__:
if prop in gizmo_prop_names:
layout.prop(door_gizmos, prop)
def draw_window_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
from bonsai.bim.module.model.window import GizmoWindowEdition
window_gizmos = self.gizmos.window
gizmo_prop_names = {p.attr_name for p in GizmoWindowEdition.gizmo_props}
for prop in window_gizmos.__annotations__:
if prop in gizmo_prop_names:
layout.prop(window_gizmos, prop)
def draw_stair_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
from bonsai.bim.module.model.stair import GizmoStairEdition
stair_gizmos = self.gizmos.stair
gizmo_prop_names = {p.attr_name for p in GizmoStairEdition.gizmo_props}
special_gizmo_names = {"lock", "plus", "minus"}
for prop in stair_gizmos.__annotations__:
if prop in gizmo_prop_names or prop in special_gizmo_names:
layout.prop(stair_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")
@@ -601,6 +834,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")
+11 -3
View File
@@ -60,10 +60,18 @@ def edit_assigned_product(
) -> None:
element = ifc.get_entity(obj)
assert element
existing_product = drawing.get_assigned_product(element)
if existing_product != product:
if existing_product:
# Temporarily accomodate existing bug and check for multiple products.
existing_products = drawing.get_assigned_product_workaround(element)
if existing_products != [product]:
if product in existing_products:
assert product is not None
existing_products.remove(product)
for existing_product in existing_products:
ifc.run("drawing.unassign_product", relating_product=existing_product, related_object=element)
if product:
ifc.run("drawing.assign_product", relating_product=product, related_object=element)
+5 -4
View File
@@ -147,7 +147,7 @@ def remove_representation(
assert element
element_type = geometry.get_element_type(element)
data = None
has_switched_from_data = False
data_removed_by_switch_representation = False
if element_type and (geometry.is_mapped_representation(representation) or geometry.is_type_product(element)):
representation = geometry.resolve_mapped_representation(representation)
data = geometry.get_representation_data(representation)
@@ -155,7 +155,7 @@ def remove_representation(
for element in geometry.get_elements_of_type(element_type):
obj = ifc.get_object(element)
if obj:
has_switched_from_data = True
data_removed_by_switch_representation = True
geometry.switch_from_representation(obj, representation)
obj = ifc.get_object(element_type)
if obj:
@@ -164,12 +164,13 @@ def remove_representation(
else:
data = geometry.get_representation_data(representation)
if data and geometry.has_data_users(data):
has_switched_from_data = True
data_removed_by_switch_representation = True
geometry.switch_from_representation(obj, representation)
ifc.run("geometry.unassign_representation", product=element, representation=representation)
ifc.run("geometry.remove_representation", representation=representation)
if data and not has_switched_from_data:
if data and not data_removed_by_switch_representation:
geometry.delete_data(data)
+1 -2
View File
@@ -70,8 +70,7 @@ def assign_class(
ifc_representation_class: Optional[str] = None,
) -> Optional[ifcopenshell.entity_instance]:
"""
Args:
context: is not optional if `should_add_representation` is True
:param context: is not optional if ``should_add_representation`` is True
TODO: Do NOT use should_add_representation. Because it internally calls
geometry.add_representation which is 1,000 lines of Blender -> IFC magic.
+15 -2
View File
@@ -308,6 +308,7 @@ class Document:
class Drawing:
def activate_drawing(cls, camera): pass
def add_literal(cls, **attributes): pass
def clear_annotation_relationships(cls, drawing): pass
def copy_representation(cls, source, dest): pass
def create_annotation_context(cls, target_view, object_type=None): pass
def create_annotation_object(cls, drawing, object_type): pass
@@ -342,6 +343,7 @@ class Drawing:
def get_annotation_context(cls, target_view, object_type=None): pass
def get_annotation_representation(cls, element_type): pass
def get_assigned_product(cls, element): pass
def get_assigned_product_workaround(cls, element): pass
def get_body_context(cls): pass
def get_default_drawing_path(cls, name): pass
def get_default_drawing_resource_path(cls, resource): pass
@@ -485,6 +487,19 @@ class Georeference:
def xyz2enh(cls, coordinates): pass
@interface
class Group:
def get_group_props(cls): pass
def get_groups_data(cls, group_type): pass
def import_groups(cls, group_type): pass
def enable_group_editing_ui(cls) : pass
def disable_group_editing_ui(cls): pass
def disable_editing_group(cls): pass
def set_active_group_to_edit(cls, group): pass
def toggle_group(cls, group, group_type, option): pass
def update_uilist_index(cls, group_type): pass
@interface
class Ifc:
def get(cls): pass
@@ -854,8 +869,6 @@ class Sequence:
def get_animation_product_frames(cls, work_schedule, settings): pass
def get_animation_settings(cls): pass
def get_checked_tasks(cls): pass
def get_direct_nested_tasks(cls, task):pass
def get_direct_task_outputs(cls, task): pass
def get_finish_date(cls): pass
def get_highlighted_task(cls): pass
def get_lag_time_attributes(cls): pass

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