Compare commits

...

174 Commits

Author SHA1 Message Date
Thomas Krijnen 2fe491b7b5 Merge remote-tracking branch 'origin/v0.8.0' into test-ci 2025-10-27 06:54:10 +01:00
Thomas Krijnen 6df5632f91 black 2025-10-26 13:33:10 +01:00
Thomas Krijnen 391a412e65 test cases #7291 2025-10-26 13:31:31 +01:00
Thomas Krijnen b10415b1b3 Rework iterator constructor #7291 2025-10-26 13:31:31 +01:00
Thomas Krijnen b1c44d42e3 Reset logger product to none 2025-10-26 13:31:31 +01:00
Dion Moult 5f65e21878 Fix bug where you couldn't get elements using a predefined property set. 2025-10-26 23:05:05 +11:00
Dion Moult 0d3be9d101 Fix #5108. Selector now supports both GlobalId and Name as filter values. 2025-10-26 22:52:31 +11:00
Dion Moult eea585a3cd Fix #6258. Fix #5727 properly. Only assume layerset axis on "standard case" elements, nothing else. 2025-10-26 22:33:39 +11:00
Thomas Krijnen 83f7416679 Revert "[TMP] start a RelWithDebInfo build"
This reverts commit 9c8f3be52a.
2025-10-26 12:04:49 +01:00
Thomas Krijnen dc5ac872e3 Revert "[TEMP] IFCOS_SCHEMAS=4 probably disk is full with RelWithDebInfo"
This reverts commit e05318162d.
2025-10-26 12:04:46 +01:00
Thomas Krijnen 1789406299 Ruff 2025-10-26 12:04:22 +01:00
Thomas Krijnen e05318162d [TEMP] IFCOS_SCHEMAS=4 probably disk is full with RelWithDebInfo 2025-10-26 11:52:52 +01:00
Dion Moult b503061dc0 Fix #3868. Automatic annotation references are now auto deleted if their reference is also gone. 2025-10-26 20:58:15 +11:00
Dion Moult 90ccaf4aab Let users manually create automatic references too. 2025-10-26 20:46:32 +11:00
Dion Moult 05ff47fcff Remap deprecated entities in Brick 1.4 to new RealEstateCore URIs. 2025-10-26 20:38:58 +11:00
Louis Casteleyn c40ecdd550 deleted deprecated entities for v1.4 (#6511) 2025-10-26 20:32:28 +11:00
daniel-augusto 3a0622e9df Update introduction with hyperlinks for tools (#7290) 2025-10-26 20:29:30 +11:00
Dion Moult 0bb03750dc Fix #4914. Write docs for syntax and schema validation and schema querying 2025-10-26 20:27:07 +11:00
Thomas Krijnen 89defa9b6b build-all.py ruff: noqa: UP035 2025-10-26 10:20:13 +01:00
Thomas Krijnen 459aad54aa Black 2025-10-26 10:17:49 +01:00
Thomas Krijnen 6f3065f6ce ci.yml USE_MMAP=On 2025-10-26 10:15:52 +01:00
Thomas Krijnen 9c8f3be52a [TMP] start a RelWithDebInfo build 2025-10-26 10:11:40 +01:00
Thomas Krijnen 69863ce4a9 Try and fix stub test 2025-10-26 10:03:43 +01:00
Thomas Krijnen c4a992a9b5 Merge remote-tracking branch 'origin/v0.8.0' into test-ci 2025-10-26 09:50:01 +01:00
Dion Moult e5ae19fd34 See #7291. Temporary workaround whilst segfaults are sorted. 2025-10-26 19:36:41 +11:00
Dion Moult afe617fb93 Revert "Bump IOS - necessary as last bunch of commits are incompatible with previous binaries"
This reverts commit d922f0b9ad.
2025-10-26 19:32:35 +11:00
Dion Moult d922f0b9ad Bump IOS - necessary as last bunch of commits are incompatible with previous binaries 2025-10-26 19:12:22 +11:00
Dion Moult f9d2d494b1 Fix #4275. Using psets_only kwarg in get_pset utility now includes predefined psets. 2025-10-26 18:21:06 +11:00
Thomas Krijnen 806aa14bd0 Compatibility for older builds 2025-10-25 21:55:06 +02:00
Thomas Krijnen 80066ba9e7 Different hack for allowing f.header.file_description.file.header.file_description.file 2025-10-25 21:54:42 +02:00
Thomas Krijnen 7cb1f07a0b hacky way to retain attribute too many messages on header entities in validate.py 2025-10-25 21:12:12 +02:00
Thomas Krijnen bdb52dcffd Pass header entity types during pass to resolve empty list type 2025-10-25 20:29:41 +02:00
Thomas Krijnen 933ad6c801 Fix header and schema validation errors in pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add2.ifc 2025-10-25 20:29:19 +02:00
Thomas Krijnen 1594b9505c Don't emit Parsing terminated on benign files by not reading past stream in skipComment() 2025-10-25 20:14:02 +02:00
Thomas Krijnen 325745a822 Test IfcConvert only on single file 2025-10-25 15:54:15 +02:00
Thomas Krijnen 74f8b047fb osx compat 2025-10-25 15:40:08 +02:00
Thomas Krijnen 0f8c6ffe9e Run the bulk of pytest in parallel 2025-10-25 15:38:25 +02:00
Thomas Krijnen fa46fe0cf0 Typos 2025-10-25 15:30:36 +02:00
Thomas Krijnen f0a1efa1f2 ruff --fix 2025-10-25 15:27:46 +02:00
Thomas Krijnen 71c1058e9e .gitignore for .exp files 2025-10-25 15:27:38 +02:00
Thomas Krijnen 7575bd4dfb Commit some files because I do not want them cleaned 2025-10-25 15:25:56 +02:00
Thomas Krijnen d697ed3384 Commit some files because I do not want them cleaned 2025-10-25 15:25:10 +02:00
Thomas Krijnen aed70c8a02 Run black 2025-10-25 15:20:36 +02:00
Thomas Krijnen e82ca0e932 Less back and forths by returning semi count in page 2025-10-25 15:08:21 +02:00
Thomas Krijnen 5910b39766 geom.stats module for preemptive scanning through file 2025-10-25 13:58:32 +02:00
Thomas Krijnen ea9e0c9ab4 readInstancePy() option to obtain type decls instead of strings 2025-10-25 13:55:55 +02:00
Thomas Krijnen 2e951eb952 Whitespace handling now that token strings are built on the fly 2025-10-25 13:55:15 +02:00
Thomas Krijnen 2497611abd Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into v0.8.0 2025-10-25 13:54:52 +02:00
Bruno Postle 90bf56d9ba Fix ifc5d documentation
Documentation claimed that ODS/XLSX output was to a given filename, but
the code actually writes files with generated names in a folder.

(also create the folder if necessary for convenience)
2025-10-24 22:19:01 +01:00
Bruno Postle e1b73ef0ef Fix build broken by a6d20c0 - link iostreams
FileReader.cpp now uses boost::iostreams::mapped_file regardless of
USE_MMAP setting
2025-10-24 15:35:19 +01:00
Thomas Krijnen bcb5fd7810 Missing return type 2025-10-24 13:32:18 +02:00
Thomas Krijnen 40c5145a13 Address some omissions in the FileReader rewrite regarding unicode paths on windows 2025-10-24 13:26:27 +02:00
Thomas Krijnen e304c3ad31 Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into v0.8.0 2025-10-24 13:25:05 +02:00
Thomas Krijnen 0494bd9677 Option to bypass storing types when opening model 2025-10-24 12:06:59 +02:00
Andrej730 6d4f45c586 ifcwrap cmake - fix issue with debug build after migration to FindPython
In FindPython variable was renamed to Python_LIBRARY_DEBUG
2025-10-24 13:53:22 +05:00
Andrej730 0f9030e33b ifcwrap cmake - avoid using deprecated swig_link_libraries 2025-10-24 13:28:33 +05:00
Dion Moult fc0cdfabb9 Fix #7255. Bug where you couldn't edit header data. 2025-10-24 19:11:29 +11:00
Andrej730 ab0b44c8a2 ifcwrap cmake - use usual name for wrapper target 2025-10-24 12:52:37 +05:00
Andrej730 37e9a0f4fe cache_dependencies.py - fix tarfile deprecation warnings in 3.12-3.13 2025-10-24 12:33:44 +05:00
nameloCmaS 2dac137801 Limit clipping plane euler rotation angles to <= 360 degrees (#6877)
The "Flip Clipping Plane" operator adds 180 degrees to the angle's value, meaning the angle will keep increasing with each press.
2025-10-24 14:10:38 +11:00
Dion Moult 8ffa6fcf47 Fix #7267. Bug where duplicating a drawing would cause annotation references to be associated with both the original and duplicate. 2025-10-24 14:09:00 +11:00
Dion Moult 8824e6e2c2 Fix #7268. Automatic annotation references (sections, elevations, etc) are now intelligently regenerated 2025-10-24 12:47:58 +11:00
Andrej730 0787eabdab ifcwrap cmake - move DEBUG_POSTFIX logic to one place 2025-10-23 20:03:12 +05:00
Andrej730 0a34a37d65 cmake - build without Python libraries on Unix 2025-10-23 20:02:03 +05:00
Ryan Schultz 5dbfe539ac Need a little more freedom to rotate a RCP 2025-10-23 08:31:31 -05:00
Andrej730 280976f77c ifcwrap cmake - accomodate bug in cmake makefiles generator 2025-10-23 17:15:17 +05:00
Andrej730 4c53f4359d build-all - use shutil.which instead of custom function
To make it more idiomatic and custom function also was misinterpreting folders with the same name as existing executables.
2025-10-23 17:15:17 +05:00
Andrej730 0a547a14ea build_pyodide - use ccache 2025-10-23 17:15:17 +05:00
Andrej730 206ec8776a build_rocky - switch to main branch ccache 2025-10-23 17:15:16 +05:00
Andrej730 c3b43d573e build_pyodide - use prebuilt pyodide build env 2025-10-23 16:18:10 +05:00
Dion Moult d1453e1139 Fix bug where panels for psets/qtos/classifications/types were showing up on irrelevant objects (e.g. grid axes) 2025-10-23 08:59:15 +11:00
Dion Moult 5b5dcb3c6d Fix #7257. Always quote psets and props in our colour by property dropdown to account for spaces and characters. 2025-10-23 08:58:23 +11:00
Dion Moult 4f467d199e See #7253. See #7268. Rewrite grid generation to use IFC data, not rely on Blender object.
This also deprecates one more instance of the add_representation API
call. As this is slowly cleaned up we can move towards "regenerating"
annotations, not just generating from scratch.
2025-10-23 08:39:25 +11:00
Thomas Krijnen a6d20c0cc4 Follow up after 4d688170e0 2025-10-22 21:42:37 +02:00
Ryan Schultz d5d942bb2e Adds a TargetView CSS class to the drawings. 2025-10-22 10:06:43 -05:00
Thomas Krijnen 9e2c7e653f Commit suggestion in #7276 2025-10-22 14:37:10 +02:00
Thomas Krijnen 200c0bf773 Commit suggestion in #7276 2025-10-22 13:23:34 +02:00
Andrej730 afb6befce6 build-all.py - switch to gmp ftpmirror
previous url is inaccessible
2025-10-22 13:53:17 +05:00
Andrej730 f42efbade2 build_pyodide - display logs in github action output 2025-10-22 13:53:17 +05:00
Andrej730 968aa710ee build_pyodide - upload build logs for debugging 2025-10-22 13:53:17 +05:00
Andrej730 61b03dc60c build_pyodide - pack cached dependencies 2025-10-22 13:53:16 +05:00
Dion Moult 381b0d379a Optimise operators when a drawing is active by not needlessly regenerating the cut after each operation. 2025-10-22 18:03:40 +11:00
Dion Moult f48b5b9d92 Fix #7273. See #7253. Auto annotation references now cannot be deleted, but can be excluded. 2025-10-22 18:03:40 +11:00
Andrej730 e87597b8d7 build_osx - remove setup-python
as it's already preinstalled on the image
2025-10-22 10:37:45 +05:00
Andrej730 b12dddc2f0 build-all.py - print BUILD_DIR 2025-10-22 10:37:44 +05:00
Andrej730 f3076ba038 build_pyodide - no need to adjust version
since it's now inferred automatically
2025-10-22 10:37:44 +05:00
Andrej730 908a001ed8 build-all.py - accomodate pyodide build system automatically
as it's the main way to build the wasm package
2025-10-22 10:37:44 +05:00
Andrej730 1a8c47888a build_pyodide.yaml - move build_pyodide to a separate script 2025-10-22 10:37:44 +05:00
Andrej730 5e46a759a6 Add notes for pyodide building process 2025-10-22 10:37:44 +05:00
Andrej730 0e3f8cba8d build-all.py wasm - move SIDE_MODULE=2 to cmake
Next to `EXPORTED_FUNCTIONS` and avoid possible issues with pyodide in the future (pyodide is using `SIDE_MODULE=1`, but currently not providing it libraries of type `MODULE`, only for shared ones).
2025-10-22 10:37:43 +05:00
Andrej730 0ce12c9297 ifcwrap cmake - provide correct suffix for wasm python extension 2025-10-22 10:37:43 +05:00
Massimo Fabbro 1f47aadc94 Fix #7227. Sync cost item assigned resources also for SOR 2025-10-22 07:14:01 +02:00
falken10vdl bb3cd7e49c Annotation rotation when doing bulk tags (#7049)
* first attempt to do annotation rotation when dong bulk tags

* Enhance annotation rotation handling based on camera orientation

* handle properly S_G

* black .

* reorder property in UI

* cleanup imports
2025-10-22 15:07:52 +11:00
daniel-augusto edd01d44de Include direct links to tools (#7128) 2025-10-22 15:06:56 +11:00
Michael Godsven 52b899d070 Add icons (#7256) 2025-10-22 11:30:54 +11:00
Dion Moult 008292df0f Revert "Always show edit UI even if you don't have a BIM Tool activated."
This reverts commit 87bfd182d7.
2025-10-22 11:08:55 +11:00
Dion Moult 87bfd182d7 Always show edit UI even if you don't have a BIM Tool activated. 2025-10-22 11:07:18 +11:00
Dion Moult 8fcc622b8a Optimisation to only recalculate all drawing cuts if the camera is moved, not just selected. 2025-10-22 10:12:24 +11:00
Thomas Krijnen 9415878f57 Remove warning about combination of RefDirection as Axis #7251 2025-10-21 13:33:32 +02:00
Thomas Krijnen 919ac41a74 Consider case of only RefDirection in sectioned shapes #7251 2025-10-21 13:32:51 +02:00
Thomas Krijnen 80093f3777 Loft all wires not only the outer one #7251 2025-10-21 13:09:10 +02:00
Thomas Krijnen 28f5d6e91c Copy internal status on interpolated loops #7251 2025-10-21 13:08:29 +02:00
Thomas Krijnen 550779deee Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into v0.8.0 2025-10-21 13:06:05 +02:00
Thomas Krijnen e2cf60f0c1 Update to gmp 6.3.0 #7200 2025-10-21 12:18:48 +02:00
Dion Moult 546a042d40 Explicitly disable any voids, openings, and booleans in IFC4 Reference View MVD
It turns out vendors are still exporting them and creating a mess.
2025-10-21 10:49:17 +11:00
Andrej730 ee8433655d pyodide setup.py - add dependencies
so micropip would be able to gather them automatically
2025-10-20 18:36:38 +05:00
Thomas Krijnen 86b89f2153 Comment behaviour #7264 2025-10-20 10:37:35 +02:00
Thomas Krijnen 34f4aa430a Keep file_ as nullptr to not bypass addEntity() #7264 2025-10-20 10:33:09 +02:00
Thomas Krijnen 67678cb7f1 Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into v0.8.0 2025-10-20 10:27:50 +02:00
Thomas Krijnen f05b0d946d if -> ifdef #7174 2025-10-19 13:20:58 +02:00
Ryan Schultz 486651e0b7 additional tweak to the preceding commit. 2025-10-18 13:43:34 -05:00
Ryan Schultz 5885924bb7 Fix #5709: The RCP will not be denied! :) 2025-10-18 12:47:35 -05:00
Andrej730 29434d623c pyodide setup.py - add a note on not switching to pyproject.toml 2025-10-17 19:26:50 +05:00
Andrej730 092705ae68 pyodide setup.py - ensure we have correct pyodide_2025_0_wasm32 suffix
Adding extension, but avoiding adding sources as we're not actually building.
As a result build will have a correct suffix `pyodide_2025_0_wasm32.whl` that's pyodide is currently using and this is pyodide is plan to use when it's going to release to PyPI, see https://peps.python.org/pep-0783/
2025-10-17 19:26:50 +05:00
Andrej730 5f777691cf build-all.py - add a note about alternative gmp mirror 2025-10-17 17:16:36 +05:00
Andrej730 46913422b3 cmake - deduplicate ccache set_property code 2025-10-17 15:34:11 +05:00
Andrej730 ebcdabff07 build-all.py - don't pass side module flags as usual flags
As they're parsed by emcmake automatically https://github.com/pyodide/pyodide-build/blob/bdd0b1422064de6d92264a08c599e85001a28567/pyodide_build/tools/cmake/Modules/Platform/Emscripten.cmake#L105-L107
2025-10-17 15:34:11 +05:00
Ryan Schultz ead849625d Automatically select annotations after creation to make it easier to move them in bulk. 2025-10-16 21:10:43 -05:00
Andrej730 2cfa5201ca build-all.py - option to build wasm outside pyodide for debugging 2025-10-16 11:18:41 +05:00
Andrej730 8dafcc6b88 build-all.py - provide exported functions to target explicitly
Otherwise it broke compilation in some cases when cmake would try to test that linker is working.
2025-10-16 11:18:41 +05:00
Andrej730 626bb8b011 build-all - wasm to use packages instead of providing includes/libs paths explicitly
More complicated than I anticipated😅

Details
https://discourse.cmake.org/t/provide-absolute-paths-to-dependencies-with-cmake-find-root-path-search-during-cross-compilation-wasm/15241
2025-10-16 11:18:41 +05:00
Andrej730 721f74f045 build-all.py - move unsupported dependencies to script 2025-10-16 11:18:40 +05:00
Ryan Schultz f52aafdd6a closes #7249: for stairs, change the first/last tread lengths to less than the typical tread run. And even go to zero, whereby removing the tread altogether. 2025-10-14 21:53:22 -05:00
Ryan Schultz b0ae0a2c82 extend https://github.com/IfcOpenShell/IfcOpenShell/commit/322a2179e51a9ec4802fe9e35039c7e3e29f9678: Merges names with number suffix, as well (ex: foo, foo.001, foo.002) 2025-10-14 11:29:44 -05:00
Thomas Krijnen 4d688170e0 Storage rework WIP 2025-10-13 20:47:43 +02:00
Ryan Schultz 322a2179e5 Merge entities, if match on name or identification attribute only. 2025-10-13 11:25:36 -05:00
Andrej730 31ace7c941 build-deps - use nuget to download ccache 2025-10-13 16:55:49 +05:00
Dion Moult 8014980b88 Fix #7076. Lock representation items scale to prevent user confusion. 2025-10-13 08:52:59 +11:00
Dion Moult 65237cab65 Fix #7080. Potential bug with merging styles that didn't replace elements of the same class. 2025-10-13 08:44:47 +11:00
Dion Moult 155827528a Visibility during collector assignment now depends on the visibility mode set in the UI. 2025-10-13 08:08:35 +11:00
Dion Moult fb15435296 See #7229. Simpler solution to ensure representation isn't affected by visibility until assign class can be cleaned up more. 2025-10-13 08:08:10 +11:00
Dion Moult 6bba3ebeeb See #7159. Make polls consistent in add foo to sheet operators. 2025-10-13 08:02:06 +11:00
Dion Moult 3343f3fb35 Revert "Closes #7229: Unhide object before mesh tessellation export to prevent "Cannot edit hidden object" error when collector.assign() hides the object."
This reverts commit 96d05300bb.
2025-10-13 07:49:07 +11:00
Ryan Schultz 1b451d45cf Closes #7159: Fix AddReferenceToSheet operator poll checking wrong data_dir property (bim_props instead of prefs) 2025-10-12 13:51:14 -05:00
Ryan Schultz f5f676cf51 black . 2025-10-12 12:46:49 -05:00
Ryan Schultz 1dd477d382 Delete aggregate if all its parts are deleted. Prevents having orphaned aggregates, if you do a bulk delete with empties turned off. 2025-10-12 12:17:57 -05:00
Ryan Schultz 96d05300bb Closes #7229: Unhide object before mesh tessellation export to prevent "Cannot edit hidden object" error when collector.assign() hides the object. 2025-10-12 09:57:27 -05:00
Dion Moult 0073a4c0a2 Fix #7205. Regression when switching from drawing mode to model mode didn't hide drawings. 2025-10-12 21:21:27 +11:00
Dion Moult d3a5071d9d Fix #7211. Explain to user why they can't remove the last grid axis. 2025-10-12 21:07:13 +11:00
Dion Moult 471bd34fc6 Fix #7206. For convenience, deleted annotation references are marked as excluded and not regenerated. 2025-10-12 21:00:55 +11:00
Ryan Schultz 44cc12763b Allows the ability to activate an attached drawing_style regardless of whether a drawing has a underlay, or not. 2025-10-11 17:47:37 -05:00
Ryan Schultz b68aca04ae added EPset_Status.UserDefinedStatus to default join_criteria 2025-10-11 17:41:41 -05:00
Bruno Postle dd22264877 Fix sheet regeneration regression from d4f3280
Remove validate_sheet_files() check in regenerate_sheet() that prevented
regenerating missing LAYOUT files. The validation was blocking the
exact scenario that regeneration was designed to handle.

This allows users to open a bare IFC file with configured drawings and
sheets and have the SVG layouts automatically recreated with drawings
placed at default positions.
2025-10-11 20:25:13 +01:00
Ryan Schultz cc6ef0268f Introduces a toggle to turn on/off material layers in printed svg. 2025-10-11 09:52:55 -05:00
Dion Moult feecb89c8e Fix #7212. IfcTester now supports IFC2X3 type element mapping. 2025-10-11 21:30:49 +11:00
Dion Moult cb5c488cfd Fix #7213. IfcTester now supports checking for USERDEFINED predefined types
Can I just take this moment to complain about the overengineered
complexity of how predefined types work in IFC.
2025-10-11 21:09:41 +11:00
Dion Moult b68e8dedd2 Fix #5675. Bug where separate islands of polygons still merged CSS classes incorrectly. 2025-10-11 20:39:48 +11:00
Dion Moult 0975eb455e All default drawing patterns now have a white background, not transparent. 2025-10-11 20:37:28 +11:00
Dion Moult df0e8bba8c Fix #7225. Handle freestyle linework regardless of generated SVG filename. 2025-10-11 19:15:50 +11:00
Dion Moult 71d5239823 Revert "Closes ##7225 - Improve generate_freestyle_linework to robustly handle SVG output by falling back to the first generated linework file if the expected file is missing."
This reverts commit 89c039252b.
2025-10-11 19:02:04 +11:00
Dion Moult 92ae79f64e See #7133. Sync viewport visibility with render visibility when generating underlays. 2025-10-11 18:48:46 +11:00
Dion Moult 9c1f636cd4 Revert "Reintroduce https://github.com/IfcOpenShell/IfcOpenShell/issues/7133 : Add hide_render synchronization to isolate_objects so hidden viewport objects are also hidden from render"
This reverts commit 23c60b38fd.
2025-10-11 18:09:52 +11:00
Ryan Schultz 23c60b38fd Reintroduce https://github.com/IfcOpenShell/IfcOpenShell/issues/7133 : Add hide_render synchronization to isolate_objects so hidden viewport objects are also hidden from render 2025-10-10 22:24:57 -05:00
Ryan Schultz 89c039252b Closes ##7225 - Improve generate_freestyle_linework to robustly handle SVG output by falling back to the first generated linework file if the expected file is missing. 2025-10-10 17:28:15 -05:00
Andrej730 612bd0b0a0 build-all.py - print IFCOS_SCHEMAS 2025-10-10 20:46:03 +05:00
Andrej730 3414d3c7ef build-all.py - document required wasm variables 2025-10-10 20:46:03 +05:00
Andrej730 6c1b3bcbdc build-all.py - fix issue building wasm on apple device 2025-10-10 17:48:29 +05:00
Andrej730 ceba8d51c5 build-all.py - remove --host none from wasm args
As it's now obsolete and producing warnings `configure: WARNING: the "none" host is obsolete, use --disable-assembly`.
2025-10-10 17:48:28 +05:00
Andrej730 0c3fbfa2b8 build-all.py - bump freetype to avoid issues on cmake 4+ 2025-10-10 17:48:28 +05:00
Andrej730 05805bb557 build-all.py - add note on --without-xxx option 2025-10-10 17:48:28 +05:00
Andrej730 3edb006c35 Add Python 3.14 builds 2025-10-09 17:13:07 +05:00
Thomas Krijnen 60d85b4259 Skip ifcxml writing tests 2025-10-09 14:03:35 +02:00
Thomas Krijnen e8a382b02a Only use cyclic dependencies when amongst static libs #7180 2025-10-09 13:24:31 +02:00
Thomas Krijnen 2b3ec66017 mark failed_on_purpose on IfcBoundingBox without --keep-bounding-boxes #7215 2025-10-09 08:45:25 +02:00
Andrej730 b36cd6442e build-deps - set NUGET_EXE from PATH if it's available 2025-10-08 18:37:15 +05:00
Andrej730 fbd6755666 build-all - deduce python components from python path 2025-10-08 18:37:15 +05:00
Andrej730 ffdee45cf9 ifcwrap cmake - migrate to FindPython
Also simplify run-cmake to just prefix the path, instead of providing 3 paths explicitly.
2025-10-08 17:16:34 +05:00
Andrej730 51bf4951b3 build-deps - support overriding PYTHON_VERSION 2025-10-07 11:50:43 +05:00
Andrej730 9c8ae71323 build-deps - download Python using nuget 2025-10-06 19:59:47 +05:00
Andrej730 8a95ec130f build-deps - download nuget
to later download python using it
2025-10-06 19:59:46 +05:00
Andrej730 b2f5e4b11c build-deps - include ccache for convenience 2025-10-06 19:59:46 +05:00
Andrej730 91a2504629 build_win - extend cache size 2025-10-06 19:59:46 +05:00
Andrej730 1eaf110d3d build_win - use fork to resolve cache misses and duplicated cache 2025-10-06 19:59:46 +05:00
Andrej730 9688db67b6 build_win - test ccache 2025-10-06 19:59:46 +05:00
143 changed files with 4604 additions and 1604 deletions
+4 -5
View File
@@ -34,11 +34,6 @@ jobs:
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install Dependencies
run: |
brew update
@@ -72,6 +67,10 @@ jobs:
# We don't use gmpcxx, but it comes preinstalled on macos runner as arm64 bottle
# and CGAL detects it and breaks cross compilation.
brew uninstall --ignore-dependencies gmp
# Otherwise Python will fallback to use arm64 `pkg-config`,
# will pick up arm64 libraries ('zstd' in particular),
# and break the build.
/usr/local/bin/brew install pkg-config
# Required by Python.
/usr/local/bin/brew install gettext openssl
fi
+42 -21
View File
@@ -14,33 +14,54 @@ jobs:
submodules: recursive
path: IfcOpenShell
- name: Checkout Pyodide
- name: Checkout Build Repository
uses: actions/checkout@v3
with:
submodules: recursive
repository: pyodide/pyodide
ref: '0.28.0a3'
repository: IfcOpenShell/build-outputs
path: ifcopenshell_build
ref: wasm
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
path: pyodide
- name: Unpack Dependencies
run: |
cd ifcopenshell_build
python ../IfcOpenShell/pyodide/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ubuntu-22.04-${{ runner.arch }}
- name: Build
run: |
VERSION=`cat IfcOpenShell/VERSION`
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/meta.yaml
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/setup.py
echo '#!/usr/bin/bash' > script.sh
echo 'cd pyodide' >> script.sh
echo 'make' >> script.sh
echo 'cd ..' >> script.sh
echo 'mkdir -p packages/ifcopenshell' >> script.sh
echo 'cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell' >> script.sh
echo 'PYODIDE_ROOT=/src/pyodide \' >> script.sh
echo 'PATH=/src/pyodide/emsdk/emsdk:/src/pyodide/emsdk/emsdk/node/22.16.0_64bit/bin:/src/pyodide/emsdk/emsdk/upstream/emscripten:$PATH \' >> script.sh
echo 'pyodide build-recipes ifcopenshell --install' >> script.sh
chmod +x script.sh
sed -i s/--tty// pyodide/run_docker
pyodide/run_docker ./script.sh
mv dist/ifcopenshell-$VERSION-py3-none-any.whl dist/ifcopenshell-$VERSION+${GITHUB_SHA:0:7}-cp313-cp313-emscripten_4_0_9_wasm32.whl
./IfcOpenShell/pyodide/build_pyodide.sh
FILE=`echo dist/ifcopenshell-*.whl`
NEW_FILE=`echo $FILE | sed "s/-/+${GITHUB_SHA:0:7}-/2"`
mv $FILE $NEW_FILE
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v4
with:
name: build-logs-pyodide
path: |
ifcopenshell_build/*/*/logs/*.log
retention-days: 30
- name: Pack Dependencies
run: |
cd ifcopenshell_build
python ../IfcOpenShell/pyodide/cache_dependencies.py pack
- name: Commit and Push Changes to Build Repository
run: |
cd ifcopenshell_build
git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org"
git add */*/install/cache-*.tar.gz
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
git push || echo "Push failed"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
+2 -2
View File
@@ -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
# Using fork to support Rocky.
uses: Andrej730/ccache-action@main
# Latest release (1.2.19) doesn't support Rocky, so using a specific commit.
uses: hendrikmuhs/ccache-action@eca11c308176d48942455b9e5b1b70ff6950a778
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
+2 -2
View File
@@ -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
# Using fork to support Rocky.
uses: Andrej730/ccache-action@main
# Latest release (1.2.19) doesn't support Rocky, so using a specific commit.
uses: hendrikmuhs/ccache-action@eca11c308176d48942455b9e5b1b70ff6950a778
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
+11 -49
View File
@@ -9,7 +9,6 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ['3.9.11', '3.10.3', '3.11.8', '3.12.1', '3.13.0']
arch: ['x64']
steps:
- name: Checkout Repository
@@ -30,14 +29,6 @@ jobs:
run: |
choco install -y sed 7zip.install awscli
- name: Install Python
run: |
$installer = "python-${{ matrix.python }}-amd64.exe"
$url = "https://www.python.org/ftp/python/${{ matrix.python }}/$installer"
Invoke-WebRequest -Uri $url -OutFile $installer
Start-Process -Wait -FilePath .\$installer -ArgumentList '/quiet InstallAllUsers=0 PrependPath=0 Include_test=0 TargetDir=C:\Python\${{ matrix.python }}'
Remove-Item .\$installer
- name: Unpack Dependencies
run: |
cd _deps-vs2022-x64-installed
@@ -45,21 +36,21 @@ jobs:
7z x $_.FullName
}
- name: Run Build Script
- name: ccache
# Use fork to resolve cache misses / duplicated cache entries on Windows.
uses: Andrej730/ccache-action@main
with:
key: win-${{ matrix.arch }}
# Windows ccache needs ~1GB
# and with default 500MB some cache gets deleted, leading to misses.
max-size: 5000MB
- name: Run Build Script And Pack .zip Archives
shell: cmd
run: |
setlocal EnableDelayedExpansion
SET PYTHON_VERSION=${{ matrix.python }}
for /f "tokens=1,2,3 delims=." %%a in ("%PYTHON_VERSION%") do (
set PY_VER_MAJOR_MINOR=%%a%%b
)
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
SET IFCOS_INSTALL_PYTHON=FALSE
cd win
echo y | call build-deps.cmd vs2022-x64 Release
SET PYTHONHOME=C:\Python\${{ matrix.python }}
call run-cmake.bat vs2022-x64 -DENABLE_BUILD_OPTIMIZATIONS=On -DGLTF_SUPPORT=ON -DADD_COMMIT_SHA=ON -DVERSION_OVERRIDE=ON
call install-ifcopenshell.bat vs2022-x64 Release
python build-all-win.py
- name: Pack Dependencies
run: |
@@ -81,35 +72,6 @@ jobs:
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
git push || echo "Push failed"
- name: Package .zip Archives
run: |
$VERSION = 'v' + ((Get-Content VERSION).Trim())
$SHA = ${env:GITHUB_SHA}.Substring(0, 7)
$OUTPUT_DIR = "$env:USERPROFILE\output"
New-Item -ItemType Directory -Force -Path $OUTPUT_DIR
if ("${{ matrix.python }}" -eq "3.9.11") {
# only for the first python version the executables are assembled for upload
cd _installed-vs2022-x64/bin
Get-ChildItem -Path . | ForEach-Object {
echo $_
$exe = $_.Name
$baseName = $exe.Substring(0, $exe.Length - 4)
$zipName = "${baseName}-$VERSION-$SHA-win64.zip"
7z a $zipName $exe
}
mv *.zip $OUTPUT_DIR
}
$pyVersion = "${{ matrix.python }}"
$pyVersionMajor = ($pyVersion -split '\.')[0..1] -join ''
cd C:\Python\${{ matrix.python }}\Lib\site-packages
Remove-Item -Recurse -Force ifcopenshell\__pycache__ -ErrorAction SilentlyContinue
Get-ChildItem -Path ifcopenshell -Filter "*.pyc" -Recurse | Remove-Item -Force
$zipName = "ifcopenshell-python-$pyVersionMajor-$VERSION-$SHA-win64.zip"
7z a $zipName ifcopenshell
mv $zipName $OUTPUT_DIR
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
@@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312, py313]
pyver: [py39, py310, py311, py312, py313, py314]
config:
- {
name: "Windows 64bit",
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312, py313]
pyver: [py39, py310, py311, py312, py313, py314]
config:
- {
name: "Windows 64bit",
+19 -14
View File
@@ -50,6 +50,7 @@ jobs:
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install src/bcf --no-deps
pip install git+https://github.com/zdhoward/aud
pip install pytest-xdist==3.8.0
- name: Install C++ dependencies
run: |
@@ -113,6 +114,7 @@ jobs:
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
-DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \
-DCOLLADA_SUPPORT=Off \
-DUSE_MMAP=On \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
-DGLTF_SUPPORT=On \
-DWITH_ROCKSDB=On \
@@ -120,20 +122,23 @@ jobs:
sudo make -j $(nproc)
sudo make install
- name: Run IfcConvert on Sample files
run: |
(find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
echo $i | tee -a log; \
timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
echo $i $? >> statuses; \
done) || true
echo Failed
grep -v 0$ statuses
grep -v 0$ statuses | wc -l
echo Succeeded
grep 0$ statuses
grep 0$ statuses | wc -l
# - name: Run IfcConvert on Sample files
# run: |
# (find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
# echo $i | tee -a log; \
# timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
# echo $i $? >> statuses; \
# done) || true
# echo Failed
# grep -v 0$ statuses
# grep -v 0$ statuses | wc -l
# echo Succeeded
# grep 0$ statuses
# grep 0$ statuses | wc -l
- name: Run IfcConvert on Sample file
run: |
IfcConvert test/input/acad2010_walls.ifc test/input/acad2010_walls.obj
- name: Test ifcopenshell-python
run: |
@@ -143,7 +148,7 @@ jobs:
mv ifcopenshell ifcopenshell-local # Force testing on installed module
pip install -e ../ifcpatch --no-deps # Needed for sql.py tests.
ERROR=0
make test || ERROR=1
make test-parallel || ERROR=1
cd ../bcf && make test || ERROR=1
pip install requests
cd ../bsdd && make test || ERROR=1
+4 -1
View File
@@ -109,4 +109,7 @@ src/bonsai/bonsai/bim/schema/Brick.ttl
bonsaiDecoratorForLoads.code-workspace
dev_environment.bat
.pixi/
.pixi/
src/ifcopenshell-python/ifcopenshell/express/*.exp
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
+18 -18
View File
@@ -37,25 +37,25 @@ Contents
| Name | Description | License | Service |
| ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- |
| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bcf-client?label=PyPI&color=006dad)](https://pypi.org/project/bcf-client/) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/bcf-client/badges/version.svg)](https://anaconda.org/conda-forge/bcf-client) |
| bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BonsaiBIM.org-Download-70ba35)](https://bonsaibim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=bonsai-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) |
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bsdd?label=PyPI&color=006dad)](https://pypi.org/project/bsdd/) |
| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc4d?label=PyPI&color=006dad)](https://pypi.org/project/ifc4d/) |
| ifc5d | Report and optimise cost information from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc5d?label=PyPI&color=006dad)](https://pypi.org/project/ifc5d/) |
| ifcbimtester | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
| [bcf](https://docs.ifcopenshell.org/bcf.html) | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bcf-client?label=PyPI&color=006dad)](https://pypi.org/project/bcf-client/) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/bcf-client/badges/version.svg)](https://anaconda.org/conda-forge/bcf-client) |
| [bonsai](https://docs.ifcopenshell.org/bonsai.html) | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BonsaiBIM.org-Download-70ba35)](https://bonsaibim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=bonsai-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) |
| [bsdd](https://docs.ifcopenshell.org/bsdd.html) | Library to query the bSDD API | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bsdd?label=PyPI&color=006dad)](https://pypi.org/project/bsdd/) |
| [ifc2ca](https://docs.ifcopenshell.org/ifc2ca.html) | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
| [ifc4d](https://docs.ifcopenshell.org/ifc4d.html) | Convert to and from IFC and project management software | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc4d?label=PyPI&color=006dad)](https://pypi.org/project/ifc4d/) |
| [ifc5d](https://docs.ifcopenshell.org/ifc5d.html) | Report and optimise cost information from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc5d?label=PyPI&color=006dad)](https://pypi.org/project/ifc5d/) |
| [ifcbimtester](https://docs.ifcopenshell.org/bimtester.html) | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
| ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccityjson?label=PyPI&color=006dad)](https://pypi.org/project/ifccityjson/) |
| ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcclash?label=PyPI&color=006dad)](https://pypi.org/project/ifcclash/) |
| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcconvert-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
| ifccsv | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccsv?label=PyPI&color=006dad)](https://pypi.org/project/ifccsv/) |
| ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) |
| ifcfm | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) |
| ifcmax | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html)
| ifcopenshell-python | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) |
| ifcpatch | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) |
| ifcsverchok | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
| ifctester | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) |
| [ifccityjson](https://docs.ifcopenshell.org/ifccityjson.html) | Convert CityJSON to IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccityjson?label=PyPI&color=006dad)](https://pypi.org/project/ifccityjson/) |
| [ifcclash](https://docs.ifcopenshell.org/ifcclash.html) | Clash detection library and CLI app | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcclash?label=PyPI&color=006dad)](https://pypi.org/project/ifcclash/) |
| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcconvert-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccsv?label=PyPI&color=006dad)](https://pypi.org/project/ifccsv/) |
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) |
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) |
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html)
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) |
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) |
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) |
The IfcOpenShell C++ codebase is split into multiple interal libraries:
+15 -15
View File
@@ -27,12 +27,6 @@ endif()
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
message(STATUS "`ccache` is found, using it as a compiler launcher.")
endif()
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
if(POLICY CMP0141) # 3.25+
@@ -127,8 +121,10 @@ if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM)
set(BUILD_IFCGEOM ON)
endif()
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
message(STATUS "`ccache` is found, using it as a compiler launcher.")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
if(MSVC)
# By default Visual Studio generators will use /Zi which is not compatible
# with ccache, so tell Visual Studio to use /Z7 instead.
@@ -212,6 +208,8 @@ if(NOT MINIMAL_BUILD)
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)
@@ -428,7 +426,7 @@ if(WASM_BUILD)
else()
# @todo review this, shouldn't this be all possible header-only now?
# ... or rewritten using C++17 features?
set(BOOST_COMPONENTS system program_options regex thread date_time)
set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
endif()
if(USE_MMAP)
@@ -453,8 +451,11 @@ if(NOT MINIMAL_BUILD)
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?)
find_package(LibXml2 QUIET CONFIG)
# 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 REQUIRED CONFIG)
clear_wasm_sysroot()
if(NOT LibXml2_FOUND)
# Fallback to CMake's builtin FindLibXml2 module (works on Ubuntu)
@@ -1043,11 +1044,7 @@ if(BUILD_IFCGEOM)
foreach(schema ${SCHEMA_VERSIONS})
set(IFCGEOM_SCHEMA_LIBRARIES ${IFCGEOM_SCHEMA_LIBRARIES} geometry_mapping_ifc${schema})
endforeach()
if(WASM_BUILD)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
else()
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom)
endif()
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
endif()
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
@@ -1177,6 +1174,9 @@ if(BUILD_IFCGEOM)
add_library(geometry_mapping_ifc${schema} STATIC ${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()
+160 -110
View File
@@ -18,6 +18,8 @@
# #
###############################################################################
# ruff: noqa: UP035
"""
Example usage:
# Build all targets by default.
@@ -31,6 +33,7 @@ Available arguments:
``-py-313`` - build for specific Python version
(building for all supported Python version by default).
``-wasm`` - compile for wasm
``-without-xxx`` - do not build dependency ``xxx`` (e.g. ``--without-swig``)
``-mac-cross-compile-intel`` - cross compile for Intel Mac on Apple Silicon host
``-shared`` - build shared libraries. By default will build static.
``-diskcleanup`` - clean up build directories after finishing building dependencies
@@ -50,6 +53,13 @@ Used environment variables:
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (IFC2X3; IFC4; IFC4X3_ADD2) - to be supplied as `2x3;4`
- ``USE_OCCT`` - whether to use official Open CASCADE instead of Community Edition
(`true` by default, any other value is considered `false`)
- ``WASM_PYTHON_PATH`` - path to WASM Python installation,
used to deduce `PYVERSION` (e.g. '3.13.2'), `PYTHONINCLUDE`,
`SIDE_MODULE_CFLAGS`, `SIDE_MODULE_LDFLAGS`.
Allows to build wasm without pyodide build environment, which can be useful for debugging build issues.
Example value: 'pyodide/cpython/installs/python-3.13.2'
- ``WASM_TOOLCHAIN_FILE`` - path to emscripten toolchain file from pyodide ('Emscripten.cmake')
needed only if ``WASM_PYTHON_PATH`` is provided.
- ``ADD_COMMIT_SHA`` - if defined with any non-empty value then
`ADD_COMMIT_SHA` and `VERSION_OVERRIDE` will be set to `ON` while configuring IfcOpenShell
@@ -133,7 +143,7 @@ PROJECT_NAME = "IfcOpenShell"
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1", "3.13.6"]
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1", "3.13.6", "3.14.0"]
JSON_VERSION = "3.11.3"
OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.8.1"
@@ -146,7 +156,7 @@ SWIG_VERSION = "4.1.0"
OPENCOLLADA_VERSION = "v1.6.68"
HDF5_VERSION = "1.13.1"
GMP_VERSION = "6.2.1"
GMP_VERSION = "6.3.0"
MPFR_VERSION = "3.1.6" # latest is 4.1.0
CGAL_VERSION = "v5.6.3"
USD_VERSION = "23.05"
@@ -193,19 +203,40 @@ def cecho(message, color=NO_COLOR):
logger.info(f"{color}{message}\033[0m")
def which(cmd: str) -> Union[str, None]:
PATH = os.getenv("PATH")
assert PATH
for path in PATH.split(":"):
if os.path.exists(path) and cmd in os.listdir(path):
return cmd
return None
# Flags.
APPLE = platform.system() == "Darwin"
MAC_CROSS_COMPILE_INTEL = "mac-cross-compile-intel" in flags
assert platform.system() == "Darwin" or not MAC_CROSS_COMPILE_INTEL
WASM = "wasm" in flags
"""Build WASM outside pyodide build environment."""
WASM_DEBUG = False
if WASM:
if "WASM_PYTHON_PATH" in os.environ:
wasm_python_path = os.environ["WASM_PYTHON_PATH"]
# Deduce version from path, assuming format .../python-X.Y.Z
version_match = re.search(r"python-(\d+)\.(\d+)\.(\d+)", wasm_python_path)
assert version_match, f"Could not deduce python version from '{wasm_python_path}'"
python_version = version_match.group(1)
os.environ["PYVERSION"] = python_version
os.environ["PYTHONINCLUDE"] = f"{wasm_python_path}/include/python{python_version.rpartition('.')[0]}"
os.environ["SIDE_MODULE_CFLAGS"] = ""
# Required, otherwise library will compile as .a, not .so.
os.environ["SIDE_MODULE_LDFLAGS"] = "-s SIDE_MODULE=1"
# We're outside pyodide build environment, so need to provide toolchain file ourselves.
assert "WASM_TOOLCHAIN_FILE" in os.environ, "WASM_TOOLCHAIN_FILE must be set when WASM_PYTHON_PATH is provided"
WASM_DEBUG = True
required_vars = (
# E.g. '3.13.2'.
"PYVERSION",
# 'include' folder in WASM-Python installation.
# e.g. '/pyodide/cpython/installs/python-3.13.2/include/python3.13'
"PYTHONINCLUDE",
"SIDE_MODULE_CFLAGS",
"SIDE_MODULE_LDFLAGS",
)
missing_vars = [v for v in required_vars if v not in os.environ]
assert not missing_vars, f"Some variables required for WASM compilation are missing: {', '.join(missing_vars)}"
# Set defaults for missing empty environment variables
@@ -221,9 +252,11 @@ if platform.system() == "Darwin":
IFCOS_NUM_BUILD_PROCS = os.getenv("IFCOS_NUM_BUILD_PROCS", multiprocessing.cpu_count() + 1)
CMAKE_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "cmake"))
SCRIPT_PATH = Path(__file__).parent
REPO_PATH = SCRIPT_PATH.parent
CMAKE_DIR = (REPO_PATH / "cmake").resolve().__str__()
build_dir = os.environ.get("BUILD_DIR", os.path.join(os.path.dirname(__file__), "..", "build"))
BUILD_DIR = os.environ.get("BUILD_DIR", (REPO_PATH / "build").__str__())
if WASM:
@@ -232,7 +265,7 @@ elif MAC_CROSS_COMPILE_INTEL:
arch = "x86_64"
else:
arch = platform.machine()
DEFAULT_DEPS_DIR = Path(build_dir) / platform.system() / arch
DEFAULT_DEPS_DIR = Path(BUILD_DIR) / platform.system() / arch
if TOOLSET:
DEFAULT_DEPS_DIR = DEFAULT_DEPS_DIR / TOOLSET
@@ -264,6 +297,7 @@ if USE_OCCT:
cecho(" - Compiling against official Open Cascade")
else:
cecho(" - Compiling against Open Cascade Community Edition")
cecho(f"* Build Directory = {BUILD_DIR}", MAGENTA)
cecho(f"* Dependency Directory = {DEPS_DIR}", MAGENTA)
cecho(f" - The directory where {PROJECT_NAME} dependencies are installed.")
cecho(f"* Build Config Type = {BUILD_CFG}", MAGENTA)
@@ -280,6 +314,11 @@ cecho(
""" - How many compiler processes may be run in parallel.
"""
)
cecho(f" * IFCOS_SCHEMAS = '{os.environ.get('IFCOS_SCHEMAS')}'", MAGENTA)
cecho(
""" - IFC Schemas to compile. If not provided, fallback to default provided in cmake.
"""
)
dependency_tree: "dict[str, tuple[str, ...]]" = {
"IfcParse": ("boost", "libxml2", "hdf5", "rocksdb"),
@@ -355,6 +394,23 @@ else:
targets = set(dependency_tree.keys())
targets = set(t for t in targets if "without-%s" % t.lower() not in flags)
if WASM:
SKIP_TARGETS_FOR_WASM = {
"hdf5",
"rocksdb",
"opencollada",
"swig",
"pcre",
"pcre2",
"IfcGeom",
"IfcConvert",
"IfcGeomServer",
}
SKIP_TARGETS_FOR_WASM = {t.lower() for t in SKIP_TARGETS_FOR_WASM}
skip_targets = {t for t in targets if t.lower() in SKIP_TARGETS_FOR_WASM}
if skip_targets:
cecho(f"Skipping targets for wasm build: {', '.join(sorted(skip_targets))}", YELLOW)
targets.difference_update(skip_targets)
print("Building:", *sorted(targets, key=lambda t: len(list(gather_dependencies(t)))))
@@ -363,16 +419,20 @@ yacc = "yacc" # Used during swig building process, installed with `bison` on De
missing_commands: "list[str]" = []
required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz]
if "wasm" in flags:
required_commands.remove(yacc) # yacc not needed for wasm builds
# Skip swig build for WASM.
required_commands.append("swig")
required_commands.remove(yacc)
for cmd in required_commands:
if which(cmd) is None:
if shutil.which(cmd) is None:
missing_commands.append(cmd)
if missing_commands:
raise ValueError(f"Required tools not installed or not added to PATH: {', '.join(missing_commands)}")
MAC_INTEL_BIN_PATH = "/usr/local/bin"
if MAC_CROSS_COMPILE_INTEL:
brew = "/usr/local/bin/brew"
brew = f"{MAC_INTEL_BIN_PATH}/brew"
assert os.path.exists(brew), f"For intel cross compilation the brew path is expected to be '{brew}'."
# identifiers for the download tool (could be less memory consuming as ints, but are more verbose as strings)
@@ -396,6 +456,13 @@ except:
pass
def restore_env(var_name: str, old_value: Union[str, None]) -> None:
if old_value is None:
del os.environ[var_name]
else:
os.environ[var_name] = old_value
def run(cmds: "Sequence[str]", cwd: "Union[str, None]" = None, can_fail: bool = False) -> str:
"""
Wraps `subprocess.Popen.communicate()` and logs the command being executed,
@@ -635,7 +702,7 @@ def build_dependency(
if isinstance(patch, str):
patch = [patch]
for p in patch:
patch_abs = os.path.abspath(os.path.join(os.path.dirname(__file__), p))
patch_abs = (SCRIPT_PATH / p).absolute().__str__()
if os.path.exists(patch_abs):
try:
run(["patch", "-p1", "--batch", "--forward", "-i", patch_abs], cwd=extract_dir)
@@ -722,12 +789,13 @@ LDFLAGS = os.environ.get("LDFLAGS", "")
ADDITIONAL_ARGS_STR = " ".join(ADDITIONAL_ARGS)
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
if "wasm" in flags:
CFLAGS_MINIMAL = CXXFLAGS_MINIMAL = CFLAGS = CXXFLAGS = os.environ["SIDE_MODULE_CFLAGS"]
LDFLAGS = os.environ["SIDE_MODULE_LDFLAGS"]
# WASM `SIDE_MODULE_` are absorbed by `emcmake` automatically.
CXXFLAGS = CXXFLAGS_MINIMAL
CFLAGS = CFLAGS_MINIMAL
elif sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev/null"]) != 0:
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
if BUILD_STATIC:
CXXFLAGS = f"{CXXFLAGS} {PIC} -fdata-sections -ffunction-sections -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
CFLAGS = f"{CFLAGS} {PIC} -fdata-sections -ffunction-sections -fvisibility=hidden {ADDITIONAL_ARGS_STR}"
@@ -736,8 +804,6 @@ elif sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev
CFLAGS = CFLAGS_MINIMAL
LDFLAGS = f"{LDFLAGS} -Wl,--gc-sections {ADDITIONAL_ARGS_STR}"
else:
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
if BUILD_STATIC:
CXXFLAGS = f"{CXXFLAGS} {PIC} -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
CFLAGS = f"{CFLAGS} {PIC} -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
@@ -817,9 +883,9 @@ if "eigen" in targets:
)
if "pcre" in targets:
OLD_CC, OLD_CCXX = None, None
OLD_CC, OLD_CXX = None, None
if MAC_CROSS_COMPILE_INTEL:
OLD_CC, OLD_CCXX = os.environ.get("CC"), os.environ.get("CXX")
OLD_CC, OLD_CXX = os.environ.get("CC"), os.environ.get("CXX")
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
os.environ["CXX"] = MAC_CROSS_COMPILE_INTEL_CXX
# Keep it autoconf as OpenCOLLADA is pretty old and might break
@@ -832,14 +898,8 @@ if "pcre" in targets:
download_name=f"pcre-{PCRE_VERSION}.tar.bz2",
)
if MAC_CROSS_COMPILE_INTEL:
if OLD_CC is None:
del os.environ["CC"]
else:
os.environ["CC"] = OLD_CC
if OLD_CCXX is None:
del os.environ["CXX"]
else:
os.environ["CXX"] = OLD_CCXX
restore_env("CC", OLD_CC)
restore_env("CXX", OLD_CXX)
if "pcre2" in targets:
build_dependency(
@@ -871,7 +931,7 @@ if "freetype" in targets:
download_url="https://github.com/freetype/freetype",
download_name="freetype2",
download_tool=download_tool_git,
revision="VER-2-11-1",
revision="VER-2-14-0",
)
if USE_OCCT and "occ" in targets:
@@ -1010,6 +1070,7 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
# On OSX a dynamic python library is built or it would not be compatible
# with the system python because of some threading initialization
PYTHON_CONFIGURE_ARGS: "list[str]" = []
original_path = ""
if platform.system() == "Darwin":
PYTHON_CONFIGURE_ARGS = ["--enable-shared"]
open_ssl_prefix = run([brew, "--prefix", "openssl@3"]).strip()
@@ -1018,6 +1079,10 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
PYTHON_CONFIGURE_ARGS.append(f"--with-openssl={open_ssl_prefix}")
if MAC_CROSS_COMPILE_INTEL:
original_path = os.environ["PATH"]
# Need to ensure python will pick up intel's `pkg-config`,
# otherwise it might attempt to use ARM libraries (e.g. `zstd`) and fail.
os.environ["PATH"] = f"{MAC_INTEL_BIN_PATH}{os.pathsep}{original_path}"
PYTHON_CONFIGURE_ARGS.extend(["--with-universal-archs=intel-64", "--enable-universalsdk"])
for PYTHON_VERSION in PYTHON_VERSIONS:
@@ -1039,6 +1104,9 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
if not os.path.exists(os.path.join(DEPS_DIR, "install", f"python-{PYTHON_VERSION}")):
raise e
if MAC_CROSS_COMPILE_INTEL:
assert original_path
os.environ["PATH"] = original_path
os.environ["CPPFLAGS"] = OLD_CPP_FLAGS
os.environ["CXXFLAGS"] = OLD_CXX_FLAGS
os.environ["CFLAGS"] = OLD_C_FLAGS
@@ -1084,8 +1152,15 @@ if "boost" in targets:
if "cgal" in targets:
gmp_args: "list[str]" = []
mpfr_args: "list[str]" = []
if "wasm" in flags:
gmp_args.extend(("--disable-assembly", "--host", "none", "--enable-cxx"))
OLD_HOST_CC = None
if WASM:
if APPLE:
# Override `HOST_CC`, otherwise `emcc` will try to use it's own `clang` which can only build
# wasm executables and build will fail.
os.environ["HOST_CC"] = "clang"
# Disable assembly, otherwise `emcc -c conftest.s` will crash due to assembly mismatch.
gmp_args.extend(("--disable-assembly", "--enable-cxx"))
mpfr_args.extend(("--host", "none"))
OLD_CC = None
@@ -1100,10 +1175,14 @@ if "cgal" in targets:
pre_compile_subs=(
[("build/config.h", "HAVE_OBSTACK_VPRINTF 1", "HAVE_OBSTACK_VPRINTF 0")] if "wasm" in flags else []
),
download_url="https://ftp.gnu.org/gnu/gmp/",
# Sometimes ftp.gnu.org is very slow, use ftpmirror.gnu.org as a workaround.
download_url="https://ftpmirror.gnu.org/gnu/gmp/",
download_name=f"gmp-{GMP_VERSION}.tar.bz2",
)
if WASM and APPLE:
restore_env("HOST_CC", OLD_HOST_CC)
build_dependency(
name=f"mpfr-{MPFR_VERSION}",
mode="autoconf",
@@ -1113,10 +1192,7 @@ if "cgal" in targets:
)
if MAC_CROSS_COMPILE_INTEL:
if OLD_CC is None:
del os.environ["CC"]
else:
os.environ["CC"] = OLD_CC
restore_env("CC", OLD_CC)
build_dependency(
name=f"cgal-{CGAL_VERSION}",
@@ -1247,15 +1323,19 @@ def get_cmake_args_prefix_path(additional_paths: "Sequence[str]" = ()) -> "list[
args_prefix_path = cmake_args_prefix_path.copy()
args_prefix_path.extend(additional_paths)
prefix_path = ";".join(args_prefix_path)
return [f"-DCMAKE_PREFIX_PATH={prefix_path}"]
if WASM:
# `emcmake` is disabling search in PATH, so we provide root paths instead.
# Provide '/' to PATH, so it will be combined with provided root paths,
# otherwise, depending on environment, it might not search the root path itself.
return [f"-DCMAKE_FIND_ROOT_PATH={prefix_path}", "-DCMAKE_PREFIX_PATH=//"]
else:
return [f"-DCMAKE_PREFIX_PATH={prefix_path}"]
if "wasm" in flags:
# Boost is built by the build script so should not be found
# inside of the sysroot set by the emscriptem toolchain
cmake_args.append("-DWASM_BUILD=On")
# set Eigen3 path for WASM to avoid find_package issues
cmake_args.append(f"-DEIGEN_DIR={DEPS_DIR}/install/eigen-install-{EIGEN_VERSION}/include/eigen3")
schemas = os.environ.get("IFCOS_SCHEMAS")
if schemas:
@@ -1265,26 +1345,9 @@ 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}")
if "wasm" in flags:
cmake_args.extend(
[
f"-DCGAL_INCLUDE_DIR={DEPS_DIR}/install/cgal-{CGAL_VERSION}/include",
f"-DGMP_INCLUDE_DIR={DEPS_DIR}/install/gmp-{GMP_VERSION}/include",
f"-DGMP_LIBRARY_DIR={DEPS_DIR}/install/gmp-{GMP_VERSION}/lib",
f"-DMPFR_INCLUDE_DIR={DEPS_DIR}/install/mpfr-{MPFR_VERSION}/include",
f"-DMPFR_LIBRARY_DIR={DEPS_DIR}/install/mpfr-{MPFR_VERSION}/lib",
]
)
if "occ" in targets and USE_OCCT:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/occt-{OCCT_VERSION}")
if "wasm" in flags:
cmake_args.extend(
[
f"-DOCC_INCLUDE_DIR={DEPS_DIR}/install/occt-{OCCT_VERSION}/include/opencascade",
f"-DOCC_LIBRARY_DIR={DEPS_DIR}/install/occt-{OCCT_VERSION}/lib",
]
)
elif "occ" in targets:
# We don't support find_package for OCE.
@@ -1305,13 +1368,6 @@ else:
if "libxml2" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/libxml2-{LIBXML2_VERSION}")
if "wasm" in flags:
cmake_args.extend(
[
f"-DLIBXML2_INCLUDE_DIR={DEPS_DIR}/install/libxml2-{LIBXML2_VERSION}/include/libxml2",
f"-DLIBXML2_LIBRARIES={DEPS_DIR}/install/libxml2-{LIBXML2_VERSION}/lib/libxml2.{LIBRARY_EXT}",
]
)
if "hdf5" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/hdf5-{HDF5_VERSION}")
@@ -1341,7 +1397,7 @@ if "rocksdb" in targets:
]
)
if not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets):
if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets)):
logger.info("\rConfiguring executables...")
exec_args = [
@@ -1366,7 +1422,10 @@ if "IfcOpenShell-Python" in targets:
ADDITIONAL_ARGS = "-Wl,-flat_namespace,-undefined,suppress"
if "wasm" in flags:
ADDITIONAL_ARGS = f"-Wl,-undefined,suppress -sSIDE_MODULE=2 -sEXPORTED_FUNCTIONS=_PyInit__ifcopenshell_wrapper"
ADDITIONAL_ARGS = "-Wl,-undefined,suppress"
# Override CMAKE_TOOLCHAIN_FILE because by default emscripten doesn't support building shared binaries.
if WASM_DEBUG:
os.environ["CMAKE_TOOLCHAIN_FILE"] = os.environ["WASM_TOOLCHAIN_FILE"]
# 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)
@@ -1380,40 +1439,47 @@ if "IfcOpenShell-Python" in targets:
os.makedirs(python_dir, exist_ok=True)
def compile_python_wrapper(
python_version: str, python_library: str, python_include: str, python_executable: Union[str, None]
python_version: str,
python_include: Union[str, None] = None,
python_executable: Union[str, None] = None,
python_path: Union[Path, None] = None,
) -> Union[str, None]:
"""
:return: Path to module dir if ``python_executable`` was provided, otherwise ``None``.
"""
assert bool(python_path) ^ bool(python_include)
logger.info(f"\rConfiguring python {python_version} wrapper...")
cache_path = os.path.join(python_dir, "CMakeCache.txt")
if os.path.exists(cache_path):
os.remove(cache_path)
os.environ["PYTHON_LIBRARY_BASENAME"] = os.path.basename(python_library)
swig_prefix_paths: list[str] = []
prefix_paths: list[str] = []
if "swig" in targets:
swig_prefix_paths.append(f"{DEPS_DIR}/install/swig")
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.
python_executable = (Path(python_path) / "bin" / "python3").__str__()
python_include = run(
[
python_executable,
"-c",
"import sysconfig; print(sysconfig.get_config_var('INCLUDEPY'))",
]
)
assert python_include
run_cmake(
"",
cmake_args
+ get_cmake_args_prefix_path(swig_prefix_paths)
+ get_cmake_args_prefix_path(prefix_paths)
+ [
"-DPYTHON_LIBRARY=" + python_library,
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
# *([f"-DPYTHON_MODULE_INSTALL_DIR={os.environ['PYTHONPATH']}/ifcopenshell"] if "wasm" in flags else []),
*(
[
"-DPYTHON_MODULE_INSTALL_DIR="
+ os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "package"))
]
if "wasm" in flags
else []
),
"-DPYTHON_INCLUDE_DIR=" + python_include,
# Needed because pyodide is expecting setup.py to be in the root.
*([f"-DPYTHON_MODULE_INSTALL_DIR={REPO_PATH}"] * WASM),
f"-DPYTHON_INCLUDE_DIR={python_include}",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell/tmp",
"-DUSERSPACE_PYTHON_PREFIX="
+ ["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
@@ -1450,33 +1516,17 @@ if "IfcOpenShell-Python" in targets:
return module_dir
if "wasm" in flags:
compile_python_wrapper(
f"{os.environ['PYMAJOR']}.{os.environ['PYMINOR']}.{os.environ['PYMICRO']}",
f"{os.environ['TARGETINSTALLDIR']}/lib/libpython{os.environ['PYMAJOR']}.{os.environ['PYMINOR']}.a",
os.environ["PYTHONINCLUDE"],
None,
)
compile_python_wrapper(os.environ["PYVERSION"], os.environ["PYTHONINCLUDE"])
# Copy setup.py where pyodide build system expects it.
shutil.copy(REPO_PATH / "pyodide" / "setup.py", REPO_PATH)
elif USE_CURRENT_PYTHON_VERSION:
python_info = sysconfig.get_paths()
py_path_components = [sysconfig.get_config_var("LIBDIR"), sysconfig.get_config_var("INSTSONAME")]
if sysconfig.get_config_var("multiarchsubdir"):
py_path_components.insert(1, sysconfig.get_config_var("multiarchsubdir").replace("/", ""))
python_lib = os.path.join(*py_path_components)
compile_python_wrapper(platform.python_version(), python_lib, python_info["include"], sys.executable)
compile_python_wrapper(platform.python_version(), python_info["include"], sys.executable)
else:
for python_version in PYTHON_VERSIONS:
python_library = run([bash, "-c", f"ls {DEPS_DIR}/install/python-{python_version}/lib/libpython*.*"])
python_include = run([bash, "-c", f"ls -d {DEPS_DIR}/install/python-{python_version}/include/python*"])
python_executable = os.path.join(
DEPS_DIR, "install", f"python-{python_version}", "bin", f"python{python_version[0]}"
)
module_dir = compile_python_wrapper(python_version, python_library, python_include, python_executable)
python_path = Path(DEPS_DIR) / "install" / f"python-{python_version}"
module_dir = compile_python_wrapper(python_version, python_path=python_path)
assert module_dir
# Not sure why, but added after reading this in the logs
# cp: /Users/runner/work/IfcOpenShell/IfcOpenShell/build/Darwin/x86_64/10.15/install/ifcopenshell/python-3.9.11: No such file or directory
+33
View File
@@ -0,0 +1,33 @@
There are two ways to build pyodide ifcopenshell Python wrapper wheel.
1. Using pyodide build system (`build_pyodide.yml` does it):
- install prebuilt pyodide build and emscripten environment (see `build_pyodide.sh`)
- clone IfcOpenShell to `IfcOpenShell` folder
- create `packages/ifcopenshell` folder that will be used by pyodide build system
- from `IfcOpenShell` move building recipe `pyodide/meta.yaml` to `packages/ifcopenshell`
- run `pyodide build-recipes ifcopenshell --install`, it will
- execute `meta.yaml` recipe - it will:
- copy IfcOpenShell source to build folder `packages/ifcopenhell/build/ifcopenshell-0.8.0`
- build ifcopenshell and its dependencies
- note that rerunning `pyodide build-recipes` will remove previous build folder and rebuild all dependencies.
The way to avoid it, if build fails, is to use `pyodide build-recipes-no-deps ifcopenshell --continue` instead.
- run `setup.py` in `IfcOpenShell` root, producing a wheel in `IfcOpenShell/dist`
- copy that wheel to `packages/ifcopenshell/dist`
- `--install` it to current build envrionment
- copy the wheel next to `dist` folder (in root directory, next to `packages`)
- add wheel to `dist/pyodide-lock.json`
2. Build it outside of pyodide build system.
Building inside pyodide build system should be preferred, option to build it outside is useful for debugging purposes,
since it's pure cmake without any additional moving parts.
- setup pyodide environment, see above
- clone IfcOpenShell repo next to it to `IfcOpenShell` folder
- setup debug build environment using `source pyodide/debug_build_env.sh /path/to/pyodide_root`
- run `python nix/build-all.py -wasm -py-313` in `IfcOpenShell`
- it will produce Python package in `IfcOpenShell/ifcopenshell`
- run `pyodide build`
- it will produce a wheel in `IfcOpenShell/dist`
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/bash
set -ex
# Install uv.
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv --python 3.13
source .venv/bin/activate
# Install pyodide cross build environment.
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
uv pip install pyodide-build
# `uv run` is required, so xbuildenv would skip using `pip`.
uv run pyodide xbuildenv install
# Emscripten doesn't come with xbuildenv.
git clone https://github.com/emscripten-core/emsdk
pushd emsdk
PYODIDE_EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version)
./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION}
./emsdk activate ${PYODIDE_EMSCRIPTEN_VERSION}
source emsdk_env.sh
which emcc
popd
mkdir -p packages/ifcopenshell
VERSION=`cat IfcOpenShell/VERSION`
cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell
sed -i s/0.8.0/$VERSION/g packages/ifcopenshell/meta.yaml
# Use custom build ifcopenshell directory in build-all to make caching simpler
# Otherwise pyodide build path typically includes package version, so cached cmake configs might break.
export BUILD_DIR=`readlink -f ifcopenshell_build`
# Use build-recipes-no-deps first, so logs would be printed to stdout.
pyodide build-recipes-no-deps ifcopenshell
pyodide build-recipes ifcopenshell --install
+64
View File
@@ -0,0 +1,64 @@
"""
Cache built dependencies for builds.
This script is finding common install directory and either
packs each folder into a tar.gz archive, if it wasn't packed before,
or unpacks existing archives.
Usage: python cache_dependencies.py [pack|unpack]
"""
import tarfile
import sys
from pathlib import Path
from typing import Literal
CACHE_PREFIX = "cache-"
def get_install_dir() -> Path:
for data in Path.cwd().glob("*/*/install"):
return data
raise Exception("No install dir found")
def pack_dependencies(install_dir: Path) -> None:
# Process each install_dir
for dependency_path in install_dir.iterdir():
if not dependency_path.is_dir():
continue
dependency_name = dependency_path.name
tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz"
if tar_path.exists():
print(f"Skipping existing cache: '{tar_path}'")
else:
with tarfile.open(tar_path, "w:gz") as tar:
tar.add(dependency_path, arcname=dependency_path.name)
print(f"Created cache: '{tar_path}'")
def unpack_dependencies(install_dir: Path) -> None:
# `filter` argument was fully introduced in 3.12
# and results in deprecation warnings in 3.12-3.13, if not provided.
tar_filter: "dict[Literal['filter'], Literal['data']]" = (
{"filter": "data"} if bool(sys.version_info >= (3, 12)) else {}
)
for tar_path in install_dir.glob(f"{CACHE_PREFIX}*.tar.gz"):
with tarfile.open(tar_path, "r:gz") as tar:
tar.extractall(path=install_dir, **tar_filter)
print(f"Extracted cache: '{tar_path.name}'.")
if __name__ == "__main__":
if len(sys.argv) != 2 or (action := sys.argv[1].lower()) not in ("pack", "unpack"):
print(__doc__)
sys.exit(1)
install_dir = get_install_dir()
print(f"Found install dir: '{install_dir}'")
if action == "pack":
pack_dependencies(install_dir)
else:
unpack_dependencies(install_dir)
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
if [ $# -ne 1 ]; then
echo "Usage: $0 <pyodide_root>"
exit 1
fi
export PYODIDE_ROOT=$(readlink -f "$1")
export PATH="$PYODIDE_ROOT/emsdk/emsdk:$PYODIDE_ROOT/emsdk/emsdk/node/22.16.0_64bit/bin:$PYODIDE_ROOT/emsdk/emsdk/upstream/emscripten:$PATH"
# build-all.py vars.
export WASM_PYTHON_PATH="$PYODIDE_ROOT/cpython/installs/python-3.13.2"
export WASM_TOOLCHAIN_FILE="$PYODIDE_ROOT/pyodide-build/pyodide_build/tools/cmake/Modules/Platform/Emscripten.cmake"
+2 -3
View File
@@ -3,13 +3,12 @@ package:
version: 0.8.0
source:
# meta.yaml is placed as `packages/ifcopenshell/meta.yaml`.
path: ../../IfcOpenShell
build:
script: |
BUILD_CFG=Release python nix/build-all.py --without-rocksdb --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py313 IfcOpenShell-Python
mv package/ifcopenshell .
cp pyodide/setup.py .
BUILD_CFG=Release python nix/build-all.py -v --wasm --py313
about:
home: http://ifcopenshell.org
+46 -9
View File
@@ -1,11 +1,48 @@
from setuptools import setup, find_packages
# setup.py is getting deprecated, but we still use it,
# because `tool.setuptools.ext-modules` is still experimental in pyproject.toml
# and we need it to get the wheel suffix right.
import os
from pathlib import Path
setup(name='ifcopenshell',
version='0.8.0',
description='IfcOpenShell is an open source (LGPL) software library for working with the Industry Foundation Classes (IFC) file format.',
author='Thomas Krijnen',
author_email='thomas@aecgeeks.com',
url='http://ifcopenshell.org',
packages=find_packages(),
package_data={'ifcopenshell': ['util/schema/*.json', 'util/schema/*.ifc'], '': ['*.so', '*.json', '*.ifc']},
import tomllib
from setuptools import Extension, setup
REPO_FOLDER = Path(__file__).parent
def get_version() -> str:
if "PKG_VERSION" in os.environ:
# Inside pyodide build environment.
return os.environ["PKG_VERSION"]
return (REPO_FOLDER / "VERSION").read_text().strip()
# Read dependencies from pyproject.toml
def get_dependencies() -> list[str]:
pyproject_toml = REPO_FOLDER / "src" / "ifcopenshell-python" / "pyproject.toml"
pyproject_data = tomllib.loads(pyproject_toml.read_text())
dependencies = pyproject_data["project"]["dependencies"]
return dependencies
setup(
name="ifcopenshell",
version=get_version(),
description=(
"IfcOpenShell is an open source (LGPL) software library "
"for working with the Industry Foundation Classes (IFC) file format."
),
author="Thomas Krijnen",
author_email="thomas@aecgeeks.com",
url="https://ifcopenshell.org",
install_requires=get_dependencies(),
packages=["ifcopenshell"],
package_data={
# "*.so" is needed to include prebuilt binary extension. Otherwise it would try to build it and fail.
"ifcopenshell": ["util/schema/*.json", "util/schema/*.ifc", "*.so"],
"": ["*.json", "*.ifc"],
},
# Has to provide extension to get the correct wheel suffix.
ext_modules=[Extension("ifcopenshell._ifcopenshell_wrapper", sources=[])],
)
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

@@ -13,10 +13,10 @@ DATA;
#6=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','Whether or not this drawing can be referenced in other drawings.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#14=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#15=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
+11 -1
View File
@@ -36,6 +36,7 @@ from bonsai.bim.ifc import IfcStore
from mathutils import Vector
from typing import Union
from logging import Logger
from math import radians
class IfcExporter:
@@ -104,7 +105,16 @@ class IfcExporter:
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
if tool.Geometry.is_scaled(obj):
# Handle camera scales specially
if obj.type == "CAMERA":
# Check if this is a reflected ceiling plan camera
camera = tool.Ifc.get_entity(obj)
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
# Ensure reflected ceiling cameras have the correct scale
if obj.scale != (-1, -1, -1):
obj.scale = (-1, -1, -1)
# Skip all other scale handling for cameras
elif tool.Geometry.is_scaled(obj):
bpy.ops.bim.update_representation(obj=obj.name)
# update_representation might not apply scale if the object has openings
# reset it, so let user know that the scale wasn't saved.
-2
View File
@@ -222,8 +222,6 @@ def refresh_ui_data():
if isinstance(ifc_file := tool.Ifc.get(), ifcopenshell.sqlite):
ifc_file.clear_cache()
props = tool.Drawing.get_document_props()
props.should_draw_decorations = props.should_draw_decorations
if tool.Web.get_web_props().is_connected:
tool.Web.send_webui_data()
+3
View File
@@ -952,6 +952,9 @@ class IfcImporter:
if not props.ifc_file:
props.ifc_file = self.ifc_import_settings.input_file
self.file = tool.Ifc.get()
# IFC4 Reference View shall have no booleans https://github.com/BuildingSMART/IFC4-CV/issues/14
if self.file.schema == "IFC4" and "ReferenceView" in str(self.file.header.file_description.description):
self.ifc_import_settings.void_limit = 0
def calculate_unit_scale(self):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -94,6 +94,9 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
aggregates_to_check = set()
# First pass: unassign all parts and track their aggregates
for obj in tool.Blender.get_selected_objects():
element = tool.Ifc.get_entity(obj)
if not element:
@@ -101,6 +104,10 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
continue
# Track this aggregate for later checking
aggregates_to_check.add(aggregate)
core.unassign_object(
tool.Ifc,
tool.Aggregate,
@@ -116,6 +123,29 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
# Second pass: delete aggregates that now have no parts
deleted_aggregates = []
for aggregate in aggregates_to_check:
related_objects = ifcopenshell.util.element.get_parts(aggregate)
if len(related_objects) == 0:
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
deleted_aggregates.append(aggregate_name)
aggregate_obj = tool.Ifc.get_object(aggregate)
if aggregate_obj:
ifcopenshell.api.root.remove_product(tool.Ifc.get(), product=aggregate)
bpy.data.objects.remove(aggregate_obj, do_unlink=True)
# Show info message if aggregates were deleted
if deleted_aggregates:
if len(deleted_aggregates) == 1:
self.report(
{"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
)
else:
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
"""Enable editing aggregation relationship"""
@@ -282,7 +282,11 @@ class BIM_PT_classification_references(Panel, ReferenceUI):
@classmethod
def poll(cls, context):
return bool((obj := context.active_object) and tool.Ifc.get_entity(obj))
return (
(obj := tool.Blender.get_active_object())
and (element := tool.Ifc.get_entity(obj))
and element.is_a("IfcObjectDefinition")
)
def get_object_name(self, context: bpy.types.Context) -> str:
assert (obj := context.active_object)
+27 -3
View File
@@ -818,7 +818,12 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.merge_identical_objects"
bl_label = "Merge Identical Objects"
bl_description = "For materials currently only IfcMaterials are supported"
bl_description = (
"Merge identical IFC objects (that match all attributes).\n"
"\n"
"SHIFT + CLICK to merge by name/identification attribute only.\n"
"Merges names with number suffix, as well (ex: foo, foo.001, foo.002)\n"
)
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
@@ -826,18 +831,36 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
)
by_name_or_identification_only: bpy.props.BoolProperty(
name="By Name/Identification Only",
description="Merge based only on Name or Identification attribute, ignoring other properties",
default=False,
)
if TYPE_CHECKING:
object_type: tool.Debug.PurgeMergeObjectType
def invoke(self, context, event):
# Check if shift key is pressed
if event.shift:
self.by_name_or_identification_only = True
else:
self.by_name_or_identification_only = False
return self.execute(context)
def _execute(self, context):
object_type: str = self.object_type
if object_type in ("PROFILE", "TYPE"):
self.report({"ERROR"}, f"Unsupported object type {object_type}.")
return {"CANCELLED"}
merged_data = tool.Debug.merge_identical_objects(object_type)
merged_data = tool.Debug.merge_identical_objects(
object_type, by_name_or_identification_only=self.by_name_or_identification_only
)
plural_object_type = f"{object_type.lower().replace('_', ' ')}s"
if merged_data:
merge_mode = " by name/identification" if self.by_name_or_identification_only else ""
for element_type, element_names in merged_data.items():
print(f"- {element_type}:")
for name in element_names:
@@ -846,7 +869,8 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
merged = sum(len(v) for v in merged_data.values())
msg = " See system console for details." if merged else ""
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged.{msg}")
merge_mode = " (by name/identification)" if self.by_name_or_identification_only else ""
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged{merge_mode}.{msg}")
if merged == 0:
return
@@ -61,6 +61,7 @@ classes = (
operator.EnableEditingAssignedProduct,
operator.EnableEditingElementFilter,
operator.EnableEditingText,
operator.ExcludeAnnotation,
operator.ExpandSheet,
operator.LoadDrawings,
operator.LoadReferences,
+8 -3
View File
@@ -235,13 +235,18 @@ class DecoratorData:
cut_cache = {}
slice_cache = {}
fill_cache = {}
camera_location_checksum = None
camera_rotation_checksum = None
@classmethod
def clear_cache(cls):
cls.cut_cache = {}
cls.layerset_cache = {}
cls.fill_cache = {}
@classmethod
def load(cls, handler):
cls.is_loaded = True
cls.cut_cache = {}
cls.layerset_cache = {}
cls.fill_cache = {}
text = {}
dimension = {}
@@ -22,6 +22,7 @@ import blf
import math
import bmesh
import shapely
import numpy as np
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
@@ -1625,6 +1626,7 @@ class CutDecorator:
if cls.installed:
cls.uninstall()
handler = cls()
handler.cache_camera_matrix()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
@@ -1727,6 +1729,30 @@ class CutDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
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())
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()
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())
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
self.cache_camera_matrix()
return True
return False
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
has_cut_cache = element.id() in DecoratorData.cut_cache
has_fill_cache = element.id() in DecoratorData.fill_cache
@@ -1734,9 +1760,9 @@ class CutDecorator:
# Currently selected objects must be recalculated as they may be being moved / edited.
# If the camera is selected, we also recalculate as the user may be moving the camera.
if not has_cut_cache or obj.select_get() or context.scene.camera.select_get():
if not has_cut_cache or obj.select_get() or self.is_camera_moved():
self.recalculate_cut(context, obj, element)
if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
if not has_fill_cache or obj.select_get() or self.is_camera_moved():
self.recalculate_fill(context, obj, element)
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
@@ -1933,8 +1959,7 @@ class DecorationsHandler:
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
if not DecoratorData.is_loaded:
DecoratorData.load(handler)
DecoratorData.clear_cache()
@classmethod
def uninstall(cls):
@@ -1961,5 +1986,8 @@ class DecorationsHandler:
if not DrawingsData.is_loaded:
DrawingsData.load()
if not DecoratorData.is_loaded:
DecoratorData.load(self)
for obj, decorator in DecoratorData.data["object_decorators"]:
decorator.decorate(context, obj)
@@ -210,12 +210,6 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
should_duplicate_annotations=self.should_duplicate_annotations,
)
# TODO: Why need to resync active drawing, if it wasn't changed.
drawing = props.get_active_drawing()
if drawing is None:
return
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
class CreateDrawing(bpy.types.Operator):
"""Creates/refreshes a .svg drawing
@@ -442,6 +436,8 @@ class CreateDrawing(bpy.types.Operator):
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
assert (drawing_style := self.cprops.get_active_drawing_style())
tool.Blender.sync_render_visibility()
if drawing_style.render_type == "DEFAULT":
bpy.ops.render.render(write_still=True)
else:
@@ -625,7 +621,7 @@ class CreateDrawing(bpy.types.Operator):
path.attrib["d"] = d
group.append(g)
def generate_wall_layers(self, context: bpy.types.Context, root) -> None:
def generate_material_layers(self, context: bpy.types.Context, root) -> None:
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
if "projection" in el.get("class", "").split():
continue
@@ -794,8 +790,9 @@ class CreateDrawing(bpy.types.Operator):
edge_bm.to_mesh(edge_mesh)
edge_bm.free()
actual_path = svg_path[0:-4] + "0001.svg"
freestyle_svg_exporter = tool.Blender.get_addon("freestyle_svg_exporter")
context.scene.render.filepath = svg_path[0:-4]
actual_path = freestyle_svg_exporter.create_path(bpy.context.scene)
bpy.ops.render.render(write_still=False)
os.replace(actual_path, svg_path)
@@ -839,7 +836,8 @@ class CreateDrawing(bpy.types.Operator):
if tool.Drawing.is_camera_orthographic():
self.generate_bisect_linework(context, root)
self.generate_wall_layers(context, root)
if self.cprops.generate_material_layers:
self.generate_material_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
@@ -937,12 +935,14 @@ class CreateDrawing(bpy.types.Operator):
if self.cprops.cut_mode == "BISECT":
self.remove_cut_linework(root)
self.generate_bisect_linework(context, root)
self.generate_wall_layers(context, root)
if self.cprops.generate_material_layers:
self.generate_material_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
elif self.cprops.cut_mode == "OPENCASCADE":
self.move_projection_to_bottom(root)
self.generate_wall_layers(context, root)
if self.cprops.generate_material_layers:
self.generate_material_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
@@ -1252,6 +1252,8 @@ class CreateDrawing(bpy.types.Operator):
def get_svg_classes(self, element, layer=None):
classes = [element.is_a()]
# ─── Material ──────────────────────────────────────────────
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
material_name = ""
if material:
@@ -1264,6 +1266,7 @@ class CreateDrawing(bpy.types.Operator):
else:
classes.append("material-null")
# ─── Layer ─────────────────────────────────────────────────
if layer:
classes.append(layer.is_a())
layer_material = layer.Material
@@ -1276,12 +1279,19 @@ class CreateDrawing(bpy.types.Operator):
layer_material_category = tool.Drawing.canonicalise_class_name(layer_material.Category)
classes.append(f"layer-material-category-{layer_material_category}")
# ─── Metadata ──────────────────────────────────────────────
for key in self.metadata:
value = ifcopenshell.util.selector.get_element_value(element, key)
if value:
classes.append(
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:
@@ -1348,7 +1358,13 @@ class CreateDrawing(bpy.types.Operator):
join_criteria = join_criteria.split(",")
else:
# Drawing convention states that same objects classes with the same material are merged when cut.
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status", "Material.Name"]
join_criteria = [
"class",
"material.Name",
"/Pset_.*Common/.Status",
"EPset_Status.Status",
"EPset_Status.UserDefinedStatus",
]
group = root.find("{http://www.w3.org/2000/svg}g")
joined_paths = {}
@@ -1477,11 +1493,10 @@ class CreateDrawing(bpy.types.Operator):
joined_paths.setdefault(hash_keys, []).append(el)
for key, els in joined_paths.items():
polygons = []
classes = set()
queue = []
for el in els:
classes.update(el.attrib["class"].split())
classes = set(el.attrib["class"].split())
classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
is_closed_polygon = False
for path in el.findall("{http://www.w3.org/2000/svg}path"):
@@ -1496,31 +1511,30 @@ class CreateDrawing(bpy.types.Operator):
coords.append(coords[0])
if len(coords) > 2 and coords[0] == coords[-1]:
is_closed_polygon = True
polygons.append(shapely.Polygon(coords))
queue.append((shapely.Polygon(coords), classes))
if is_closed_polygon:
el.getparent().remove(el)
try:
merged_polygons = shapely.ops.unary_union(polygons)
except:
print("Warning. Portions of the merge failed. Please report a bug!", polygons)
merged_polygons = polygons
while queue:
polygon, polygon_classes = queue.pop()
for polygon2, polygon2_classes in queue[:]:
try:
merged_polygon = shapely.union(polygon, polygon2)
except:
print("Warning. Portions of the merge failed. Please report a bug!", polygon, polygon2)
continue
if type(merged_polygon) == shapely.Polygon:
polygon = merged_polygon
polygon_classes.update(polygon2_classes)
queue.remove((polygon2, polygon2_classes))
if type(merged_polygons) == shapely.MultiPolygon:
merged_polygons = merged_polygons.geoms
elif type(merged_polygons) == shapely.Polygon:
merged_polygons = [merged_polygons]
else:
merged_polygons = []
for polygon in merged_polygons:
g = etree.Element("g")
path = etree.SubElement(g, "path")
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
for interior in polygon.interiors:
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
path.attrib["d"] = d
g.set("class", " ".join(list(classes)))
g.set("class", " ".join(list(polygon_classes)))
group.append(g)
def drawing_to_model_co(self, x: float, y: float) -> Vector:
@@ -1862,13 +1876,15 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = tool.Drawing.get_document_props()
# Won't be visible in UI anyway.
prefs = tool.Blender.get_addon_preferences()
if not props.sheets or not prefs.data_dir:
return False
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
if not props.sheets:
cls.poll_message_set("No sheets available.")
return False
if not tool.Blender.get_user_data_dir():
cls.poll_message_set("BIM data directory not set.")
return False
return True
def _execute(self, context):
@@ -2222,6 +2238,7 @@ class ActivateModel(bpy.types.Operator):
)
tool.Blender.reset_object_visibility()
tool.Drawing.hide_all_drawing_collections()
tool.Blender.update_viewport()
bonsai.bim.handler.refresh_ui_data()
@@ -2318,9 +2335,8 @@ class ActivateDrawingBase(tool.Ifc.Operator):
dprops.active_drawing_id = self.drawing
dprops.drawing_styles.clear()
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
if tool.Drawing.is_camera_orthographic():
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
@@ -2333,10 +2349,22 @@ class ActivateDrawingBase(tool.Ifc.Operator):
camera = context.scene.camera
assert camera
camera_props = tool.Drawing.get_camera_props(camera)
# Check if this is a reflected ceiling camera and preserve its scale
camera_element = tool.Ifc.get_entity(camera)
is_reflected = False
if camera_element:
is_reflected = (
ifcopenshell.util.element.get_pset(camera_element, "EPset_Drawing", "TargetView")
== "REFLECTED_PLAN_VIEW"
)
if is_reflected and camera.scale != (-1, -1, -1):
camera.scale = (-1, -1, -1)
if camera_props.update_representation(camera.matrix_world):
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
# See 6452 and 6478.
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
# Restore the scale after update if needed
if is_reflected:
camera.scale = (-1, -1, -1)
return {"FINISHED"}
@@ -2812,8 +2840,13 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
if not props.schedules:
cls.poll_message_set("No schedule selected.")
return False
prefs = tool.Blender.get_addon_preferences()
return props.schedules and props.sheets and prefs.data_dir
if not props.sheets:
cls.poll_message_set("No sheets available.")
return False
if not tool.Blender.get_user_data_dir():
cls.poll_message_set("BIM data directory not set.")
return False
return True
def _execute(self, context):
props = tool.Drawing.get_document_props()
@@ -2880,8 +2913,13 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
if not props.references:
cls.poll_message_set("No reference selected.")
return False
bim_props = tool.Blender.get_bim_props()
return props.references and props.sheets and bim_props.data_dir
if not props.sheets:
cls.poll_message_set("No sheets available.")
return False
if not tool.Blender.get_user_data_dir():
cls.poll_message_set("BIM data directory not set.")
return False
return True
def _execute(self, context):
props = tool.Drawing.get_document_props()
@@ -3730,3 +3768,19 @@ class OpenDocumentationWebUi(bpy.types.Operator):
else:
bpy.ops.bim.open_web_browser(page="documentation")
return {"FINISHED"}
class ExcludeAnnotation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.exclude_annotation"
bl_label = "Exclude Annotation"
bl_description = "Excludes the automatic annotation reference from the drawing"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
if not (obj := bpy.context.scene.camera) or not (drawing := tool.Ifc.get_entity(obj)):
return
for obj in tool.Blender.get_selected_objects(include_active=False):
if (element := tool.Ifc.get_entity(obj)) and tool.Drawing.is_auto_annotation(element):
if referenced_element := tool.Drawing.get_annotation_element(element):
tool.Drawing.exclude_annotation_from_drawing(referenced_element, drawing)
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
@@ -498,6 +498,9 @@ class BIMCameraProperties(PropertyGroup):
name="Linework Mode",
update=get_update_layer_callback("linework_mode", "LineworkMode"),
)
generate_material_layers: bpy.props.BoolProperty(
name="Generate Material Layers", description="Generate material layer linework in drawings", default=True
)
fill_mode: EnumProperty(
items=[
("NONE", "None", "Disable filling areas seen in projection"),
@@ -859,6 +862,19 @@ class BIMAnnotationProperties(PropertyGroup):
)
is_adding_type: bpy.props.BoolProperty(default=False)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
tag_rotation_mode: bpy.props.EnumProperty(
name="Tag Rotation Mode",
description="How to orient the tag relative to the tagged object",
items=[
("NONE", "No Rotation", "Keep tag in default orientation"),
("LOCAL_X", "Local X Axis", "Align tag with object's local X axis"),
("LOCAL_Y", "Local Y Axis", "Align tag with object's local Y axis"),
("LOCAL_Z", "Local Z Axis", "Align tag with object's local Z axis"),
("CAMERA_Horizontal", "Camera Horizontal", "Align tag with camera X axis"),
("CAMERA_Vertical", "Camera Vertical", "Align tag with camera Y axis"),
],
default="NONE",
)
if TYPE_CHECKING:
object_type: str
+3 -1
View File
@@ -99,12 +99,13 @@ class BIM_PT_camera(Panel):
row = self.layout.row()
row.prop(props, "linework_mode")
row = self.layout.row()
row.prop(props, "generate_material_layers")
if props.linework_mode == "OPENCASCADE":
row = self.layout.row()
row.prop(props, "fill_mode")
row = self.layout.row()
row.prop(props, "cut_mode")
row = self.layout.row()
row.prop(props, "width")
row = self.layout.row()
@@ -193,6 +194,7 @@ class BIM_PT_element_filters(Panel):
text = "Exclude Filter" if ElementFiltersData.data["has_exclude_filter"] else "No Exclude Filter Found"
icon = "GREASEPENCIL" if ElementFiltersData.data["has_exclude_filter"] else "ADD"
row.label(text=text, icon="FILTER")
row.operator("bim.exclude_annotation", icon="REMOVE", text="")
row.operator("bim.enable_editing_element_filter", icon=icon, text="").filter_mode = "EXCLUDE"
@@ -228,7 +228,6 @@ class AnnotationToolUI:
def draw_type_selection_interface(cls):
# shared by both sidebar and header
object_type = cls.props.object_type
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, cls.props, "object_type", text="")
@@ -248,6 +247,9 @@ class AnnotationToolUI:
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
row = cls.layout.row(align=True)
row.label(text="", icon="DRIVER_ROTATIONAL_DIFFERENCE")
row.prop(cls.props, "tag_rotation_mode", text="")
add_layout_hotkey_operator(
cls.layout,
"Bulk Tag",
@@ -299,6 +301,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return
related_objects = bpy.context.selected_objects
created_objects = []
for related_object in related_objects:
obj = core.add_annotation(
tool.Ifc,
@@ -311,7 +315,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
),
enable_editing=False,
)
tool.Drawing.setup_annotation_object(obj, object_type, related_object)
tool.Drawing.setup_annotation_object(obj, object_type, related_object, props.tag_rotation_mode)
created_objects.append(obj)
# Select the created annotation objects
bpy.ops.object.select_all(action="DESELECT")
for obj in created_objects:
obj.select_set(True)
if created_objects:
bpy.context.view_layer.objects.active = created_objects[-1]
def hotkey_S_A(self):
if bpy.ops.bim.add_annotation.poll():
@@ -341,4 +353,5 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
continue
related_object = tool.Ifc.get_object(related_product)
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object)
rotation_mode = tool.Drawing.get_annotation_props().tag_rotation_mode
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object, rotation_mode)
@@ -109,8 +109,9 @@ def block_scale(scene: bpy.types.Scene) -> None:
if obj.type == "CAMERA":
camera = tool.Ifc.get_entity(obj)
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
obj.scale = (-1, -1, -1)
obj.rotation_euler = (0.0, 0.0, math.radians(180))
# Only update if scale isn't already (-1, -1, -1)
if obj.scale != (-1, -1, -1):
obj.scale = (-1, -1, -1)
else:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
@@ -858,6 +858,10 @@ class OverrideDelete(bpy.types.Operator):
objects_to_remove = context.selected_objects
self.process_arrays(context)
# Track aggregates before deleting their parts
aggregates_to_check = self.track_aggregates(objects_to_remove)
clear_active_object = True
for i, obj in enumerate(objects_to_remove, 1):
@@ -882,13 +886,28 @@ class OverrideDelete(bpy.types.Operator):
continue
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
return {"FINISHED"}
continue
if element.is_a("IfcGridAxis"):
# Deleting the last W axis is OK
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
(grid := element.PartOfV) and len(grid[0].VAxes) == 1
):
self.report(
{"INFO"}, "The last grid axis of a grid cannot be deleted. Delete the grid instead."
)
continue
if tool.Drawing.is_auto_annotation(element):
self.report({"INFO"}, "References cannot be deleted. Exclude the referenced element instead.")
continue
tool.Geometry.delete_ifc_object(obj)
elif tool.Geometry.is_representation_item(obj):
tool.Geometry.delete_ifc_item(obj)
else:
bpy.data.objects.remove(obj)
# Delete empty aggregates after deleting their parts
self.delete_empty_aggregates(aggregates_to_check)
for opening in tool.Model.get_model_props().openings:
if opening.obj is not None and not tool.Ifc.get_entity(opening.obj):
bpy.data.objects.remove(opening.obj)
@@ -921,6 +940,49 @@ class OverrideDelete(bpy.types.Operator):
data["old_file"].redo()
tool.Ifc.set(data["new_file"])
def track_aggregates(self, objects_to_remove):
"""Track aggregates that contain objects being deleted"""
aggregates_to_check = set()
for obj in objects_to_remove:
if not tool.Blender.is_valid_data_block(obj):
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
if aggregate:
aggregates_to_check.add(aggregate)
return aggregates_to_check
def delete_empty_aggregates(self, aggregates_to_check):
"""Delete aggregates that now have no parts"""
deleted_aggregates = []
for aggregate in aggregates_to_check:
# Check if aggregate still exists (might have been deleted already)
try:
aggregate.id()
except:
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()}"
deleted_aggregates.append(aggregate_name)
aggregate_obj = tool.Ifc.get_object(aggregate)
if aggregate_obj and tool.Blender.is_valid_data_block(aggregate_obj):
tool.Geometry.delete_ifc_object(aggregate_obj)
# Show info message if aggregates were deleted
if deleted_aggregates:
if len(deleted_aggregates) == 1:
self.report(
{"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
)
else:
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
def process_arrays(self, context: bpy.types.Context) -> None:
ifc_file = tool.Ifc.get()
selected_objects = set(context.selected_objects)
@@ -2973,6 +3035,7 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
item_mesh = bpy.data.meshes.new("tmp")
tool.Ifc.link(item, item_mesh)
item_obj = bpy.data.objects.new("tmp", item_mesh)
tool.Geometry.lock_scale(item_obj)
tool.Geometry.name_item_object(item_obj, item)
item_obj.matrix_world = obj.matrix_world
bpy.context.collection.objects.link(item_obj)
+1 -1
View File
@@ -41,7 +41,7 @@ class SolarData:
@classmethod
def sun_position(cls):
return tool.Blender.get_sun_position_addon()
return tool.Blender.get_addon("sun_position")
@classmethod
def sites(cls):
@@ -202,7 +202,7 @@ class RadianceRender(bpy.types.Operator):
print(f"Camera position: {camera_position}")
print(f"Camera direction: {camera_direction}")
# sun_position = tool.Blender.get_sun_position_addon()
# sun_position = tool.Blender.get_addon("sun_position")
# azimuth, elevation = sun_position.sun_calc.get_sun_coordinates(
# sun_pos_props.time,
# sun_pos_props.latitude,
+1 -1
View File
@@ -40,7 +40,7 @@ from bpy.types import PropertyGroup
from bonsai.bim.module.light.data import SolarData
from bonsai.bim.module.light.decorator import SolarDecorator
sun_position = tool.Blender.get_sun_position_addon()
sun_position = tool.Blender.get_addon("sun_position")
now = datetime.datetime.now()
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
+34 -3
View File
@@ -373,7 +373,12 @@ class BIMStairProperties(PropertyGroup):
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
self["nosing_length"] = 0
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type")
def update_custom_tread_lock(self, context: bpy.types.Context) -> None:
"""When lock is enabled, sync custom treads with tread_run"""
if self.custom_tread_lock:
self["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type", "custom_tread_lock")
is_editing: bpy.props.BoolProperty(default=False)
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
@@ -407,6 +412,12 @@ class BIMStairProperties(PropertyGroup):
default="CONCRETE",
update=validate_nosing_value,
)
custom_tread_lock: bpy.props.BoolProperty(
name="Lock First/Last Treads to Tread Run",
description="When enabled, first and last treads automatically use the Tread Run value",
default=True,
update=update_custom_tread_lock,
)
custom_first_last_tread_run: bpy.props.FloatVectorProperty(
name="Custom First / Last Treads Widths",
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.',
@@ -442,6 +453,7 @@ class BIMStairProperties(PropertyGroup):
top_slab_depth: float
has_top_nib: bool
stair_type: str
custom_tread_lock: bool
custom_first_last_tread_run: tuple[float, float]
nosing_length: float
nosing_depth: float
@@ -480,8 +492,11 @@ class BIMStairProperties(PropertyGroup):
}
stair_kwargs.update(generic_props)
# defined here to appear last in UI
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
# 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)
else:
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
if not convert_to_project_units:
return stair_kwargs
@@ -489,8 +504,24 @@ class BIMStairProperties(PropertyGroup):
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
return stair_kwargs
def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None):
"""Get props including custom_tread_lock for saving to IFC"""
stair_kwargs = self.get_props_kwargs(convert_to_project_units, stair_type)
# Add the lock state for IFC storage (after getting base kwargs to avoid passing to generate function)
stair_kwargs["custom_tread_lock"] = self.custom_tread_lock
return stair_kwargs
def set_props_kwargs_from_ifc_data(self, kwargs):
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
# 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)
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
+4 -2
View File
@@ -188,7 +188,8 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Model.get_stair_props(obj)
ifc_file = tool.Ifc.get()
stair_data = props.get_props_kwargs(convert_to_project_units=True)
# 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")
if not pset:
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Stair")
@@ -241,7 +242,8 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
assert element
props = tool.Model.get_stair_props(obj)
data = props.get_props_kwargs(convert_to_project_units=True)
# Use the special method that includes custom_tread_lock for IFC storage
data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
props.is_editing = False
regenerate_stair_mesh(obj)
tool.Model.add_body_representation(obj)
+26 -1
View File
@@ -302,13 +302,38 @@ class BIM_PT_stair(bpy.types.Panel):
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)
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
# 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")
@@ -2522,6 +2522,7 @@ class FlipClippingPlane(bpy.types.Operator):
obj = context.active_object
if obj in tool.Project.get_project_props().clipping_planes_objs:
obj.rotation_euler[0] += radians(180)
obj.rotation_euler[0] %= radians(360)
context.view_layer.update()
return {"FINISHED"}
+3 -3
View File
@@ -450,9 +450,9 @@ class BIMProjectProperties(PropertyGroup):
self.mvd = header_data.mvd
self.author_name = header_data.author_name
self.author_email = header_data.author_email
self.organisation_name = header_data.organization_name
self.organisation_email = header_data.organization_email
self.authorisation = header_data.authorization
self.organisation_name = header_data.organisation_name
self.organisation_email = header_data.organisation_email
self.authorisation = header_data.authorisation
if TYPE_CHECKING:
is_editing: bool
+2 -1
View File
@@ -653,4 +653,5 @@ class BIM_PT_purge(Panel):
row = layout.row(align=True)
row.label(text=f"{object_type.replace('_', ' ').capitalize()}:")
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type
row.operator("bim.merge_identical_objects", text="Merge Identical").object_type = object_type
merge_op = row.operator("bim.merge_identical_objects", text="Merge Identical")
merge_op.object_type = object_type
+10 -16
View File
@@ -242,14 +242,11 @@ class BIM_PT_object_psets(Panel):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
return False
ifc_id = tool.Blender.get_ifc_definition_id(obj)
if not ifc_id:
return False
if not tool.Ifc.get_object_by_identifier(ifc_id):
return False
return True
return (
(obj := tool.Blender.get_active_object())
and (element := tool.Ifc.get_entity(obj))
and element.is_a("IfcObjectDefinition")
)
def draw(self, context):
if not ObjectPsetsData.is_loaded:
@@ -388,14 +385,11 @@ class BIM_PT_object_qtos(Panel):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
return False
ifc_id = tool.Blender.get_ifc_definition_id(obj)
if not ifc_id:
return False
if not tool.Ifc.get_object_by_identifier(ifc_id):
return False
return True
return (
(obj := tool.Blender.get_active_object())
and (element := tool.Ifc.get_entity(obj))
and element.is_a("IfcObjectDefinition")
)
def draw(self, context):
if not ObjectQtosData.is_loaded:
@@ -317,6 +317,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
f"Mesh '{obj.data.name}' has loose geometry, loose geometry will be ignored to save mesh to IFC as a tessellation.",
)
representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
element = core.assign_class(
tool.Ifc,
tool.Collector,
@@ -326,13 +327,9 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
predefined_type=predefined_type,
should_add_representation=False,
)
representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
tool.Ifc, tool.Geometry, obj=obj, representation=representation
)
else:
+1 -1
View File
@@ -101,7 +101,7 @@ class ColourByPropertyData:
elif pset.endswith("BaseQuantities"):
keys.extend([f'/.*BaseQuantities/."{name}"' for name in properties.keys() if name != "id"])
else:
keys.extend([f"{pset}.{name}" for name in properties.keys() if name != "id"])
keys.extend([f'"{pset}"."{name}"' for name in properties.keys() if name != "id"])
results = [(k, k, "") for k in keys]
return default + results
@@ -523,8 +523,12 @@ class BIMWorkScheduleProperties(PropertyGroup):
active_task_input_index: IntProperty(name="Active Task Input Index")
task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct)
active_task_output_index: IntProperty(name="Active Task Output Index")
show_nested_outputs: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_outputs)
show_nested_resources: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_resources)
show_nested_outputs: BoolProperty(
name="Show Nested Task Elements", default=False, update=update_active_task_outputs
)
show_nested_resources: BoolProperty(
name="Show Nested Task Elements", default=False, update=update_active_task_resources
)
show_nested_inputs: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_inputs)
product_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct)
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
+5 -8
View File
@@ -34,14 +34,11 @@ class BIM_PT_type(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
return False
element = tool.Ifc.get_entity(context.active_object)
if not element:
return False
if not element.is_a("IfcProduct") and not element.is_a("IfcTypeProduct"):
return True
return True
return (
(obj := tool.Blender.get_active_object())
and (element := tool.Ifc.get_entity(obj))
and (element.is_a("IfcObject") or element.is_a("IfcTypeObject"))
)
def draw(self, context):
if not TypeData.is_loaded:
+30 -35
View File
@@ -108,10 +108,6 @@ def add_sheet(ifc: type[tool.Ifc], drawing: type[tool.Drawing], titleblock: ifco
def regenerate_sheet(
drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance
) -> Union[list[tool.Drawing.SheetWarningType], None]:
warnings = drawing.validate_sheet_files(sheet)
if warnings:
return warnings
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
assert titleblock_uri
@@ -320,6 +316,7 @@ def duplicate_drawing(
) -> ifcopenshell.entity_instance:
drawing_name = drawing_tool.ensure_unique_drawing_name(drawing_tool.get_name(drawing))
new_drawing = ifc.run("root.copy_class", product=drawing)
drawing_tool.clear_annotation_relationships(new_drawing)
drawing_tool.copy_representation(drawing, new_drawing)
drawing_tool.set_name(new_drawing, drawing_name)
group = drawing_tool.get_drawing_group(new_drawing)
@@ -475,46 +472,44 @@ def sync_references(
if not drawing_tool.has_annotation(drawing):
return
context = drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))
if not context:
if not (context := drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))):
return
group = drawing_tool.get_drawing_group(drawing)
for reference_element in drawing_tool.get_potential_reference_elements(drawing):
potential_reference_elements = drawing_tool.get_potential_reference_elements(drawing)
for element in potential_reference_elements:
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
drawing_tool.sync_object_placement(obj)
for element in drawing_tool.get_group_elements(group):
if not drawing_tool.is_auto_annotation(element):
continue
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
drawing_tool.sync_object_placement(obj)
if not (reference_element := drawing_tool.get_assigned_product(element)):
if obj := ifc.get_object(element):
drawing_tool.delete_object(obj)
ifc.run("root.remove_product", product=element)
continue
reference_obj = ifc.get_object(reference_element)
annotation = drawing_tool.get_drawing_reference_annotation(drawing, reference_element)
if reference_element not in potential_reference_elements:
# It was auto created, so it makes sense to auto delete
if obj := ifc.get_object(element):
drawing_tool.delete_object(obj)
ifc.run("root.remove_product", product=element)
elif not drawing_tool.regenerate_reference_annotation(drawing, element, reference_element, context):
if obj := ifc.get_object(element):
drawing_tool.delete_object(obj)
ifc.run("root.remove_product", product=element)
should_delete_existing_annotation = False
should_create_annotation = False
# remove annotation only if the reference object was changed
# otherwise we rely on the existing annotation
if annotation:
if reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)):
should_delete_existing_annotation = True
if should_delete_existing_annotation or not annotation:
should_create_annotation = True
if should_delete_existing_annotation:
annotation_obj = ifc.get_object(annotation)
if annotation_obj:
drawing_tool.delete_object(annotation_obj)
ifc.run("root.remove_product", product=annotation)
if should_create_annotation:
annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context)
if annotation:
for reference_element in potential_reference_elements:
if not drawing_tool.get_drawing_reference_annotation(drawing, reference_element):
if annotation := drawing_tool.generate_reference_annotation(drawing, reference_element, context):
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation)
ifc.run("group.assign_group", group=group, products=[annotation])
collector.assign(ifc.get_object(annotation))
if reference_obj and ifc.is_moved(reference_obj):
drawing_tool.sync_object_placement(reference_obj)
if reference_obj and ifc.is_edited(reference_obj):
drawing_tool.sync_object_representation(reference_obj)
def select_assigned_product(drawing: type[tool.Drawing], context: bpy.types.Context) -> None:
drawing.select_assigned_product(context)
+23 -12
View File
@@ -1506,28 +1506,21 @@ class Blender(bonsai.core.tool.Blender):
return bpy.context.preferences.addons[blender_package_name].preferences
@classmethod
def get_sun_position_addon(cls) -> Union[types.ModuleType, None]:
# Check if it's installed as legacy Blender addon.
def get_addon(cls, name: str) -> Union[types.ModuleType, None]:
import importlib
try:
sun_position = importlib.import_module("sun_position")
return importlib.import_module(name) # Legacy Blender addon
except ImportError:
sun_position = None
if sun_position:
return sun_position
pass
for package_name in bpy.context.preferences.addons.keys():
if package_name.endswith(".sun_position"):
if package_name.endswith(f".{name}"):
try:
sun_position = importlib.import_module(package_name)
return sun_position
return importlib.import_module(package_name)
except ModuleNotFoundError:
pass
return sun_position
@classmethod
def get_sun_props(cls) -> Union[SunPosProperties, None]:
assert (scene := bpy.context.scene)
@@ -1889,6 +1882,24 @@ class Blender(bonsai.core.tool.Blender):
obj.select_set(True)
bpy.context.view_layer.objects.active = previously_active
@classmethod
def sync_render_visibility(cls):
# Doing bpy.ops.object.hide_render_clear_all() or
# bpy.ops.object.isolate_type_render() is extremely slow.
# Hopefully this doesn't crash on Windows, it doesn't crash on Linux.
should_hides = [0 if obj.visible_get() else 1 for obj in bpy.data.objects]
should_hides = np.fromiter(should_hides, dtype=np.uint8, count=len(should_hides))
bpy.data.objects.foreach_set("hide_render", should_hides)
return # Otherwise...
# for obj in bpy.data.objects:
# if not obj.data:
# continue
# # For speed, check equality prior to change to prevent needless updates
# if (is_visible := obj.visible_get()) and obj.hide_render is True:
# obj.hide_render = False
# elif not is_visible and obj.hide_render is False:
# obj.hide_render = True
@classmethod
def hide_objects(cls, objs):
previously_selected = {o.name for o in bpy.context.selected_objects}
+10 -3
View File
@@ -51,9 +51,13 @@ class Collector(bonsai.core.tool.Collector):
if tool.Geometry.is_locked(element):
tool.Geometry.lock_object(obj)
element = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
if not tool.Spatial.get_grid_props().is_visible:
obj.hide_viewport = True
elif element.is_a("IfcGrid"):
if tool.Geometry.is_locked(element):
tool.Geometry.lock_object(obj)
if not tool.Spatial.get_grid_props().is_visible:
obj.hide_viewport = True
if element.is_a("IfcProject"):
if tool.Geometry.is_locked(element):
@@ -69,7 +73,8 @@ class Collector(bonsai.core.tool.Collector):
tool.Geometry.lock_object(obj)
collection = cls._create_project_child_collection("IfcSpace")
cls.link_collection_object_safe(collection, obj)
obj.hide_viewport = True
if not tool.Spatial.get_spatial_props().is_visible:
obj.hide_viewport = True
elif element.is_a("IfcStructuralItem"):
collection = cls._create_project_child_collection("IfcStructuralItem")
cls.link_collection_object_safe(collection, obj)
@@ -93,7 +98,8 @@ class Collector(bonsai.core.tool.Collector):
cls.link_collection_object_safe(collection, obj)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
obj.hide_viewport = True
if not tool.Spatial.get_spatial_props().is_visible:
obj.hide_viewport = True
elif (
tool.Ifc.get_schema() != "IFC2X3"
and element.is_a("IfcSpatialElement")
@@ -105,7 +111,8 @@ class Collector(bonsai.core.tool.Collector):
cls.link_collection_object_safe(collection, obj)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
obj.hide_viewport = True
if not tool.Spatial.get_spatial_props().is_visible:
obj.hide_viewport = True
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
if collection := cls._create_own_collection(obj):
cls.link_collection_object_safe(collection, obj)
+6 -8
View File
@@ -273,18 +273,16 @@ class Cost(bonsai.core.tool.Cost):
return
props = cls.get_cost_props()
props.cost_item_type_products.clear()
# TODO implement process and resource types
# props.cost_item_processes.clear()
# props.cost_item_resources.clear()
props.cost_item_processes.clear()
props.cost_item_resources.clear()
for rel in cost_item.Controls or []:
for related_object in rel.RelatedObjects:
if related_object.is_a("IfcTypeProduct"):
new = props.cost_item_type_products.add()
# TODO implement process and resource types
# elif related_object.is_a("IfcProcess"):
# new = props.cost_item_processes.add()
# elif related_object.is_a("IfcResource"):
# new = props.cost_item_resources.add()
elif related_object.is_a("IfcProcess"):
new = props.cost_item_processes.add()
elif related_object.is_a("IfcResource"):
new = props.cost_item_resources.add()
new.ifc_definition_id = related_object.id()
new.name = related_object.Name or "Unnamed"
+104 -17
View File
@@ -18,6 +18,7 @@
from __future__ import annotations
import os
import re
import json
import bmesh
import bpy
@@ -33,7 +34,7 @@ import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from mathutils import Vector
from collections import defaultdict
from typing import Literal, TYPE_CHECKING, assert_never
from typing import Literal, TYPE_CHECKING, assert_never, Union
from collections.abc import Iterable
if TYPE_CHECKING:
@@ -136,12 +137,30 @@ class Debug(bonsai.core.tool.Debug):
"PERSON",
"PERSON_AND_ORGANIZATION",
],
by_name_or_identification_only: bool = False,
) -> dict[str, list[str]]:
"""Merge identical objects.
Note that Styles UI (or other UI) should be updated manually after using this method.
Args:
object_type: The type of object to merge
by_name_or_identification_only: If True, merge based only on Name attribute (or equivalent identifier).
Strips .XXX suffix patterns (e.g., 'foo.001' matches 'foo', 'foo.002').
For PERSON, uses Identification. For APPLICATION, uses ApplicationFullName.
For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers.
"""
def normalize_name(name: str) -> str:
"""Remove .XXX suffix pattern from names (e.g., 'foo.001' -> 'foo')"""
if not name:
return ""
# Match pattern: name ending with .digits
match = re.match(r"^(.+)\.\d+$", name)
if match:
return match.group(1)
return name
def get_hash(element: ifcopenshell.entity_instance) -> int:
data = element.get_info_2(include_identifier=False, recursive=True)
if object_type == "APPLICATION":
@@ -152,6 +171,49 @@ class Debug(bonsai.core.tool.Debug):
data["TheOrganization"] = element.TheOrganization.id()
return hash(json.dumps(data, sort_keys=True))
def get_name_key(element: ifcopenshell.entity_instance) -> str:
"""Get key based on name/identifier attribute for the given object type"""
if object_type == "STYLE":
name = element.Name if element.Name else ""
return normalize_name(name)
elif object_type == "MATERIAL":
name = element.Name if element.Name else ""
return normalize_name(name)
elif object_type == "ORGANIZATION":
name = element.Name if element.Name else ""
return normalize_name(name)
elif object_type == "APPLICATION":
name = element.ApplicationFullName if element.ApplicationFullName else ""
return normalize_name(name)
elif object_type == "PERSON":
ident = element.Identification if element.Identification else ""
return normalize_name(ident)
elif object_type == "PERSON_AND_ORGANIZATION":
person_id = element.ThePerson.Identification if element.ThePerson.Identification else ""
org_name = element.TheOrganization.Name if element.TheOrganization.Name else ""
return f"{normalize_name(person_id)}|{normalize_name(org_name)}"
else:
assert_never(object_type)
def get_element_name(element: ifcopenshell.entity_instance) -> str:
"""Get the actual name/identifier from element for sorting purposes"""
if object_type == "STYLE":
return element.Name if element.Name else ""
elif object_type == "MATERIAL":
return element.Name if element.Name else ""
elif object_type == "ORGANIZATION":
return element.Name if element.Name else ""
elif object_type == "APPLICATION":
return element.ApplicationFullName if element.ApplicationFullName else ""
elif object_type == "PERSON":
return element.Identification if element.Identification else ""
elif object_type == "PERSON_AND_ORGANIZATION":
person_id = element.ThePerson.Identification if element.ThePerson.Identification else ""
org_name = element.TheOrganization.Name if element.TheOrganization.Name else ""
return f"{person_id}|{org_name}"
else:
assert_never(object_type)
ifc_file = tool.Ifc.get()
merged_element_types: dict[str, list[str]] = {}
@@ -179,22 +241,47 @@ class Debug(bonsai.core.tool.Debug):
for element_type in element_types:
elements = ifc_file.by_type(element_type, include_subtypes=False)
# Calculate hashes.
hash_to_elements: defaultdict[int, list[ifcopenshell.entity_instance]] = defaultdict(list)
for element in elements:
# Except for styles, ignore unnamed elements as they may be not safe to merge
merge_optional_names = ("STYLE", "PERSON")
not_optional_name = ("APPLICATION", "ORGANIZATION")
has_no_name = ("PERSON_AND_ORGANIZATION",)
if (
object_type not in merge_optional_names
and object_type not in not_optional_name
and object_type not in has_no_name
and not element.Name
):
continue
element_hash = get_hash(element)
hash_to_elements[element_hash].append(element)
# Calculate hashes or name keys.
hash_to_elements: defaultdict[Union[int, str], list[ifcopenshell.entity_instance]]
if by_name_or_identification_only:
# Group by name/identifier only (with .XXX suffix normalization)
hash_to_elements = defaultdict(list)
for element in elements:
name_key = get_name_key(element)
# Skip elements without a valid identifier
if not name_key:
continue
hash_to_elements[name_key].append(element)
# Sort elements within each group to keep the one without suffix (or lowest suffix)
for name_key in hash_to_elements:
# Sort by: 1) prefer names without .XXX suffix, 2) then by original name
def sort_key(el):
name = get_element_name(el)
# Check if name has .XXX suffix
has_suffix = bool(re.match(r"^.+\.\d+$", name))
# Return tuple: (has_suffix, name) - sort by no suffix first, then alphabetically
return (has_suffix, name)
hash_to_elements[name_key].sort(key=sort_key)
else:
# Group by full hash
hash_to_elements = defaultdict(list)
for element in elements:
# Except for styles, ignore unnamed elements as they may be not safe to merge
merge_optional_names = ("STYLE", "PERSON")
not_optional_name = ("APPLICATION", "ORGANIZATION")
has_no_name = ("PERSON_AND_ORGANIZATION",)
if (
object_type not in merge_optional_names
and object_type not in not_optional_name
and object_type not in has_no_name
and not element.Name
):
continue
element_hash = get_hash(element)
hash_to_elements[element_hash].append(element)
merged_elements_names: list[str] = []
# Merge elements.
+498 -280
View File
@@ -208,6 +208,27 @@ class Drawing(bonsai.core.tool.Drawing):
return obj
@classmethod
def get_annotation_drawing(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance | None:
for rel in element.HasAssignments:
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
for e in rel.RelatedObjects:
if e.ObjectType == "DRAWING":
return e
@classmethod
def exclude_annotation_from_drawing(
cls, element: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance
) -> None:
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
if not pset:
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=drawing, name="EPset_Drawing")
exclude = ifcopenshell.util.element.get_property_definition(pset, prop="Exclude") or ""
if exclude:
exclude += "+"
exclude += element.GlobalId
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": exclude})
@classmethod
def ensure_annotation_in_drawing_plane(
cls, obj: bpy.types.Object, camera: Optional[bpy.types.Object] = None
@@ -218,11 +239,7 @@ class Drawing(bonsai.core.tool.Drawing):
entity = tool.Ifc.get_entity(obj)
if not entity:
return
for rel in entity.HasAssignments:
if rel.is_a("IfcRelAssignsToGroup"):
for e in rel.RelatedObjects:
if e.ObjectType == "DRAWING":
return tool.Ifc.get_object(e)
return tool.Ifc.get_object(cls.get_annotation_drawing(entity))
if not camera:
camera = get_camera_from_annotation_object(obj) or bpy.context.scene.camera
@@ -236,7 +253,11 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def setup_annotation_object(
cls, obj: bpy.types.Object, object_type: str, related_object: Optional[bpy.types.Object] = None
cls,
obj: bpy.types.Object,
object_type: str,
related_object: Optional[bpy.types.Object] = None,
rotation_mode: str = "NONE",
) -> None:
"""Finish object's adjustments after both object and entity are created"""
@@ -308,6 +329,55 @@ class Drawing(bonsai.core.tool.Drawing):
ifc_file, relating_product=related_entity, related_object=obj_entity
)
if rotation_mode != "NONE" and related_object:
cls.apply_annotation_rotation(obj, related_object, rotation_mode)
@classmethod
def apply_annotation_rotation(
cls, tag_obj: bpy.types.Object, related_object: bpy.types.Object, rotation_mode: str
) -> None:
"""Apply rotation to annotation based on the selected rotation mode"""
camera = bpy.context.scene.camera
camera_right = camera.matrix_world.to_3x3() @ mathutils.Vector((1, 0, 0))
if rotation_mode == "CAMERA_Horizontal":
location = tag_obj.location.copy()
camera_matrix = camera.matrix_world.copy()
camera_matrix.translation = location
tag_obj.matrix_world = camera_matrix
elif rotation_mode == "CAMERA_Vertical":
location = tag_obj.location.copy()
camera_matrix = camera.matrix_world.copy()
camera_matrix.translation = location
rotation_90z = mathutils.Matrix.Rotation(math.pi / 2, 4, "Z")
camera_matrix = camera_matrix @ rotation_90z
tag_obj.matrix_world = camera_matrix
elif rotation_mode == "LOCAL_X":
local_x = related_object.matrix_world.to_3x3() @ mathutils.Vector((1, 0, 0))
local_x = local_x.normalized()
if local_x.dot(camera_right) < 0:
local_x = -local_x
tag_obj.rotation_euler = local_x.to_track_quat("X", "Z").to_euler()
elif rotation_mode == "LOCAL_Y":
local_y = related_object.matrix_world.to_3x3() @ mathutils.Vector((0, 1, 0))
local_y = local_y.normalized()
if local_y.dot(camera_right) < 0:
local_y = -local_y
tag_obj.rotation_euler = local_y.to_track_quat("X", "Z").to_euler()
elif rotation_mode == "LOCAL_Z":
local_z = related_object.matrix_world.to_3x3() @ mathutils.Vector((0, 0, 1))
local_z = local_z.normalized()
if local_z.dot(camera_right) < 0:
local_z = -local_z
tag_obj.rotation_euler = local_z.to_track_quat("X", "Z").to_euler()
@classmethod
def is_annotation_object_type(
cls, element: ifcopenshell.entity_instance, object_types: Union[str, Sequence[str]]
@@ -815,7 +885,13 @@ class Drawing(bonsai.core.tool.Drawing):
def get_assigned_product(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
for rel in element.HasAssignments:
if rel.is_a("IfcRelAssignsToProduct"):
return rel.RelatingProduct
product = rel.RelatingProduct
if product.is_a("IfcGrid") and rel.Name:
for attribute in ("UAxes", "VAxes", "WAxes"):
for axis in getattr(product, attribute) or []:
if axis.AxisTag == rel.Name:
return axis
return product
@classmethod
def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None:
@@ -1401,7 +1477,10 @@ class Drawing(bonsai.core.tool.Drawing):
cls, drawing: ifcopenshell.entity_instance
) -> list[ifcopenshell.entity_instance]:
elements = []
existing_references = cls.get_group_elements(cls.get_drawing_group(drawing))
existing_references = set(cls.get_group_elements(cls.get_drawing_group(drawing)))
if exclude := ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "Exclude"):
existing_references.update(ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), exclude))
for element in tool.Ifc.get().by_type("IfcAnnotation"):
if element in existing_references or element == drawing:
continue
@@ -1411,14 +1490,25 @@ class Drawing(bonsai.core.tool.Drawing):
"GlobalReferencing", False
):
elements.append(element)
for element in tool.Ifc.get().by_type("IfcGridAxis"):
elements.append(element)
for element in tool.Ifc.get().by_type("IfcGrid"):
if element in existing_references:
continue
for axis in element.UAxes + element.VAxes + (element.WAxes or tuple()):
if axis in existing_references:
continue
elements.append(axis)
target_view = tool.Drawing.get_drawing_target_view(drawing)
if target_view in ("SECTION_VIEW", "ELEVATION_VIEW"):
for element in tool.Ifc.get().by_type("IfcBuildingStorey"):
if element in existing_references:
continue
elements.append(element)
return elements
@classmethod
def is_auto_annotation(cls, element: ifcopenshell.entity_instance):
return element.is_a("IfcAnnotation") and element.ObjectType in ("GRID", "SECTION", "ELEVATION", "SECTION_LEVEL")
@classmethod
def get_drawing_reference_annotation(
cls, drawing: ifcopenshell.entity_instance, reference_element: ifcopenshell.entity_instance
@@ -1430,7 +1520,7 @@ class Drawing(bonsai.core.tool.Drawing):
# IfcRelAssignsToProduct.RelatingProduct = IfcGrid
# IfcRelAssignsToProduct.Name = IfcGridAxis.AxisTag
grid = None
for attribute in ("PartOfW", "PartOfV", "PartOfU"):
for attribute in ("PartOfU", "PartOfV", "PartOfW"):
if getattr(reference_element, attribute, None):
grid = getattr(reference_element, attribute)[0]
break
@@ -1452,6 +1542,26 @@ class Drawing(bonsai.core.tool.Drawing):
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == reference_element:
return element
@classmethod
def regenerate_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
annotation: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
if reference_element.is_a("IfcGridAxis"):
return cls.regenerate_grid_axis_reference_annotation(drawing, annotation, reference_element, context)
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
if target_view == "ELEVATION_VIEW":
return cls.regenerate_elevation_reference_annotation(drawing, annotation, reference_element, context)
elif target_view == "SECTION_VIEW":
return cls.regenerate_section_reference_annotation(drawing, annotation, reference_element, context)
elif reference_element.is_a("IfcBuildingStorey"):
return cls.regenerate_storey_annotation(drawing, annotation, reference_element, context)
return annotation
@classmethod
def generate_reference_annotation(
cls,
@@ -1461,333 +1571,410 @@ class Drawing(bonsai.core.tool.Drawing):
) -> ifcopenshell.entity_instance:
if reference_element.is_a("IfcGridAxis"):
return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context)
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
def ensure_referenced_drawing_obj_exists(drawing: ifcopenshell.entity_instance):
obj = tool.Ifc.get_object(drawing)
if obj is None:
# Annotations in that drawing are lazy loaded as needed
obj = cls.import_drawing(drawing)
tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
if target_view == "ELEVATION_VIEW":
ensure_referenced_drawing_obj_exists(reference_element)
return cls.generate_elevation_reference_annotation(drawing, reference_element, context)
elif target_view == "SECTION_VIEW":
ensure_referenced_drawing_obj_exists(reference_element)
return cls.generate_section_reference_annotation(drawing, reference_element, context)
elif reference_element.is_a("IfcBuildingStorey"):
return cls.generate_storey_annotation(drawing, reference_element, context)
@classmethod
def generate_storey_points(
cls, drawing: ifcopenshell.entity_instance, storey: ifcopenshell.entity_instance
) -> list | None:
import bonsai.bim.module.drawing.helper as helper
camera = tool.Ifc.get_object(drawing)
if camera.data.type != "ORTHO":
return
if not cls.is_matrix_perpendicular(camera.matrix_world, Matrix()):
return
xmin, xmax, ymin, ymax = helper.ortho_view_frame(camera.data)[:4]
rl = ifcopenshell.util.placement.get_local_placement(storey.ObjectPlacement)[2][3]
y = (camera.matrix_world.inverted() @ Vector((0.0, 0.0, rl))).y
if y < ymin or y > ymax:
return
return (Vector((xmax, y, 0.0)), Vector((xmin, y, 0.0)))
@classmethod
def generate_storey_annotation(
cls,
drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance,
storey: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
if not (points := cls.generate_storey_points(drawing, storey)):
return
camera = tool.Ifc.get_object(drawing)
mesh = bpy.data.meshes.new("Mesh")
obj = bpy.data.objects.new(storey.Name or "Unnamed", mesh)
obj.matrix_world = cls.get_default_annotation_matrix(camera)
element = cls.run_root_assign_class(
obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION_LEVEL", should_add_representation=False
)
tool.Geometry.run_edit_object_placement(obj)
element.Name = storey.Name or "Unnamed"
builder = ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
points = [p / unit_scale for p in points]
representation = builder.get_representation(context, [builder.polyline(points)])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
return element
@classmethod
def regenerate_storey_annotation(
cls,
drawing: ifcopenshell.entity_instance,
annotation: ifcopenshell.entity_instance,
storey: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
if not (points := cls.generate_storey_points(drawing, storey)):
return
camera = tool.Ifc.get_object(drawing)
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, annotation)
m = ifcopenshell.util.shape.get_shape_matrix(shape)
mw = cls.get_default_annotation_matrix(camera)
existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
new_points = None
if not np.allclose(m, np.array(mw), atol=1e-4):
new_points = points
elif len(existing_verts) != 2:
new_points = points
else:
existing_verts = sorted(existing_verts, key=lambda v: v.x)
xmin, xmax = [v.x for v in existing_verts]
y = points[0].y
if not tool.Cad.is_x(y, existing_verts[0].y) or not tool.Cad.is_x(y, existing_verts[1].y):
new_points = (Vector((xmax, y, 0.0)), Vector((xmin, y, 0.0)))
if new_points:
if representation := ifcopenshell.util.representation.get_representation(annotation, context):
ifcopenshell.api.geometry.unassign_representation(
tool.Ifc.get(), product=annotation, representation=representation
)
builder = ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
new_points = [p / unit_scale for p in new_points]
representation = builder.get_representation(context, [builder.polyline(new_points)])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
if obj := tool.Ifc.get_object(annotation):
obj.matrix_world = mw
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
bonsai.core.geometry.switch_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=representation
)
else:
ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
annotation.Name = storey.Name or "Unnamed"
return annotation
@classmethod
def generate_section_reference_points(
cls, drawing: ifcopenshell.entity_instance, section: ifcopenshell.entity_instance
) -> list | None:
import bonsai.bim.module.drawing.helper as helper
camera = tool.Ifc.get_object(drawing)
assert isinstance(camera, bpy.types.Object)
assert isinstance((camera_data := camera.data), bpy.types.Camera)
props = tool.Drawing.get_camera_props(camera_data)
if camera.data.type != "ORTHO":
return
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, section)
m = ifcopenshell.util.shape.get_shape_matrix(shape)
if not cls.is_matrix_perpendicular(camera.matrix_world, Matrix(m)):
return
if not cls.does_shape_intersect_camera(shape, camera):
return
bounds = helper.ortho_view_frame(camera_data) if camera_data.type == "ORTHO" else None
reference_obj = tool.Ifc.get_object(reference_element)
assert isinstance(reference_obj, bpy.types.Object)
# Get cutting plane as a line
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
cutting_plane_verts = sorted(verts, key=lambda v: v[2])[-4:]
v1, *_, v2 = sorted(cutting_plane_verts, key=lambda v: v[0]) # Cut is in +X direction
im = camera.matrix_world.inverted()
v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in [v1, v2]]
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
mat = reference_obj.matrix_world.copy()
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
xyz[2] = 0
xyz = camera.matrix_world @ xyz
mat.translation = xyz
annotation_offset = mathutils.Vector((0, 0, -camera_data.clip_start - 0.05))
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
mat.translation += annotation_offset
return mat
def project_point_onto_camera(point: Vector, camera: bpy.types.Object) -> Vector:
projection = camera.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1))
return camera.matrix_world.inverted() @ mathutils.geometry.intersect_line_plane(
point.xyz, point.xyz - projection, camera.location, projection
)
obj_matrix = to_camera_coords(camera, reference_obj)
if props.raster_x > props.raster_y:
width = camera_data.ortho_scale
height = width / props.raster_x * props.raster_y
else:
height = camera_data.ortho_scale
width = height / props.raster_y * props.raster_x
projection = project_point_onto_camera(reference_obj.location, camera)
co1 = camera.matrix_world @ mathutils.Vector((width / 2, projection[1], -1))
co2 = camera.matrix_world @ mathutils.Vector((-(width / 2), projection[1], -1))
co1 = obj_matrix.inverted() @ co1
co2 = obj_matrix.inverted() @ co2
data = bpy.data.curves.new("Annotation", type="CURVE")
data.dimensions = "3D"
data.resolution_u = 2
polyline = data.splines.new("POLY")
polyline.points.add(1)
polyline.points[-2].co = list(co1) + [1]
polyline.points[-1].co = list(co2) + [1]
obj = bpy.data.objects.new(reference_obj.name, data)
obj.matrix_world = obj_matrix
element = cls.run_root_assign_class(
obj=obj,
ifc_class="IfcAnnotation",
predefined_type="SECTION_LEVEL",
should_add_representation=True,
context=context,
ifc_representation_class=None,
)
if representation := ifcopenshell.util.representation.get_representation(element, context):
cls.reload_representation(obj=obj, representation=representation)
bpy.data.curves.remove(data)
return element
bounds = helper.ortho_view_frame(camera.data)
if not (points := helper.clip_segment(bounds, [v1, v2])):
return
for v in points:
v.z = 0
return points
@classmethod
def generate_section_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance,
section: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
import bonsai.bim.module.drawing.helper as helper
if not (points := cls.generate_section_reference_points(drawing, section)):
return
reference_obj = tool.Ifc.get_object(reference_element)
reference_obj.matrix_world
camera = tool.Ifc.get_object(drawing)
bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None
mesh = bpy.data.meshes.new("Mesh")
obj = bpy.data.objects.new(section.Name or "Unnamed", mesh)
obj.matrix_world = cls.get_default_annotation_matrix(camera)
element = cls.run_root_assign_class(
obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION", should_add_representation=False
)
element.Name = section.Name or "Unnamed"
builder = ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
points = [p / unit_scale for p in points]
representation = builder.get_representation(context, [builder.polyline(points)])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
return element
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
mat = reference_obj.matrix_world.copy()
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
xyz[2] = 0
xyz = camera.matrix_world @ xyz
mat.translation = xyz
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
mat.translation += annotation_offset
return mat
@classmethod
def regenerate_section_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
annotation: ifcopenshell.entity_instance,
section: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
if not (points := cls.generate_section_reference_points(drawing, section)):
return
def clip_to_camera_boundary(
mesh: bpy.types.Mesh, bounds: tuple[float, float, float, float, float, float]
) -> Union[bpy.types.Mesh, None]:
mesh.verts.ensure_lookup_table()
points = [v.co for v in mesh.verts[0:2]]
points = helper.clip_segment(bounds, points)
if points is None:
return None
mesh.verts[0].co = points[0]
mesh.verts[1].co = points[1]
return mesh
camera = tool.Ifc.get_object(drawing)
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, annotation)
m = ifcopenshell.util.shape.get_shape_matrix(shape)
mw = cls.get_default_annotation_matrix(camera)
existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj):
reference_mesh = cls.get_camera_block(reference_obj)
obj_matrix = to_camera_coords(camera, reference_obj)
new_points = None
if not np.allclose(m, np.array(mw), atol=1e-4):
new_points = points
elif len(existing_verts) != 2:
new_points = points
else:
if not tool.Cad.are_edges_collinear(existing_verts, points):
# Attempt to update the section line by projecting existing verts onto the new line
v1 = tool.Cad.point_on_edge(existing_verts[0], points)
v2 = tool.Cad.point_on_edge(existing_verts[1], points)
existing_length = (existing_verts[0] - existing_verts[1]).length
new_length = (v2 - v1).length
if abs((existing_length - new_length) / existing_length) <= 0.10:
# If the projected line is within 10% of the previous length ...
new_points = (v1, v2)
else:
new_points = points
# The reference mesh vertices represent a view cube. To convert
# this into a section line we:
# 1. Select the 4 +Z vertices local to the reference element. This
# is the cutting plane.
verts_local_to_reference = [reference_obj.matrix_world.inverted() @ v for v in reference_mesh["verts"]]
cutting_plane_verts = sorted(verts_local_to_reference, key=lambda x: x.z)[-4:]
global_cutting_plane_verts = [reference_obj.matrix_world @ v for v in cutting_plane_verts]
# 2. Project the cutting plane onto our viewing camera.
verts_local_to_camera = [camera.matrix_world.inverted() @ v for v in global_cutting_plane_verts]
# 3. Collapse verts with the same XY coords, and set Z to be just
# below the clip_start so it's visible
collapsed_verts = []
for vert in verts_local_to_camera:
if not [True for v in collapsed_verts if (vert.xy - v.xy).length < 1e-2]:
collapsed_verts.append(mathutils.Vector((vert.x, vert.y, -camera.data.clip_start - 0.05)))
# 4. The first two vertices is the section line
section_line = collapsed_verts[0:2]
# 5. Sort the vertices in the +X direction so that the vertices are
# ordered to "point" in the direction of the section cut.
section_line = sorted(
section_line, key=lambda co: (reference_obj.matrix_world.inverted() @ camera.matrix_world @ co).x
)
global_section_line = [camera.matrix_world @ v for v in section_line]
local_section_line = [obj_matrix.inverted() @ v for v in global_section_line]
mesh = bpy.data.meshes.new(name="Annotation")
mesh.from_pydata(local_section_line, [(0, 1)], [])
bm = bmesh.new()
bm.from_mesh(mesh)
bm = clip_to_camera_boundary(bm, bounds)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(reference_obj.name, mesh)
obj.matrix_world = obj_matrix
element = cls.run_root_assign_class(
obj=obj,
ifc_class="IfcAnnotation",
predefined_type="SECTION",
should_add_representation=True,
context=context,
ifc_representation_class=None,
)
if representation := ifcopenshell.util.representation.get_representation(element, context):
cls.reload_representation(obj=obj, representation=representation)
return element
if new_points:
if representation := ifcopenshell.util.representation.get_representation(annotation, context):
ifcopenshell.api.geometry.unassign_representation(
tool.Ifc.get(), product=annotation, representation=representation
)
builder = ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
new_points = [p / unit_scale for p in new_points]
representation = builder.get_representation(context, [builder.polyline(new_points)])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
if obj := tool.Ifc.get_object(annotation):
obj.matrix_world = mw
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
bonsai.core.geometry.switch_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=representation
)
else:
ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
annotation.Name = section.Name or "Unnamed"
return annotation
@classmethod
def generate_elevation_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance,
elevation: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
reference_obj = tool.Ifc.get_object(reference_element)
reference_obj.matrix_world
camera = tool.Ifc.get_object(drawing)
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
mat = reference_obj.matrix_world.copy()
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
xyz[2] = 0
xyz = camera.matrix_world @ xyz
mat.translation = xyz
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
mat.translation += annotation_offset
return mat
if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj):
obj = bpy.data.objects.new(reference_obj.name, None)
if camera.data.type != "ORTHO":
return
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, elevation)
m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera):
obj = bpy.data.objects.new(elevation.Name or "Unnamed", None)
obj.empty_display_size = 0.1
obj.matrix_world = to_camera_coords(camera, reference_obj)
obj.matrix_world = cls.get_default_annotation_matrix(camera, matrix_world=m)
element = cls.run_root_assign_class(
obj=obj,
ifc_class="IfcAnnotation",
predefined_type="ELEVATION",
should_add_representation=False,
context=context,
ifc_representation_class=None,
obj=obj, ifc_class="IfcAnnotation", predefined_type="ELEVATION", should_add_representation=False
)
if representation := ifcopenshell.util.representation.get_representation(element, context):
cls.reload_representation(obj=obj, representation=representation)
element.Name = elevation.Name or "Unnamed"
return element
@classmethod
def regenerate_elevation_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
annotation: ifcopenshell.entity_instance,
elevation: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
camera = tool.Ifc.get_object(drawing)
if camera.data.type != "ORTHO":
return
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, elevation)
m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera):
existing_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(annotation.ObjectPlacement))
# The user is allowed to shift the elevation, but not rotate it
if not np.allclose(np.array(m.to_3x3()), np.array(existing_matrix.to_3x3()), atol=1e-4):
mw = cls.get_default_annotation_matrix(camera, matrix_world=m)
if obj := tool.Ifc.get_object(annotation):
obj.matrix_world = mw
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
else:
ifcopenshell.api.geometry.edit_object_placement(
tool.Ifc.get(), product=annotation, matrix=np.array(mw)
)
annotation.Name = elevation.Name or "Unnamed"
return annotation
@classmethod
def generate_grid_axis_reference_points(
cls, drawing: ifcopenshell.entity_instance, axis: ifcopenshell.entity_instance
) -> list | None:
import bonsai.bim.module.drawing.helper as helper
camera = tool.Ifc.get_object(drawing)
if camera.data.type != "ORTHO":
return
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
geometry = ifcopenshell.geom.create_shape(settings, axis.AxisCurve)
verts = ifcopenshell.util.shape.get_vertices(geometry)
grid = (axis.PartOfU or axis.PartOfV or axis.PartOfW)[0]
m = ifcopenshell.util.placement.get_local_placement(grid.ObjectPlacement)
im = camera.matrix_world.inverted()
v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in verts[:2]]
target_view = tool.Drawing.get_drawing_target_view(drawing)
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW"):
bounds = helper.ortho_view_frame(camera.data)
if not (points := helper.clip_segment(bounds, [v1, v2])):
return
elif target_view in ("ELEVATION_VIEW", "SECTION_VIEW"):
bounds = helper.ortho_view_frame(camera.data)
if not (points := helper.elevate_segment(bounds, [v1, v2])):
return
else:
return
for v in points:
v.z = 0
return points
@classmethod
def generate_grid_axis_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance,
axis: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]:
import bonsai.bim.module.drawing.helper as helper
target_view = tool.Drawing.get_drawing_target_view(drawing)
if not (points := cls.generate_grid_axis_reference_points(drawing, axis)):
return
camera = tool.Ifc.get_object(drawing)
assert isinstance(camera, bpy.types.Object)
assert isinstance(camera_data := camera.data, bpy.types.Camera)
is_ortho = camera_data.type == "ORTHO"
bounds = helper.ortho_view_frame(camera_data) if is_ortho else None
clipping = is_ortho and target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW")
elevating = is_ortho and target_view in ("ELEVATION_VIEW", "SECTION_VIEW")
def clone(src: bpy.types.Object) -> bpy.types.Object:
dst = src.copy()
assert isinstance(dst.data, bpy.types.Mesh)
dst.data = dst.data.copy()
dst.name = dst.name.replace("IfcGridAxis/", "")
tool.Blender.get_object_bim_props(dst).ifc_definition_id = 0
tool.Geometry.get_mesh_props(dst.data).ifc_definition_id = 0
return dst
def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]:
assert isinstance(obj.data, bpy.types.Mesh)
mesh = bmesh.new()
mesh.verts.ensure_lookup_table()
mesh.from_mesh(obj.data)
return obj, mesh
def assemble(obj: bpy.types.Object, mesh: bmesh.types.BMesh) -> bpy.types.Object:
assert isinstance(obj.data, bpy.types.Mesh)
mesh.to_mesh(obj.data)
return obj
def to_camera_coords(
obj: bpy.types.Object, mesh: bmesh.types.BMesh
) -> tuple[bpy.types.Object, bmesh.types.BMesh]:
mesh.transform(camera.matrix_world.inverted() @ obj.matrix_world)
obj.matrix_world = camera.matrix_world
annotation_offset = mathutils.Vector((0, 0, -camera_data.clip_start - 0.05))
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
obj.matrix_world.translation += annotation_offset
return obj, mesh
def clip_to_camera_boundary(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh:
mesh.verts.ensure_lookup_table()
points = [v.co for v in mesh.verts[0:2]]
points = helper.clip_segment(bounds, points)
if points is None:
return None
mesh.verts[0].co = points[0]
mesh.verts[1].co = points[1]
return mesh
def draw_grids_vertically(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh:
mesh.verts.ensure_lookup_table()
points = [v.co for v in mesh.verts[0:2]]
points = helper.elevate_segment(bounds, points)
if points is None:
return None
points = helper.clip_segment(bounds, points)
if points is None:
return None
mesh.verts[0].co = points[0]
mesh.verts[1].co = points[1]
return mesh
obj = tool.Ifc.get_object(reference_element)
if not obj:
return
assert isinstance(obj, bpy.types.Object)
obj, mesh = to_camera_coords(*disassemble(clone(obj)))
if clipping:
mesh = clip_to_camera_boundary(mesh)
elif elevating:
mesh = draw_grids_vertically(mesh)
if mesh is None:
return
assemble(obj, mesh)
mesh = bpy.data.meshes.new("Mesh")
obj = bpy.data.objects.new(axis.AxisTag or "-", mesh)
obj.matrix_world = cls.get_default_annotation_matrix(camera)
element = cls.run_root_assign_class(
obj=obj,
ifc_class="IfcAnnotation",
predefined_type="GRID",
should_add_representation=True,
context=context,
ifc_representation_class=None,
obj=obj, ifc_class="IfcAnnotation", predefined_type="GRID", should_add_representation=False
)
if representation := ifcopenshell.util.representation.get_representation(element, context):
cls.reload_representation(obj=obj, representation=representation)
element.Name = axis.AxisTag or "-"
builder = ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
points = [p / unit_scale for p in points]
representation = builder.get_representation(context, [builder.polyline(points)])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
return element
@classmethod
def regenerate_grid_axis_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
annotation: ifcopenshell.entity_instance,
axis: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]:
if not (points := cls.generate_grid_axis_reference_points(drawing, axis)):
return
camera = tool.Ifc.get_object(drawing)
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, annotation)
m = ifcopenshell.util.shape.get_shape_matrix(shape)
mw = cls.get_default_annotation_matrix(camera)
existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
new_points = None
if not np.allclose(m, np.array(mw), atol=1e-4):
new_points = points
elif len(existing_verts) != 2:
new_points = points
else:
if not tool.Cad.are_edges_collinear(existing_verts, points):
# Attempt to update the section line by projecting existing verts onto the new line
v1 = tool.Cad.point_on_edge(existing_verts[0], points)
v2 = tool.Cad.point_on_edge(existing_verts[1], points)
existing_length = (existing_verts[0] - existing_verts[1]).length
new_length = (v2 - v1).length
if abs((existing_length - new_length) / existing_length) <= 0.10:
# If the projected line is within 10% of the previous length ...
new_points = (v1, v2)
else:
new_points = points
if new_points:
if representation := ifcopenshell.util.representation.get_representation(annotation, context):
ifcopenshell.api.geometry.unassign_representation(
tool.Ifc.get(), product=annotation, representation=representation
)
builder = ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
new_points = [p / unit_scale for p in new_points]
representation = builder.get_representation(context, [builder.polyline(new_points)])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
if obj := tool.Ifc.get_object(annotation):
obj.matrix_world = mw
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
bonsai.core.geometry.switch_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=representation
)
else:
ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
annotation.Name = axis.AxisTag or "-"
return annotation
@classmethod
def get_default_annotation_matrix(cls, camera, matrix_world=None):
if matrix_world is None:
matrix_world = camera.matrix_world.copy()
annotation_offset = Vector((0, 0, -camera.data.clip_start - 0.05))
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
matrix_world.translation += annotation_offset
return matrix_world
@classmethod
def is_perpendicular(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool:
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
@@ -1798,6 +1985,16 @@ class Drawing(bonsai.core.tool.Drawing):
return True
return False
@classmethod
def is_matrix_perpendicular(cls, a: Matrix, b: Matrix) -> bool:
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
a_quaternion = a.to_quaternion()
b_quaternion = b.to_quaternion()
for axis in axes:
if abs((a_quaternion @ axis).angle(b_quaternion @ axis) - (math.pi / 2)) < 1e-5:
return True
return False
@classmethod
def get_camera_block(cls, obj: bpy.types.Object) -> dict:
assert isinstance(camera := obj.data, bpy.types.Camera)
@@ -1841,6 +2038,16 @@ class Drawing(bonsai.core.tool.Drawing):
b_tree = mathutils.bvhtree.BVHTree.FromPolygons(b_block["verts"], b_block["faces"])
return bool(a_tree.overlap(b_tree))
@classmethod
def does_shape_intersect_camera(cls, shape, camera) -> bool:
a_block = cls.get_camera_block(camera)
a_tree = mathutils.bvhtree.BVHTree.FromPolygons(a_block["verts"], a_block["faces"])
m = ifcopenshell.util.shape.get_shape_matrix(shape)
verts = [(m @ np.append(v, 1.0))[:3] for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
b_tree = mathutils.bvhtree.BVHTree.FromPolygons(verts, faces)
return bool(a_tree.overlap(b_tree))
@classmethod
def replace_text_literal_variables(
cls,
@@ -2568,3 +2775,14 @@ class Drawing(bonsai.core.tool.Drawing):
ifcopenshell.api.document.remove_reference(tool.Ifc.get(), reference=reference)
tool.Drawing.import_sheets()
@classmethod
def hide_all_drawing_collections(cls) -> None:
for element in tool.Ifc.get().by_type("IfcAnnotation"):
if element.ObjectType == "DRAWING" and (obj := tool.Ifc.get_object(element)):
tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
@classmethod
def clear_annotation_relationships(cls, drawing: ifcopenshell.entity_instance) -> None:
for rel in drawing.ReferencedBy:
tool.Ifc.get().remove(rel)
+5 -5
View File
@@ -233,20 +233,20 @@ class Geometry(bonsai.core.tool.Geometry):
element = tool.Ifc.get_entity(obj)
if not element:
return
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
elif element.is_a("IfcAnnotation"):
if element.ObjectType == "DRAWING":
return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
elif tool.Drawing.is_auto_annotation(element):
return # For now, these are special referenced objects and cannot be deleted. Exclude instead.
elif element.is_a("IfcRelSpaceBoundary"):
ifcopenshell.api.boundary.remove_boundary(ifc_file, boundary=element)
tool.Boundary.undecorate_boundary(obj)
return bpy.data.objects.remove(obj)
elif element.is_a("IfcGridAxis"):
is_last_axis = False
# Deleting the last W axis is OK
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
(grid := element.PartOfV) and len(grid[0].VAxes) == 1
):
is_last_axis = True
if is_last_axis:
return
ifcopenshell.api.grid.remove_grid_axis(ifc_file, axis=element)
return bpy.data.objects.remove(obj)
+68 -42
View File
@@ -786,17 +786,21 @@ class Model(bonsai.core.tool.Model):
elif material.is_a("IfcMaterialLayerSet"):
axis = ifcopenshell.util.element.get_pset(element, "EPset_Parametric", "LayerSetDirection")
if axis is None:
if element.is_a() in [
if element.is_a() in (
"IfcSlabType",
"IfcRoofType",
"IfcRampType",
"IfcPlateType",
"IfcCovering",
"IfcFurniture",
]:
"IfcSlab",
"IfcRoof",
"IfcRamp",
"IfcPlate",
):
axis = "AXIS3"
else:
elif element.is_a() in ("IfcWallType", "IfcWall"):
axis = "AXIS2"
else:
return
return f"LAYER{axis[-1]}"
elif material.is_a("IfcMaterialProfileSetUsage"):
# TODO: remove after we support editing profile usages with IfcRevolvedAreaSolid.
@@ -1399,7 +1403,6 @@ class Model(bonsai.core.tool.Model):
number_of_risers = number_of_treads + 1
tread_rise = height / number_of_risers
custom_tread_run = any(run != 0 for run in custom_first_last_tread_run)
nosing_overlap = max(nosing_length, 0)
nosing_tread_gap = -min(nosing_length, 0)
nosing_overlap_offset = -V_(nosing_overlap, 0)
@@ -1430,19 +1433,26 @@ class Model(bonsai.core.tool.Model):
default_tread_offset = Vector([tread_run, tread_rise])
def get_tread_data(i):
if custom_tread_run:
current_tread_run = None
if i == 0:
current_tread_run = custom_first_last_tread_run[0]
elif i == number_of_risers - 1:
current_tread_run = custom_first_last_tread_run[1]
# Check if this is first or last tread with custom run
current_tread_run = None
if i == 0 and custom_first_last_tread_run[0] is not None:
current_tread_run = custom_first_last_tread_run[0]
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
current_tread_run = custom_first_last_tread_run[1]
if current_tread_run is not None:
tread_offset = default_tread_offset.copy()
tread_offset.x = current_tread_run
# Handle zero-width treads
if current_tread_run == 0:
# For zero width, just return vertical offset with no horizontal tread
return tread_offset, ()
tread_verts = deepcopy(default_tread_verts)
tread_verts[-1].x = current_tread_run
return tread_offset, tread_verts
if current_tread_run:
tread_offset = default_tread_offset.copy()
tread_offset.x = current_tread_run
tread_verts = deepcopy(default_tread_verts)
tread_verts[-1].x = current_tread_run
return tread_offset, tread_verts
return default_tread_offset, default_tread_verts
# treads
@@ -1450,9 +1460,13 @@ class Model(bonsai.core.tool.Model):
for i in range(number_of_risers):
last_vert_i = len(vertices) - 1
tread_offset, tread_verts = get_tread_data(i)
current_tread_verts = [v + current_offset for v in tread_verts]
edges.extend(default_tread_edges + last_vert_i)
vertices.extend(current_tread_verts)
# Skip adding vertices/edges for zero-width treads
if tread_verts:
current_tread_verts = [v + current_offset for v in tread_verts]
edges.extend(default_tread_edges + last_vert_i)
vertices.extend(current_tread_verts)
current_offset += tread_offset
if stair_type == "WOOD/STEEL":
@@ -1467,35 +1481,47 @@ class Model(bonsai.core.tool.Model):
default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
def get_tread_data(i):
if custom_tread_run:
current_tread_run = None
if i == 0 and custom_first_last_tread_run[0] != 0:
current_tread_run = custom_first_last_tread_run[0]
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] != 0:
current_tread_run = custom_first_last_tread_run[1]
# Check if this is first or last tread with custom run
current_tread_run = None
if i == 0 and custom_first_last_tread_run[0] is not None:
current_tread_run = custom_first_last_tread_run[0]
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
current_tread_run = custom_first_last_tread_run[1]
if current_tread_run is not None:
tread_offset = default_tread_offset.copy()
tread_offset.x = current_tread_run + nosing_tread_gap
# Handle zero-width treads
if current_tread_run == 0:
return tread_offset, ()
tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
return tread_offset, tread_verts
if current_tread_run:
tread_offset = default_tread_offset.copy()
tread_offset.x = current_tread_run + nosing_tread_gap
tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
return tread_offset, tread_verts
return default_tread_offset, default_tread_verts
# each tread is a separate shape
cur_offset = V_(0, 0)
tread_index = 0
for i in range(number_of_risers):
tread_offset, tread_verts = get_tread_data(i)
cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
vertices.extend(cur_trade_shape)
cur_vertex = i * 4
verts_to_add = (
(cur_vertex, cur_vertex + 1),
(cur_vertex + 1, cur_vertex + 2),
(cur_vertex + 2, cur_vertex + 3),
(cur_vertex + 3, cur_vertex),
)
edges.extend(verts_to_add)
# Skip adding vertices/edges for zero-width treads
if tread_verts:
cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
vertices.extend(cur_trade_shape)
cur_vertex = tread_index * 4
verts_to_add = (
(cur_vertex, cur_vertex + 1),
(cur_vertex + 1, cur_vertex + 2),
(cur_vertex + 2, cur_vertex + 3),
(cur_vertex + 3, cur_vertex),
)
edges.extend(verts_to_add)
tread_index += 1
cur_offset += tread_offset
elif stair_type == "GENERIC":
+238
View File
@@ -176,6 +176,27 @@ class TestStairCalculatedParams(NewFile):
calculated_data["Length"] += -0.2 + 0.1
self.compare_data(pset_data, calculated_data)
# zero-width first tread
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
self.compare_data(pset_data, calculated_data)
# zero-width last tread
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (0.3, 0.0)
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
self.compare_data(pset_data, calculated_data)
# both first and last treads zero-width
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
calculated_data["Length"] = 0.6 # Only 2 middle treads at 0.3 each
self.compare_data(pset_data, calculated_data)
# overlap affects stair length only by first tread
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
@@ -296,6 +317,94 @@ class TestGenerateStair2DProfile(NewFile):
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_concrete_stair_zero_width_first_tread(self):
"""Test concrete stair with zero-width first tread"""
kwargs = {
"base_slab_depth": 0.25,
"has_top_nib": False,
"height": 1.0,
"number_of_treads": 3,
"stair_type": "CONCRETE",
"top_slab_depth": 0.25,
"tread_depth": 0.25,
"tread_run": 0.3,
"width": 1.2,
"custom_first_last_tread_run": (0.0, 0.0),
}
verts_data = (
V(0.0, 0, 0.0),
# First tread skipped - goes straight to second tread
V(0.0, 0, 0.5),
V(0.3, 0, 0.5),
V(0.3, 0, 0.75),
V(0.6, 0, 0.75),
V(0.6, 0, 1.0),
V(0.9, 0, 1.0),
V(0.9, 0, 0.67457),
V(0.0, 0, -0.25),
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 6),
(6, 7),
(8, 0),
(7, 8),
)
edges_data = [e[::-1] for e in edges_data]
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_concrete_stair_zero_width_last_tread(self):
"""Test concrete stair with zero-width last tread"""
kwargs = {
"base_slab_depth": 0.25,
"has_top_nib": False,
"height": 1.0,
"number_of_treads": 3,
"stair_type": "CONCRETE",
"top_slab_depth": 0.25,
"tread_depth": 0.25,
"tread_run": 0.3,
"width": 1.2,
"custom_first_last_tread_run": (0.0, 0.0),
}
verts_data = (
V(0.0, 0, 0.0),
V(0.0, 0, 0.25),
V(0.3, 0, 0.25),
V(0.3, 0, 0.5),
V(0.6, 0, 0.5),
V(0.6, 0, 0.75),
V(0.9, 0, 0.75),
# Last tread skipped
V(0.9, 0, 0.67457),
V(0.1, 0, -0.25),
V(0.0, 0, -0.25),
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 6),
(6, 7),
(9, 0),
(8, 9),
(7, 8),
)
edges_data = [e[::-1] for e in edges_data]
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_wood_steel_stair(self):
kwargs = {
"height": 1.0,
@@ -348,6 +457,100 @@ class TestGenerateStair2DProfile(NewFile):
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_wood_steel_stair_zero_width_first_tread(self):
"""Test wood/steel stair with zero-width first tread"""
kwargs = {
"height": 1.0,
"number_of_treads": 3,
"stair_type": "WOOD/STEEL",
"tread_depth": 0.25,
"tread_run": 0.3,
"width": 1.2,
"custom_first_last_tread_run": (0.0, 0.0),
}
verts_data = (
# First tread skipped - start at second tread
V(0.0, 0, 0.25),
V(0.3, 0, 0.25),
V(0.3, 0, 0.5),
V(0.0, 0, 0.5),
V(0.3, 0, 0.5),
V(0.6, 0, 0.5),
V(0.6, 0, 0.75),
V(0.3, 0, 0.75),
V(0.6, 0, 0.75),
V(0.9, 0, 0.75),
V(0.9, 0, 1.0),
V(0.6, 0, 1.0),
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 0),
(4, 5),
(5, 6),
(6, 7),
(7, 4),
(8, 9),
(9, 10),
(10, 11),
(11, 8),
)
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_wood_steel_stair_zero_width_last_tread(self):
"""Test wood/steel stair with zero-width last tread"""
kwargs = {
"height": 1.0,
"number_of_treads": 3,
"stair_type": "WOOD/STEEL",
"tread_depth": 0.25,
"tread_run": 0.3,
"width": 1.2,
"custom_first_last_tread_run": (0.0, 0.0),
}
verts_data = (
V(0.0, 0, 0.0),
V(0.3, 0, 0.0),
V(0.3, 0, 0.25),
V(0.0, 0, 0.25),
V(0.3, 0, 0.25),
V(0.6, 0, 0.25),
V(0.6, 0, 0.5),
V(0.3, 0, 0.5),
V(0.6, 0, 0.5),
V(0.9, 0, 0.5),
V(0.9, 0, 0.75),
V(0.6, 0, 0.75),
# Last tread skipped
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 0),
(4, 5),
(5, 6),
(6, 7),
(7, 4),
(8, 9),
(9, 10),
(10, 11),
(11, 8),
)
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_generic_stair(self):
kwargs = {"height": 1.0, "number_of_treads": 3, "stair_type": "GENERIC", "tread_run": 0.3, "width": 1.2}
verts_data = (
@@ -381,6 +584,41 @@ class TestGenerateStair2DProfile(NewFile):
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_generic_stair_zero_width_treads(self):
"""Test generic stair with zero-width first and last treads"""
kwargs = {
"height": 1.0,
"number_of_treads": 3,
"stair_type": "GENERIC",
"tread_run": 0.3,
"width": 1.2,
"custom_first_last_tread_run": (0.0, 0.0),
}
verts_data = (
V(0.0, 0, 0.0),
# First tread skipped
V(0.0, 0, 0.5),
V(0.3, 0, 0.5),
V(0.3, 0, 0.75),
V(0.6, 0, 0.75),
# Last tread skipped
V(0.6, 0, 0.0),
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 0),
)
edges_data = [e[::-1] for e in edges_data]
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
class TestUsingArrays(NewFile):
def setup_array(self, add_second_layer=False, sync_children=False):
+2 -2
View File
@@ -96,11 +96,11 @@ writer.write()
### CLI app for converting IFC files to CSV, ODS or XLSX format.
Usage:
python ifc5Dspreadsheet.py input_file output_file [-l log_file] [-f format_type]
python ifc5Dspreadsheet.py input_file output_dir [-l log_file] [-f format_type]
Arguments:
input_file (str): The path to the input IFC file to process.
output_file (str): The output directory for CSV or filename for other formats.
output_dir (str): The output directory.
Options:
-l, --log log_file (str): The path to the file where errors should be logged. Default is process.log.
+6 -3
View File
@@ -319,8 +319,8 @@ class Ifc5Dwriter:
cost_schedule: Optional[ifcopenshell.entity_instance] = None,
):
"""
:param file: IFC file to exprot cost schedules from.
:param output: Output directory for csv files.
:param file: IFC file to export cost schedules from.
:param output: Output directory.
:param cost_schedule: exported cost schedule. If not provided, will export all available schedules.
"""
self.output = output
@@ -404,6 +404,7 @@ class Ifc5DCsvWriter(Ifc5Dwriter):
import csv
super().write()
os.makedirs(self.output, exist_ok=True)
for sheet, data in self.sheet_data.items():
with open(
os.path.join(self.output, "{}.csv".format(data["Name"])), "w", newline="", encoding="utf-8"
@@ -436,6 +437,7 @@ class Ifc5DOdsWriter(Ifc5Dwriter):
ns1.addElement(Number(decimalplaces="2", minintegerdigits="1", grouping="true"))
self.doc.styles.addElement(ns1)
os.makedirs(self.output, exist_ok=True)
file_name = ""
for cost_schedule in self.cost_schedules:
if file_name:
@@ -537,6 +539,7 @@ class Ifc5DXlsxWriter(Ifc5Dwriter):
import xlsxwriter
super().write()
os.makedirs(self.output, exist_ok=True)
file_name = ""
for cost_schedule in self.cost_schedules:
if file_name:
@@ -683,7 +686,7 @@ class Ifc5DPdfWriter(Ifc5Dwriter):
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("input", type=str, help="Specify an IFC file to process")
parser.add_argument("output", help="The output directory for CSV or filename for other formats")
parser.add_argument("output", help="The output directory")
parser.add_argument("-l", "--log", type=str, help="Specify where errors should be logged", default="process.log")
parser.add_argument(
"-f", "--format", type=str, help="Choose which format to export in (CSV/ODS/XLSX)", default="CSV"
+28 -9
View File
@@ -207,7 +207,7 @@ size_t read_filters_from_file(const std::string&, inclusion_filter&, inclusion_t
void parse_filter(geom_filter &, const std::vector<std::string>&);
std::vector<IfcGeom::filter_t> setup_filters(const std::vector<geom_filter>&, const std::string&);
bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap);
bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap, bool bypass_properties=false);
// from https://stackoverflow.com/questions/31696328/boost-program-options-using-zero-parameter-options-multiple-times
struct verbosity_counter {
@@ -965,7 +965,10 @@ int main(int argc, char** argv) {
time_t start,end;
time(&start);
if (!init_input_file(IfcUtil::path::to_utf8(input_filename), ifc_file, no_progress || quiet, mmap)) {
// @nb last argument true -> bypass_properties which are not read by any of the geometry serializers
// XML, RocksDB, IFC are already special-cased above
// SVG requires properties for IfcAnnotation/DRAWING properties
if (!init_input_file(IfcUtil::path::to_utf8(input_filename), ifc_file, no_progress || quiet, mmap, output_extension != SVG)) {
write_log(!quiet);
serializer.reset();
IfcUtil::path::delete_file(IfcUtil::path::to_utf8(output_temp_filename)); /**< @todo Windows Unicode support */
@@ -1336,7 +1339,7 @@ void write_log(bool header) {
#include <boost/algorithm/string/predicate.hpp>
bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap) {
bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap, bool bypass_properties) {
time_t start, end;
// Prevent IfcFile::Init() prints by setting output to null temporarily
@@ -1344,20 +1347,36 @@ bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file,
time(&start);
bool requires_init = false;
#ifdef WITH_IFCXML
if (boost::ends_with(boost::to_lower_copy(filename), ".ifcxml")) {
ifc_file = IfcParse::parse_ifcxml(filename);
} else
} else
#endif
{
ifc_file = new IfcParse::IfcFile(IfcParse::uninitialized_tag{});
requires_init = true;
}
{
ifc_file->bypass_type("IfcRelDefinesByProperties");
ifc_file->bypass_type("IfcPropertySetDefinition");
ifc_file->bypass_type("IfcProperty");
ifc_file->bypass_type("IfcMaterialProperties");
ifc_file->bypass_type("IfcProfileProperties");
ifc_file->bypass_type("IfcPhysicalQuantity");
#ifdef USE_MMAP
ifc_file = new IfcParse::IfcFile(filename, mmap);
if (mmap) {
ifc_file->initialize(filename, mmap);
requires_init = false;
}
#else
(void)mmap;
ifc_file = new IfcParse::IfcFile(filename);
(void)mmap;
#endif
}
if (requires_init) {
ifc_file->initialize(filename);
}
if (!ifc_file || !ifc_file->good()) {
Logger::Error("Unable to parse input file '" + filename + "'");
+2 -1
View File
@@ -24,7 +24,8 @@ import importlib
import ifcopenshell.util.selector
from pathlib import Path
from collections import defaultdict
from typing import Literal, Union, Any, Callable, TYPE_CHECKING
from typing import Literal, Union, Any, TYPE_CHECKING
from collections.abc import Callable
try:
from openpyxl import Workbook
+4
View File
@@ -367,11 +367,13 @@ void IfcGeom::Iterator::create_element_(ifcopenshell::geometry::Converter* kerne
}));
if (!brep) {
Logger::SetProduct(boost::none);
return;
}
auto elem = process_based_on_settings(settings, brep);
if (!elem) {
Logger::SetProduct(boost::none);
return;
}
@@ -394,6 +396,8 @@ void IfcGeom::Iterator::create_element_(ifcopenshell::geometry::Converter* kerne
}
}
}
Logger::SetProduct(boost::none);
}
IfcGeom::Element* IfcGeom::Iterator::process_based_on_settings(ifcopenshell::geometry::Settings settings, IfcGeom::BRepElement* elem, IfcGeom::TriangulationElement* previous)
+1
View File
@@ -172,6 +172,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
}
auto interpolated_loop = polygon_from_points(points);
interpolated_loop->external = w1->external;
if (interpolated->kind() == taxonomy::FACE) {
std::static_pointer_cast<taxonomy::face>(interpolated)->children.push_back(interpolated_loop);
} else {
+44 -31
View File
@@ -118,7 +118,8 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
auto jt = it + 1;
std::array<taxonomy::item::ptr, 2> fa = { *it, *jt };
std::array<TopoDS_Shape, 2> shps;
std::array<TopoDS_Wire, 2> ws;
std::vector<std::array<TopoDS_Wire, 2>> ws;
ws.emplace_back();
for (int i = 0; i < 2; ++i) {
if (fa[i]->kind() == taxonomy::FACE) {
if (!convert(std::static_pointer_cast<taxonomy::face>(fa[i]), shps[i])) {
@@ -135,11 +136,20 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
if (shps[i].ShapeType() != TopAbs_FACE && shps[i].ShapeType() != TopAbs_WIRE) {
return false;
}
// @todo this is only outer wire
if (shps[i].ShapeType() == TopAbs_FACE) {
ws[i] = BRepTools::OuterWire(TopoDS::Face(shps[i]));
ws[0][i] = BRepTools::OuterWire(TopoDS::Face(shps[i]));
size_t j = 1;
for (TopExp_Explorer exp(shps[i], TopAbs_WIRE); exp.More(); exp.Next()) {
if (exp.Current() != ws[0][i]) {
while (ws.size() <= j) {
ws.emplace_back();
}
ws[j++][i] = TopoDS::Wire(exp.Current());
}
}
} else {
ws[i] = TopoDS::Wire(shps[i]);
ws[0][i] = TopoDS::Wire(shps[i]);
}
}
if (shps[0].ShapeType() == TopAbs_FACE) {
@@ -154,35 +164,38 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
BB.Add(comp, shps[1]);
}
}
BRepTools_WireExplorer a(ws[0]);
BRepTools_WireExplorer b(ws[1]);
for (; a.More() && b.More(); a.Next(), b.Next()) {
auto& e1 = a.Current();
// auto e3 = TopoDS::Edge(b.Current().Reversed());
auto& e3 = b.Current();
// Documentation says unconnected edges are automatically connected, but this is not the case
TopoDS_Vertex e1a, e1b, e3a, e3b;
TopExp::Vertices(e1, e1a, e1b, true);
TopExp::Vertices(e3, e3a, e3b, true);
auto e2 = BRepBuilderAPI_MakeEdge(e1b, e3a).Edge();
auto e4 = BRepBuilderAPI_MakeEdge(e3b, e1a).Edge();
/*
BRepFill_Filling fill;
fill.Add(e1, GeomAbs_C0);
fill.Add(e2, GeomAbs_C0);
fill.Add(e3, GeomAbs_C0);
fill.Add(e4, GeomAbs_C0);
fill.Build();
// faces.Append(fill.Face());
BB.Add(comp, fill.Face());
*/
for (auto& wp : ws) {
BRepTools_WireExplorer a(wp[0]);
BRepTools_WireExplorer b(wp[1]);
for (; a.More() && b.More(); a.Next(), b.Next()) {
auto& e1 = a.Current();
// auto e3 = TopoDS::Edge(b.Current().Reversed());
auto& e3 = b.Current();
auto f = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e1a, e1b, e3b, true).Wire()).Face();
BB.Add(comp, f);
auto g = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e3b, e3a, e1a, true).Wire()).Face();
BB.Add(comp, g);
// Documentation says unconnected edges are automatically connected, but this is not the case
TopoDS_Vertex e1a, e1b, e3a, e3b;
TopExp::Vertices(e1, e1a, e1b, true);
TopExp::Vertices(e3, e3a, e3b, true);
auto e2 = BRepBuilderAPI_MakeEdge(e1b, e3a).Edge();
auto e4 = BRepBuilderAPI_MakeEdge(e3b, e1a).Edge();
/*
BRepFill_Filling fill;
fill.Add(e1, GeomAbs_C0);
fill.Add(e2, GeomAbs_C0);
fill.Add(e3, GeomAbs_C0);
fill.Add(e4, GeomAbs_C0);
fill.Build();
// faces.Append(fill.Face());
BB.Add(comp, fill.Face());
*/
auto f = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e1a, e1b, e3b, true).Wire()).Face();
BB.Add(comp, f);
auto g = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e3b, e3a, e1a, true).Wire()).Face();
BB.Add(comp, g);
}
}
}
+41 -45
View File
@@ -25,62 +25,58 @@ using namespace ifcopenshell::geometry;
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst) {
if (!inst->Location()->as<IfcSchema::IfcPointByDistanceExpression>())
Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear"));
if (!inst->Location()->as<IfcSchema::IfcPointByDistanceExpression>()) {
Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear"));
}
Eigen::Vector3d o, axis(0, 0, 1), refDirection;
Eigen::Vector3d o, axis(0, 0, 1), refDirection;
taxonomy::matrix4::ptr m = taxonomy::cast<taxonomy::matrix4>(map(inst->Location()));
o = m->components().col(3).head<3>();
taxonomy::matrix4::ptr m = taxonomy::cast<taxonomy::matrix4>(map(inst->Location()));
o = m->components().col(3).head<3>();
// From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
// 1) Axis is given but not RefDirection
// 2) RefDirection is given but not Axis
// 3) Neither Axis or RefDirection are provided
// 4) Both Axis and RefDirection are provided
// From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
// 1) Axis is given but not RefDirection
// 2) RefDirection is given but not Axis
// 3) Neither Axis or RefDirection are provided
// 4) Both Axis and RefDirection are provided
const bool hasAxis = inst->Axis() != nullptr;
const bool hasRef = inst->RefDirection() != nullptr;
const bool hasAxis = inst->Axis() != nullptr;
const bool hasRef = inst->RefDirection() != nullptr;
if (hasAxis != hasRef) {
/*
if (hasAxis != hasRef) {
Logger::Warning("Axis and RefDirection should be specified together", inst);
}
*/
if (hasAxis && !hasRef)
{
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
axis = *a->components_;
if (hasAxis && !hasRef) {
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
axis = *a->components_;
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
// refDirection is not necessarily orthogonal to axis.
// axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection
refDirection = axis.cross(refDirection).cross(axis);
}
else if (!hasAxis && hasRef)
{
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
refDirection = *r->components_;
Eigen::Vector3d up(0, 0, 1);
axis = refDirection.cross(up.cross(refDirection));
}
else if (!hasAxis && !hasRef)
{
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
Eigen::Vector3d up(0, 0, 1);
axis = refDirection.cross(up.cross(refDirection));
}
else
{
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
axis = *a->components_;
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
// refDirection is not necessarily orthogonal to axis.
// axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection
refDirection = axis.cross(refDirection).cross(axis);
} else if (!hasAxis && hasRef) {
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
refDirection = *r->components_;
Eigen::Vector3d up(0, 0, 1);
axis = refDirection.cross(up.cross(refDirection));
} else if (!hasAxis && !hasRef) {
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
Eigen::Vector3d up(0, 0, 1);
axis = refDirection.cross(up.cross(refDirection));
} else {
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
axis = *a->components_;
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
refDirection = *r->components_;
refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis
}
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
refDirection = *r->components_;
refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis
}
// axis and refDirection need to be orthogonal
return taxonomy::make<taxonomy::matrix4>(o, axis, refDirection);
// axis and refDirection need to be orthogonal
return taxonomy::make<taxonomy::matrix4>(o, axis, refDirection);
}
#endif
+1 -1
View File
@@ -24,7 +24,7 @@ using namespace ifcopenshell::geometry;
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcBoundingBox* inst) {
if (!settings_.get<settings::KeepBoundingBoxes>().get()) {
// @todo make sure it doesn't log.
failed_on_purpose_.insert(inst);
return nullptr;
}
@@ -81,7 +81,13 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
rot = taxonomy::matrix4(
Eigen::Vector3d(0, 0, 0),
taxonomy::cast<taxonomy::direction3>(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0);
}
} else if (csp->RefDirection()) {
rot = taxonomy::matrix4(
Eigen::Vector3d(0, 0, 0),
Eigen::Vector3d(0, 0, 1),
taxonomy::cast<taxonomy::direction3>(map(csp->RefDirection()))->ccomponents()
).ccomponents().block<3, 3>(0, 0);
}
profile_rotations.push_back(rot);
}
if (faces.size() != profile_offsets.size()) {
+8 -1
View File
@@ -82,7 +82,14 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) {
rot = taxonomy::matrix4(
Eigen::Vector3d(0, 0, 0),
taxonomy::cast<taxonomy::direction3>(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0);
}
} else if (csp->RefDirection()) {
rot = taxonomy::matrix4(
Eigen::Vector3d(0, 0, 0),
Eigen::Vector3d(0, 0, 1),
taxonomy::cast<taxonomy::direction3>(map(csp->RefDirection()))->ccomponents())
.ccomponents()
.block<3, 3>(0, 0);
}
profile_rotations.push_back(rot);
}
#else
+12
View File
@@ -372,6 +372,18 @@ const IfcUtil::IfcBaseEntity* mapping::get_single_material_association(const Ifc
}
}
#endif
#ifdef SCHEMA_HAS_IfcMaterialConstituentSet
if (associated_material->as<IfcSchema::IfcMaterialConstituentSet>()) {
IfcSchema::IfcMaterialConstituentSet* constituentset = associated_material->as<IfcSchema::IfcMaterialConstituentSet>();
if (settings_.get<settings::LayersetFirst>().value ? constituentset->MaterialConstituents()->get()->size() >= 1 : constituentset->MaterialConstituents()->get()->size() == 1) {
IfcSchema::IfcMaterialConstituent* constituent = (*constituentset->MaterialConstituents()->get()->begin());
if (auto* m_ = constituent->Material()) {
single_material = m_;
}
}
}
#endif
}
}
}
+8
View File
@@ -43,6 +43,9 @@ endif
ifeq ($(PYVERSION), py313)
PYNUMBER:=313
endif
ifeq ($(PYVERSION), py314)
PYNUMBER:=314
endif
ifndef PYNUMBER
$(error Unsupported PYVERSION '$(PYVERSION)')
endif
@@ -76,6 +79,11 @@ build-urls:
test:
pytest -p no:pytest-blender test --ignore=test/util/test_shape_builder.py
.PHONY: test-parallel
test-parallel:
@NPROCS=$$(nproc 2>/dev/null || sysctl -n hw.ncpu); \
pytest -p no:pytest-blender -n $$NPROCS test --ignore=test/util/test_shape_builder.py
.PHONY: test-mathutils
test-mathutils:
pytest -p no:pytest-blender test/util/test_shape_builder.py
+1
View File
@@ -43,6 +43,7 @@ Examples
Learning how to use the bSDD is best done by reading the official Swagger API docs.
.. code-block:: python
from bsdd import Client,apply_ifc_classification_properties
from pprint import pprint
@@ -17,4 +17,5 @@ capabilities of the C++ core are available in Python.
ifcopenshell-python/geometry_creation
ifcopenshell-python/geometry_tree
ifcopenshell-python/selector_syntax
ifcopenshell-python/developer_guide
ifcopenshell-python/schema_querying
ifcopenshell-python/validation
@@ -1,15 +0,0 @@
Developer Guide
===============
The core module implements low-level functionality to read and write IFC data. This includes:
- Reading IFC data from different serialisations into Python objects
- Accessing direct and indirect attributes of IFC entities
- Creating IFC entities
- Generating GlobalIds
- Removing IFC entities and all references
- Modifying IFC direct attributes
- Checking IFC class inheritance
- Validating IFC data
TODO
@@ -44,6 +44,8 @@ ZIP packages
+-------------+---------------------------------+-------------------------------+---------------------------------+-----------------------------------+
| Python 3.13 | :ios_python_url:`py313-linux64` | :ios_python_url:`py313-win64` | :ios_python_url:`py313-macos64` | :ios_python_url:`py313-macosm164` |
+-------------+---------------------------------+-------------------------------+---------------------------------+-----------------------------------+
| Python 3.14 | :ios_python_url:`py314-linux64` | :ios_python_url:`py314-win64` | :ios_python_url:`py314-macos64` | :ios_python_url:`py314-macosm164` |
+-------------+---------------------------------+-------------------------------+---------------------------------+-----------------------------------+
2. Unzip the downloaded file and copy the ``ifcopenshell`` directory into your
Python path. If you're not sure where your Python path is, run the following
@@ -0,0 +1,73 @@
Schema querying
===============
Schema declarations
-------------------
IfcOpenShell can query the IFC schema itself without instantiating or loading an IFC dataset.
.. code-block:: python
import ifcopenshell
ifc4 = ifcopenshell.schema_by_name("IFC4")
A schema definition is known as a declaration. You may loop through all declarations or retrieve a declaration by name. All declarations have a name.
.. code-block:: python
for declaration in ifc4.declarations():
print(declaration.name()) # 'IfcAbsorbedDoseMeasure', 'IfcAccelerationMeasure', 'IfcActionRequest', ...
ifcwall = ifc4.declaration_by_name("IfcWall")
You can check if an entity is abstract, and retrive both the supertype and subtypes of an entity:
.. code-block:: python
print(ifcwall.is_abstract()) # False
print(ifcwall.supertype()) # <entity IfcBuildingElement>
print(ifcwall.subtypes()) # (<entity IfcWallElementedCase>, <entity IfcWallStandardCase>)
You can retrieve only the direct attributes of an entity, or all the direct attributes including inherited attributes, or inverse attributes:
.. code-block:: python
print(ifcwall.attributes())
print(ifcwall.all_attributes())
print(ifcwall.all_inverse_attributes())
buildingSMART property set templates
------------------------------------
For each IFC schema version, buildingSMART publishes built in property and quantity set templates for standardised properties. These define property names, property sets, data types, and which IFC class they are applicable to. You can query these templates.
.. code-block:: python
import ifcopenshell.util.pset
templates = ifcopenshell.util.pset.PsetQto("IFC4")
To get just the names of applicable templates for an entity:
.. code-block:: python
# ['Pset_EnvironmentalImpactIndicators', 'Pset_EnvironmentalImpactValues', 'Pset_WallCommon', 'Qto_WallBaseQuantities', ...]
print(templates.get_applicable_names("IfcWall"))
They may also be retrieved as an ``IfcPropertySetTemplate`` entity:
.. code-block:: python
print(templates.get_applicable("IfcWall"))
A single template may be retrieved by name:
.. code-block:: python
templates.get_by_name('Pset_WallCommon')
You may add your own IFC files containing pset template definitions:
.. code-block:: python
my_pset_library = ifcopenshell.open('/path/to/library.ifc')
templates.templates.append(my_pset_library)
@@ -0,0 +1,78 @@
Validation
==========
SPF syntax validation
---------------------
IfcOpenShell can validate whether or not an IFC-SPF file contains correct SPF syntax.
.. code-block::
$ python -m ifcopenshell.simple_spf path/to/model.ifc
Valid
Here are some examples of failures:
.. code-block::
$ python -m ifcopenshell.simple_spf fixtures/fail_double_comma.ifc
On line 8 column 21:
Unexpected comma (',')
Expecting one of DBLQUOTE DOT HASH INT LPAR NONE QUOTE REAL STAR UPPER
00008 | #1=IFCPERSON($,$,'',,$,$,$,$);
^
$ python -m ifcopenshell.simple_spf fixtures/fail_double_semi.ifc
On line 27 column 66:
Unexpected semicolon (';')
Expecting one of ENDSEC HASH
00027 | #20=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);;
^
$ python -m ifcopenshell.simple_spf fixtures/fail_duplicate_id.ifc
On line 27:
Duplicate instance name #19
00027 | #19=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
$ python -m ifcopenshell.simple_spf fixtures/fail_no_header.ifc
On line 2 column 1:
Unexpected hex ('F')
Expecting HEADER
00002 | FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
^
The optional ``--json`` argument may be used to instead get results in JSON.
.. code-block::
$ python -m ifcopenshell.simple_spf test.ifc
{"type": "unexpected_token", "lineno": 8, "column": 48, "found_type": "semicolon", "found_value": ";", "expected": ["ENDSEC"], "line": "#1= IFCPERSON($,'Nicht definiert',$,$,$,$,$,$);;", "message": "On line 8 column 48:\nUnexpected semicolon (';')\nExpecting ENDSEC\n00008 | #1= IFCPERSON($,'Nicht definiert',$,$,$,$,$,$);;\n ^"}
IFC schema validation
---------------------
IfcOpenShell can validate models against the IFC schema itself. It checks against attributes, entity names, data types, cardinality, and where rules.
.. code-block:: console
$ python -m ifcopenshell.validate -h
usage: validate.py [-h] [--rules] [--json] [--fields] [--spf] files [files ...]
positional arguments:
files The IFC file to validate.
options:
-h, --help show this help message and exit
--rules Run express rules.
--json Output in JSON format.
--fields Output more detailed information about failed entities (only with --json).
--spf Output entities in SPF format (only with --json).
For example:
.. code-block:: bash
python -m ifcopenshell.validate /path/to/model.ifc --rules
+21 -21
View File
@@ -54,27 +54,27 @@ IfcOpenShell is a modular ecosystem of tools that work together, where each tool
.. csv-table::
:header: "Name", "Description"
"**IfcOpenShell**", "The core library for C++ developers. The library includes the ability to parse schemas, tessellate and process implicit geometry."
"**IfcOpenShell-Python**", "Python bindings to the core IfcOpenShell C++ system, as well as high level analysis and authoring functions."
"**IfcConvert**", "A command-line application for converting IFC geometry into file formats such as OBJ, DAE, GLB, STP, IGS, XML, SVG, H5, and IFC itself."
"**Bonsai**", "A graphical add-on for Blender that lets you analyse, author, and modify IFC with Blender. Graphically create BIM models from scratch!"
"**BCF**", "BIM Collaboration Format (BCF) is a standard to manage and exchange coordination topics between disciplines collaborating on a project by changing XML files or querying an API."
"**BIMServer-Plugin**", "A plugin to the open source BIMServer CDE to allow you to use IfcOpenShell to parse, view, and audit models."
"**BIMTester**", "A utility that allows you to write Gherkin-based tests for models."
"**bSDD**", "A Python library to query the buildingSMART Data Dictionary API to search for standardised classifications and properties."
"**Ifc2CA**", "Converts IFC models to FEM structural analytical models to be used in Code_Aster."
"**Ifc4D**", "A series of utilities for converting to and from various 4D software like MS Project, PowerProject, and Oracle P6."
"**Ifc5D**", "A collection of utilities of manipulating cost-related data to and from formats, reports, and optimisation engines."
"**IfcCityJSON**", "A converter for CityJSON files and IFC. It currently only supports one-way conversion from CityJSON to IFC."
"**IfcClash**", "A CLI utility and library that lets you perform clash detection on one or more IFC models. Clashes are defined in terms of clash sets with filters using the IFC query syntax."
"**IfcCSV**", "View and edit IFC data using spreadsheets or tabular datasets, such as CSV, ODS, XLSX, Pandas DataFrames, and regular Python lists."
"**IfcDiff**", "A CLI utility and library that lets you compare the changes between two IFC models."
"**IfcFM**", "A highly standards-compliant tool (e.g. COBie 2.4, COBie 3.0, AOH-BSEM) to convert FM data in IFC databases to spreadsheets and other machine readable formats, such as ODS, XLSX, CSV, Pandas, XML, and JSON."
"**IfcMax**", "A 3ds Max importer plugin able to import the IFC file format."
"**IfcPatch**", "A CLI utility and library that lets you run and distribute predetermined modifications on an IFC file, known as a patch recipe. Useful in deploying a data pipeline or batch-fixing external models."
"**IfcSverchok**", "A node based visual programming add-on for Blender to interact with IFC and Sverchok."
"**IfcTester**", "Author and read Information Delivery Specification (IDS) files. You can validate IFC models against IDS and generate reports in multiple formats. It works from the command line, as a web app, or as a library."
"**VoxelisationToolkit**", "Converts .ifc geometry into voxels, and lets you perform voxel based geometric analysis."
"`IfcOpenShell <https://docs.ifcopenshell.org/ifcopenshell.html>`_", "The core library for C++ developers. The library includes the ability to parse schemas, tessellate and process implicit geometry."
"`IfcOpenShell-Python <https://docs.ifcopenshell.org/ifcopenshell-python.html>`_", "Python bindings to the core IfcOpenShell C++ system, as well as high level analysis and authoring functions."
"`IfcConvert <https://docs.ifcopenshell.org/ifcconvert.html>`_", "A command-line application for converting IFC geometry into file formats such as OBJ, DAE, GLB, STP, IGS, XML, SVG, H5, and IFC itself."
"`Bonsai <https://docs.ifcopenshell.org/bonsai.html>`_", "A graphical add-on for Blender that lets you analyse, author, and modify IFC with Blender. Graphically create BIM models from scratch!"
"`BCF <https://docs.ifcopenshell.org/bcf.html>`_", "BIM Collaboration Format (BCF) is a standard to manage and exchange coordination topics between disciplines collaborating on a project by changing XML files or querying an API."
"`BIMServer-Plugin <https://docs.ifcopenshell.org/bimserver-plugin.html>`_", "A plugin to the open source BIMServer CDE to allow you to use IfcOpenShell to parse, view, and audit models."
"`BIMTester <https://docs.ifcopenshell.org/bimtester.html>`_", "A utility that allows you to write Gherkin-based tests for models."
"`bSDD <https://docs.ifcopenshell.org/bsdd.html>`_", "A Python library to query the buildingSMART Data Dictionary API to search for standardised classifications and properties."
"`Ifc2CA <https://docs.ifcopenshell.org/ifc2ca.html>`_", "Converts IFC models to FEM structural analytical models to be used in Code_Aster."
"`Ifc4D <https://docs.ifcopenshell.org/ifc4d.html>`_", "A series of utilities for converting to and from various 4D software like MS Project, PowerProject, and Oracle P6."
"`Ifc5D <https://docs.ifcopenshell.org/ifc5d.html>`_", "A collection of utilities of manipulating cost-related data to and from formats, reports, and optimisation engines."
"`IfcCityJSON <https://docs.ifcopenshell.org/ifccityjson.html>`_", "A converter for CityJSON files and IFC. It currently only supports one-way conversion from CityJSON to IFC."
"`IfcClash <https://docs.ifcopenshell.org/ifcclash.html>`_", "A CLI utility and library that lets you perform clash detection on one or more IFC models. Clashes are defined in terms of clash sets with filters using the IFC query syntax."
"`IfcCSV <https://docs.ifcopenshell.org/ifccsv.html>`_", "View and edit IFC data using spreadsheets or tabular datasets, such as CSV, ODS, XLSX, Pandas DataFrames, and regular Python lists."
"`IfcDiff <https://docs.ifcopenshell.org/ifcdiff.html>`_", "A CLI utility and library that lets you compare the changes between two IFC models."
"`IfcFM <https://docs.ifcopenshell.org/ifcfm.html>`_", "A highly standards-compliant tool (e.g. COBie 2.4, COBie 3.0, AOH-BSEM) to convert FM data in IFC databases to spreadsheets and other machine readable formats, such as ODS, XLSX, CSV, Pandas, XML, and JSON."
"`IfcMax <https://docs.ifcopenshell.org/ifcmax.html>`_", "A 3ds Max importer plugin able to import the IFC file format."
"`IfcPatch <https://docs.ifcopenshell.org/ifcpatch.html>`_", "A CLI utility and library that lets you run and distribute predetermined modifications on an IFC file, known as a patch recipe. Useful in deploying a data pipeline or batch-fixing external models."
"`IfcSverchok <https://docs.ifcopenshell.org/ifcsverchok.html>`_", "A node based visual programming add-on for Blender to interact with IFC and Sverchok."
"`IfcTester <https://docs.ifcopenshell.org/ifctester.html>`_", "Author and read Information Delivery Specification (IDS) files. You can validate IFC models against IDS and generate reports in multiple formats. It works from the command line, as a web app, or as a library."
"`VoxelisationToolkit <https://github.com/opensourceBIM/voxelization_toolkit>`_", "Converts .ifc geometry into voxels, and lets you perform voxel based geometric analysis."
.. note::
@@ -60,6 +60,7 @@ import zipfile
import tempfile
from pathlib import Path
from typing import Optional, Union, TYPE_CHECKING, Any, overload, Literal
from collections.abc import Sequence
if TYPE_CHECKING:
import ifcopenshell.express.schema_class
@@ -138,7 +139,12 @@ def open(
path: Union[os.PathLike, str], format: Optional[str] = None, *, should_stream: bool = False, readonly: bool = False
) -> Union[_file, sqlite, _stream]: ...
def open(
path: Union[os.PathLike, str], format: Optional[str] = None, should_stream: bool = False, readonly: bool = False
path: Union[os.PathLike, str],
format: Optional[str] = None,
should_stream: bool = False,
readonly: bool = False,
mmap: bool = False,
bypass_types: Optional[Sequence[str]] = None,
) -> Union[_file, sqlite, _stream]:
"""Loads an IFC dataset from a filepath
@@ -186,7 +192,17 @@ def open(
if should_stream:
return stream(path)
if readonly: # Temporary conditional see #7131. Remove once newer builds don't segfault on Linux.
f = ifcopenshell_wrapper.open(str(path.absolute()), readonly)
f = ifcopenshell_wrapper.open(str(path.absolute()), readonly=readonly)
elif bypass_types:
f = ifcopenshell_wrapper.file(ifcopenshell_wrapper.uninitialized_tag())
for ty in bypass_types:
f.bypass_type(ty)
if mmap:
f.initialize(str(path.absolute()), mmap=mmap)
else:
f.initialize(str(path.absolute()))
elif mmap:
f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap)
else:
f = ifcopenshell_wrapper.open(str(path.absolute()))
return file(f)
@@ -299,20 +315,44 @@ def guess_format(path: Path) -> Literal[".ifc", ".ifcZIP", ".ifcXML", ".ifcJSON"
return None
def stream2(path: Union[Path, str]):
def stream2(path: Union[Path, str], mmap: bool = False, page_size: int = 0):
"""Streams the content of a file path from disk, yielding each instance
as a dictionary.
Args:
path (Union[Path, str]): input file path
mmap (bool): open the file contents using memory mapping
page_size (int): open file in python and feed chunks to the parser
Yields:
dict: entity instance dictionaries
"""
streamer = ifcopenshell_wrapper.InstanceStreamer(str(path))
while streamer:
if inst := streamer.read_instance_py():
yield inst
if page_size:
import builtins
f = builtins.open(path, encoding="ascii")
strm = ifcopenshell_wrapper.InstanceStreamer()
strm.pushPage(f.read(page_size))
finished = False
while True:
while strm.hasSemicolon():
if inst := strm.readInstancePy():
yield inst
else:
finished = True
break
if finished:
break
else:
if data := f.read(page_size):
strm.pushPage(data)
else:
break
else:
streamer = ifcopenshell_wrapper.InstanceStreamer(str(path), mmap)
while streamer:
if inst := streamer.readInstancePy():
yield inst
def stream2_from_string(data: str):
@@ -0,0 +1,15 @@
import sys, fileinput
if sys.platform == "win32" and not hasattr(sys.stdout, 'buffer'):
import os, msvcrt
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
files = sys.argv[1:]
if files[0] == '-o':
b = open(files[1], 'wb')
files = files[2:]
else:
b = getattr(sys.stdout, 'buffer', sys.stdout)
for line in fileinput.input(files=files, mode='rb'):
b.write(line)
@@ -0,0 +1,185 @@
@ECHO OFF
:: python bootstrap.py express.bnf > express_parser.py
IF EXIST IFC2X3_TC1.exp (
python express_parser.py IFC2X3_TC1.exp header implementation schema_class definitions
IF EXIST Ifc2x3-schema.cpp (
:: v0.6.0
python cat.py -o ..\..\..\ifcparse\Ifc2x3.cpp txt/header_ifc2x3.txt Ifc2x3.cpp
python cat.py -o ..\..\..\ifcparse\Ifc2x3.h txt/header_ifc2x3.txt Ifc2x3.h
python cat.py -o ..\..\..\ifcparse\Ifc2x3-schema.cpp txt/header_ifc2x3.txt Ifc2x3-schema.cpp
python cat.py -o ..\..\..\ifcparse\Ifc2x3-definitions.h txt/header_ifc2x3.txt Ifc2x3-definitions.h
) ELSE (
:: v0.5.0
python cat.py -o ..\..\..\ifcparse\Ifc2x3.cpp txt/header_ifc2x3.txt txt/ifndef_ifc4.txt Ifc2x3.cpp txt/endif.txt
python cat.py -o ..\..\..\ifcparse\Ifc2x3.h txt/header_ifc2x3.txt Ifc2x3.h
python cat.py -o ..\..\..\ifcparse\Ifc2x3enum.h txt/header_ifc2x3.txt Ifc2x3enum.h
python cat.py -o ..\..\..\ifcparse\Ifc2x3-latebound.cpp txt/header_ifc2x3.txt txt/ifndef_ifc4.txt Ifc2x3-latebound.cpp txt/endif.txt
python cat.py -o ..\..\..\ifcparse\Ifc2x3-latebound.h txt/header_ifc2x3.txt Ifc2x3-latebound.h
)
del *.cpp *.h
)
IF EXIST IFC4_ADD2TC1.exp (
python express_parser.py IFC4_ADD2TC1.exp header implementation schema_class definitions
IF EXIST Ifc4-schema.cpp (
:: v0.6.0
python cat.py -o ..\..\..\ifcparse\Ifc4.cpp txt/header_ifc4.txt Ifc4.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4.h txt/header_ifc4.txt Ifc4.h
python cat.py -o ..\..\..\ifcparse\Ifc4-schema.cpp txt/header_ifc4.txt Ifc4-schema.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4-definitions.h txt/header_ifc4.txt Ifc4-definitions.h
) ELSE (
:: v0.5.0
python cat.py -o ..\..\..\ifcparse\Ifc4.cpp txt/header_ifc4.txt txt/ifdef_ifc4.txt Ifc4.cpp txt/endif.txt
python cat.py -o ..\..\..\ifcparse\Ifc4.h txt/header_ifc4.txt Ifc4.h
python cat.py -o ..\..\..\ifcparse\Ifc4enum.h txt/header_ifc4.txt Ifc4enum.h
python cat.py -o ..\..\..\ifcparse\Ifc4-latebound.cpp txt/header_ifc4.txt txt/ifdef_ifc4.txt Ifc4-latebound.cpp txt/endif.txt
python cat.py -o ..\..\..\ifcparse\Ifc4-latebound.h txt/header_ifc4.txt Ifc4-latebound.h
)
del *.cpp *.h
)
IF EXIST IFC4x1.exp (
python express_parser.py IFC4x1.exp header implementation schema_class definitions
IF EXIST Ifc4x1-schema.cpp (
:: v0.6.0
python cat.py -o ..\..\..\ifcparse\Ifc4x1.cpp txt/header_ifc4x1.txt Ifc4x1.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x1.h txt/header_ifc4x1.txt Ifc4x1.h
python cat.py -o ..\..\..\ifcparse\Ifc4x1-schema.cpp txt/header_ifc4x1.txt Ifc4x1-schema.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x1-definitions.h txt/header_ifc4x1.txt Ifc4x1-definitions.h
)
del *.cpp *.h
)
IF EXIST IFC4x2.exp (
python express_parser.py IFC4x2.exp header implementation schema_class definitions
IF EXIST Ifc4x2-schema.cpp (
:: v0.6.0
python cat.py -o ..\..\..\ifcparse\Ifc4x2.cpp txt/header_ifc4x2.txt Ifc4x2.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x2.h txt/header_ifc4x2.txt Ifc4x2.h
python cat.py -o ..\..\..\ifcparse\Ifc4x2-schema.cpp txt/header_ifc4x2.txt Ifc4x2-schema.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x2-definitions.h txt/header_ifc4x2.txt Ifc4x2-definitions.h
)
del *.cpp *.h
)
IF EXIST IFC4x3_RC1.exp (
python express_parser.py IFC4x3_RC1.exp header implementation schema_class definitions
IF EXIST Ifc4x3_rc1-schema.cpp (
:: v0.6.0
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1.cpp txt/header_ifc4x3_rc1.txt Ifc4x3_rc1.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1.h txt/header_ifc4x3_rc1.txt Ifc4x3_rc1.h
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1-schema.cpp txt/header_ifc4x3_rc1.txt Ifc4x3_rc1-schema.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1-definitions.h txt/header_ifc4x3_rc1.txt Ifc4x3_rc1-definitions.h
)
del *.cpp *.h
)
IF EXIST IFC4x3_RC2.exp (
python express_parser.py IFC4x3_RC2.exp header implementation schema_class definitions
IF EXIST Ifc4x3_rc2-schema.cpp (
:: v0.6.0
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc2.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc2.h
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc2-schema.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc2-definitions.h
)
del *.cpp *.h
)
IF EXIST IFC4x3_RC3.exp (
python express_parser.py IFC4x3_RC3.exp header implementation schema_class definitions
IF EXIST Ifc4x3_rc3-schema.cpp (
:: v0.6.0
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc3.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc3.h
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc3-schema.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc3-definitions.h
)
del *.cpp *.h
)
IF EXIST IFC4x3_RC4.exp (
python express_parser.py IFC4x3_RC4.exp header implementation schema_class definitions
IF EXIST Ifc4x3_rc4-schema.cpp (
:: v0.6.0
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc4.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc4.h
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc4-schema.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc4-definitions.h
)
del *.cpp *.h
)
IF EXIST IFC4X3.exp (
python express_parser.py IFC4X3.exp header implementation schema_class definitions
IF EXIST Ifc4x3-schema.cpp (
:: v0.6.0
python cat.py -o ..\..\..\ifcparse\Ifc4x3.cpp txt/header_ifc4x3_rc2.txt Ifc4x3.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3.h txt/header_ifc4x3_rc2.txt Ifc4x3.h
python cat.py -o ..\..\..\ifcparse\Ifc4x3-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3-schema.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3-definitions.h
)
del *.cpp *.h
)
IF EXIST IFC4X3_TC1.exp (
python express_parser.py IFC4X3_TC1.exp header implementation schema_class definitions
IF EXIST Ifc4x3_tc1-schema.cpp (
:: v0.6.0
python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1.cpp txt/header_ifc4x3_tc1.txt Ifc4x3_tc1.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1.h txt/header_ifc4x3_tc1.txt Ifc4x3_tc1.h
python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1-schema.cpp txt/header_ifc4x3_tc1.txt Ifc4x3_tc1-schema.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1-definitions.h txt/header_ifc4x3_tc1.txt Ifc4x3_tc1-definitions.h
)
del *.cpp *.h
)
IF EXIST IFC4X3_ADD1.exp (
python express_parser.py IFC4X3_ADD1.exp header implementation schema_class definitions
IF EXIST Ifc4x3_add1-schema.cpp (
:: v0.6.0
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1.cpp txt/header_ifc4x3_add1.txt Ifc4x3_add1.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1.h txt/header_ifc4x3_add1.txt Ifc4x3_add1.h
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1-schema.cpp txt/header_ifc4x3_add1.txt Ifc4x3_add1-schema.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1-definitions.h txt/header_ifc4x3_add1.txt Ifc4x3_add1-definitions.h
)
del *.cpp *.h
)
IF EXIST IFC4X3_ADD2.exp (
python express_parser.py IFC4X3_ADD2.exp header implementation schema_class definitions
IF EXIST Ifc4x3_add2-schema.cpp (
:: v0.6.0
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2.cpp txt/header_ifc4x3_add2.txt Ifc4x3_add2.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2.h txt/header_ifc4x3_add2.txt Ifc4x3_add2.h
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2-schema.cpp txt/header_ifc4x3_add2.txt Ifc4x3_add2-schema.cpp
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2-definitions.h txt/header_ifc4x3_add2.txt Ifc4x3_add2-definitions.h
)
del *.cpp *.h
)
@@ -0,0 +1 @@
#endif
@@ -0,0 +1,25 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* This file has been generated from IFC2X3_TC1.exp. Do not make modifications *
* but instead modify the python script that has been used to generate this. *
* *
********************************************************************************/
@@ -0,0 +1,25 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* This file has been generated from IFC4.exp. Do not make modifications *
* but instead modify the python script that has been used to generate this. *
* *
********************************************************************************/
@@ -0,0 +1,25 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* This file has been generated from IFC4x1.exp. Do not make modifications *
* but instead modify the python script that has been used to generate this. *
* *
********************************************************************************/
@@ -0,0 +1,25 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* This file has been generated from IFC4x2.exp. Do not make modifications *
* but instead modify the python script that has been used to generate this. *
* *
********************************************************************************/
@@ -0,0 +1,25 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* This file has been generated from IFC4X3_ADD1.exp. Do not make modifications *
* but instead modify the python script that has been used to generate this. *
* *
********************************************************************************/
@@ -0,0 +1,25 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* This file has been generated from IFC4X3_ADD2.exp. Do not make modifications *
* but instead modify the python script that has been used to generate this. *
* *
********************************************************************************/
@@ -0,0 +1,25 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* This file has been generated from IFC4x3_RC1.exp. Do not make modifications *
* but instead modify the python script that has been used to generate this. *
* *
********************************************************************************/
@@ -0,0 +1,25 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* This file has been generated from IFC4x3_RC2.exp. Do not make modifications *
* but instead modify the python script that has been used to generate this. *
* *
********************************************************************************/
@@ -0,0 +1,25 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* This file has been generated from IFC4X3_TC1.exp. Do not make modifications *
* but instead modify the python script that has been used to generate this. *
* *
********************************************************************************/
@@ -0,0 +1,2 @@
#ifdef USE_IFC4
@@ -0,0 +1,2 @@
#ifdef USE_IFC4X1
@@ -0,0 +1,2 @@
#ifndef USE_IFC4
@@ -0,0 +1,2 @@
#ifndef USE_IFC4X1
@@ -0,0 +1,2 @@
#ifndef USE_IFC4X1
+29 -6
View File
@@ -248,6 +248,10 @@ READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR
NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX
try:
UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN
except:
UNKNOWN = 5 # Workaround
import struct
@@ -496,6 +500,24 @@ class rocksdb_file_storage:
return "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, version_tuple[0:2]))
class file_header:
def __init__(self, file, header_data):
self.file = file
self.header_data = header_data
@property
def file_description(self) -> entity_instance:
return entity_instance.wrap_value(self.header_data.file_description_py(), file=self.file)
@property
def file_name(self) -> entity_instance:
return entity_instance.wrap_value(self.header_data.file_name_py(), file=self.file)
@property
def file_schema(self) -> entity_instance:
return entity_instance.wrap_value(self.header_data.file_schema_py(), file=self.file)
class file:
"""Base class for containing IFC files.
@@ -513,7 +535,7 @@ class file:
"""
wrapped_data: ifcopenshell_wrapper.file
header: ifcopenshell_wrapper.IfcSpfHeader
header: file_header
units: dict[str, entity_instance] = {}
history_size: int = 64
history: list[Transaction]
@@ -586,8 +608,11 @@ class file:
"Unsupported schema: %s" % ",".join(self.header.file_schema.schema_identifiers),
),
INVALID_SYNTAX: lambda: (Error, "Syntax error during parse, check logs"),
# This is the case when passing uninitialized_tag
UNKNOWN: lambda: (None, None),
}[f.good().value()]()
raise exc(msg)
if exc is not None:
raise exc(msg)
else:
args = filter(None, [schema])
args = map(ifcopenshell_wrapper.schema_by_name, args)
@@ -1025,12 +1050,10 @@ class file:
@property
def header(self):
try: # Temporary workaround until new builds are ready. See #7131.
h = self.wrapped_data.header()
try:
return file_header(self, self.wrapped_data.header())
except:
return self.wrapped_data.header
object.__setattr__(h, "file_ref", lambda inst: entity_instance.wrap_value(inst, file=self))
return h
@property
def storage(self) -> Optional[rocksdb_file_storage]:
@@ -22,7 +22,7 @@ import os
import sys
import operator
from .. import ifcopenshell_wrapper
from .. import open, ifcopenshell_wrapper
from ..file import file
from ..entity_instance import entity_instance
@@ -307,10 +307,7 @@ class iterator(ifcopenshell_wrapper.Iterator):
self.file = file
file_or_filename = file_or_filename.wrapped_data
else:
# @todo?
self.file = None
# Makes sure people are able to use python's platform agnostic paths
file_or_filename = os.path.abspath(file_or_filename)
file_or_filename = self.file = open(file_or_filename)
if include is not None and exclude is not None:
raise ValueError("include and exclude cannot be specified simultaneously")
@@ -342,7 +339,9 @@ class iterator(ifcopenshell_wrapper.Iterator):
geometry_library, self.settings, file_or_filename, include_or_exclude, include is not None, num_threads
)
else:
ifcopenshell_wrapper.Iterator.__init__(self, geometry_library, settings, file_or_filename, num_threads)
self.this = ifcopenshell_wrapper.construct_iterator(
geometry_library, self.settings, file_or_filename, num_threads
)
if has_occ:

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