Compare commits

...

257 Commits

Author SHA1 Message Date
Andrej730 ca2f683af3 Update BBIM version in docs 2024-04-13 10:59:56 +05:00
Andrej730 eb46de9658 material.unassign_material - support batching #4474 2024-04-12 17:41:37 +05:00
Andrej730 59cc9c0a16 material.assign_material - support batching #4474 2024-04-12 17:41:37 +05:00
Andrej730 32d2f1d629 material.assign_material - update owner history 2024-04-12 17:41:37 +05:00
Andrej730 38a73d6510 ifcpatch - add ifc2x3 tests and test for MergeDuplicateTypes 2024-04-12 17:41:36 +05:00
Andrej730 59b496d4b5 typing 2024-04-12 17:41:36 +05:00
Dion Moult e56aa45a82 Fix #4526. Bug when exporting IfcTester results to BCF due to new "failures" attribute. 2024-04-12 10:09:52 +10:00
Dion Moult a3c99b8362 See #4541. Add documentation examples on how to use include filters in IfcConvert. 2024-04-12 10:09:52 +10:00
Andrej730 e66ac3d7db system.unassign_system - support batching #4474 2024-04-11 17:50:31 +05:00
Andrej730 6b48e8b4b9 system.assign_system - support batching #4474 2024-04-11 17:50:31 +05:00
Andrej730 3d243b5eee system.add_system - fix failing default agrument in ifc2x3 2024-04-11 17:50:31 +05:00
Andrej730 fa0dbac914 new util method ifcopenshell.util.element.get_groups 2024-04-11 17:50:30 +05:00
Andrej730 49e7abb4dc type.unassign_type - support batching #4474 2024-04-11 17:09:41 +05:00
Andrej730 5098550399 assign_type not to remap if it's type was assigned previously 2024-04-11 17:09:41 +05:00
Andrej730 e12c05eea0 typing 2024-04-11 17:09:41 +05:00
Andrej730 39433fb5a5 remove unused lines from tests 2024-04-11 17:09:41 +05:00
Andrej730 1ea80016cd fix bug generating profiles on assign_type after df4943205 2024-04-11 17:09:41 +05:00
Vukas Pajic 58b39d3926 fix: address test fails 2024-04-11 13:03:13 +02:00
Vukas Pajic 946c868388 fix: cascade_schedule correctly handles timelag with IfcRatioMeasure 2024-04-11 11:35:34 +02:00
Andrej730 df4943205a type.assign_type - support batching #4474 2024-04-10 18:58:44 +05:00
Andrej730 ba4eb2a891 fix bug in nest.assign_object 2024-04-10 18:58:44 +05:00
Andrej730 90bb2f0e16 unify tests for ifc4 and ifc2x3 2024-04-10 18:45:37 +05:00
Andrej730 c2840d92c2 fix test files names 2024-04-10 18:45:37 +05:00
Andrej730 81ff9c168e nest.assign_object to support ifc2x3 2024-04-10 18:45:37 +05:00
Andrej730 239ab5b587 get_pset() fix bug getting inherited psets with psets_only/qtos_only = True 2024-04-10 18:45:37 +05:00
Andrej730 37a33d62d5 further ifcopenshell.util.element optimization
and readibility for props addressed by the index
2024-04-10 18:45:37 +05:00
Andrej730 13c6fdd3c6 maintain verbosity getting complex property subproperties 2024-04-10 18:45:36 +05:00
Andrej730 e69597f72b typing 2024-04-10 18:45:36 +05:00
Andrej730 9c633c8b6c black format 2024-04-10 18:17:19 +05:00
Alper Kantarci 4d906578c7 Use "gltf" instead of "gtlf" (#4538)
- Fix "gtlf" typo
2024-04-10 21:02:56 +10:00
Dion Moult de744dddda Fix #4290. You can now format custom thousands separators and decimal separators in IfcCSV. 2024-04-10 17:32:39 +10:00
Dion Moult 0a04b88a52 Document how MacOS users using Blender from Steam should still use Mac Intel 2024-04-10 14:30:38 +10:00
Dion Moult bee1e21f0e Fix #4490. Fix fundamental bug where get_pset would return none for inherited props with a boolean False value. 2024-04-10 12:57:52 +10:00
Dion Moult f5c5fba151 Fix issue where the underlay cache option was not yet implemented and ignored. 2024-04-10 11:09:58 +10:00
Dion Moult 4f9badc063 Fix issue where reloading the drawing list would cause drawing names to be synced
It would run useless sync / API calls to edit psets on potentially hundreds of drawings.
2024-04-10 11:07:17 +10:00
Dion Moult b61d870922 See #4481. Defer setting render visibility to drawing generation to speed up switching between drawings.
Doing obj.hide_render = True/False in a loop is slow and there is no bulk set command.
2024-04-10 10:37:33 +10:00
Andrej730 153389fe4a ifcopenshell.util.element optimization
Tried to reduce amount of IFC attributes retrieved by reusing already retrieved values.

E.g. previously get_nest() was retrieving same attribute 3 times on each call, now it does it only once and it's 2 times faster (2.4 ms -> 1.2 ms).
2024-04-09 18:36:55 +05:00
Andrej730 965a45d65b ifcopenshell.util.element.get_nest test 2024-04-09 18:33:26 +05:00
Andrej730 06b84cfa24 nest.unassign_object - support batching #4474 2024-04-09 18:33:25 +05:00
Andrej730 97db95085a nest.assign_object - support batching #4474 2024-04-09 18:33:25 +05:00
Andrej730 d1bcec7a6a typing 2024-04-09 18:33:25 +05:00
Andrej730 7b46c6a443 black format 2024-04-09 17:05:36 +05:00
Andrej730 6db0c14fa8 ifcopenshell.api.run optimization
cache usecase classes instead of importing their modules and getting them each time, makes empty api call twice as fast
2024-04-09 16:18:11 +05:00
Andrej730 d03f9237e2 deprecate spatial.remove_container in favor of spatial.assign_container
added temporary support for old version with warnings:

WARNING. `spatial.remove_container` api method is deprecated and should be replaced with spatial.unassign_container
WARNING. `product` argument is deprecated for API method "spatial.remove_container" and should be replaced with `products`.
2024-04-09 16:09:04 +05:00
Dion Moult 24716a0089 See #4481. Faster implementation of object hiding and toggline viewport visibility. 2024-04-09 18:39:41 +10:00
Andrej730 437d19e1f5 normalize line endings in CCI(EN).ifc (was commited as CRLF, not LF) 2024-04-09 12:43:50 +05:00
Dion Moult bd5fd3539a Fix #4497. Drawing text annotations are now based on actual line breaks. 2024-04-09 15:56:16 +10:00
Dion Moult bcc67ecb31 See #4497. Show invisible newlines and allow newline editing in attributes. 2024-04-09 15:25:10 +10:00
Dion Moult c50810a24a Fix #4533. Add convenience link to launch type manager when no types exist. 2024-04-09 14:10:01 +10:00
Benoit DAMAY 7f9ce37a98 Inline help feature (#4409)
* Help operator in grouping and filtering Panel

Opens up a webbrowser on click

* Help button on the right of filtering panel header

* replacing os.rename() by shutil.move in ifcpatch

* removing debug print from previous commit

* Removing extra operator
Using bim.open_uri to open help page

* Update docs url

* Clean up whitespace

---------

Co-authored-by: Dion Moult <dion@thinkmoult.com>
2024-04-09 11:50:08 +10:00
ppaawweeuu ac23d08717 Update CCI(EN).ifc
added last two rows with ending markers
2024-04-09 11:45:34 +10:00
ppaawweeuu a8211ed592 Add CCI Classification schema
I'd like to add CCI classification to BBIM. It is very similar to CSS, developed by CCI-C and licensed under CC BY 4.0
More details here: https://cci-collaboration.org/the-standard/

This is my first commit, I used CCS(EN).ifc file as reference. Any feedback welcomed.
2024-04-09 11:45:34 +10:00
Gorgious e38f3beb91 New UI List in Sandbox panel to access, flip, remove, add, edit clipping planes
and some tweaks :
- Disable possibility to edit clipping plane quad geometry
- Add select and delete operators
- Remove polls from CreateClippingPlane that prevented from using it in the Properties editor. The clipping plane is added near the origin when using the Add button in the panel
2024-04-08 21:31:23 +02:00
Andrej730 f14bf8557a aggregate.unassign_object - support batching #4474 2024-04-08 17:49:48 +05:00
Andrej730 cd11d2de27 aggregate.assign_object - support batching #4474 2024-04-08 17:49:48 +05:00
Andrej730 d764c0af23 temporary support for deprecated api arguments #4531 2024-04-08 17:49:48 +05:00
Andrej730 d24e5df7c1 typing 2024-04-08 17:27:41 +05:00
Andrej730 de5075aafe black format 2024-04-08 17:27:36 +05:00
Andrej730 9474d17867 fix test after 7dd0d7601 2024-04-08 16:28:05 +05:00
Andrej730 abe6dbdfc9 layer.unassign_layer - support batching #4474 2024-04-08 16:28:05 +05:00
Andrej730 7ecec973dd layer.assign_layer - support batching #4474 2024-04-08 14:51:07 +05:00
Andrej730 f34aef1ebf validate - add examples 2024-04-08 13:56:52 +05:00
Andrej730 3253bdf7aa fix possible infinite recursion in bim.debug_active_drawing #4500
also fixed the issue with mixed up results at the end of the operator execution
2024-04-08 12:19:57 +05:00
Dion Moult 8e2f9d2bb5 Fix #4509. Don't show scary error message if you haven't specified a filter query. 2024-04-08 17:19:17 +10:00
Dion Moult 7dd0d7601a Fix #3759. IFC4X3 files should use ADD2 identifier. 2024-04-08 12:34:59 +10:00
Dion Moult 5bd53b973f Fix #4303. Declare dependencies for ifctester. 2024-04-08 11:57:21 +10:00
Dion Moult 911cfe010e Publish IfcClash to PyPI 2024-04-08 11:19:13 +10:00
Dion Moult 35dd03664a Publish IfcCSV to PyPI 2024-04-08 10:55:59 +10:00
Dion Moult c89b84ff57 Whoops again 2024-04-08 10:31:37 +10:00
Dion Moult cda13af7a3 See #4529. Publish IfcPatch to PyPI. 2024-04-08 10:24:04 +10:00
Dion Moult 6e84eb0acc Add bSDD makefile 2024-04-08 10:09:58 +10:00
Dion Moult b9ede5c739 Whoops 2024-04-08 10:08:01 +10:00
Dion Moult 44b980c1e0 Publish bSDD to PyPI 2024-04-08 10:06:28 +10:00
Dion Moult 08f4dea47f Materials with no style colours now default to Blender's default colour when linking models 2024-04-08 08:33:18 +10:00
Dion Moult 8f2a7be903 Fix #4521. 2024-04-08 08:32:28 +10:00
Dion Moult 63b541917e Minor fix in doc utility for entities which have no predefined types 2024-04-06 23:48:14 +11:00
Dion Moult c8706bddc9 Fix bug where drawing psets were needlessly updated and random objects would get drawing psets added on them
The prop update callbacks on has_underlay, has_linework, and has_annotation would edit the active object, which is not necessarily the camera. Yikes! Also, this means that when activating the drawing, it would sync references and every time it synced a reference it would attempt to sync these properties too which was slow and wasteful.
2024-04-06 23:37:42 +11:00
Dion Moult e969d407d3 Fix #4524. Update README.md 2024-04-06 22:21:16 +11:00
Dion Moult 066b198ac9 Fix #4492. Refix bug where there was an extra equal sign when loading saved queries. 2024-04-06 21:42:49 +11:00
sebphi 5d6119e542 Fix import of ifcopenshell.util.unit in Python <3.10 #4520 2024-04-05 17:46:55 +02:00
Andrej730 845e229a9c group.unassign_group - support batching #4474 2024-04-05 17:31:13 +05:00
Andrej730 cefa088b59 spatial.unassign_container - support batching #4474 2024-04-05 17:31:13 +05:00
Andrej730 109787d869 spatial.assign_container - support batching #4474 2024-04-05 17:28:30 +05:00
Andrej730 78abca16e6 black format 2024-04-05 16:47:14 +05:00
Andrej730 02aaf1b409 typing 2024-04-05 16:47:13 +05:00
Andrej730 02e102f2d2 Collector.sync skip_unlinking option #4500
To speed up the drawing process in large projects and reduce time on syncing stage I've found that if object is already assigned to it's parent collection (both as part of blender collection and in IFC) we can skip assigning stage significantly reducing time on syncing in general (from 20-25 secs -> 9 secs).

The only thing we'll be missing in that case that object won't be unlinked from other unrelated blender collections which shouldn't break anything in the case of drawing generation.
2024-04-05 16:30:44 +05:00
Andrej730 ec0dcb7f8c documentation and small refactor 2024-04-05 16:30:43 +05:00
Andrej730 ba63ed81a1 small optimizations
1) stop on first parent collection
2) ensure object's own collection's parent has BIMCollectionProperties.obj during the loop
3) collector._get_collections - returns project's own collection for consistency
4) couple tweaks skipping unnecessary iterations in loops
2024-04-05 15:55:01 +05:00
Dion Moult fb6660361d See #4518. Fix pypi python version requirements. 2024-04-05 21:13:24 +11:00
Dion Moult ecae9943e2 Add missing build dep 2024-04-05 21:12:27 +11:00
Thomas Krijnen 104e896531 #4278 Update cd.yml bump ubuntu version 2024-04-05 09:27:33 +02:00
Dion Moult 6555e56647 Fix evil hacks in ifcopenshell pip dist packaging and support py312 on PyPI 2024-04-05 16:54:29 +11:00
Dion Moult f9a590a22c Ship py39 to py312 for PyPI 2024-04-05 16:49:14 +11:00
Dion Moult ad4a1576d3 Allow manual workflow dispatch of ci-ifcopenshell-pypi.yml 2024-04-05 16:44:53 +11:00
Dion Moult c35c3f06b5 Fix #4504. Extract CAD layers when linking in models. 2024-04-05 15:03:47 +11:00
Dion Moult fada808a2f See #4504. Extract material data for linked models. 2024-04-05 14:53:11 +11:00
Dion Moult e221e6fa6a See #4508. Update example code to serialise to obj. 2024-04-05 08:25:59 +11:00
jaymeandretta 725dac04d0 Updates links to BlenderBIM and IfcOpenShell documentation in README 2024-04-05 08:12:43 +11:00
Andrej730 5c365990b9 bim.debug_active_drawing debug operator #4500
Operator can help narrow down failing elements when drawing is failing with "RuntimeError: An unknown error occurred".

it's located in debug section - https://i.imgur.com/b03jmkZ.png

The output is noisy but the results are highlighted:
https://i.imgur.com/0ZA0u60.png
https://i.imgur.com/oXzqIXU.png
https://i.imgur.com/Y0nMRMz.png
2024-04-04 16:42:00 +05:00
Andrej730 797e2e41a5 bim.create_drawing to sync only once if printing multiple drawings
Turned out syncing process takes too much time on large projects, so we can speed up the printing process by syncing just once. Also exposed option not to sync to operator's properties to allow debugging failing elements.

A bit related to #4500
2024-04-04 16:41:59 +05:00
Andrej730 57fe40839b black format, cleanup, notes
bim.generate_global_id - forgot to remove it previously, this operator isn't intended for debug section
2024-04-04 16:21:10 +05:00
Andrej730 b4c5ad1e35 typing 2024-04-04 16:07:31 +05:00
Dion Moult 635ddce93d Autodetect CSG representation types in shape builder 2024-04-04 18:28:26 +11:00
Dion Moult 24e807cac7 Detect binary data types in util.attribute 2024-04-04 18:28:26 +11:00
Massimo Fabbro 33ceba38a8 Qto_Calculator more consistent calculation about the PredefinedType 2024-04-03 22:26:50 +02:00
Massimo Fabbro 90ed1f9b28 improve the flooring height object quantity calculation 2024-04-03 22:26:50 +02:00
Massimo Fabbro e431cb205c improve the ceiling height object quantity calculation 2024-04-03 22:26:50 +02:00
Massimo Fabbro 09bb45f5bc fix add ceiling from walls elevation bug 2024-04-03 22:15:11 +02:00
Andrej730 7087452cde log external styles names when they fail to load 2024-04-03 17:37:38 +05:00
Andrej730 af4b58444d typing 2024-04-03 17:37:38 +05:00
Andrej730 711423cae5 use default shading_styles.json in case if project's file is missing
to make it consistent with how we use in similar cases default styles, symbols, etc
2024-04-03 17:37:38 +05:00
Andrej730 eb8b6062b4 move override panel methods to tool.Blender
to keep them close, so it will be easier to keep them in sync
2024-04-03 17:37:38 +05:00
Andrej730 5ac96cb5b9 change .poll for overridden panels instead of creating new classes
Needed to avoid errors disabling addons that have overridden panels
2024-04-03 17:37:38 +05:00
Andrej730 c462fc9337 move scene panels from different addons to "Blender Properties"
now we also automatically get list of default blender scene panels instead of hardcoding them
2024-04-03 17:37:38 +05:00
Andrej730 95f7a2bcde override blender ui panels when should_setup_workspace = False
Previously with should_setup_workspace = False it wouldn't override Blender UI scene panels and they would get mixed up with BBIM panels. After fix Blender panels are always moved to "Blender Properties" tab

before - https://i.imgur.com/bPvcAEe.png
now - https://i.imgur.com/lRUox29.png
2024-04-03 17:37:38 +05:00
Dion Moult 04cf9c870b Fix bug when linking in models with no chunks 2024-04-03 21:24:03 +11:00
Andrej730 bf9bddc867 ifcopenshell.validate to check unique guids (IfcRoot.UR1) 2024-04-02 17:17:23 +05:00
Andrej730 20a2fda066 typing 2024-04-02 15:45:42 +05:00
Andrej730 f03afb20f3 validate.py - black format 2024-04-02 15:45:42 +05:00
Gorgious56 e0df22ffec The button to remove an inherited pset is no longer enabled on instances
Previously it would fail silently if clicked
2024-04-02 09:52:43 +02:00
Andrej730 96302a5c07 Return of the missing button for regenerating global id
Was about to implement to find that we already have "bim.generate_global_id" and it just was lost at some point. Returned it to the attributes UI. It also allows Updating guids for all selected elements on Shift+Click.

This also can be useful to solve MergeProject and other guids conflicts, see example issue in https://community.osarch.org/discussion/2062/bbim-mergeproject-patch-not-separating-elements-in-different-buildings

Example - https://imgur.com/a/mvRtyER
2024-04-02 12:44:06 +05:00
Andrej730 38afce1628 RegenerateGlobalIds - tests 2024-04-02 12:32:36 +05:00
Andrej730 cd5afacc97 Fix test after cf9a94a79 2024-04-01 17:22:49 +05:00
Andrej730 c2623e5dce prevent reloading reference annotations on loading .ifc #4478
1) if reference annotation was changed, it would reload discarding the changes
2) even if reference annotation wasn't changed it would still reload them producing noise in .ifc changes
2024-04-01 17:06:20 +05:00
Andrej730 42ecd7e127 typing 2024-04-01 17:04:00 +05:00
Andrej730 c80a7cfcc9 fix typo 2024-04-01 16:00:18 +05:00
Andrej730 6d8f1a3980 experimental svg to dxf conversion for drawings
Example - https://imgur.com/a/AkvLlbZ
2024-04-01 15:50:43 +05:00
Andrej730 5c4c61bb15 fix issue with prohibited facets in 0065c5f, add a test 2024-04-01 13:49:42 +05:00
Andrej730 d8af8b58bb ifctester.Specification.set_usage convenience method
sets minOccurs and maxOccurs according to the agreement mentioned in the latest ids.xsd
https://github.com/buildingSMART/IDS/blob/6d71cdf3547a0383c6cfbbead81a7cef7521ac3a/Development/ids.xsd#L67-L72
2024-04-01 12:26:07 +05:00
Andrej730 3c0405249b typing 2024-04-01 12:26:07 +05:00
Dion Moult 7f57649d10 Whoops. 2024-04-01 17:49:44 +11:00
Dion Moult fbde23870e Fix #4321. The PurgeData recipe now also works on IFC2X3. 2024-04-01 17:00:47 +11:00
Dion Moult 7b968030ca Fix #4060. The colour by property panel now shows the number of results in the legend. 2024-04-01 15:45:34 +11:00
Dion Moult b5d672ba42 Fix #4069. IfcClash Blender interface now uses the new filter UI. 2024-04-01 15:28:34 +11:00
Dion Moult efa8da83ba Bump IOS. 2024-04-01 10:19:40 +11:00
Dion Moult 64cf7237db IfcDiff now uses the new selector syntax with the new UI 2024-04-01 10:16:30 +11:00
Dion Moult cf9a94a799 Fix #4306. Remove superseded selector UI. 2024-04-01 09:44:50 +11:00
Ryan Schultz 46c1a8c8ff added search for styles - in BIM_PT_representation_items 2024-03-31 11:57:55 -05:00
Thomas Krijnen ac7f2aeb62 #4450 todo note 2024-03-31 15:16:30 +02:00
Dion Moult 9378a763f3 Fix #4328. Fix incorrect documentation about how to use the imperial_length formatting function. 2024-03-31 22:32:07 +11:00
Dion Moult 0bc83fedae Use indices instead of WorkTimeDatesInterface
Checking start/end dates are done very, very often in date calculations (often looping through every day in a date range). So I'd prefer more opaque code but less overhead.
2024-03-31 22:11:17 +11:00
Thomas Krijnen f7c03db752 #4450 don't fail on empty bbox in prefiltering 2024-03-31 11:25:56 +02:00
Dion Moult b6b8c21741 See #4484. Editing predefined type attributes now enforces valid combinations of userdefined and object/element type 2024-03-31 15:50:31 +11:00
Dion Moult c8af51cfd2 Fix #4323. Bug where running a filter could fail if you tried to compare values which had incompatible types. 2024-03-30 23:07:06 +11:00
Dion Moult 77de3820e7 More minor doc changes and polish 2024-03-30 23:06:07 +11:00
Dion Moult adc5bea4e9 Fix #4265. Bug where when loading a saved filter it showed an unnecessary = comparison (which is by default). 2024-03-30 23:05:37 +11:00
Dion Moult 1eddcb9ee0 Minor doc fixes 2024-03-30 17:37:50 +11:00
Dion Moult eae542f90f Fix #4461. Write documentation on how to build and edit documentation. 2024-03-30 17:37:14 +11:00
Dion Moult fab0d05279 Fix #4483. 2024-03-30 17:14:13 +11:00
Dion Moult d2e495a69f Fix #4318. Theme docs to match website branding and add edit button, footer links, and icons. 2024-03-30 16:47:56 +11:00
Dion Moult 02058efa4d Fix #4286. IfcCSV summaries are now formatted the same as the rows. 2024-03-30 15:40:02 +11:00
Dion Moult ed79cbb1cc Fix bug where loading drawings didn't correctly link shared meshes (e.g. from a type of an annotation) 2024-03-30 15:05:49 +11:00
Dion Moult 0065c5fba8 Fix incorrect handling of prohibited specifications 2024-03-30 00:13:02 +11:00
Andrej730 1ab24e8362 import referenced drawings activating the current drawing #4478 2024-03-29 15:13:40 +05:00
Andrej730 232b4ec34d typing 2024-03-29 15:11:47 +05:00
Thomas Krijnen 75e6147dbc #4474 Commented out code perhaps for future use 2024-03-29 10:46:25 +01:00
Dion Moult 8d7b293381 Fix #4305. Bug where importing spreadsheets into IFC didn't intelligently cast attribute data types 2024-03-29 20:37:34 +11:00
Andrej730 19c8afb652 Fix #4477 2024-03-29 11:51:14 +05:00
Dion Moult 84dcee6b36 Fix #4368. Update to latest Brickschema alpha to cut dep size 2024-03-29 15:48:52 +11:00
Dion Moult dadcf03f55 See #4473. Cull entourage library.
We haven't confirmed the legality of the other assets.
2024-03-29 15:33:47 +11:00
Dion Moult 413886ac6b Fix #4411. Fallback to absolute paths if relative paths fail. 2024-03-29 15:29:19 +11:00
Dion Moult 377f68c020 Run black on IfcTester 2024-03-29 15:28:33 +11:00
Dion Moult b718a37db2 Fix #4427. Failures are now stored in single dictionary.
There was a missing clear() call to failed_reasons which could lead to incorrect reporting. It also makes no sense to store them separately.
2024-03-29 15:06:59 +11:00
Dion Moult e21043b038 Fix #4465. Update IfcTester to IDS 0.9.7 2024-03-28 22:47:48 +11:00
Andrej730 395598d8e3 Fix invalid profiles after subdividing edges near arcs #4471 2024-03-28 14:02:03 +05:00
Andrej730 813518bcb5 use blender units subtype for cad props 2024-03-28 14:02:03 +05:00
Andrej730 4ce7a898c9 typing, format 2024-03-28 14:02:02 +05:00
Thomas Krijnen 78f235ea7c #4273 ignore degenerate triangles in mesher output 2024-03-28 09:06:41 +01:00
Vukas Pajic bda2f50f9d sync with latest 2024-03-28 17:15:58 +11:00
Vukas Pajic 5e5ba5b3d4 Additional metadata as property set #4425
https://github.com/IfcOpenShell/IfcOpenShell/issues/4425
2024-03-28 17:15:58 +11:00
Dion Moult 22b0e9dcc5 Minor doc fixes to refer to Mac intel and silicon 2024-03-28 17:13:50 +11:00
Dion Moult 68420d43ea Clash results now are sorted and show text overlays of clash type and distance 2024-03-28 17:11:46 +11:00
Dion Moult 924e337c90 Don't consider borderline points to be inside a shape for more robust protrusion results 2024-03-28 16:57:54 +11:00
Dion Moult 04599a10d2 Change precision check of "is point on line" to be 0.01mm
Precision::Confusion() is 1e-7 which is too small apparently when doing this math. There are plenty of results which are things like 5e-7.
2024-03-28 16:57:54 +11:00
Dion Moult 84c931bb5f Fix bug where hidden elements weren't selected sometimes 2024-03-28 16:52:30 +11:00
Dion Moult 572aedb3eb Fix #4269. Include dateutil for ifcopenshell on pypi 2024-03-28 10:48:19 +11:00
Dion Moult 1db86c24e3 Bump IfcOpenShell and remove py37 py38 from installation docs 2024-03-28 10:47:44 +11:00
Andrej730 74adde3030 ExtractElemenets - add some tests 2024-03-27 17:52:18 +05:00
Andrej730 f68e8f0e00 ExtractElements - fix bug where IfcElementAssembly elements wasn't added to the spatial tree
Example issue - https://community.osarch.org/discussion/2054/bbim-extractelements-patch-and-missing-elements-in-ifcelementassembly
2024-03-27 16:59:10 +05:00
Andrej730 98386984f3 Fix tests involving changes in Blender 4.1 (will fail on Blender <4.1)
Basically in Blender 4.1 they've changed a way renaming IDs works if name already existed before and those tests will stop working on Blender <4.1, see https://projects.blender.org/blender/blender/issues/119955
2024-03-27 15:23:46 +05:00
Andrej730 7b35ed97ca tests - reduce precision to our usual Cad tool precision to fix some tests 2024-03-27 15:07:49 +05:00
Andrej730 b3302ed679 Fix test in 1c1035e
tag @brunoperdigao for information
2024-03-27 15:07:49 +05:00
Andrej730 e12f060af8 black format 2024-03-27 15:07:49 +05:00
Dion Moult 6d4f47a2ca New entourage library including people and cars 2024-03-27 20:23:44 +11:00
Thomas Krijnen e49ab7e0cc #4070 don't increment aggregate length on invalid members 2024-03-27 09:36:05 +01:00
Dion Moult ccfe7ab80a Optimise property / quantity checks for IDS and other purposes 2024-03-27 16:09:13 +11:00
Dion Moult 7b95358975 Forgot to commit clash tool 2024-03-27 16:06:47 +11:00
Dion Moult bcc50aaadc Switch over clash to latest add_element signature 2024-03-27 16:06:15 +11:00
Dion Moult 30f63b7d67 Minor note about which OCCT version is currently supported 2024-03-27 15:59:25 +11:00
Dion Moult ead13e23bc Write docs about new geometry tree clash functions 2024-03-27 15:59:25 +11:00
Thomas Krijnen 24257d2a85 checkout v0.7.0 -- src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp 2024-03-26 15:13:36 +01:00
Dion Moult 5c4dbb924d Reinclude missing max_protrusions_ 2024-03-26 15:13:36 +01:00
Thomas Krijnen 88e1b3d2f9 Fix is_manifold(), sorry :( 2024-03-26 15:13:36 +01:00
Thomas Krijnen f67b80c6a6 Integrate @Moult's add_element(TriangulationElement*) 2024-03-26 15:13:36 +01:00
Dion Moult c7fd42f2cf Clash detection features 2024-03-26 15:13:36 +01:00
Thomas Krijnen bed03bf83c #4307 2024-03-26 12:27:05 +01:00
Andrej730 1e49400e93 fix styles tests 2024-03-26 16:12:13 +05:00
Andrej730 608c0dc05d Fix tests, clean up file creating empty project
To fix tests changed bim.unlink_object not to unlink original blender materials as they might be used elsewhere.
2024-03-26 15:59:23 +05:00
Andrej730 921dbe9dae restore accidentally unregistered operator in 9be7d95 (caused issue in tests) 2024-03-26 15:54:46 +05:00
Andrej730 a7e0837f96 small optimization
not to calculate each quantity twice
2024-03-26 15:54:46 +05:00
Andrej730 071c80f845 fix tests 2024-03-26 15:54:46 +05:00
Andrej730 06d823b9d8 typing 2024-03-26 15:54:45 +05:00
Andrej730 b15e33e5e9 fix test after 7634e71 2024-03-26 14:32:16 +05:00
Thomas Krijnen 55885f7a24 #4329 Don't accept non-finite floats 2024-03-26 10:23:14 +01:00
Ian e5fd6d9152 Update build-deps.cmd 2024-03-26 19:32:56 +11:00
Gorgious56 4dda7e84a0 You can now select only the aggregate or the aggregate and all its part using the interface 2024-03-26 08:33:18 +01:00
Dion Moult bb6be4d52d Blender IfcClash interface now supports new IfcClash modes
The dumb clash manager has been merged in with the main clash sets window. Loading and saving clashes now also bring in clash results, so the dumb clash manager's load/save is being deprecated. New decorator highlights clash result visually.
2024-03-26 15:24:47 +11:00
Dion Moult 01d487f585 Upgrade IfcClash to use new clash capabilities (currently unmerged from feature-clash2) 2024-03-26 15:22:56 +11:00
Andrej730 67a17ddd1b Fix hiding annotation objects without repr on drawing activation 2024-03-25 14:44:51 +05:00
Andrej730 f916edce1e typing 2024-03-25 14:43:53 +05:00
Andrej730 b0d810199b fix duplicated function names in test_feature 2024-03-25 14:18:35 +05:00
Andrej730 515a72cec9 fix linked ifc selectability toggle 2024-03-25 14:01:08 +05:00
Andrej730 7464f7519f fix load/unload linked ifc tests 2024-03-25 14:01:08 +05:00
Andrej730 f0dde4f825 fix linked ifc visibility toggle tests 2024-03-25 14:01:08 +05:00
Andrej730 54fefb3684 fix linked ifc wireframe mode toggle 2024-03-25 14:01:08 +05:00
tim 628fcb2999 Remove object text from subheadings (#4445)
https://github.com/IfcOpenShell/IfcOpenShell/issues/4312
2024-03-25 13:37:11 +05:00
Thomas Krijnen 22f830b053 #4130 setOnlyValid() option that skips and logs invalid topology 2024-03-24 16:45:31 +01:00
Bruno Postle 579c72fc69 Drawings panel: fix select_all_drawings operator
Previously it always deselected all drawings, now works as advertised
2024-03-24 10:31:47 +00:00
Andrej730 642a0f7084 black format 2024-03-22 17:46:21 +05:00
Andrej730 7cd5aab1c5 Fix ifc linking tests 2024-03-22 17:44:38 +05:00
Andrej730 4b15535c9c Fix issue removing material set
1) core.remove_material_set were removing material set and calling material_tool.get_active_material_type()
2) material_tool.get_active_material_type() called MaterialsData which failed with the traceback below since props.materias[current_index] had ifc id that were just removed

Traceback (most recent call last):
  File "\prop.py", line 96, in get_material_types
    MaterialsData.load()
  File "\data.py", line 43, in load
    "active_styles": cls.active_styles(),
                     ^^^^^^^^^^^^^^^^^^^
  File "\data.py", line 119, in active_styles
    material = tool.Ifc.get().by_id(material)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \blenderbim\libs\site\packages\ifcopenshell\file.py", line 411, in by_id
    return self[id]
           ~~~~^^^^
  File \blenderbim\libs\site\packages\ifcopenshell\file.py", line 396, in __getitem__
    return entity_instance(self.wrapped_data.by_id(key), self)
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \blenderbim\libs\site\packages\ifcopenshell\ifcopenshell_wrapper.py", line 4619, in by_id
    return _ifcopenshell_wrapper.file_by_id(self, id)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Instance # 66 not found
File "\prop.py", line 94, in get_material_types
WARN (bpy.rna): C:\Users\blender\git\blender-v410\blender.git\source\blender\python\intern\bpy_rna.cc:1366 pyrna_enum_to_py: current value '2' matches no enum in 'BIMMaterialProperties', '', 'material_type'
2024-03-22 16:54:42 +05:00
Andrej730 b978da24d9 fix ui error getting active styles for non IfcMaterial materials
Error was:

```
Traceback (most recent call last):
  File "\blenderbim\bim\module\material\ui.py", line 43, in draw
    MaterialsData.load()
  File "\blenderbim\bim\module\material\data.py", line 43, in load
    "active_styles": cls.active_styles(),
                     ^^^^^^^^^^^^^^^^^^^
  File "\blenderbim\bim\module\material\data.py", line 117, in active_styles
    return results

  File "\blenderbim\libs\site\packages\ifcopenshell\entity_instance.py", line 194, in __getattr__
    raise AttributeError(
AttributeError: entity instance of type 'IFC4.IfcMaterialLayerSet' has no attribute 'HasRepresentation'
```
2024-03-22 16:54:42 +05:00
Andrej730 7132c4e9bf typing and small refactor 2024-03-22 16:54:42 +05:00
Andrej730 5aa6123806 Blender 4.1 - fix errors due to renamed type manager icon 2024-03-22 15:58:45 +05:00
FabioPiccinini e48299cb3f Update sample.css (#4459)
Changed unescaped ; with , because it may cause problems if the style is included in svg
2024-03-22 11:51:57 +01:00
FabioPiccinini c65088a7a7 Unescaped ; was causing problems in Inkscape (#4456)
* Update default.css

An unescaped ; was causing problems when opening svgs that included this style with Inkscape

* Update default.css
2024-03-22 10:06:57 +01:00
Thomas Krijnen 4f53d8c5f1 #4452 release gil in ifcopenshell.open() and read() 2024-03-21 14:11:34 +01:00
Andrej730 424b2589d9 add macos py3.11-3.12 to docs 2024-03-21 16:59:53 +05:00
Andrej730 05f6ed1084 bump ifc version #4174 2024-03-21 15:10:57 +05:00
Andrej730 9ec80cee0d TesselateElements - avoid processing queries that affect no elements
Previously it was failing with

 File "\ifcopenshell\geom\main.py", line 119, in __init__
    self.this = initializer(
  File "\ifcopenshell\ifcopenshell_wrapper.py", line 2467, in construct_iterator_with_include_exclude
    return _ifcopenshell_wrapper.construct_iterator_with_include_exclude(settings, file, elems, include, num_threads)
TypeError: Attribute of type AGGREGATE OF STRING needs a python sequence of strs
2024-03-21 15:06:32 +05:00
Andrej730 a5daf4177a add_mesh_representation - option to force faceted breps 2024-03-21 15:06:32 +05:00
Andrej730 1b1eccb0c1 typing 2024-03-21 15:06:32 +05:00
Andrej730 ece9e5c834 grammar 😬 2024-03-21 11:57:42 +05:00
Andrej730 6005ff641b Clarifying info message saving guids to the text inside .blend #4446
Example - https://i.imgur.com/YtuQ9YA.png
2024-03-21 11:55:28 +05:00
Andrej730 fa603244b9 black format 2024-03-21 11:55:20 +05:00
Andrej730 6d6010be3b typing 2024-03-21 11:32:53 +05:00
Andrej730 3691052f06 bim.select_similar_type optimization
1) avoid selecting elements of the same type multiple times
2) iterate over occurrences of the type rather than visible objects, as it's usually smaller amount of objects
2024-03-21 11:23:45 +05:00
Thomas Krijnen e38eafd034 #4447 build-all.py 2024-03-20 14:58:06 +01:00
Andrej730 976eeaef20 make spatial elements unselectable from viewport by default
It seems most of the time they only get in the way trying to select some objects at scene origin.

fyi @theoryshaw
2024-03-20 17:30:58 +05:00
Andrej730 09da42d649 typing 2024-03-20 17:30:58 +05:00
Andrej730 97b23a9ac6 unload decorations on unlinking ifc file 2024-03-20 17:30:58 +05:00
Andrej730 5e8a57f089 fix issue with removing collections and scenes unlinking .ifc
it couldn't find linked collections and scenes since it was comparing `filepath` from `props.links` (.ifc file) and collection.library.filepath (which is .blend file)
2024-03-20 17:30:58 +05:00
Andrej730 7f34d18bbf hide "no object selected" label when there are no linked ifc files 2024-03-20 17:30:58 +05:00
Andrej730 97cc19ed43 Remove appending occurrences from library in favor of fcaac6b 2024-03-20 17:30:57 +05:00
aothms d49faf7412 #4174 debug osx install of py311 and 312 2024-03-20 06:22:33 -04:00
Andrej730 fcaac6bd98 Append IfcElements from linked IFC files
Example - https://imgur.com/a/phPcKSo

Appending ifc elements from library files will be deprecated as it's much more limited than appending it from linked files (in library you can only see object's name).
2024-03-19 17:45:48 +05:00
Andrej730 c35cdcfcad util.selector.set_element_value to work w/o preassigned placement #4439 2024-03-19 17:45:21 +05:00
Andrej730 205da63d8e util.selector.set_element_value ignore values within tolerance #4439 2024-03-19 17:45:21 +05:00
Andrej730 3f529ae21b collector.sync note 2024-03-19 17:45:21 +05:00
Andrej730 89f7105e9c typing 2024-03-19 17:45:20 +05:00
Andrej730 e5dd06c461 Fix for loading IFC files with non-IfcElement based IfcRelNests
Example is below, it was using IfcAlignment to aggregate IfcAnnotations and IfcReferents.

I've added support loading collections for IfcPositioningElements besides IfcGrid (grids already handled separately).

https://community.osarch.org/discussion/2034/ifc-not-visible-after-loaded-in-blender-bim
2024-03-19 15:25:38 +05:00
Andrej730 ec7af4e719 typing 2024-03-19 15:24:14 +05:00
Andrej730 a755ec99f9 small fix 2024-03-18 17:08:39 +05:00
Andrej730 812389bce2 ifccsv - support for assigning x/y/z coordinates from csv 2024-03-18 17:06:00 +05:00
Andrej730 b67b5e8d9f typing 2024-03-18 17:05:55 +05:00
M!l!nd e2a06e10b6 fixed qsets to display separately (#4423)
* fixed qsets to display separately

Signed-off-by: milinddethe15 <milinddethe15@gmail.com>

* updated data.py and run black formattor

---------

Signed-off-by: milinddethe15 <milinddethe15@gmail.com>
2024-03-18 15:14:15 +05:00
Ryan Schultz 7fa26b2d60 small fix to cdd332d1ee, for clarity 2024-03-17 14:37:38 -05:00
356 changed files with 13537 additions and 4387 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
- run: echo ok go
build:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
needs: activate
steps:
- uses: actions/checkout@v2
@@ -80,7 +80,7 @@ jobs:
path: build/assets/Ifc*
deliver:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
needs: build
name: Docker Build, Tag, Push
+48
View File
@@ -0,0 +1,48 @@
name: ci-bsdd-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
workflow_dispatch:
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
cd src/bsdd &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/bsdd/dist
+48
View File
@@ -0,0 +1,48 @@
name: ci-ifcclash-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
workflow_dispatch:
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
cd src/ifcclash &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcclash/dist
+48
View File
@@ -0,0 +1,48 @@
name: ci-ifccsv-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
workflow_dispatch:
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
cd src/ifccsv &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifccsv/dist
+3 -2
View File
@@ -1,6 +1,7 @@
name: ci-ifcopenshell-pypi
on:
workflow_dispatch:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
@@ -35,7 +36,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py36, py37, py38, py39, py310, py311]
pyver: [py39, py310, py311, py312]
config:
- {
name: "Windows Build",
@@ -57,7 +58,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- run: echo ${{ env.DATE }}
- name: Get current date
+48
View File
@@ -0,0 +1,48 @@
name: ci-ifcpatch-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
workflow_dispatch:
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
cd src/ifcpatch &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcpatch/dist
+1 -10
View File
@@ -9,9 +9,6 @@ on:
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
push:
paths:
- '.github/workflows/ci-ifctester-pypi.yml'
workflow_dispatch:
env:
@@ -30,7 +27,6 @@ jobs:
build:
needs: activate
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -38,12 +34,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- run: echo ${{ env.DATE }}
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%y%m%d')"
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
+3
View File
@@ -81,6 +81,9 @@ src/blenderbim/blenderbim/translations.py
# blenderbim test temp files
src/blenderbim/test/files/temp
src/blenderbim/test/files/basic.ifc.cache.blend
src/blenderbim/test/files/basic.ifc.cache.sqlite
src/blenderbim/drawings
src/blenderbim/layouts
+7 -7
View File
@@ -18,14 +18,14 @@ and many other libraries, CLI apps, and more. Support is also provided for auxil
For more information, see:
* [IfcOpenShell Website](http://ifcopenshell.org)
* [IfcOpenShell Documentation](http://blenderbim.org/docs-python)
* [IfcOpenShell C++ Installation](https://blenderbim.org/docs-python/ifcopenshell/installation.html)
* [IfcOpenShell Python Installation](https://blenderbim.org/docs-python/ifcopenshell-python/installation.html)
* [IfcOpenShell Python Hello World Tutorial](https://blenderbim.org/docs-python/ifcopenshell-python/hello_world.html)
* [IfcOpenShell Documentation](https://docs.ifcopenshell.org)
* [IfcOpenShell C++ Installation](https://docs.ifcopenshell.org/ifcopenshell/installation.html)
* [IfcOpenShell Python Installation](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
* [IfcOpenShell Python Hello World Tutorial](https://docs.ifcopenshell.org/ifcopenshell-python/hello_world.html)
* [BlenderBIM Add-on Website](https://blenderbim.org)
* [BlenderBIM Add-on Documentation](http://blenderbim.org/docs)
* [Add-on Installation](https://blenderbim.org/docs/users/installation.html)
* [Exploring an IFC model](https://blenderbim.org/docs/users/exploring_an_ifc_model.html)
* [BlenderBIM Add-on Documentation](https://docs.blenderbim.org/index.html)
* [Add-on Installation](https://docs.blenderbim.org/users/installation.html)
* [Exploring an IFC model](https://docs.blenderbim.org/users/exploring_an_ifc_model.html)
| Service | Status |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
+9 -3
View File
@@ -76,7 +76,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.7.13", "3.8.13", "3.9.11", "3.10.3", "3.11.7", "3.12.1"]
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1"]
JSON_VERSION = "v3.6.1"
OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.5.3"
@@ -254,6 +254,11 @@ if not os.path.exists(LOG_FILE):
open(LOG_FILE, "w").close()
logger.info(f"using command log file '{LOG_FILE}'")
# Causing havoc in python 3.11 build
try:
del os.environ['__PYVENV_LAUNCHER__']
except: pass
def run(cmds, cwd=None, can_fail=False):
"""
@@ -284,8 +289,9 @@ if platform.system() == "Darwin":
# Apparently not supported
PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) >= (3, 7)]
if run(["sw_vers", "-productVersion"]) < "10.16":
# Apparently not supported
PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) < (3, 11)]
# This is now solved with the '__PYVENV_LAUNCHER__' hack
# PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) < (3, 11)]
pass
BOOST_VERSION_UNDERSCORE = BOOST_VERSION.replace(".", "_")
+5 -3
View File
@@ -137,7 +137,7 @@ endif
cp -r blenderbim/* dist/blenderbim/
# Provides IfcOpenShell Python functionality
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-eaa2aa0-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f7c03db-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -362,7 +362,9 @@ endif
# Provides express rule validation for ifcopenshell.validate
cd dist/working && . env/bin/activate && $(PIP) install pytest --target=./site-packages
# Provides Brickschema functionality
cd dist/working && . env/bin/activate && $(PIP) install brickschema[persistence] --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install "brickschema[persistence]==0.7.6a2" --target=./site-packages
# Required for SVG to DXF conversion
cd dist/working && . env/bin/activate && $(PIP) install "ezdxf" --target=./site-packages
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
rm -rf dist/working
@@ -430,7 +432,7 @@ endif
# Required by brickschema
# This is a bit of a dodgy one, it should be provided by setuptools which Blender doesn't ship.
mkdir dist/working
cd dist/working && wget https://blenderbim.org/builds/pkg_resources.tar.gz
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/raw/v0.7.0/src/blenderbim/scripts/pkg_resources.tar.gz
cd dist/working && tar -xzvf pkg_resources*
cd dist/working/ && cp -r pkg_resources ../blenderbim/libs/site/packages/
rm -rf dist/working
+10 -6
View File
@@ -23,6 +23,7 @@ import bpy.utils.previews
import blenderbim
import importlib
from . import handler, ui, prop, operator, helper
from typing import Callable, Union
try:
from blenderbim.translations import translations_dict
@@ -96,8 +97,10 @@ for name in modules.keys():
classes = [
operator.AddIfcFile,
operator.BIM_OT_add_section_plane,
operator.BIM_OT_delete_object,
operator.BIM_OT_open_webbrowser,
operator.BIM_OT_remove_section_plane,
operator.BIM_OT_select_object,
operator.BIM_OT_show_description,
operator.ClippingPlaneCutWithCappings,
operator.EditBlenderCollection,
@@ -130,6 +133,7 @@ classes = [
prop.BIMMeshProperties,
prop.BIMFacet,
prop.BIMFilterGroup,
ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic,
ui.BIM_UL_topics,
ui.BIM_ADDON_preferences,
@@ -187,7 +191,7 @@ addon_keymaps = []
icons = None
is_registering = False
last_commit_hash = "8888888"
overridden_scene_panels = dict()
original_scene_panels_polls: dict[bpy.types.Panel, Union[Callable, None]] = dict()
def on_register(scene):
@@ -295,10 +299,10 @@ def unregister():
km.keymap_items.remove(kmi)
addon_keymaps.clear()
for panel in tuple(overridden_scene_panels.keys()):
original_panel, override_panel = overridden_scene_panels[panel]
bpy.utils.unregister_class(override_panel)
bpy.utils.register_class(original_panel)
del overridden_scene_panels[panel]
import blenderbim.tool as tool
# use tuple() as method will be removing keys from dict
for panel in tuple(original_scene_panels_polls.keys()):
tool.Blender.remove_scene_panel_override(panel)
bpy.app.translations.unregister("blenderbim")
@@ -10,7 +10,7 @@
* (at your option) any later version.
*
* BlenderBIM Add-on is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* but WITHOUT ANY WARRANTY, without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
@@ -10,7 +10,7 @@
* (at your option) any later version.
*
* BlenderBIM Add-on is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* but WITHOUT ANY WARRANTY, without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
File diff suppressed because one or more lines are too long
+12 -9
View File
@@ -27,6 +27,7 @@ import addon_utils
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.placement
import ifcopenshell.util.unit
import blenderbim.tool as tool
import blenderbim.core.geometry
import blenderbim.core.aggregate
@@ -34,6 +35,7 @@ import blenderbim.core.spatial
import blenderbim.core.style
from blenderbim.bim.ifc import IfcStore
from mathutils import Vector
from typing import Union
class IfcExporter:
@@ -85,8 +87,8 @@ class IfcExporter:
self.get_application_name(), tool.Blender.get_blenderbim_version()
)
def sync_all_objects(self):
results = []
def sync_all_objects(self, skip_unlinking=False) -> list[ifcopenshell.entity_instance]:
results: list[ifcopenshell.entity_instance] = []
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
for ifc_definition_id in list(IfcStore.id_map.keys()):
obj = IfcStore.id_map[ifc_definition_id]
@@ -95,19 +97,21 @@ class IfcExporter:
continue
if obj.library:
continue
tool.Collector.sync(obj)
tool.Collector.sync(obj, skip_unlinking)
result = self.sync_object_placement(obj)
if result:
results.append(result)
result = self.sync_object_material(obj)
# TODO: sync_object_material always returns None
# so it's never really appended
if result:
results.append(result)
except ReferenceError:
pass # The object is likely deleted
return results
def sync_edited_objects(self):
results = []
def sync_edited_objects(self) -> list[ifcopenshell.entity_instance]:
results: list[ifcopenshell.entity_instance] = []
for obj in IfcStore.edited_objs.copy():
if not obj:
continue
@@ -125,7 +129,7 @@ class IfcExporter:
IfcStore.edited_objs.clear()
return results
def sync_object_material(self, obj):
def sync_object_material(self, obj: bpy.types.Object) -> None:
if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
return
if not self.has_changed_materials(obj):
@@ -136,11 +140,10 @@ class IfcExporter:
checksum = obj.data.BIMMeshProperties.material_checksum
return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s])
def sync_object_placement(self, obj):
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if not tool.Ifc.is_moved(obj):
return
blender_matrix = np.array(obj.matrix_world)
if (obj.scale - Vector((1.0, 1.0, 1.0))).length > 1e-4:
bpy.ops.bim.update_representation(obj=obj.name)
return element
@@ -151,7 +154,7 @@ class IfcExporter:
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
return element
def sync_grid_axis_object_placement(self, obj, element):
def sync_grid_axis_object_placement(self, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
grid_obj = tool.Ifc.get_object(grid)
if grid_obj:
+8 -65
View File
@@ -31,6 +31,7 @@ from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MAP
from mathutils import Vector
from math import cos, degrees
from typing import Union
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -254,60 +255,6 @@ def viewport_shading_changed_callback(area):
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
@classmethod
def poll_check_blender_tab(cls, context):
return tool.Blender.is_tab(context, "BLENDER")
def get_override_scene_panel(panel_name):
original_panel = getattr(bpy.types, panel_name)
override_panel = type(f"Override_{panel_name}", (original_panel,), {})
override_panel.bl_idname = f"{panel_name}_override"
# some blender panel depend on other and register_class will throw an error
# if we won't override their `bl_parent_id`
bl_parent_id = getattr(override_panel, "bl_parent_id", None)
if bl_parent_id is not None:
override_panel.bl_parent_id = f"{bl_parent_id}_override"
# override poll method
poll = getattr(override_panel, "poll", None)
if poll is None:
override_panel.poll = poll_check_blender_tab
else:
@classmethod
def wrapped_poll(cls, context):
return super(override_panel, cls).poll(context) and poll_check_blender_tab.__func__(cls, context)
override_panel.poll = wrapped_poll
return override_panel
# TODO: possibly override scene panels from other addons too?
# list can be updated from
# https://projects.blender.org/blender/blender/src/branch/main/scripts/startup/bl_ui/properties_scene.py#L421
OVERRIDE_SCENE_PANELS = (
"SCENE_PT_scene",
"SCENE_PT_unit",
"SCENE_PT_physics",
"SCENE_PT_rigid_body_world",
"SCENE_PT_rigid_body_world_settings",
"SCENE_PT_rigid_body_cache",
"SCENE_PT_rigid_body_field_weights",
"SCENE_PT_audio",
"SCENE_PT_keying_sets",
"SCENE_PT_custom_props",
# after SCENE_PT_keying_sets
"SCENE_PT_keying_set_paths",
"SCENE_PT_keyframing_settings",
)
if bpy.app.version >= (4, 0):
OVERRIDE_SCENE_PANELS += ("SCENE_PT_simulation",)
@persistent
def load_post(scene):
global global_subscription_owner
@@ -343,15 +290,11 @@ def load_post(scene):
else:
bpy.ops.workspace.append_activate(idname="BIM", filepath=os.path.join(cwd, "data", "workspace.blend"))
# To improve usability for new users, we hijack the scene properties
# tab. We override default scene properties panels with our own poll
# to hide them unless the user has chosen to view Blender properties.
for panel in OVERRIDE_SCENE_PANELS:
if panel in blenderbim.bim.overridden_scene_panels:
continue
override_panel = get_override_scene_panel(panel)
original_panel = getattr(bpy.types, panel)
bpy.utils.register_class(override_panel)
bpy.utils.unregister_class(original_panel)
blenderbim.bim.overridden_scene_panels[panel] = (original_panel, override_panel)
# To improve usability for new users, we hijack the scene properties
# tab. We override default scene properties panels with our own poll
# to hide them unless the user has chosen to view Blender properties.
for panel in tool.Blender.get_scene_panels_list():
if panel in blenderbim.bim.original_scene_panels_polls:
continue
tool.Blender.override_scene_panel(panel)
tool.Blender.setup_tabs()
+10 -40
View File
@@ -29,6 +29,7 @@ from mathutils import geometry
from mathutils import Vector
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from typing import Optional, Callable, Any
def draw_attributes(props, layout, copy_operator=None, popup_active_attribute=None):
@@ -75,6 +76,10 @@ def draw_attribute(attribute, layout, copy_operator=None):
op.data_path = attribute.path_from_id("string_value")
if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
if attribute.name == "GlobalId":
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
if copy_operator:
op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN")
op.name = attribute.name
@@ -116,7 +121,7 @@ def import_attribute(attribute, props, data, callback=None):
elif is_handled_by_callback is False:
props.remove(len(props) - 1)
elif data_type == "string":
new.string_value = "" if new.is_null else str(data[attribute.name()])
new.string_value = "" if new.is_null else str(data[attribute.name()]).replace("\n", "\\n")
if attribute.type_of_attribute().declared_type().name() == "IfcURIReference":
new.is_uri = True
elif data_type == "boolean":
@@ -178,7 +183,7 @@ def add_attribute_description(attribute_blender, attribute_ifc=None):
attribute_blender.description = description
def export_attributes(props, callback=None):
def export_attributes(props, callback: Optional[Callable] = None) -> dict[str, Any]:
attributes = {}
for prop in props:
is_handled_by_callback = callback(attributes, prop) if callback else False
@@ -216,39 +221,6 @@ def get_enum_items(data, prop_name, context=None):
return items
def get_obj_ifc_definition_id(context, obj, obj_type):
if obj_type == "Object":
return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "Material":
return bpy.data.materials.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "MaterialSet":
return ifcopenshell.util.element.get_material(
tool.Ifc.get_entity(bpy.data.objects.get(obj)), should_skip_usage=True
).id()
elif obj_type == "MaterialSetItem":
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
elif obj_type == "Task":
return context.scene.BIMTaskTreeProperties.tasks[
context.scene.BIMWorkScheduleProperties.active_task_index
].ifc_definition_id
elif obj_type == "Cost":
return context.scene.BIMCostProperties.cost_items[
context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id
elif obj_type == "Resource":
return context.scene.BIMResourceTreeProperties.resources[
context.scene.BIMResourceProperties.active_resource_index
].ifc_definition_id
elif obj_type == "Profile":
return context.scene.BIMProfileProperties.profiles[
context.scene.BIMProfileProperties.active_profile_index
].ifc_definition_id
elif obj_type == "WorkSchedule":
return context.scene.BIMWorkScheduleProperties.active_work_schedule_id
elif obj_type == "Group":
prop = context.scene.BIMGroupProperties
return prop.groups[prop.active_group_index].ifc_definition_id
# hack to close popup
# https://blender.stackexchange.com/a/202576/130742
def close_operator_panel(event):
@@ -304,6 +276,9 @@ def draw_filter(layout, filter_groups, data, module):
op.type = sprops.facet
op.index = i
op.module = module
op = row.operator("bim.remove_filter_group", text="", icon="X")
op.index = i
op.module = module
for j, ifc_filter in enumerate(filter_group.filters):
if ifc_filter.type == "entity":
@@ -350,11 +325,6 @@ def draw_filter(layout, filter_groups, data, module):
op.index = j
op.module = module
row = box.row()
op = row.operator("bim.remove_filter_group", icon="X")
op.index = i
op.module = module
# TODO this should move into ifcopenshell.util
+81 -38
View File
@@ -32,12 +32,15 @@ import ifcopenshell.geom
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import blenderbim.tool as tool
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
from itertools import chain, accumulate
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from blenderbim.tool.loader import OBJECT_DATA_TYPE
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
from typing import Dict, Union
from typing import Dict, Union, Optional, Any
class MaterialCreator:
@@ -200,20 +203,26 @@ class IfcImporter:
self.ifc_import_settings = ifc_import_settings
self.diff = None
self.file: ifcopenshell.file = None
self.context_settings = []
self.gross_context_settings = []
self.context_settings: list[ifcopenshell.geom.main.settings] = []
self.gross_context_settings: list[ifcopenshell.geom.main.settings] = []
self.contexts = []
self.project = None
self.has_existing_project = False
self.collections = {}
self.elements = set()
self.type_collection = None
# element guids to blender collections mapping
self.collections: dict[str, bpy.types.Collection] = {}
self.elements: set[ifcopenshell.entity_instance] = set()
self.annotations: set[ifcopenshell.entity_instance] = set()
self.gross_elements: set[ifcopenshell.entity_instance] = set()
self.element_types: set[ifcopenshell.entity_instance] = set()
self.spatial_elements: set[ifcopenshell.entity_instance] = set()
self.type_collection: bpy.types.Collection = None
self.type_products = {}
self.meshes = {}
self.mesh_shapes = {}
self.time = 0
self.unit_scale = 1
self.added_data = {}
# ifc definition ids to blender elements mapping
self.added_data: dict[int, IFC_CONNECTED_TYPE] = {}
self.native_elements = set()
self.native_data = {}
self.progress = 0
@@ -452,7 +461,9 @@ class IfcImporter:
for element in self.gross_elements:
print(element)
def get_spatial_elements_filtered_by_elements(self, elements):
def get_spatial_elements_filtered_by_elements(
self, elements: set[ifcopenshell.entity_instance]
) -> set[ifcopenshell.entity_instance]:
leaf_spatial_elements = set([ifcopenshell.util.element.get_container(e) for e in elements])
results = set()
for spatial_element in leaf_spatial_elements:
@@ -788,7 +799,7 @@ class IfcImporter:
continue
self.create_element_type(element_type)
def create_element_type(self, element):
def create_element_type(self, element: ifcopenshell.entity_instance) -> None:
self.ifc_import_settings.logger.info("Creating object %s", element)
mesh = None
if self.ifc_import_settings.should_load_geometry:
@@ -845,17 +856,19 @@ class IfcImporter:
self.create_product(element, mesh=mesh)
print("Done creating geometry")
def create_spatial_elements(self):
self.create_generic_elements(self.spatial_elements)
def create_spatial_elements(self) -> None:
self.create_generic_elements(self.spatial_elements, unselectable=True)
def create_elements(self):
def create_elements(self) -> None:
self.create_generic_elements(self.elements)
tmp = self.context_settings
self.context_settings = self.gross_context_settings
self.create_generic_elements(self.gross_elements)
self.context_settings = tmp
def create_generic_shape(self, element):
def create_generic_shape(
self, element: ifcopenshell.entity_instance
) -> Union[ifcopenshell_wrapper.TriangulationElement, None]:
for settings in self.context_settings:
try:
result = ifcopenshell.geom.create_shape(settings, element)
@@ -864,7 +877,7 @@ class IfcImporter:
except:
pass
def create_generic_elements(self, elements):
def create_generic_elements(self, elements: set[ifcopenshell.entity_instance], unselectable=False) -> None:
if isinstance(self.file, ifcopenshell.sqlite):
return self.create_generic_sqlite_elements(elements)
@@ -878,12 +891,17 @@ class IfcImporter:
elements -= products
total = len(elements)
objects = set()
for i, element in enumerate(elements):
if i % 250 == 0:
print("{} / {} elements processed ...".format(i, total))
self.create_product(element)
objects.add(self.create_product(element))
def create_generic_sqlite_elements(self, elements):
if unselectable:
for obj in objects:
obj.hide_select = True
def create_generic_sqlite_elements(self, elements: set[ifcopenshell.entity_instance]) -> None:
self.geometry_cache = self.file.get_geometry([e.id() for e in elements])
for geometry_id, geometry in self.geometry_cache["geometry"].items():
mesh_name = tool.Loader.get_mesh_name(type("Geometry", (), {"id": geometry_id}))
@@ -930,7 +948,9 @@ class IfcImporter:
mesh = self.meshes.get(mesh_name)
self.create_product(element, mesh=mesh)
def create_products(self, products, settings=None):
def create_products(
self, products, settings: Optional[ifcopenshell.geom.main.settings] = None
) -> set[ifcopenshell.entity_instance]:
results = set()
if not products:
return results
@@ -1053,7 +1073,7 @@ class IfcImporter:
return mapped_representation
return None
def create_pointclouds(self, products):
def create_pointclouds(self, products: set[ifcopenshell.entity_instance]) -> set[ifcopenshell.entity_instance]:
result = set()
for product in products:
representation = self.get_pointcloud_representation(product)
@@ -1064,7 +1084,9 @@ class IfcImporter:
return result
def create_pointcloud(self, product, representation):
def create_pointcloud(
self, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
placement_matrix = self.get_element_matrix(product)
vertex_list = []
for item in representation.Items:
@@ -1092,7 +1114,12 @@ class IfcImporter:
self.link_element(product, obj)
return product
def create_product(self, element, shape=None, mesh=None):
def create_product(
self,
element: ifcopenshell.entity_instance,
shape: Optional[Any] = None,
mesh: Optional[OBJECT_DATA_TYPE] = None,
) -> Union[bpy.types.Object, None]:
if element is None:
return
@@ -1137,6 +1164,9 @@ class IfcImporter:
return obj
def load_existing_meshes(self) -> None:
self.meshes.update({m.name: m for m in bpy.data.meshes})
def get_representation_item_material_name(self, item):
if not item.StyledByItem:
return
@@ -1543,18 +1573,19 @@ class IfcImporter:
"{}/{}".format(self.project["ifc"].is_a(), self.project["ifc"].Name)
)
obj = self.create_product(self.project["ifc"])
obj.hide_select = True
self.project["blender"].objects.link(obj)
self.project["blender"].BIMCollectionProperties.obj = obj
obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"]
def create_collections(self):
def create_collections(self) -> None:
self.create_spatial_decomposition_collections()
if self.ifc_import_settings.collection_mode == "DECOMPOSITION":
self.create_aggregate_and_nest_collections()
elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION":
pass
def create_spatial_decomposition_collections(self):
def create_spatial_decomposition_collections(self) -> None:
for rel_aggregate in self.project["ifc"].IsDecomposedBy or []:
self.create_spatial_decomposition_collection(self.project["blender"], rel_aggregate.RelatedObjects)
@@ -1567,7 +1598,9 @@ class IfcImporter:
tool.Loader.create_project_collection("Views")
self.type_collection = tool.Loader.create_project_collection("Types")
def create_spatial_decomposition_collection(self, parent, related_objects):
def create_spatial_decomposition_collection(
self, parent: Union[bpy.types.Collection, bpy.types.Object], related_objects: list[ifcopenshell.entity_instance]
) -> None:
for element in related_objects:
if element not in self.spatial_elements:
continue
@@ -1607,7 +1640,11 @@ class IfcImporter:
] + [
r
for r in self.file.by_type("IfcRelNests")
if (r.RelatingObject.is_a("IfcElement") or r.RelatingObject.is_a("IfcElementType"))
if (
r.RelatingObject.is_a("IfcElement")
or r.RelatingObject.is_a("IfcElementType")
or (r.RelatingObject.is_a("IfcPositioningElement") and not r.RelatingObject.is_a("IfcGrid"))
)
and [e for e in r.RelatedObjects if not e.is_a("IfcPort")]
]
@@ -1617,9 +1654,9 @@ class IfcImporter:
self.ifc_import_settings.collection_mode = "SPATIAL_DECOMPOSITION"
return
aggregates = {}
aggregates: dict[str, dict] = {}
for rel_aggregate in rel_aggregates:
element = rel_aggregate.RelatingObject
element: ifcopenshell.entity_instance = rel_aggregate.RelatingObject
collection = bpy.data.collections.new(tool.Loader.get_name(element))
aggregates[element.GlobalId] = {"element": element, "collection": collection}
self.collections[element.GlobalId] = collection
@@ -1671,7 +1708,9 @@ class IfcImporter:
continue
self.create_style(style)
def create_style(self, style, blender_material=None):
def create_style(
self, style: ifcopenshell.entity_instance, blender_material: Optional[bpy.types.Material] = None
) -> None:
if not blender_material:
name = style.Name or str(style.id())
blender_material = bpy.data.materials.new(name)
@@ -1688,18 +1727,20 @@ class IfcImporter:
else:
blender_material.BIMStyleProperties.active_style_type = "Shading"
def place_objects_in_collections(self):
def place_objects_in_collections(self) -> None:
for ifc_definition_id, obj in self.added_data.items():
if isinstance(obj, bpy.types.Object):
self.place_object_in_collection(self.file.by_id(ifc_definition_id), obj)
def place_object_in_collection(self, element, obj):
def place_object_in_collection(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None:
if self.ifc_import_settings.collection_mode == "DECOMPOSITION":
self.place_object_in_decomposition_collection(element, obj)
elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION":
self.place_object_in_spatial_decomposition_collection(element, obj)
def place_object_in_decomposition_collection(self, element, obj):
def place_object_in_decomposition_collection(
self, element: ifcopenshell.entity_instance, obj: bpy.types.Object
) -> None:
if element.is_a("IfcProject"):
return
elif element.is_a("IfcGridAxis"):
@@ -1716,10 +1757,12 @@ class IfcImporter:
elif getattr(element, "Nests", None) and not element.is_a("IfcPort"):
nest = ifcopenshell.util.element.get_nest(element)
return self.collections[nest.GlobalId].objects.link(obj)
else:
return self.place_object_in_spatial_decomposition_collection(element, obj)
def place_object_in_spatial_decomposition_collection(self, element, obj):
return self.place_object_in_spatial_decomposition_collection(element, obj)
def place_object_in_spatial_decomposition_collection(
self, element: ifcopenshell.entity_instance, obj: bpy.types.Object
) -> None:
if element.is_a("IfcProject"):
return
elif element.is_a("IfcGridAxis"):
@@ -1763,7 +1806,7 @@ class IfcImporter:
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
return rel.RelatingGroup
def get_element_matrix(self, element):
def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.array:
if isinstance(element, ifcopenshell.sqlite_entity):
result = self.geometry_cache["shapes"][element.id()]["matrix"]
else:
@@ -1773,7 +1816,7 @@ class IfcImporter:
result[2][3] *= self.unit_scale
return result
def scale_matrix(self, matrix):
def scale_matrix(self, matrix: np.array) -> np.array:
matrix[0][3] *= self.unit_scale
matrix[1][3] *= self.unit_scale
matrix[2][3] *= self.unit_scale
@@ -1805,7 +1848,7 @@ class IfcImporter:
):
return representation.Items[0].MappingTarget
def create_curve(self, element, shape):
def create_curve(self, element: ifcopenshell.entity_instance, shape) -> bpy.types.Curve:
if hasattr(shape, "geometry"):
geometry = shape.geometry
else:
@@ -2002,7 +2045,7 @@ class IfcImportSettings:
self.has_filter = None
self.should_filter_spatial_elements = True
self.should_setup_viewport_camera = True
self.elements = set()
self.elements: set[ifcopenshell.entity_instance] = set()
self.collection_mode = "DECOMPOSITION"
@staticmethod
@@ -46,7 +46,7 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, Operator):
if self.relating_object:
element = tool.Ifc.get().by_id(self.relating_object)
if element.IsDecomposedBy:
relating_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object))
relating_obj = tool.Ifc.get_object(element)
else:
assembly = element.Decomposes[0].RelatingObject
relating_obj = tool.Ifc.get_object(assembly)
@@ -94,9 +94,9 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
relating_obj=tool.Ifc.get_object(aggregate),
related_obj=tool.Ifc.get_object(element),
)
# Removes Pset related to Linked Aggregates
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Linked_Aggregate")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
@@ -206,15 +206,29 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
bl_label = "Select Aggregate"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
select_parts: bpy.props.BoolProperty(default=False)
@classmethod
def description(cls, context, properties):
if properties.select_parts:
return "Select Aggregate and Parts"
else:
return "Select Aggregate"
def execute(self, context):
self.file = IfcStore.get_file()
obj = bpy.data.objects.get(self.obj) or context.active_object
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj))
aggregate_obj = tool.Ifc.get_object(aggregate)
if self.select_parts:
bpy.ops.bim.select_parts(obj=aggregate_obj.name)
if aggregate_obj in context.selectable_objects:
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
tool.Blender.set_objects_selection(
context,
aggregate_obj,
[aggregate_obj],
clear_previous_selection=not self.select_parts,
)
return {"FINISHED"}
@@ -242,6 +256,7 @@ class BIM_OT_add_part_to_object(bpy.types.Operator, Operator):
part_name=self.part_name,
)
class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
bl_idname = "bim.break_link_to_other_aggregates"
bl_label = "Break link to other aggregates"
@@ -256,22 +271,23 @@ class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
return []
parts = ifcopenshell.util.element.get_parts(aggregate)
for part in parts:
pset = ifcopenshell.util.element.get_pset(part, 'BBIM_Linked_Aggregate')
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Linked_Aggregate")
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], product=aggregate)
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], products=[aggregate])
return {"FINISHED"}
class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator):
bl_idname = "bim.select_linked_aggregates"
bl_label = "Select linked aggregates"
@@ -284,17 +300,17 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator):
return []
if not element:
return []
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
for obj in context.selected_objects:
obj.select_set(False)
for rel in linked_aggregate_group[0].IsGroupedBy or []:
for element in rel.RelatedObjects:
obj = tool.Ifc.get_object(element)
@@ -62,8 +62,12 @@ class BIM_PT_aggregate(Panel):
row = layout.row(align=True)
if AggregateData.data["has_relating_object"]:
row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP")
op = row.operator("bim.select_aggregate", icon="OBJECT_DATA", text="")
op.obj = context.active_object.name
op.select_parts = False
op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="")
op.obj = context.active_object.name
op.select_parts = True
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
op = row.operator("bim.aggregate_unassign_object", icon="X", text="")
@@ -120,20 +120,46 @@ class EditAttributes(bpy.types.Operator, Operator):
return {"FINISHED"}
class GenerateGlobalId(bpy.types.Operator):
class GenerateGlobalId(bpy.types.Operator, Operator):
bl_idname = "bim.generate_global_id"
bl_label = "Regenerate GlobalId"
bl_description = "Regenerate GlobalId\n\nSHIFT+CLICK to regenerate GlobalIds for all selected objects"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
index = context.active_object.BIMAttributeProperties.attributes.find("GlobalId")
if index >= 0:
global_id = context.active_object.BIMAttributeProperties.attributes[index]
use_selected: bpy.props.BoolProperty(name="Use All Selected Objects", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# using all selected objects on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.shift:
self.use_selected = True
return self.execute(context)
def _execute(self, context):
if self.use_selected:
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcRoot"):
continue
element.GlobalId = ifcopenshell.guid.new()
obj = context.active_object
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
return {"FINISHED"}
props = obj.BIMAttributeProperties
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcRoot"):
return {"FINISHED"}
if self.use_selected and obj in context.selected_objects:
# guid value was already regenerated, just update the ui prop
guid_value = element.GlobalId
else:
global_id = context.active_object.BIMAttributeProperties.attributes.add()
global_id.name = "GlobalId"
global_id.data_type = "string"
global_id.string_value = ifcopenshell.guid.new()
guid_value = ifcopenshell.guid.new()
props.attributes["GlobalId"].string_value = guid_value
return {"FINISHED"}
@@ -24,10 +24,10 @@ from math import pi
class BIMCadProperties(PropertyGroup):
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
x: bpy.props.FloatProperty(name="X", default=0.2)
y: bpy.props.FloatProperty(name="Y", default=0.1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
x: bpy.props.FloatProperty(name="X", default=0.2, subtype="DISTANCE")
y: bpy.props.FloatProperty(name="Y", default=0.1, subtype="DISTANCE")
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
@@ -20,6 +20,7 @@ import os
import bpy
import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
import ifcopenshell.util.unit
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
@@ -239,8 +240,9 @@ class CadHotkey(bpy.types.Operator):
row.prop(props, "resolution")
def hotkey_S_C(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.is_profile():
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
else:
bpy.ops.bim.cad_arc_from_2_points()
@@ -248,13 +250,15 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_trim_extend()
def hotkey_S_F(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.is_profile():
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
else:
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
def hotkey_S_O(self):
bpy.ops.bim.cad_offset(distance=self.props.distance)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
def hotkey_S_Q(self):
element = tool.Ifc.get_entity(bpy.context.active_object)
@@ -270,7 +274,8 @@ class CadHotkey(bpy.types.Operator):
def hotkey_S_R(self):
if self.is_profile():
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
@@ -25,15 +25,12 @@ classes = (
operator.ExecuteIfcClash,
operator.ExportClashSets,
operator.ImportClashSets,
operator.LoadIfcClashes,
operator.SaveIfcClashes,
operator.LoadSmartGroupsForActiveClashSet,
operator.RemoveClashSet,
operator.RemoveClashSource,
operator.SelectClash,
operator.SelectClashResults,
operator.SelectClashSource,
operator.SelectIfcClashResults,
operator.SelectSmartGroup,
operator.SelectSmartGroupedClashesPath,
operator.SmartClashGroup,
@@ -44,7 +41,6 @@ classes = (
prop.BIMClashProperties,
ui.BIM_PT_ifcclash,
ui.BIM_PT_clash_manager,
ui.BIM_PT_dumb_clash_manager,
ui.BIM_PT_smart_clash_manager,
ui.BIM_UL_clashes,
ui.BIM_UL_clash_sets,
@@ -0,0 +1,52 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import ifcopenshell.util.element
import blenderbim.tool as tool
def refresh():
ClashData.is_loaded = False
class ClashData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {}
cls.data["saved_searches"] = cls.saved_searches()
@classmethod
def saved_searches(cls):
if not tool.Ifc.get():
return []
groups = tool.Ifc.get().by_type("IfcGroup")
results = []
for group in groups:
try:
data = json.loads(group.Description)
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
results.append(group)
except:
pass
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
@@ -0,0 +1,102 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import blf
import gpu
import bmesh
import blenderbim.tool as tool
from bpy.types import SpaceView3D
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d
class ClashDecorator:
is_installed = False
handlers = []
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@classmethod
def uninstall(cls):
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_text(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
text = context.scene.BIMClashProperties.active_clash_text
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
font_id = 0
blf.size(font_id, 12)
coords_2d = location_3d_to_region_2d(context.region, context.region_data, p)
color = self.addon_prefs.decorations_colour
blf.color(font_id, *color)
if coords_2d:
w, h = blf.dimensions(font_id, text)
coords_2d -= Vector((w * .5, 0))
blf.position(font_id, coords_2d[0], coords_2d[1], 0)
blf.draw(font_id, text) # Set your text here
def draw_geometry(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2]
selected_edges = []
if selected_vertices[0] != selected_vertices[1]:
selected_edges = [[0, 1]]
self.draw_batch("POINTS", selected_vertices, special_elements_color)
if selected_edges:
self.draw_batch("LINES", selected_vertices, special_elements_color, selected_edges)
@@ -23,10 +23,11 @@ import bmesh
import logging
import numpy as np
import ifcopenshell
from mathutils import Matrix, Vector
from math import radians
from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool
from math import radians
from mathutils import Matrix, Vector
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.clash.decorator import ClashDecorator
class ExportClashSets(bpy.types.Operator):
@@ -45,18 +46,7 @@ class ExportClashSets(bpy.types.Operator):
def execute(self, context):
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets = []
for clash_set in context.scene.BIMClashProperties.clash_sets:
self.a = []
self.b = []
for ab in ["a", "b"]:
for data in getattr(clash_set, ab):
clash_source = {"file": data.name}
if data.selector:
clash_source["selector"] = data.selector
clash_source["mode"] = data.mode
getattr(self, ab).append(clash_source)
clash_sets.append({"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a, "b": self.b})
clash_sets = tool.Clash.export_clash_sets()
with open(self.filepath, "w") as destination:
destination.write(json.dumps(clash_sets, indent=4))
return {"FINISHED"}
@@ -78,25 +68,34 @@ class ImportClashSets(bpy.types.Operator):
return {"RUNNING_MODAL"}
def execute(self, context):
with open(self.filepath) as f:
clash_sets = json.load(f)
for clash_set in clash_sets:
tool.Clash.load_clash_sets(self.filepath)
context.scene.BIMClashProperties.clash_sets.clear()
for clash_set in tool.Clash.get_clash_sets():
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = clash_set["name"]
new.tolerance = clash_set["tolerance"]
new.mode = clash_set["mode"]
if new.mode == "intersection":
new.tolerance = clash_set["tolerance"]
new.check_all = clash_set["check_all"]
elif new.mode == "collision":
new.allow_touching = clash_set["allow_touching"]
elif new.mode == "clearance":
new.clearance = clash_set["clearance"]
new.check_all = clash_set["check_all"]
for clash_source in clash_set["a"]:
new_source = new.a.add()
new_source.name = clash_source["file"]
if "selector" in clash_source:
new_source.selector = clash_source["selector"]
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
new_source.mode = clash_source["mode"]
if clash_set["b"]:
if "b" in clash_set and clash_set["b"]:
for clash_source in clash_set["b"]:
new_source = new.b.add()
new_source.name = clash_source["file"]
if "selector" in clash_source:
new_source.selector = clash_source["selector"]
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
new_source.mode = clash_source["mode"]
tool.Clash.import_active_clashes()
return {"FINISHED"}
@@ -109,7 +108,6 @@ class AddClashSet(bpy.types.Operator):
def execute(self, context):
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = "New Clash Set"
new.tolerance = 0.01
return {"FINISHED"}
@@ -211,8 +209,11 @@ class ExecuteIfcClash(bpy.types.Operator):
def invoke(self, context, event):
_, extension = os.path.splitext(self.filepath)
if extension != ".json":
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
# TODO Temporarily until BCF support comes back
# if extension != ".json":
# self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
@@ -220,7 +221,12 @@ class ExecuteIfcClash(bpy.types.Operator):
def execute(self, context):
from ifcclash import ifcclash
self.props = context.scene.BIMClashProperties
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
# TODO Temporarily until BCF support comes back
if extension != ".json":
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
@@ -230,7 +236,7 @@ class ExecuteIfcClash(bpy.types.Operator):
settings.logger.setLevel(logging.DEBUG)
clasher = ifcclash.Clasher(settings)
if context.scene.BIMClashProperties.should_create_clash_snapshots:
if self.props.should_create_clash_snapshots:
def get_viewpoint_snapshot(viewpoint):
camera = bpy.data.objects.get("IFC Clash Camera")
@@ -271,23 +277,13 @@ class ExecuteIfcClash(bpy.types.Operator):
clasher.get_viewpoint_snapshot = get_viewpoint_snapshot
clasher.clash_sets = []
for clash_set in context.scene.BIMClashProperties.clash_sets:
self.a = []
self.b = []
for ab in ["a", "b"]:
for data in getattr(clash_set, ab):
clash_source = {"file": data.name}
if data.selector:
clash_source["selector"] = data.selector
clash_source["mode"] = data.mode
getattr(self, ab).append(clash_source)
clash_set_data = {"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a}
if self.b:
clash_set_data["b"] = self.b
clasher.clash_sets.append(clash_set_data)
clasher.clash_sets = tool.Clash.export_clash_sets()
clasher.clash()
clasher.export()
if extension == ".json":
tool.Clash.load_clash_sets(self.filepath)
tool.Clash.import_active_clashes()
return {"FINISHED"}
@@ -306,6 +302,7 @@ class SelectIfcClashResults(bpy.types.Operator):
return {"RUNNING_MODAL"}
def execute(self, context):
# TODO refactor into new clash results system
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
@@ -352,83 +349,6 @@ class SelectIfcClashResults(bpy.types.Operator):
return {"FINISHED"}
class LoadIfcClashes(bpy.types.Operator):
bl_idname = "bim.load_ifc_clashes"
bl_label = "Load IFC Clashes"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load the clashing IFC geometry stored in a file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
clash_sets_json = json.load(f)
active_clash_set = context.scene.BIMClashProperties.active_clash_set
for clash_set in clash_sets_json:
if not "clashes" in clash_set.keys():
self.report({"WARNING"}, "No clashes found for the selected Clash Set.")
return {"CANCELLED"}
active_clash_set.clashes.clear()
for clash in clash_set["clashes"].values():
blender_clash = active_clash_set.clashes.add()
blender_clash.a_global_id = clash["a_global_id"]
blender_clash.b_global_id = clash["b_global_id"]
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
return {"FINISHED"}
class SaveIfcClashes(bpy.types.Operator):
bl_idname = "bim.save_ifc_clashes"
bl_label = "Save IFC Clashes"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Save the clashing IFC geometry stored in a file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets_json = self.load_json()
active_clash_set = context.scene.BIMClashProperties.active_clash_set
self.update_clash_sets(clash_sets_json, active_clash_set)
self.save_json(clash_sets_json)
return {"FINISHED"}
def load_json(self):
with open(self.filepath) as f:
return json.load(f)
def update_clash_sets(self, clash_sets_json, active_clash_set):
for clash_set in clash_sets_json:
if clash_set["name"] != active_clash_set.name or "clashes" not in clash_set.keys():
continue
for clash in active_clash_set.clashes:
clash_key = clash.a_global_id + "-" + clash.b_global_id
if clash_set.get("clashes", {}).get(clash_key, None) is not None:
clash_set["clashes"][clash_key]["status"] = clash.status
def save_json(self, clash_sets_json):
with open(self.filepath, "w") as destination:
json.dump(clash_sets_json, destination, indent=4)
class SelectClash(bpy.types.Operator):
bl_idname = "bim.select_clash"
bl_label = "Select Clash"
@@ -437,31 +357,29 @@ class SelectClash(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
clash = context.scene.BIMClashProperties.active_clash
self.props = context.scene.BIMClashProperties
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
active_clash = self.props.active_clash
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
if not clash:
return {"FINISHED"}
products = []
linked_objects = []
try:
products.append(tool.Ifc.get().by_guid(clash.a_global_id))
except:
linked_objects.append(clash.a_name)
try:
products.append(tool.Ifc.get().by_guid(clash.b_global_id))
except:
linked_objects.append(clash.b_name)
for global_id in (clash["a_global_id"], clash["b_global_id"]):
try:
products.append(tool.Ifc.get().by_guid(global_id))
except:
pass
tool.Spatial.select_products(products, unhide=True)
print(linked_objects)
for obj_name in linked_objects:
obj = bpy.data.objects.get(obj_name)
if obj:
obj.select_set(True)
else:
print("Object not found:", obj_name)
if context.scene.BIMClashProperties.sould_focus_on_clash:
context_override = tool.Blender.get_viewport_context()
with bpy.context.temp_override(**context_override):
bpy.ops.view3d.view_selected()
ClashDecorator.install(bpy.context)
target = Vector(clash["p1"])
tool.Clash.look_at(target, target + Vector((5, 5, 5)))
self.props.p1 = clash["p1"]
self.props.p2 = clash["p2"]
self.props.active_clash_text = clash["type"].title() + " " + str(round(clash["distance"] * 1000)) + "mm"
return {"FINISHED"}
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.prop import StrProperty, Attribute
from blenderbim.bim.prop import StrProperty, Attribute, BIMFilterGroup
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -33,11 +33,12 @@ from bpy.props import (
class ClashSource(PropertyGroup):
name: StringProperty(name="File")
selector: StringProperty(name="Selector")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
mode: EnumProperty(
items=[
("i", "Include", "Only the selected objects are included for clashing"),
("e", "Exclude", "All objects except the selected objects are included for clashing"),
("a", "All Elements", "All elements will be used for clashing"),
("i", "Include", "Only the selected elements are included for clashing"),
("e", "Exclude", "All elements except the selected elements are included for clashing"),
],
name="Mode",
)
@@ -53,7 +54,24 @@ class Clash(PropertyGroup):
class ClashSet(PropertyGroup):
name: StringProperty(name="Name")
tolerance: FloatProperty(name="Tolerance")
mode: EnumProperty(
items=[
(
"intersection",
"Intersection",
"Detect objects that protrude or pierce another object",
"PIVOT_MEDIAN",
1,
),
("collision", "Collision", "Detect touching objects with any surface collision", "PIVOT_INDIVIDUAL", 2),
("clearance", "Clearance", "Detect objects within a proximity threshold", "PIVOT_ACTIVE", 3),
],
name="Mode",
)
tolerance: FloatProperty(name="Tolerance", default=0.002)
clearance: FloatProperty(name="Clearance", default=0.01)
allow_touching: BoolProperty(name="Allow Touching", default=False)
check_all: BoolProperty(name="Check All", default=False)
a: CollectionProperty(name="Group A", type=ClashSource)
b: CollectionProperty(name="Group B", type=ClashSource)
clashes: CollectionProperty(name="Clashes", type=Clash)
@@ -78,22 +96,21 @@ class BIMClashProperties(PropertyGroup):
smart_clash_grouping_max_distance: IntProperty(
name="Smart Clash Grouping Max Distance", default=3, soft_min=1, soft_max=10
)
sould_focus_on_clash: BoolProperty(name="Show Focus Clash", default=False)
p1: FloatVectorProperty(name="P1", default=(0.0, 0.0, 0.0), subtype="XYZ")
p2: FloatVectorProperty(name="P2", default=(0.0, 0.0, 0.0), subtype="XYZ")
active_clash_text: StringProperty(name="Active Clash Text")
@property
def active_clash_set(self):
if not self.clash_sets:
return None
return self.clash_sets[self.active_clash_set_index]
if self.active_clash_set_index < len(self.clash_sets):
return self.clash_sets[self.active_clash_set_index]
@property
def active_smart_group(self):
if not self.smart_clash_groups:
return None
return self.smart_clash_groups[self.active_smart_group_index]
if self.active_smart_group_index < len(self.smart_clash_groups):
return self.smart_clash_groups[self.active_smart_group_index]
@property
def active_clash(self):
if not self.active_clash_set:
return None
return self.active_clash_set.clashes[self.active_clash_index]
if self.active_clash_index < len(self.active_clash_set.clashes):
return self.active_clash_set.clashes[self.active_clash_index]
@@ -17,7 +17,9 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.bim.helper
from bpy.types import Panel
from blenderbim.bim.module.clash.data import ClashData
class BIM_PT_ifcclash(Panel):
@@ -30,6 +32,9 @@ class BIM_PT_ifcclash(Panel):
bl_parent_id = "BIM_PT_tab_clash_detection"
def draw(self, context):
if not ClashData.is_loaded:
ClashData.load()
layout = self.layout
scene = context.scene
@@ -45,57 +50,83 @@ class BIM_PT_ifcclash(Panel):
layout.template_list("BIM_UL_clash_sets", "", props, "clash_sets", props, "active_clash_set_index")
if props.active_clash_set_index < len(props.clash_sets):
clash_set = props.active_clash_set
if not props.active_clash_set:
return
row = layout.row(align=True)
row.prop(clash_set, "name")
row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index
clash_set = props.active_clash_set
row = layout.row(align=True)
row = layout.row(align=True)
row.prop(clash_set, "name")
row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index
row = layout.row()
row.prop(clash_set, "mode")
if clash_set.mode == "intersection":
row = layout.row()
row.prop(clash_set, "tolerance")
layout.label(text="Group A:")
row = layout.row()
row.operator("bim.add_clash_source").group = "a"
for index, source in enumerate(clash_set.a):
row = layout.row(align=True)
row.prop(source, "name", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "a"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "a"
row = layout.row(align=True)
row.prop(source, "mode", text="")
row.prop(source, "selector", text="")
layout.label(text="Group B:")
row.prop(clash_set, "check_all")
elif clash_set.mode == "collision":
row = layout.row()
row.operator("bim.add_clash_source").group = "b"
for index, source in enumerate(clash_set.b):
row = layout.row(align=True)
row.prop(source, "name", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "b"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "b"
row = layout.row(align=True)
row.prop(source, "mode", text="")
row.prop(source, "selector", text="")
row.prop(clash_set, "allow_touching")
elif clash_set.mode == "clearance":
row = layout.row()
row.prop(props, "should_create_clash_snapshots")
row.prop(clash_set, "clearance")
row = layout.row()
row.prop(clash_set, "check_all")
row = layout.row(align=True)
row.label(text="Group A:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = "a"
for index, source in enumerate(clash_set.a):
row = layout.row(align=True)
row.operator("bim.execute_ifc_clash")
row.operator("bim.select_ifc_clash_results")
row.prop(source, "name", text="")
row.prop(source, "mode", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "a"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "a"
if source.mode != "a":
blenderbim.bim.helper.draw_filter(
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_a_{index}"
)
row = layout.row(align=True)
row.label(text="Group B:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = "b"
for index, source in enumerate(clash_set.b):
row = layout.row(align=True)
row.prop(source, "name", text="")
row.prop(source, "mode", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "b"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "b"
if source.mode != "a":
blenderbim.bim.helper.draw_filter(
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_b_{index}"
)
row = layout.row()
row.prop(props, "should_create_clash_snapshots")
row = layout.row()
row.operator("bim.execute_ifc_clash")
row = layout.row()
row.label(text=f"{len(clash_set.clashes)} Clashes Found", icon="PIVOT_CURSOR")
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
row = layout.row()
row.operator("bim.select_clash")
class BIM_PT_clash_manager(Panel):
@@ -111,29 +142,6 @@ class BIM_PT_clash_manager(Panel):
pass
class BIM_PT_dumb_clash_manager(Panel):
bl_idname = "BIM_PT_dumb_clash_manager"
bl_label = "Dumb Manager"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_clash_manager"
def draw(self, context):
layout = self.layout
props = context.scene.BIMClashProperties
row = layout.row(align=True)
row.operator("bim.load_ifc_clashes", text="LOAD IFC CLASHES", icon="IMPORT")
row.operator("bim.save_ifc_clashes", text="SAVE CLASH RESULTS", icon="EXPORT")
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
# bim.select_clash
row = layout.row(align=True)
row.operator("bim.select_clash", text="SELECT CLASH", icon="IMPORT")
row.prop(props, "sould_focus_on_clash", icon="RESTRICT_VIEW_OFF")
class BIM_PT_smart_clash_manager(Panel):
bl_idname = "BIM_PT_smart_clash_manager"
bl_label = "Smart Clash Manager"
@@ -293,7 +293,7 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
identification = active_reference[1]
for obj in objects:
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
references = ifcopenshell.util.classification.get_references(element, should_inherit=False)
for reference in references:
@@ -358,7 +358,7 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
break
for obj in objects:
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
if not ifc_definition_id:
continue
ifcopenshell.api.run(
@@ -406,7 +406,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
classification.Location = bsdd_classification.domain_namespace_uri
for obj in objects:
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
if not ifc_definition_id:
continue
element = tool.Ifc.get().by_id(ifc_definition_id)
@@ -191,11 +191,11 @@ class CoveringToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
else:
row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR")
row = cls.layout.row()
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
@classmethod
def draw_basic_bim_tool_interface(cls):
@@ -24,6 +24,7 @@ classes = (
operator.CopyDebugInformation,
operator.CreateAllShapes,
operator.CreateShapeFromStepId,
operator.DebugActiveDrawing,
operator.InspectFromObject,
operator.InspectFromStepId,
operator.OverrideDisplayType,
@@ -20,11 +20,14 @@ import os
import sys
import bpy
import time
import random
import logging
import platform
import subprocess
import addon_utils
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import blenderbim.tool as tool
@@ -660,3 +663,104 @@ class PipInstall(bpy.types.Operator):
]
)
return {"FINISHED"}
class DebugActiveDrawing(bpy.types.Operator):
bl_idname = "bim.debug_active_drawing"
bl_label = "Search Active Drawing For Failing Elements"
bl_description = (
"Will iterate over all visible drawing's objects, trying to narrow down the list of possible failing objects"
)
def execute(self, context: bpy.types.Context):
props = context.scene.DocProperties
drawing_item = props.drawings[props.active_drawing_index]
drawing = tool.Ifc.get().by_id(drawing_item.ifc_definition_id)
GREEN = "\033[92m"
CYAN = "\033[96m"
END = "\033[0m"
ATTEMPS = 10
# run create drawing with sync for once
# to make sure everything is actually in sync
try:
bpy.ops.bim.create_drawing(sync=True)
except:
pass
else:
self.report({"INFO"}, "No errors creating drawing, nothing to investigate.")
return {"FINISHED"}
all_elements = [e for obj in context.visible_objects if (e := tool.Ifc.get_entity(obj))]
all_elements = set(all_elements)
original_exclude = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "Exclude")
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
def drawing_fails_to_load(chunk_to_include: set) -> bool:
current_elements = all_elements - chunk_to_include
excluded_guids = ", ".join([e.GlobalId for e in current_elements if hasattr(e, "GlobalId")])
tool.Ifc.run("pset.edit_pset", pset=pset, properties={"Exclude": f"{original_exclude}, {excluded_guids}"})
try:
bpy.ops.bim.create_drawing(sync=False)
result = False
except Exception as e:
# print(e)
result = True
tool.Ifc.run("pset.edit_pset", pset=pset, properties={"Exclude": original_exclude})
return result
def test_elements(elements: list[ifcopenshell.entity_instance], attempts: int = ATTEMPS) -> None:
print(f"{CYAN}processing {len(elements)} elements{END}")
if not elements:
print(f"Empty list of elements, will stop...")
return
n_elements = len(elements)
middle = int(n_elements / 2)
chunk1, chunk2 = elements[:middle], elements[middle:]
test_chunk_1 = drawing_fails_to_load(set(chunk1))
test_chunk_2 = drawing_fails_to_load(set(chunk2))
if (
# both chunks do not fail anymore
(not test_chunk_1 and not test_chunk_2)
# or we have 1 element chunk that is still failing
or (test_chunk_1 and not chunk2)
or (test_chunk_2 and not chunk1)
):
if attempts == 0 or n_elements in (1, 2):
print(f"{GREEN}Couldn't narrow it down any further.{END}")
print(f"It's some of the {n_elements} elements:")
print(elements)
print("Let's test excluding them...")
for element in elements:
test = drawing_fails_to_load(all_elements - {element})
if test:
print(f"{CYAN}Excluding element didn't fixed the drawing: {END}")
print(element)
else:
print(f"{GREEN}Excluding element fixed the drawing: {END}")
print(element)
else:
print(f"{CYAN}Will try to reshuffle elements and try again, attempt {ATTEMPS-attempts+1}/{ATTEMPS}")
attempts -= 1
random.shuffle(elements)
test_elements(elements, attempts)
return
# if chunk fails we need to investigate it further
if test_chunk_1:
test_elements(chunk1)
if test_chunk_2:
test_elements(chunk2)
test_elements(list(all_elements))
self.report({"INFO"}, "See system console for the results")
return {"FINISHED"}
@@ -75,6 +75,9 @@ class BIM_PT_debug(Panel):
row = layout.row()
row.operator("bim.profile_import_ifc")
row = layout.row()
row.operator("bim.debug_active_drawing")
row = layout.split(factor=0.5, align=True)
row.operator("bim.create_shape_from_step_id").should_include_curves = False
row.operator("bim.create_shape_from_step_id", text="", icon="IPO_ELASTIC").should_include_curves = True
@@ -38,6 +38,7 @@ class DiffData:
cls.data["total_deleted"] = cls.total_deleted()
cls.data["total_changed"] = cls.total_changed()
cls.data["changes"] = cls.changes()
cls.data["saved_searches"] = cls.saved_searches()
cls.is_loaded = True
@classmethod
@@ -84,3 +85,18 @@ class DiffData:
elif element.GlobalId in diff["deleted"]:
changes["Deleted"] = True
return changes
@classmethod
def saved_searches(cls):
if not tool.Ifc.get():
return []
groups = tool.Ifc.get().by_type("IfcGroup")
results = []
for group in groups:
try:
data = json.loads(group.Description)
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
results.append(group)
except:
pass
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
@@ -135,6 +135,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
bl_label = "Execute IFC Diff"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
@@ -162,7 +163,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
new = ifcopenshell.open(self.props.new_file)
relationships = [r.relationship for r in self.props.diff_relationships]
query = self.props.diff_filter_elements
query = tool.Search.export_filter_query(self.props.filter_groups) or None
ifc_diff = ifcdiff.IfcDiff(old, new, relationships=relationships, filter_elements=query)
ifc_diff.diff()
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.prop import StrProperty
from blenderbim.bim.prop import StrProperty, BIMFilterGroup
from blenderbim.bim.module.diff.data import DiffData
from bpy.types import PropertyGroup
from bpy.props import (
@@ -48,7 +48,7 @@ class DiffProperties(PropertyGroup):
old_file: StringProperty(default="", name="Old IFC File")
new_file: StringProperty(default="", name="New IFC File")
diff_relationships: CollectionProperty(type=Relationships, name="Relationships")
diff_filter_elements: StringProperty(default="", name="Filter")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
should_load_changed_elements: BoolProperty(name="Load Changed Elements", default=True)
active_file: EnumProperty(
items=[
@@ -19,8 +19,8 @@
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.diff.data import DiffData
import blenderbim.bim.helper
import blenderbim.tool as tool
import json
class BIM_PT_diff(Panel):
@@ -93,9 +93,7 @@ class BIM_PT_diff(Panel):
remove.collection = "diff_relationships"
remove.index = index
row = layout.row(align=True)
row.prop(props, "diff_filter_elements")
row.operator("bim.ifc_selector", icon="FILTER", text="")
blenderbim.bim.helper.draw_filter(self.layout, props.filter_groups, DiffData, "diff")
row = layout.row()
row.operator("bim.execute_ifc_diff")
@@ -41,6 +41,7 @@ classes = (
operator.CleanWireframes,
operator.ContractSheet,
operator.ContractTargetView,
operator.ConvertSVGToDXF,
operator.CreateDrawing,
operator.CreateSheets,
operator.DisableAddAnnotationType,
@@ -606,7 +606,7 @@ class BaseDecorator:
for literal_data in literals_data:
box_alignment = literal_data["BoxAlignment"]
for line in literal_data["CurrentValue"].split("\\n"):
for line in literal_data["CurrentValue"].split("\n"):
self.draw_label(
context,
line,
@@ -188,7 +188,6 @@ def format_distance(
feet += 1
inches = 0
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches
tx_dist = ""
@@ -299,7 +298,9 @@ def parse_diagram_scale(camera):
return float(numerator) / float(denominator)
def ortho_view_frame(camera, margin=0.015):
def ortho_view_frame(
camera: bpy.types.Camera, margin: float = 0.015
) -> tuple[float, float, float, float, float, float]:
"""Calculates 2d bounding box of camera view area.
Similar to `bpy.types.Camera.view_frame`
@@ -44,7 +44,7 @@ import blenderbim.bim.module.drawing.helper as helper
import blenderbim.bim.export_ifc
from blenderbim.bim.module.drawing.decoration import CutDecorator
from blenderbim.bim.module.drawing.data import DecoratorData, DrawingsData
from typing import NamedTuple, List
from typing import NamedTuple, List, Union, Optional
from lxml import etree
from mathutils import Vector, Color, Matrix
from timeit import default_timer as timer
@@ -199,6 +199,11 @@ class CreateDrawing(bpy.types.Operator):
+ "SHIFT+CLICK to print all selected drawings"
)
print_all: bpy.props.BoolProperty(name="Print All", default=False, options={"SKIP_SAVE"})
sync: bpy.props.BoolProperty(
name="Sync Before Creating Drawing",
description="Could save some time if you're sure IFC and current Blender session are already in sync",
default=True,
)
@classmethod
def poll(cls, context):
@@ -220,7 +225,8 @@ class CreateDrawing(bpy.types.Operator):
else:
drawings_to_print = [self.props.active_drawing_id]
for drawing_id in drawings_to_print:
for drawing_i, drawing_id in enumerate(drawings_to_print):
self.drawing_index = drawing_i
if self.print_all:
bpy.ops.bim.activate_drawing(drawing=drawing_id, camera_view_point=False)
@@ -335,6 +341,13 @@ class CreateDrawing(bpy.types.Operator):
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasUnderlay"):
return
svg_path = self.get_svg_path(cache_type="underlay")
if os.path.isfile(svg_path) and self.props.should_use_underlay_cache:
return svg_path
visible_object_names = {obj.name for obj in bpy.context.visible_objects}
for obj in bpy.context.view_layer.objects:
obj.hide_render = obj.name not in visible_object_names
context.scene.render.filepath = svg_path[0:-4] + ".png"
drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index]
@@ -445,8 +458,14 @@ class CreateDrawing(bpy.types.Operator):
return LineworkContexts(body_contexts, annotation_contexts)
def serialize_contexts_elements(
self, ifc, tree: ifcopenshell.geom.tree, contexts: LineworkContexts, context_type, drawing_elements, target_view
):
self,
ifc: ifcopenshell.file,
tree: ifcopenshell.geom.tree,
contexts: LineworkContexts,
context_type: str,
drawing_elements: set[ifcopenshell.entity_instance],
target_view: str,
) -> None:
drawing_elements = drawing_elements.copy()
contexts = getattr(contexts, context_type)
for context in contexts:
@@ -472,20 +491,24 @@ class CreateDrawing(bpy.types.Operator):
tree.add_element(elem)
drawing_elements -= processed
def generate_linework(self, context):
def generate_linework(self, context: bpy.types.Context) -> Union[str, None]:
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
svg_path = self.get_svg_path(cache_type="linework")
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
return svg_path
with profile("sync"):
# All very hackish whilst prototyping
exporter = blenderbim.bim.export_ifc.IfcExporter(None)
exporter.file = tool.Ifc.get()
invalidated_elements = exporter.sync_all_objects()
invalidated_elements += exporter.sync_edited_objects()
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
# in case of printing multiple drawings we need to sync just once
if self.sync and self.drawing_index == 0:
with profile("sync"):
# All very hackish whilst prototyping
exporter = blenderbim.bim.export_ifc.IfcExporter(None)
exporter.file = tool.Ifc.get()
invalidated_elements = exporter.sync_all_objects(skip_unlinking=True)
invalidated_elements += exporter.sync_edited_objects()
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
cache = IfcStore.get_cache()
[cache.remove(guid) for guid in invalidated_guids]
# If we have already calculated it in the SVG in the past, don't recalculate
edited_guids = set()
@@ -506,6 +529,7 @@ class CreateDrawing(bpy.types.Operator):
for ifc_path, ifc in files.items():
# Don't use draw.main() just whilst we're prototyping and experimenting
# TODO: hash paths are never used
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
ifc_cache_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
@@ -518,8 +542,6 @@ class CreateDrawing(bpy.types.Operator):
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element)
self.setup_serialiser(ifc, target_view)
cache = IfcStore.get_cache()
[cache.remove(guid) for guid in invalidated_guids]
tree = ifcopenshell.geom.tree()
tree.enable_face_styles(True)
@@ -1115,14 +1137,14 @@ class CreateDrawing(bpy.types.Operator):
continue
return space
def get_material_name(self, element):
def get_material_name(self, element: ifcopenshell.entity_instance) -> str:
if hasattr(element, "Name") and element.Name:
return element.Name
elif hasattr(element, "LayerSetName") and element.LayerSetName:
return element.LayerSetName
return "mat-" + str(element.id())
def get_svg_path(self, cache_type=None):
def get_svg_path(self, cache_type: Optional[str] = None) -> str:
drawing_path = tool.Drawing.get_document_uri(self.camera_document)
drawings_dir = os.path.dirname(drawing_path)
@@ -1345,7 +1367,7 @@ class SelectAllDrawings(bpy.types.Operator):
bl_label = "Select All Drawings"
view: bpy.props.StringProperty()
bl_description = "Select all drawings in the drawing list.\n\n" + "SHIFT+CLICK to deselect all drawings"
select_all: bpy.props.BoolProperty(name="Open All", default=False, options={"SKIP_SAVE"})
select_all: bpy.props.BoolProperty(name="Open All", default=True, options={"SKIP_SAVE"})
def invoke(self, context, event):
# deselect all drawings on shift+click
@@ -1451,10 +1473,6 @@ class ActivateModel(bpy.types.Operator):
class ActivateDrawing(bpy.types.Operator):
"""
Activates the selected drawing view
"""
bl_idname = "bim.activate_drawing"
bl_label = "Activate Drawing"
bl_options = {"REGISTER", "UNDO"}
@@ -1600,8 +1618,14 @@ class ReloadDrawingStyles(bpy.types.Operator):
json_path = Path(tool.Ifc.resolve_uri(rel_path))
if not json_path.exists():
self.report({"ERROR"}, "Shading styles file not found: {}".format(json_path))
return {"CANCELLED"}
ootb_resource = Path(context.scene.BIMProperties.data_dir) / "assets" / "shading_styles.json"
print(
f"WARNING. Couldn't find shading_styles for the drawing by the path: {json_path}. "
f"Default BBIM resource will be copied from {ootb_resource}"
)
if ootb_resource.exists():
os.makedirs(json_path.parent, exist_ok=True)
shutil.copy(ootb_resource, json_path)
with open(json_path, "r") as fi:
shading_styles_json = json.load(fi)
@@ -2689,3 +2713,50 @@ class AddReferenceImage(bpy.types.Operator, Operator):
)
tool.Style.reload_material_from_ifc(material)
tool.Geometry.record_object_materials(obj)
class ConvertSVGToDXF(bpy.types.Operator):
bl_idname = "bim.convert_svg_to_dxf"
bl_label = "Convert SVG to DXF"
bl_options = {"REGISTER", "UNDO"}
view: bpy.props.StringProperty()
bl_description = "Convert current drawing's .svg to .dxf.\n\nSHIFT+CLICK to convert all selected drawings"
convert_all: bpy.props.BoolProperty(name="Convert All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# convert all drawings on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.shift:
self.open_all = True
return self.execute(context)
def execute(self, context):
if self.convert_all:
drawings = [
tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected
]
else:
drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)]
drawing_uris: list[Path] = []
drawings_not_found: list[str] = []
for drawing in drawings:
drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing))
if drawing_uri is None or not os.path.exists(drawing_uri):
drawings_not_found.append(drawing.Name)
else:
drawing_uris.append(Path(drawing_uri))
if drawings_not_found:
msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format(
"\n".join(drawings_not_found)
)
self.report({"ERROR"}, msg)
return {"CANCELLED"}
for drawing_uri in drawing_uris:
tool.Drawing.convert_svg_to_dxf(drawing_uri, drawing_uri.with_suffix(".dxf"))
self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.")
return {"FINISHED"}
@@ -194,7 +194,7 @@ def update_document_name(self, context):
def update_has_underlay(self, context):
update_layer(self, context, "HasUnderlay", self.has_underlay)
# making sure that camera is active
if self.has_underlay and (context.active_object and context.active_object.data == self.id_data):
if self.has_underlay and (context.scene.camera and context.scene.camera.data == self.id_data):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
@@ -208,10 +208,12 @@ def update_has_annotation(self, context):
def update_layer(self, context, name, value):
element = tool.Ifc.get_entity(context.active_object)
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
if not element:
return
pset = ifcopenshell.util.element.get_psets(element).get("EPset_Drawing")
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
@@ -268,6 +268,11 @@ class BIM_PT_drawings(Panel):
col = row.column()
col.alignment = "RIGHT"
convert_to_dxf = row.row(align=True)
op = convert_to_dxf.operator("bim.convert_svg_to_dxf", text="", icon="IMAGE_DATA")
op.view = active_drawing.name
convert_to_dxf.enabled = active_drawing.ifc_definition_id > 0
op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="")
open_drawing_button = row.row(align=True)
@@ -250,7 +250,7 @@ class AnnotationToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
row.operator("bim.launch_annotation_type_manager", icon="LIGHTPROBE_GRID", text="")
row.operator("bim.launch_annotation_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
@@ -21,12 +21,14 @@ import bmesh
import mathutils
import ifcopenshell
import ifcopenshell.util.unit
import blenderbim.tool as tool
from math import pi, pow
from mathutils import Vector, Matrix, geometry
from typing import Union
class Helper:
def __init__(self, file):
def __init__(self, file: ifcopenshell.file):
self.file = file
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -34,7 +36,7 @@ class Helper:
# edge that shares a single vertex only with that face to find the extrusion
# edge. A face with the normal facing down is prioritised. A limited
# dissolve ensure that faces are quads and not tris.
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh):
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh: bpy.types.Mesh) -> dict:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
@@ -115,7 +117,9 @@ class Helper:
return {"profile": profile, "extrusion": extrusion}
def auto_detect_arbitrary_profile_with_voids(self, obj, mesh):
def auto_detect_arbitrary_profile_with_voids(
self, obj: bpy.types.Object, mesh: bpy.types.Mesh
) -> Union[tuple, dict]:
groups = {"IFCARCINDEX": [], "IFCCIRCLE": []}
for i, group in enumerate(obj.vertex_groups):
if "IFCARCINDEX" in group.name:
@@ -138,28 +142,31 @@ class Helper:
total_groups = 0
is_circle = False
for group_type, group_indices in groups.items():
for group_index in group_indices:
if group_index in vert[deform_layer]:
if group_type == "IFCCIRCLE":
is_circle = True
group_verts[group_type].setdefault(group_index, 0)
group_verts[group_type][group_index] += 1
total_groups += 0
if not group_indices:
continue
is_special, group_index = tool.Blender.bmesh_check_vertex_in_groups(vert, deform_layer, group_indices)
if not is_special:
continue
if group_type == "IFCCIRCLE":
is_circle = True
group_verts[group_type].setdefault(group_index, 0)
group_verts[group_type][group_index] += 1
total_groups += 0
if total_groups > 1: # A vert can only belong to one group
return (False, "AMBIGUOUS_SPECIAL_VERTEX")
elif is_circle:
pass # Circles are allowed to be unclosed
pass # Circles are allowed to be unclosed
elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop
return (False, "UNCLOSED_LOOP")
for group_type, group_counts in group_verts.items():
if group_type == "IFCARCINDEX":
for group_count in group_counts.values():
if group_count != 3: # Each arc needs 3 verts
if group_count != 3: # Each arc needs 3 verts
return (False, "3POINT_ARC")
elif group_type == "IFCCIRCLE":
for group_count in group_counts.values():
if group_count != 2: # Each circle needs 2 verts
if group_count != 2: # Each circle needs 2 verts
return (False, "CIRCLE")
loop_edges = set(bm.edges)
@@ -269,7 +276,7 @@ class Helper:
inner_loops.remove(outer_loop)
# Copy vectors to prevent random data mangling after bmesh is freed.
points = [Vector(list(v.co)) for v in bm.verts]
points = [v.co.copy() for v in bm.verts]
bm.to_mesh(mesh)
mesh.update()
@@ -324,8 +324,8 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
# We are explicitly casting to a tessellation, so remove all parametric materials.
element_type = ifcopenshell.util.element.get_type(product)
if element_type: # Some invalid IFCs use material sets without a type.
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=element_type)
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=product)
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), products=[element_type])
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), products=[product])
else:
# These objects are parametrically based on an axis and should not be modified as a mesh
return
@@ -908,7 +908,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
index += 1
pset = tool.Ifc.get().by_id(pset['id'])
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
@@ -976,26 +976,23 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
add_linked_aggregate_pset(element, index)
index +=1
index += 1
for part in parts:
if part.is_a("IfcElementAssembly"):
select_objects_and_add_data(part)
else:
add_linked_aggregate_pset(part, index)
index += 1
obj = tool.Ifc.get_object(part)
obj.select_set(True)
def add_linked_aggregate_pset(part, index):
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
if not pset:
pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name
)
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
@@ -1018,7 +1015,9 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
return
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.group_name)
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group)
ifcopenshell.api.run(
"group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group
)
def custom_incremental_naming_for_element_assembly(old_to_new):
for new in old_to_new.values():
@@ -1029,21 +1028,20 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
][0]
number = len(group_elements) - 1
number = f"{number:02d}"
new_obj = tool.Ifc.get_object(new[0])
pattern1 = r'_\d'
pattern1 = r"_\d"
if re.findall(pattern1, new_obj.name):
split_name = new_obj.name.split("_")
new_obj.name = split_name[0] + "_" + number
continue
pattern2 = r'\.\d{3}'
pattern2 = r"\.\d{3}"
if re.findall(pattern2, new_obj.name):
split_name = new_obj.name.split(".")
new_obj.name = split_name[0] + "_" + number
if len(context.selected_objects) != 1:
return {"FINISHED"}
@@ -1065,7 +1063,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
custom_incremental_naming_for_element_assembly(old_to_new)
# Recreate aggregate relationship
for old in old_to_new.keys():
if old.is_a("IfcElementAssembly"):
@@ -1075,17 +1073,15 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
return old_to_new
@staticmethod
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i['Index'] for i in psets if i]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
class RefreshLinkedAggregate(bpy.types.Operator):
@@ -1134,9 +1130,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
original_names[group] = {}
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset['Index']
index = pset["Index"]
original_names[group][index] = tool.Ifc.get_object(element).name
parts = ifcopenshell.util.element.get_parts(element)
if parts:
for part in parts:
@@ -1144,20 +1140,22 @@ class RefreshLinkedAggregate(bpy.types.Operator):
original_names | get_original_names(part)
else:
try:
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
except:
continue
index = pset['Index']
continue
index = pset["Index"]
original_names[group][index] = tool.Ifc.get_object(part).name
return original_names
def set_original_name(obj, original_names):
element = tool.Ifc.get_entity(obj)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if ifcopenshell.util.element.get_parts(element): # if element has parts it means it is the base of and aggregate or sub-aggregate
if ifcopenshell.util.element.get_parts(
element
): # if element has parts it means it is the base of and aggregate or sub-aggregate
aggregate = element
group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
@@ -1166,18 +1164,16 @@ class RefreshLinkedAggregate(bpy.types.Operator):
]
if not group:
return
group = group[0].id()
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset['Index']
index = pset["Index"]
try:
obj.name = original_names[group][index]
except:
return
def get_element_assembly(element):
if element.is_a("IfcElementAssembly"):
@@ -1263,9 +1259,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
original_names = get_original_names(element)
delete_objects(element)
for obj in context.selected_objects:
@@ -1273,31 +1269,33 @@ class RefreshLinkedAggregate(bpy.types.Operator):
tool.Ifc.get_object(base_instance).select_set(True)
old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(
self, context
)
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix
for old, new in old_to_new.items():
if element_aggregate and new[0].is_a("IfcElementAssembly"):
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
if not new_aggregate:
blenderbim.core.aggregate.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(element_aggregate),
related_obj=tool.Ifc.get_object(new[0]),
)
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(element_aggregate),
related_obj=tool.Ifc.get_object(new[0]),
)
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
set_original_name(new_obj, original_names)
blenderbim.bim.handler.refresh_ui_data()
operator_time = time() - refresh_start_time
@@ -1445,7 +1443,9 @@ class OverrideModeSetEdit(bpy.types.Operator):
for obj in selected_objs:
if not obj:
continue
if not obj.data:
obj_supports_edit_mode, message = tool.Blender.object_supports_edit_mode(obj)
if not obj_supports_edit_mode:
self.report({"INFO"}, message)
obj.select_set(False)
continue
element = tool.Ifc.get_entity(obj)
@@ -212,7 +212,7 @@ class BIM_PT_representation_items(Panel):
if props.is_editing_item_style:
# NOTE: we currently support 1 item having just 1 style
# when IfcStyledItem can actually have multiple styles
row.prop(props, "representation_item_style", icon="SHADING_RENDERED", text="")
prop_with_search(row, props, "representation_item_style", icon="SHADING_RENDERED", text="")
row.operator("bim.edit_representation_item_style", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_representation_item_style", icon="CANCEL", text="")
else:
@@ -212,7 +212,7 @@ class UnassignGroup(bpy.types.Operator, tool.Ifc.Operator):
"group.unassign_group",
self.file,
**{
"product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id),
"products": [self.file.by_id(product.BIMObjectProperties.ifc_definition_id)],
"group": self.file.by_id(self.group),
}
)
@@ -151,7 +151,10 @@ class AssignPresentationLayer(bpy.types.Operator):
ifcopenshell.api.run(
"layer.assign_layer",
self.file,
**{"item": self.file.by_id(item.BIMMeshProperties.ifc_definition_id), "layer": self.file.by_id(self.layer)}
**{
"items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)],
"layer": self.file.by_id(self.layer),
}
)
return {"FINISHED"}
@@ -173,7 +176,10 @@ class UnassignPresentationLayer(bpy.types.Operator):
ifcopenshell.api.run(
"layer.unassign_layer",
self.file,
**{"item": self.file.by_id(item.BIMMeshProperties.ifc_definition_id), "layer": self.file.by_id(self.layer)}
**{
"items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)],
"layer": self.file.by_id(self.layer),
}
)
return {"FINISHED"}
@@ -19,6 +19,7 @@
import os
import bpy
import ifcopenshell
import ifcopenshell.util.doc
import ifcopenshell.util.schema
import blenderbim.tool as tool
@@ -109,29 +110,41 @@ class MaterialsData:
def active_styles(cls):
props = bpy.context.scene.BIMMaterialProperties
results = []
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index].ifc_definition_id
if not material:
return results
material = tool.Ifc.get().by_id(material)
for definition in material.HasRepresentation:
for representation in definition.Representations:
if not representation.is_a("IfcStyledRepresentation"):
if not props.materials or props.active_material_index >= len(props.materials):
return results
material = props.materials[props.active_material_index].ifc_definition_id
if not material:
return results
material = tool.Ifc.get_entity_by_id(material)
# NOTE: it's possible that data will be refreshed during material removal
# (when it will try to fetch active material type and will reach for material_types)
# and props.materials[i].ifc_definition_id will contain already removed id
if not material or not material.is_a("IfcMaterial"):
return results
for definition in material.HasRepresentation:
for representation in definition.Representations:
if not representation.is_a("IfcStyledRepresentation"):
continue
context = representation.ContextOfItems
for item in representation.Items:
if not item.is_a("IfcStyledItem"):
continue
context = representation.ContextOfItems
for item in representation.Items:
if not item.is_a("IfcStyledItem"):
continue
for style in item.Styles:
if style.is_a("IfcSurfaceStyle"):
results.append({
for style in item.Styles:
if style.is_a("IfcSurfaceStyle"):
results.append(
{
"context_type": context.ContextType,
"context_identifier": getattr(context, "ContextIdentifier", ""),
"target_view": getattr(context, "TargetView", ""),
"name": style.Name or "Unnamed",
"id": style.id(),
"context_id": context.id(),
})
}
)
return results
@@ -118,7 +118,7 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_material"
bl_label = "Add Material"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj: bpy.props.StringProperty(name="Material Name")
name: bpy.props.StringProperty(default="Default")
def invoke(self, context, event):
@@ -23,6 +23,7 @@ from math import sin, cos, radians
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from typing import Union
ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
@@ -35,15 +36,6 @@ def transparent_color(color, alpha=0.1):
return color
def bm_check_vertex_in_groups(vertex, deform_layer, groups):
"""returns tuple boolean (whether vertex is in any of the groups)
and related group index"""
for group_index in vertex[deform_layer].keys():
if group_index in groups:
return True, group_index
return False, None
class ProfileDecorator:
installed = None
@@ -148,12 +140,12 @@ class ProfileDecorator:
# deform_layer is None if there are no verts assigned to vertex groups
# even if there are vertex groups in the obj.vertex_groups
if deform_layer:
is_arc, group_index = bm_check_vertex_in_groups(vertex, deform_layer, arc_groups)
is_arc, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, arc_groups)
if is_arc:
arcs.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
is_circle, group_index = bm_check_vertex_in_groups(vertex, deform_layer, circle_groups)
is_circle, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, circle_groups)
if is_circle:
circles.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
@@ -542,7 +542,7 @@ class MEPGenerator:
ifc_representation_class=None,
)
rel = ifcopenshell.api.run("material.assign_material", ifc_file, product=element, type="IfcMaterialProfileSet")
rel = ifcopenshell.api.run("material.assign_material", ifc_file, products=[element], type="IfcMaterialProfileSet")
profile_set = rel.RelatingMaterial
material_profile = ifcopenshell.api.run(
"material.add_profile", ifc_file, profile_set=profile_set, material=material
@@ -24,10 +24,14 @@ import shapely
import logging
import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.geom
import ifcopenshell.util.shape
import ifcopenshell.util.element
import ifcopenshell.util.shape_builder
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import blenderbim.tool as tool
import blenderbim.core.geometry
import blenderbim.bim.import_ifc as import_ifc
@@ -107,7 +111,7 @@ class FilledOpeningGenerator:
# Equivalent to "side Z" for a wall axis, so that stuff like skylights appear on the top.
local_position_on_voided_obj.z = layers["offset"] + layers["thickness"]
new_matrix.translation.xyz = voided_obj.matrix_world @ local_position_on_voided_obj
rotation_matrix = Matrix.Rotation(radians(-90), 4, 'X')
rotation_matrix = Matrix.Rotation(radians(-90), 4, "X")
new_matrix @= rotation_matrix
filling_obj.matrix_world = new_matrix
@@ -184,7 +188,13 @@ class FilledOpeningGenerator:
)
def regenerate_from_type(self, usecase_path, ifc_file, settings):
filling = settings["related_object"]
relating_type = settings["relating_type"]
for related_object in settings["related_objects"]:
self._regenerate_from_type(related_object)
def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None:
filling = related_object
if not getattr(filling, "FillsVoids", None):
return
@@ -25,9 +25,13 @@ import ifcopenshell.util.system
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.type
import ifcopenshell.util.unit
import blenderbim.tool as tool
import blenderbim.core.aggregate
import blenderbim.core.type
import blenderbim.core.geometry
import blenderbim.core.spatial
from . import wall, slab, profile, mep
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.data import AuthoringData
@@ -273,7 +277,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
obj.location[2] = collection_obj.location[2] + bpy.context.scene.BIMModelProperties.rl2
@staticmethod
def generate_layered_element(ifc_class, relating_type):
def generate_layered_element(ifc_class: str, relating_type: ifcopenshell.entity_instance) -> bool:
layer_set_direction = None
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
@@ -297,6 +301,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
material = ifcopenshell.util.element.get_material(tool.Ifc.get_entity(obj))
material.LayerSetDirection = layer_set_direction
return True
return False
class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator):
@@ -510,7 +515,7 @@ def ensure_material_assigned(usecase_path, ifc_file, settings):
if usecase_path == "material.assign_material":
if not settings.get("material", None):
return
elements = [settings["product"]]
elements = settings["products"]
else:
elements = []
for rel in ifc_file.by_type("IfcRelAssociatesMaterial"):
@@ -546,7 +551,7 @@ def ensure_material_assigned(usecase_path, ifc_file, settings):
def ensure_material_unassigned(usecase_path, ifc_file, settings):
elements = [settings["product"]]
elements = settings["products"]
if elements[0].is_a("IfcElementType"):
elements.extend(ifcopenshell.util.element.get_types(elements[0]))
for element in elements:
@@ -21,14 +21,18 @@ import copy
import bmesh
import mathutils.geometry
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.type
import blenderbim.core.geometry
import blenderbim.core.material
import blenderbim.core.root
from math import pi, degrees, inf
from mathutils import Vector, Matrix, Quaternion
from blenderbim.bim.module.geometry.helper import Helper
@@ -92,7 +96,7 @@ class DumbProfileGenerator:
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type)
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
material = ifcopenshell.util.element.get_material(element)
material.CardinalPoint = self.cardinal_point
@@ -207,12 +211,19 @@ class DumbProfileRegenerator:
return results
def regenerate_from_type(self, usecase_path, ifc_file, settings):
obj = tool.Ifc.get_object(settings["related_object"])
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
relating_type = settings["relating_type"]
new_material = ifcopenshell.util.element.get_material(relating_type)
if not new_material or not new_material.is_a("IfcMaterialProfileSet"):
return
for related_object in settings["related_objects"]:
self._regenerate_from_type(related_object)
def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None:
obj = tool.Ifc.get_object(related_object)
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
DumbProfileRecalculator().recalculate([obj])
@@ -20,17 +20,22 @@ import bpy
import json
import bmesh
import ifcopenshell
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import ifcopenshell.util.type
import blenderbim.bim.handler
import blenderbim.core.type
import blenderbim.core.geometry
import blenderbim.core.root
import blenderbim.tool as tool
from math import radians
from mathutils import Vector, Matrix
from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.decorator import ProfileDecorator
from typing import Optional
def calculate_quantities(usecase_path, ifc_file, settings):
@@ -110,7 +115,7 @@ def calculate_quantities(usecase_path, ifc_file, settings):
class DumbSlabGenerator:
def __init__(self, relating_type):
def __init__(self, relating_type: ifcopenshell.entity_instance):
self.relating_type = relating_type
def generate(self):
@@ -173,7 +178,7 @@ class DumbSlabGenerator:
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type)
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
representation = ifcopenshell.api.run(
@@ -246,27 +251,38 @@ class DumbSlabPlaner:
self.change_thickness(element, total_thickness)
def regenerate_from_type(self, usecase_path, ifc_file, settings):
obj = tool.Ifc.get_object(settings["related_object"])
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
relating_type = settings["relating_type"]
new_material = ifcopenshell.util.element.get_material(relating_type)
if not new_material or not new_material.is_a("IfcMaterialLayerSet"):
return
parametric = ifcopenshell.util.element.get_psets(settings["relating_type"]).get("EPset_Parametric")
layer_set_direction = None
if parametric:
layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction)
new_thickness = sum([l.LayerThickness for l in new_material.MaterialLayers])
material = ifcopenshell.util.element.get_material(settings["related_object"])
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
for related_object in settings["related_objects"]:
self._regenerate_from_type(related_object, layer_set_direction, new_thickness)
def _regenerate_from_type(
self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str], new_thickness: float
) -> None:
obj = tool.Ifc.get_object(related_object)
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
material = ifcopenshell.util.element.get_material(related_object)
if not material or not material.is_a("IfcMaterialLayerSetUsage"):
return
if layer_set_direction:
material.LayerSetDirection = layer_set_direction
if material.LayerSetDirection == "AXIS3":
self.change_thickness(settings["related_object"], new_thickness)
self.change_thickness(related_object, new_thickness)
def change_thickness(self, element, thickness):
def change_thickness(self, element: ifcopenshell.entity_instance, thickness: float) -> None:
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
obj = tool.Ifc.get_object(element)
if not obj:
@@ -698,9 +714,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
new_footprint = ifcopenshell.api.run(
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
)
old_footprint = ifcopenshell.util.representation.get_representation(
element, "Plan", "FootPrint", "SKETCH_VIEW"
)
old_footprint = ifcopenshell.util.representation.get_representation(element, "Plan", "FootPrint", "SKETCH_VIEW")
if old_footprint:
for inverse in tool.Ifc.get().get_inverse(old_footprint):
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
@@ -54,8 +54,10 @@ class LaunchTypeManager(bpy.types.Operator):
props.type_page = 1
if get_ifc_class(None, context):
ifc_class = props.ifc_class or AuthoringData.data["ifc_element_type"]
props.type_class = ifc_class
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
else:
ifc_class = AuthoringData.data["ifc_element_type"]
props.type_class = ifc_class
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
return context.window_manager.invoke_popup(self, width=550)
def draw(self, context):
@@ -24,7 +24,9 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.type
import mathutils.geometry
import blenderbim.bim.handler
import blenderbim.core.type
@@ -35,6 +37,7 @@ from blenderbim.bim.ifc import IfcStore
from math import pi, sin, cos, degrees, radians
from mathutils import Vector, Matrix
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
from typing import Optional
class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
@@ -555,7 +558,10 @@ class DumbWallGenerator:
for face in sibling_obj.data.polygons:
normal = (sibling_obj.matrix_world.to_quaternion() @ face.normal).normalized()
face_center = sibling_obj.matrix_world @ face.center
if normal.z != 0 or abs(mathutils.geometry.distance_point_to_plane(self.location, face_center, normal)) > 0.01:
if (
normal.z != 0
or abs(mathutils.geometry.distance_point_to_plane(self.location, face_center, normal)) > 0.01
):
continue
rotation = math.atan2(normal[1], normal[0])
@@ -603,7 +609,7 @@ class DumbWallGenerator:
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type)
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
if self.axis_context:
representation = ifcopenshell.api.run(
"geometry.add_axis_representation",
@@ -724,18 +730,29 @@ class DumbWallPlaner:
DumbWallRecalculator().recalculate([w for w in set(walls) if w])
def regenerate_from_type(self, usecase_path, ifc_file, settings):
obj = tool.Ifc.get_object(settings["related_object"])
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
relating_type = settings["relating_type"]
new_material = ifcopenshell.util.element.get_material(relating_type)
if not new_material or not new_material.is_a("IfcMaterialLayerSet"):
return
parametric = ifcopenshell.util.element.get_psets(settings["relating_type"]).get("EPset_Parametric")
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
layer_set_direction = None
if parametric:
layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction)
material = ifcopenshell.util.element.get_material(settings["related_object"])
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
for related_object in settings["related_objects"]:
self._regenerate_from_type(related_object, layer_set_direction)
def _regenerate_from_type(
self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str]
) -> None:
obj = tool.Ifc.get_object(related_object)
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
material = ifcopenshell.util.element.get_material(related_object)
if not material or not material.is_a("IfcMaterialLayerSetUsage"):
return
if layer_set_direction:
@@ -854,11 +871,11 @@ class DumbWallJoiner:
if (location_on_base - location).length < (location_on_side - location).length:
axis_offset = location_on_side - location_on_base
offset_from_axis = location_on_base - location
opening_matrix.translation = (location_on_base - axis_offset - offset_from_axis)
opening_matrix.translation = location_on_base - axis_offset - offset_from_axis
else:
axis_offset = location_on_side - location_on_base
offset_from_axis = location_on_side - location
opening_matrix.translation = (location_on_side - axis_offset - offset_from_axis)
opening_matrix.translation = location_on_side - axis_offset - offset_from_axis
opening_matrixes[opening] = opening_matrix
for filling in [r.RelatedBuildingElement for r in opening.HasFillings]:
@@ -871,11 +888,11 @@ class DumbWallJoiner:
if (location_on_base - location).length < (location_on_side - location).length:
axis_offset = location_on_side - location_on_base
offset_from_axis = location_on_base - location
filling_matrix.translation = (location_on_base - axis_offset - offset_from_axis)
filling_matrix.translation = location_on_base - axis_offset - offset_from_axis
else:
axis_offset = location_on_side - location_on_base
offset_from_axis = location_on_side - location
filling_matrix.translation = (location_on_side - axis_offset - offset_from_axis)
filling_matrix.translation = location_on_side - axis_offset - offset_from_axis
filling_matrixes[filling] = filling_matrix
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
@@ -1120,13 +1137,8 @@ class DumbWallJoiner:
other = tool.Ifc.get_object(rel.RelatedElement)
if connection not in ["ATPATH", "NOTDEFINED"]:
self.join(
obj,
other,
connection,
rel.RelatedConnectionType,
is_relating=True,
description=rel.Description
)
obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description
)
for rel in element.ConnectedFrom:
if rel.is_a("IfcRelConnectsPathElements"):
connection = rel.RelatedConnectionType
@@ -516,20 +516,21 @@ class BimToolUI:
prop_with_search(row, cls.props, "relating_type_id", text="")
else:
row.label(text="No Construction Type", icon="FILE_3D")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
else:
if AuthoringData.data["ifc_element_type"]:
row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR")
row = cls.layout.row()
row.prop(cls.props, "type_name")
row = cls.layout.row()
row = cls.layout.row(align=True)
op = row.operator(
"bim.add_default_type", icon="ADD", text=f"Add {AuthoringData.data['ifc_element_type']}"
)
op.ifc_element_type = AuthoringData.data["ifc_element_type"]
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
else:
row.label(text="No Element Types Found", icon="ERROR")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
@classmethod
def draw_basic_bim_tool_interface(cls):
@@ -18,6 +18,7 @@
import bpy
import importlib
import importlib.util
from pathlib import Path
import ifcpatch
from blenderbim.bim.prop import StrProperty, Attribute
@@ -21,6 +21,7 @@ from . import ui, prop, operator, workspace, gizmo, decorator
classes = (
operator.AppendEntireLibrary,
operator.AppendInspectedLinkedElement,
operator.AppendLibraryElement,
operator.AppendLibraryElementByQuery,
operator.AssignLibraryDeclaration,
@@ -113,6 +113,10 @@ class CreateProject(bpy.types.Operator):
if tool.Blender.is_default_scene():
for obj in bpy.data.objects:
bpy.data.objects.remove(obj)
for mesh in bpy.data.meshes:
bpy.data.meshes.remove(mesh)
for mat in bpy.data.materials:
bpy.data.materials.remove(mat)
core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template)
tool.Blender.register_toolbar()
@@ -188,8 +192,6 @@ class RefreshLibrary(bpy.types.Operator):
types = IfcStore.library_file.wrapped_data.types_with_super()
element_classes = ["IfcTypeProduct", "IfcMaterial", "IfcCostSchedule", "IfcProfileDef"]
if self.props.library_display_elements:
element_classes += ["IfcElement"]
for importable_type in sorted(element_classes):
if importable_type in types:
new = self.props.library_elements.add()
@@ -438,6 +440,7 @@ class AppendLibraryElement(bpy.types.Operator):
if element.is_a("IfcTypeProduct"):
self.import_type_from_ifc(element, context)
elif element.is_a("IfcProduct"):
# NOTE: not used as UI doesn't allow appending non-types
self.import_product_from_ifc(element, context)
element_type = ifcopenshell.util.element.get_type(element)
obj = tool.Ifc.get_object(element_type)
@@ -454,7 +457,7 @@ class AppendLibraryElement(bpy.types.Operator):
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
def import_material_from_ifc(self, element, context):
def import_material_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
self.file = IfcStore.get_file()
logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
@@ -463,7 +466,7 @@ class AppendLibraryElement(bpy.types.Operator):
blender_material = ifc_importer.create_material(element)
self.import_material_styles(blender_material, element, ifc_importer)
def import_product_from_ifc(self, element, context):
def import_product_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
self.file = IfcStore.get_file()
logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
@@ -476,7 +479,7 @@ class AppendLibraryElement(bpy.types.Operator):
ifc_importer.create_generic_elements({element})
ifc_importer.place_objects_in_collections()
def import_type_from_ifc(self, element, context):
def import_type_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
self.file = IfcStore.get_file()
logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
@@ -501,14 +504,14 @@ class AppendLibraryElement(bpy.types.Operator):
ifc_importer.create_element_type(element)
ifc_importer.place_objects_in_collections()
def import_materials(self, element, ifc_importer):
def import_materials(self, element: ifcopenshell.entity_instance, ifc_importer: import_ifc.IfcImporter) -> None:
for material in ifcopenshell.util.element.get_materials(element):
if IfcStore.get_element(material.id()):
continue
blender_material = ifc_importer.create_material(material)
self.import_material_styles(blender_material, material, ifc_importer)
def import_styles(self, element, ifc_importer):
def import_styles(self, element: ifcopenshell.entity_instance, ifc_importer: import_ifc.IfcImporter) -> None:
if element.is_a("IfcTypeProduct"):
representations = element.RepresentationMaps or []
elif element.is_a("IfcProduct"):
@@ -521,7 +524,12 @@ class AppendLibraryElement(bpy.types.Operator):
if element2.is_a("IfcSurfaceStyle") and not IfcStore.get_element(element2.id()):
ifc_importer.create_style(element2)
def import_material_styles(self, blender_material, material, ifc_importer):
def import_material_styles(
self,
blender_material: bpy.types.Material,
material: ifcopenshell.entity_instance,
ifc_importer: import_ifc.IfcImporter,
) -> None:
if not material.HasRepresentation:
return
for element in self.file.traverse(material.HasRepresentation[0]):
@@ -852,7 +860,10 @@ class LinkIfc(bpy.types.Operator):
continue
new = context.scene.BIMProjectProperties.links.add()
if self.use_relative_path:
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
try:
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
except:
pass # Perhaps on another drive or something
new.name = filepath
bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
print(f"Finished linking {len(files)} IFCs", time.time() - start)
@@ -890,15 +901,30 @@ class UnloadLink(bpy.types.Operator):
self.filepath = self.filepath.replace("\\", "/")
filepath = self.filepath
if not os.path.isabs(filepath):
filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), filepath)).replace("\\", "/")
for collection in context.scene.collection.children:
if collection.library and collection.library.filepath.replace("\\", "/") == filepath:
context.scene.collection.children.unlink(collection)
for scene in bpy.data.scenes:
if scene.library and scene.library.filepath.replace("\\", "/") == filepath:
filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), filepath))
filepath = Path(filepath)
if filepath.suffix.lower() == ".ifc":
filepath = filepath.with_suffix(".ifc.cache.blend")
for collection in context.scene.collection.children[:]:
if collection.library and Path(collection.library.filepath) == filepath:
bpy.data.collections.remove(collection)
for scene in bpy.data.scenes[:]:
if scene.library and Path(scene.library.filepath) == filepath:
bpy.data.scenes.remove(scene)
link = context.scene.BIMProjectProperties.links.get(self.filepath)
link.is_loaded = False
links = context.scene.BIMProjectProperties.links
links.get(self.filepath).is_loaded = False
if not any([l.is_loaded for l in links]):
ProjectDecorator.uninstall()
# we make sure we don't draw queried object from the file that was just unlinked
elif queried_obj := context.scene.BIMProjectProperties.queried_obj:
queried_filepath = Path(queried_obj["ifc_filepath"])
if queried_filepath == filepath:
ProjectDecorator.uninstall()
return {"FINISHED"}
@@ -989,24 +1015,24 @@ class ToggleLinkSelectability(bpy.types.Operator):
bl_label = "Toggle Link Selectability"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle selectability"
link: bpy.props.StringProperty()
link: bpy.props.StringProperty(name="Linked IFC Filepath")
def execute(self, context):
props = context.scene.BIMProjectProperties
link = props.links.get(self.link)
self.filepath = self.link
if not os.path.isabs(self.filepath):
self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath)).replace("\\", "/")
if not os.path.isabs(self.link):
self.link = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.link))
self.library_filepath = Path(self.link).with_suffix(".ifc.cache.blend")
for collection in self.get_linked_collections():
collection.hide_select = not collection.hide_select
link.is_selectable = not collection.hide_select
return {"FINISHED"}
def get_linked_collections(self):
def get_linked_collections(self) -> list[bpy.types.Collection]:
return [
c
for c in bpy.data.collections
if "IfcProject" in c.name and c.library and c.library.filepath.replace("\\", "/") == self.filepath
if "IfcProject" in c.name and c.library and Path(c.library.filepath) == self.library_filepath
]
@@ -1015,15 +1041,15 @@ class ToggleLinkVisibility(bpy.types.Operator):
bl_label = "Toggle Link Visibility"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle visibility between SOLID and WIREFRAME"
link: bpy.props.StringProperty()
mode: bpy.props.StringProperty()
link: bpy.props.StringProperty(name="Linked IFC Filepath")
mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")))
def execute(self, context):
props = context.scene.BIMProjectProperties
link = props.links.get(self.link)
self.filepath = self.link
if not os.path.isabs(self.filepath):
self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath)).replace("\\", "/")
if not os.path.isabs(self.link):
self.link = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.link))
self.library_filepath = Path(self.link).with_suffix(".ifc.cache.blend")
if self.mode == "WIREFRAME":
self.toggle_wireframe(link)
elif self.mode == "VISIBLE":
@@ -1044,25 +1070,17 @@ class ToggleLinkVisibility(bpy.types.Operator):
def toggle_visibility(self, link):
linked_collections = self.get_linked_collections()
queue = [bpy.context.view_layer.layer_collection]
layer_collection = None
while queue:
layer = queue.pop()
if layer.collection in linked_collections:
layer_collection = layer
break
queue.extend(list(layer.children))
if layer_collection:
layer_collections = tool.Blender.get_layer_collections_mapping(linked_collections)
for layer_collection in layer_collections.values():
layer_collection.exclude = not layer_collection.exclude
link.is_hidden = layer_collection.exclude
def get_linked_collections(self):
def get_linked_collections(self) -> list[bpy.types.Collection]:
return [
c
for c in bpy.data.collections
if "IfcProject" in c.name and c.library and c.library.filepath.replace("\\", "/") == self.filepath
if "IfcProject" in c.name and c.library and Path(c.library.filepath) == self.library_filepath
]
@@ -1276,6 +1294,8 @@ class LoadLinkedProject(bpy.types.Operator):
if len(shape.geometry.faces) > 1000 and self.is_local(shape): # 333 tris
self.process_occurrence(shape)
if not iterator.next():
if not chunked_verts:
break
mats = np.concatenate(chunked_materials)
midx = np.concatenate(chunked_material_ids)
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
@@ -1311,6 +1331,9 @@ class LoadLinkedProject(bpy.types.Operator):
ms = np.vstack([default_mat, np.frombuffer(shape.geometry.colors_buffer).reshape((-1, 4))])
mi = np.frombuffer(shape.geometry.material_ids_buffer, dtype=np.int32)
for geom_material_idx, geom_material in enumerate(shape.geometry.materials):
if not geom_material.has_diffuse:
ms[geom_material_idx + 1] = (0.8, 0.8, 0.8, 1)
chunked_materials.append(ms)
chunked_material_ids.append(mi + material_offset + 1)
material_offset += len(ms)
@@ -1431,7 +1454,10 @@ class LoadLinkedProject(bpy.types.Operator):
alpha = 1.0
if material.has_transparency and material.transparency > 0:
alpha = 1.0 - material.transparency
diffuse = material.diffuse + (alpha,)
if material.has_diffuse:
diffuse = material.diffuse + (alpha,)
else:
diffuse = (0.8, 0.8, 0.8, 1) # Blender's default material
material_name = f"{diffuse[0]}-{diffuse[1]}-{diffuse[2]}-{diffuse[3]}"
blender_mat = self.blender_mats.get(material_name, None)
if not blender_mat:
@@ -1509,6 +1535,7 @@ class LoadLinkedProject(bpy.types.Operator):
obj["guids"] = list(guids)
obj["guid_ids"] = list(guid_ids)
obj["db"] = self.db_filepath
obj["ifc_filepath"] = self.filepath
self.collection.objects.link(obj)
@@ -1640,6 +1667,42 @@ class QueryLinkedElement(bpy.types.Operator):
return self.execute(context)
class AppendInspectedLinkedElement(AppendLibraryElement):
bl_idname = "bim.append_inspected_linked_element"
bl_label = "Append Inspected Linked Element"
bl_description = "Append inspected linked element"
bl_options = {"REGISTER"}
def _execute(self, context):
from blenderbim.bim.module.project.data import LinksData
if not LinksData.linked_data:
self.report({"INFO"}, "No linked element found.")
return {"CANCELLED"}
guid = LinksData.linked_data["attributes"].get("GlobalId")
if guid is None:
self.report({"INFO"}, "Cannot find Global Id for element.")
return {"CANCELLED"}
queried_obj = context.scene.BIMProjectProperties.queried_obj
ifc_file = ifcopenshell.open(queried_obj["ifc_filepath"])
element_to_append = ifc_file.by_guid(guid)
element = ifcopenshell.api.run(
"project.append_asset",
tool.Ifc.get(),
library=ifc_file,
element=element_to_append,
)
self.import_product_from_ifc(element, context)
element_type = ifcopenshell.util.element.get_type(element)
obj = tool.Ifc.get_object(element_type)
if obj is None:
self.import_type_from_ifc(element_type, context)
return {"FINISHED"}
class EnableCulling(bpy.types.Operator):
bl_idname = "bim.enable_culling"
bl_label = "Enable Culling"
@@ -1857,32 +1920,32 @@ class CreateClippingPlane(bpy.types.Operator):
bl_label = "Create Clipping Plane"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.area.type == "VIEW_3D"
def execute(self, context):
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
# Clean up deleted planes
if len(bpy.context.scene.BIMProjectProperties.clipping_planes) > 5:
if len(context.scene.BIMProjectProperties.clipping_planes) > 5:
self.report({"INFO"}, "Maximum of six clipping planes allowed.")
return {"FINISHED"}
for area in bpy.context.screen.areas:
for area in context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
region = context.region
rv3d = context.region_data
coord = (self.mouse_x, self.mouse_y)
origin = region_2d_to_origin_3d(region, rv3d, coord)
direction = region_2d_to_vector_3d(region, rv3d, coord)
hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction)
if not hit:
self.report({"INFO"}, "No object found.")
return {"FINISHED"}
if rv3d: # Called from a 3D viewport
coord = (self.mouse_x, self.mouse_y)
origin = region_2d_to_origin_3d(region, rv3d, coord)
direction = region_2d_to_vector_3d(region, rv3d, coord)
hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction)
if not hit:
self.report({"INFO"}, "No object found.")
return {"FINISHED"}
else: # Not Called from a 3D viewport
location = (0, 0, 1)
normal = (0, 0, 1)
vertices = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (0.5, 0.5, 0), (-0.5, 0.5, 0)]
@@ -1893,20 +1956,20 @@ class CreateClippingPlane(bpy.types.Operator):
mesh.update()
plane_obj = bpy.data.objects.new("ClippingPlane", mesh)
bpy.context.collection.objects.link(plane_obj)
context.collection.objects.link(plane_obj)
z_axis = Vector((0, 0, 1))
rotation_matrix = z_axis.rotation_difference(normal).to_matrix().to_4x4()
plane_obj.matrix_world = rotation_matrix
plane_obj.matrix_world.translation = location
bpy.context.scene.cursor.location = location
context.scene.cursor.location = location
new = bpy.context.scene.BIMProjectProperties.clipping_planes.add()
new = context.scene.BIMProjectProperties.clipping_planes.add()
new.obj = plane_obj
tool.Blender.set_active_object(plane_obj)
ClippingPlaneDecorator.install(bpy.context)
ClippingPlaneDecorator.install(context)
bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
return {"FINISHED"}
@@ -1928,11 +1991,11 @@ class FlipClippingPlane(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.area.type == "VIEW_3D"
return context.active_object
def execute(self, context):
obj = bpy.context.active_object
if obj in [cp.obj for cp in bpy.context.scene.BIMProjectProperties.clipping_planes]:
obj = context.active_object
if obj in context.scene.BIMProjectProperties.clipping_planes_objs:
obj.rotation_euler[0] += radians(180)
bpy.context.view_layer.update()
context.view_layer.update()
return {"FINISHED"}
@@ -59,10 +59,6 @@ def update_library_file(self, context):
)
def update_library_display_elements(self, context):
bpy.ops.bim.refresh_library()
def update_filter_mode(self, context):
self.filter_categories.clear()
if self.filter_mode == "NONE":
@@ -125,11 +121,6 @@ class BIMProjectProperties(PropertyGroup):
organisation_name: StringProperty(name="Organisation")
organisation_email: StringProperty(name="Organisation Email")
authorisation: StringProperty(name="Authoriser")
library_display_elements: BoolProperty(
name="Display IfcElements",
description="Display IfcElements from library too, not just the types",
update=update_library_display_elements,
)
active_library_element: StringProperty(name="Enable Authoring Mode", default="")
library_breadcrumb: CollectionProperty(name="Library Breadcrumb", type=StrProperty)
library_elements: CollectionProperty(name="Library Elements", type=LibraryElement)
@@ -196,6 +187,11 @@ class BIMProjectProperties(PropertyGroup):
use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=False)
queried_obj: bpy.props.PointerProperty(type=bpy.types.Object)
clipping_planes: bpy.props.CollectionProperty(type=ObjProperty)
clipping_planes_active: bpy.props.IntProperty(min=0, default=0, max=5)
@property
def clipping_planes_objs(self):
return list({cp.obj for cp in self.clipping_planes if cp.obj})
def get_library_element_index(self, lib_element):
return next((i for i in range(len(self.library_elements)) if self.library_elements[i] == lib_element))
@@ -272,9 +272,6 @@ class BIM_PT_project_library(Panel):
row.label(text="No Library Loaded", icon="ASSET_MANAGER")
def draw_library_ul(self):
self.layout.use_property_split = False
self.layout.prop(self.props, "library_display_elements")
self.layout.use_property_split = True
if not self.props.library_elements:
row = self.layout.row()
row.label(text="No Assets Found", icon="ERROR")
@@ -321,11 +318,16 @@ class BIM_PT_links(Panel):
row = self.layout.row(align=True)
row.label(text="Object Culling Enabled", icon="MOD_TRIANGULATE")
if not LinksData.linked_data:
row = self.layout.row(align=True)
row.label(text="No Object Selected", icon="QUESTION")
if not LinksData.linked_data or not self.props.links:
if self.props.links:
row = self.layout.row(align=True)
row.label(text="No Object Selected", icon="QUESTION")
return
row = self.layout.row(align=True)
row.label(text="")
row.operator("bim.append_inspected_linked_element", icon="APPEND_BLEND", text="")
for name, value in LinksData.linked_data["attributes"].items():
row = self.layout.row(align=True)
row.label(text=name)
@@ -80,10 +80,10 @@ class ObjectPsetsData(Data):
def inherited_psets(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element.is_a("IfcTypeObject"):
return
return []
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
return cls.psetqtos(element_type)
return cls.psetqtos(element_type, psets_only=True)
@classmethod
def pset_name(cls):
@@ -125,9 +125,21 @@ class ObjectQtosData(Data):
@classmethod
def load(cls):
cls.data = {"qtos": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), qtos_only=True)}
cls.data = {
"qtos": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), qtos_only=True),
"inherited_qsets": cls.inherited_qsets(),
}
cls.is_loaded = True
@classmethod
def inherited_qsets(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element.is_a("IfcTypeObject"):
return []
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
return cls.psetqtos(element_type, qtos_only=True)
class MaterialPsetsData(Data):
data = {}
@@ -85,7 +85,6 @@ class EnablePsetEditing(bpy.types.Operator):
def execute(self, context):
self.props = get_pset_props(context, self.obj, self.obj_type)
self.props.properties.clear()
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
if self.pset_id:
pset = tool.Ifc.get().by_id(self.pset_id)
@@ -250,7 +249,7 @@ class DisablePsetEditing(bpy.types.Operator, Operator):
props = get_pset_props(context, self.obj, self.obj_type)
if props.active_pset_id:
pset = tool.Ifc.get().by_id(props.active_pset_id)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
if tool.Pset.is_pset_empty(pset):
ifcopenshell.api.run(
"pset.remove_pset", tool.Ifc.get(), product=tool.Ifc.get().by_id(ifc_definition_id), pset=pset
@@ -272,7 +271,7 @@ class EditPset(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
properties = {}
@@ -343,7 +342,7 @@ class RemovePset(bpy.types.Operator, Operator):
pset_name = tool.Ifc.get().by_id(self.pset_id).Name
for obj in objects:
props = get_pset_props(context, obj, self.obj_type)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
pset = ifcopenshell.util.element.get_psets(element, should_inherit=False).get(pset_name, None)
if pset:
@@ -374,7 +373,7 @@ class AddQto(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
bpy.ops.bim.enable_pset_editing(
pset_id=0, pset_name=props.qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type
@@ -25,8 +25,16 @@ from shapely.geometry import Polygon
from shapely.ops import unary_union
import blenderbim.tool as tool
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.element
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
import blenderbim.bim
from typing import Literal, Union, Optional
AxisType = Literal["x", "y", "z"]
VectorTuple = tuple[float, float, float]
QuanityTypes = Literal["Q_LENGTH", "Q_AREA", "Q_VOLUME"]
class QtoCalculator:
@@ -45,7 +53,7 @@ class QtoCalculator:
else:
self.mapping_dict[key][item] = None
def calculate_quantity(self, qto_name, quantity_name, obj):
def calculate_quantity(self, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
"""calculates the value of the quantity in the project units"""
string = "self.mapping_dict[qto_name][quantity_name](obj"
if isinstance(mapper[qto_name][quantity_name], dict):
@@ -54,11 +62,13 @@ class QtoCalculator:
args = ""
string += args
string += ")"
value = eval(string)
value: float = eval(string)
return tool.Qto.convert_to_project_units(value, qto_name, quantity_name) or value
def guess_quantity(self, prop_name, alternative_prop_names, obj):
def guess_quantity(
self, prop_name: str, alternative_prop_names: list[str], obj: bpy.types.Object
) -> Union[float, None]:
"""guess the value of the quantity by name, returns the value in the project units"""
prop_name = prop_name.lower()
alternative_prop_names = [p.lower() for p in alternative_prop_names]
@@ -89,7 +99,7 @@ class QtoCalculator:
if value is None:
return
unit_type_keywords = {
unit_type_keywords: dict[str, QuanityTypes] = {
"length": "Q_LENGTH",
"width": "Q_LENGTH",
"height": "Q_LENGTH",
@@ -102,10 +112,10 @@ class QtoCalculator:
unit_type = next(unit_type_keywords[k] for k in unit_type_keywords if k in prop_name)
return tool.Qto.convert_to_project_units(value, quantity_type=unit_type) or value
def get_units(self, o, vg_index):
def get_units(self, o: bpy.types.Object, vg_index: int) -> int:
return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]])
def get_linear_length(self, o):
def get_linear_length(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the length of the longest edge of the object bounding box
:param blender-object o: Blender Object
@@ -116,7 +126,7 @@ class QtoCalculator:
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return max(x, y, z)
def get_length(self, o, vg_index=None, main_axis: str = ""):
def get_length(self, o: bpy.types.Object, vg_index: Optional[int] = None, main_axis: str = "") -> float:
if vg_index is None:
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
@@ -141,23 +151,23 @@ class QtoCalculator:
length += self.get_edge_distance(o, e)
return length
def get_stair_length(self, obj):
def get_stair_length(self, obj: bpy.types.Object) -> float:
length = self.get_length(obj)
height = self.get_height(obj)
stair_length = math.sqrt(pow(length, 2) + pow(height, 2))
return stair_length
def get_net_stair_area(self, obj):
def get_net_stair_area(self, obj: bpy.types.Object) -> float:
OBB_obj = self.get_OBB_object(obj)
OBB_net_footprint_area = self.get_net_footprint_area(OBB_obj)
return OBB_net_footprint_area
def get_gross_stair_area(self, obj):
def get_gross_stair_area(self, obj: bpy.types.Object) -> float:
OBB_obj = self.get_OBB_object(obj)
OBB_gross_footprint_area = self.get_gross_footprint_area(OBB_obj)
return OBB_gross_footprint_area
def get_parametric_axis(self, obj):
def get_parametric_axis(self, obj: bpy.types.Object) -> Literal["AXIS2", "AXIS3", None]:
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(obj))
if relating_type:
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
@@ -172,7 +182,7 @@ class QtoCalculator:
return None
return None
def get_covering_gross_area(self, obj):
def get_covering_gross_area(self, obj: bpy.types.Object) -> float:
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_gross_footprint_area(obj)
@@ -181,7 +191,7 @@ class QtoCalculator:
elif get_parametric_axis == "AXIS3":
return self.get_gross_footprint_area(obj)
def get_covering_net_area(self, obj):
def get_covering_net_area(self, obj: bpy.types.Object) -> float:
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_net_footprint_area(obj)
@@ -190,7 +200,7 @@ class QtoCalculator:
elif get_parametric_axis == "AXIS3":
return self.get_net_footprint_area(obj)
def get_covering_width(self, obj):
def get_covering_width(self, obj: bpy.types.Object) -> float:
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_height(obj)
@@ -199,7 +209,7 @@ class QtoCalculator:
elif get_parametric_axis == "AXIS3":
return self.get_height(obj)
def get_width(self, o):
def get_width(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the width of the object bounding box
:param blender-object o: blender object
@@ -209,7 +219,7 @@ class QtoCalculator:
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
return min(x, y)
def get_height(self, o):
def get_height(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the height of the object bounding box
:param blender-object o: blender object
@@ -217,64 +227,77 @@ class QtoCalculator:
"""
return (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
def get_opening_height(self, obj):
def get_opening_height(self, obj: bpy.types.Object) -> float:
if self.is_opening_horizontal(obj):
return self.get_width(obj)
else:
return self.get_height(obj)
def get_opening_depth(self, obj):
def get_opening_depth(self, obj: bpy.types.Object) -> float:
if self.is_opening_horizontal(obj):
return self.get_height(obj)
else:
return self.get_width(obj)
def get_opening_mapping_area(self, obj):
def get_opening_mapping_area(self, obj: bpy.types.Object) -> float:
if self.is_opening_horizontal(obj):
return self.get_net_footprint_area(obj)
else:
return self.get_net_side_area(obj)
def get_finish_ceiling_height(self, obj):
space_height = self.get_height(obj)
def get_finish_ceiling_height(self, obj: bpy.types.Object) -> float:
floor_height = self.get_finish_floor_height(obj)
ceiling_height = self.get_ceiling_height(obj)
finish_ceiling_height = space_height - floor_height - ceiling_height
finish_ceiling_height = ceiling_height - floor_height
return finish_ceiling_height
def get_finish_floor_height(self, obj):
def get_max_global_z(self, obj: bpy.types.Object) -> float:
z_values = [(obj.matrix_world @ Vector(co))[2] for co in obj.bound_box]
return max(z_values)
def get_min_global_z(self, obj: bpy.types.Object) -> float:
z_values = [(obj.matrix_world @ Vector(co))[2] for co in obj.bound_box]
return min(z_values)
def get_finish_floor_height(self, obj: bpy.types.Object) -> float:
space_min_z_value = self.get_min_global_z(obj)
space_max_z_value = self.get_max_global_z(obj)
element = tool.Ifc.get_entity(obj)
decompositions = ifcopenshell.util.element.get_decomposition(element)
finish_floor_height = 0
flooring_max_z_value = space_min_z_value
for decomposition in decompositions:
if (
decomposition.get_info()["PredefinedType"] == "FLOORING"
and decomposition.get_info()["type"] == "IfcCovering"
decomposition.is_a() == "IfcCovering"
and ifcopenshell.util.element.get_predefined_type(decomposition) == "FLOORING"
):
floor_obj = tool.Ifc.get_object(decomposition)
new_finish_floor_height = self.get_height(floor_obj)
if new_finish_floor_height > finish_floor_height:
finish_floor_height = new_finish_floor_height
flooring_obj = tool.Ifc.get_object(decomposition)
flooring_z_value = self.get_max_global_z(flooring_obj)
if flooring_z_value > space_min_z_value:
flooring_max_z_value = flooring_z_value
return finish_floor_height
return flooring_max_z_value - space_min_z_value
def get_ceiling_height(self, obj: bpy.types.Object) -> float:
space_min_z_value = self.get_min_global_z(obj)
space_max_z_value = self.get_max_global_z(obj)
def get_ceiling_height(self, obj):
element = tool.Ifc.get_entity(obj)
decompositions = ifcopenshell.util.element.get_decomposition(element)
finish_ceiling_height = 0
ceiling_min_z_value = space_max_z_value
for decomposition in decompositions:
if (
decomposition.get_info()["PredefinedType"] == "CEILING"
and decomposition.get_info()["type"] == "IfcCovering"
decomposition.is_a() == "IfcCovering"
and ifcopenshell.util.element.get_predefined_type(decomposition) == "CEILING"
):
ceiling_obj = tool.Ifc.get_object(decomposition)
new_finish_ceiling_height = self.get_height(ceiling_obj)
if new_finish_ceiling_height > finish_ceiling_height:
finish_ceiling_height = new_finish_ceiling_height
ceiling_z_value = self.get_min_global_z(ceiling_obj)
if ceiling_z_value < space_max_z_value:
ceiling_min_z_value = ceiling_z_value
return finish_ceiling_height
return ceiling_min_z_value - space_min_z_value
def get_net_perimeter(self, o):
def get_net_perimeter(self, o: bpy.types.Object) -> float:
parsed_edges = []
shared_edges = []
perimeter = 0
@@ -289,7 +312,7 @@ class QtoCalculator:
perimeter -= self.get_edge_key_distance(o, edge_key)
return perimeter
def get_gross_perimeter(self, o):
def get_gross_perimeter(self, o: bpy.types.Object) -> float:
element = tool.Ifc.get_entity(o)
mesh = self.get_gross_element_mesh(element)
gross_obj = bpy.data.objects.new("GrossObj", mesh)
@@ -297,15 +320,15 @@ class QtoCalculator:
self.delete_obj(gross_obj)
return gross_perimeter
def get_space_net_perimeter(self, obj):
def get_space_net_perimeter(self, obj: bpy.types.Object) -> float:
pass
def get_rectangular_perimeter(self, obj):
def get_rectangular_perimeter(self, obj: bpy.types.Object) -> float:
length = self.get_length(obj, main_axis="x")
height = self.get_height(obj)
return (length + height) * 2
def get_lowest_polygons(self, o):
def get_lowest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
lowest_polygons = []
lowest_z = None
for polygon in o.data.polygons:
@@ -321,7 +344,7 @@ class QtoCalculator:
lowest_z = z
return lowest_polygons
def get_highest_polygons(self, o):
def get_highest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
highest_polygons = []
highest_z = None
for polygon in o.data.polygons:
@@ -337,13 +360,13 @@ class QtoCalculator:
highest_z = z
return highest_polygons
def get_edge_key_distance(self, obj, edge_key):
def get_edge_key_distance(self, obj: bpy.types.Object, edge_key: tuple[int, int]) -> float:
return (obj.data.vertices[edge_key[1]].co - obj.data.vertices[edge_key[0]].co).length
def get_edge_distance(self, obj, edge):
def get_edge_distance(self, obj: bpy.types.Object, edge: bpy.types.MeshEdge) -> float:
return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length
def get_net_floor_area(self, obj):
def get_net_floor_area(self, obj: bpy.types.Object) -> float:
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_gross_footprint_area(obj)
@@ -359,7 +382,7 @@ class QtoCalculator:
return total_net_floor_area
def get_gross_ceiling_area(self, obj):
def get_gross_ceiling_area(self, obj: bpy.types.Object) -> float:
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_gross_top_area(obj)
@@ -367,15 +390,15 @@ class QtoCalculator:
total_gross_ceiling_area = 0
for decomposition in decompositions:
decomposition_type = decomposition.get_info()["type"]
decomposition_predefined_type = decomposition.get_info()["PredefinedType"]
if decomposition_type == "IfcCovering" and decomposition_predefined_type == "CEILING":
decomposition_class = decomposition.is_a()
decomposition_predefined_type = ifcopenshell.util.element.get_predefined_type(decomposition)
if decomposition_class == "IfcCovering" and decomposition_predefined_type == "CEILING":
decomposition_obj = tool.Ifc.get_object(decomposition)
total_gross_ceiling_area += self.get_gross_footprint_area(decomposition_obj)
return total_gross_ceiling_area
def get_net_ceiling_area(self, obj):
def get_net_ceiling_area(self, obj: bpy.types.Object) -> float:
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_net_top_area(obj)
@@ -383,19 +406,19 @@ class QtoCalculator:
total_net_ceiling_area = 0
for decomposition in decompositions:
decomposition_type = decomposition.get_info()["type"]
decomposition_predefined_type = decomposition.get_info()["PredefinedType"]
if decomposition_type == "IfcCovering" and decomposition_predefined_type == "CEILING":
decomposition_class = decomposition.is_a()
decomposition_predefined_type = ifcopenshell.util.element.get_predefined_type(decomposition)
if decomposition_class == "IfcCovering" and decomposition_predefined_type == "CEILING":
decomposition_obj = tool.Ifc.get_object(decomposition)
total_net_ceiling_area += self.get_net_footprint_area(decomposition_obj)
if decomposition_type == "IfcWall" or decomposition_type == "IfcColumn":
if decomposition_class == "IfcWall" or decomposition_class == "IfcColumn":
decomposition_obj = tool.Ifc.get_object(decomposition)
total_net_ceiling_area -= self.get_net_roofprint_area(decomposition_obj)
return total_net_ceiling_area
def get_space_net_volume(self, obj):
def get_space_net_volume(self, obj: bpy.types.Object) -> float:
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_gross_volume(obj)
@@ -410,7 +433,7 @@ class QtoCalculator:
return total_space_net_volume
def get_net_footprint_area(self, o):
def get_net_footprint_area(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the area of the footprint of the object, excluding any holes
:param blender-object o: blender object
@@ -421,7 +444,7 @@ class QtoCalculator:
area += polygon.area
return area
def get_gross_footprint_area(self, o):
def get_gross_footprint_area(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the area of the footprint of the object, without related opening and excluding any holes
:param blender-object o: blender object
@@ -437,7 +460,7 @@ class QtoCalculator:
self.delete_mesh(mesh)
return gross_footprint_area
def get_net_roofprint_area(self, o):
def get_net_roofprint_area(self, o: bpy.types.Object) -> float:
# Is roofprint the right word? Couldn't think of anything better - vulevukusej
"""_summary_: Returns the area of the net roofprint of the object, excluding any holes
@@ -449,7 +472,7 @@ class QtoCalculator:
area += polygon.area
return area
def get_side_area(self, o):
def get_side_area(self, o: bpy.types.Object) -> float:
# There are a few dumb options for this, but this seems the dumbest
# until I get more practical experience on what works best.
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
@@ -457,8 +480,7 @@ class QtoCalculator:
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return max(x * z, y * z)
def get_cross_section_area(self, obj):
element = tool.Ifc.get_entity(obj)
def get_cross_section_area(self, obj: bpy.types.Object) -> float:
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
item = representation.Items[0]
while True:
@@ -474,7 +496,7 @@ class QtoCalculator:
return area
# TODO handle other types of sections, and then fall back to mesh parsing
def get_gross_surface_area(self, o, vg_index=None):
def get_gross_surface_area(self, o: bpy.types.Object, vg_index: Optional[int] = None) -> float:
if vg_index is None:
if not self.has_openings(o):
return self.get_net_surface_area(o)
@@ -492,29 +514,29 @@ class QtoCalculator:
area += polygon.area
return area
def get_net_surface_area(self, obj):
def get_net_surface_area(self, obj: bpy.types.Object) -> float:
return self.get_mesh_area(obj.data)
def get_mesh_area(self, mesh):
def get_mesh_area(self, mesh: bpy.types.Mesh) -> float:
area = 0
for polygon in mesh.polygons:
area += polygon.area
return area
def is_polygon_in_vg(self, polygon, vertices_in_vg):
def is_polygon_in_vg(self, polygon: bpy.types.MeshPolygon, vertices_in_vg: list[bpy.types.MeshVertex]) -> bool:
for v in polygon.vertices:
if v not in vertices_in_vg:
return False
return True
def get_net_volume(self, o):
def get_net_volume(self, o: bpy.types.Object) -> float:
o_mesh = bmesh.new()
o_mesh.from_mesh(o.data)
volume = o_mesh.calc_volume()
o_mesh.free()
return volume
def get_gross_volume(self, o):
def get_gross_volume(self, o: bpy.types.Object) -> float:
if not self.has_openings(o):
return self.get_net_volume(o)
@@ -529,16 +551,18 @@ class QtoCalculator:
return gross_volume
def has_openings(self, obj):
def has_openings(
self, obj: bpy.types.Object
) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
element = tool.Ifc.get_entity(obj)
return element and getattr(element, "HasOpenings", [])
def get_obj_decompositions(self, obj):
def get_obj_decompositions(self, obj: bpy.types.Object) -> list[ifcopenshell.entity_instance]:
element = tool.Ifc.get_entity(obj)
decompositions = ifcopenshell.util.element.get_decomposition(element)
return decompositions
def get_gross_weight(self, obj):
def get_gross_weight(self, obj: bpy.types.Object) -> Union[float, None]:
obj_mass_density = self.get_obj_mass_density(obj)
if not obj_mass_density:
return
@@ -546,7 +570,7 @@ class QtoCalculator:
gross_weight = obj_mass_density * gross_volume
return gross_weight
def get_net_weight(self, obj):
def get_net_weight(self, obj: bpy.types.Object) -> Union[float, None]:
obj_mass_density = self.get_obj_mass_density(obj)
if not obj_mass_density:
return
@@ -554,7 +578,7 @@ class QtoCalculator:
net_weight = obj_mass_density * net_volume
return net_weight
def get_obj_mass_density(self, obj):
def get_obj_mass_density(self, obj: bpy.types.Object) -> Union[float, None]:
entity = tool.Ifc.get_entity(obj)
material = ifcopenshell.util.element.get_material(entity)
if material is None:
@@ -629,7 +653,7 @@ class QtoCalculator:
# volume += v1.dot(v2.cross(v3)) / 6.0
# return volume
def get_opening_type(self, opening, obj):
def get_opening_type(self, opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]:
"""_summary_: Returns the opening type - OPENING / RECESS
:param blender-object opening: blender opening object
@@ -649,14 +673,22 @@ class QtoCalculator:
return "OPENING" if ray_intersections % 2 == 0 else "RECESS"
def get_opening_area(
self, obj, angle_z1: int = 45, angle_z2: int = 135, min_area: int = 0, ignore_recesses: bool = False
):
self,
obj: bpy.types.Object,
angle_z1: int = 45,
angle_z2: int = 135,
min_area: int = 0,
ignore_recesses: bool = False,
) -> float:
"""_summary_: Returns the lateral area of the openings in the object.
:param obj: blender object
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector lower than this value will be ignored, defaults to 45
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector greater than this value will be ignored,defaults to 135
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored, defaults to 0
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area.
Openings with a normal_vector lower than this value will be ignored, defaults to 45
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area.
Openings with a normal_vector greater than this value will be ignored,defaults to 135
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored,
defaults to 0
:param bool ignore_recesses: Toggle whether recess areas should be considered, defaults to False
:return float: Opening Area
"""
@@ -702,14 +734,14 @@ class QtoCalculator:
def get_lateral_area(
self,
obj,
obj: bpy.types.Object,
subtract_openings: bool = True,
exclude_end_areas: bool = False,
exclude_side_areas: bool = False,
angle_z1: int = 45,
angle_z2: int = 135,
main_axis: str = "",
):
) -> float:
"""_summary_
:param blender-object obj: blender object, bpy.types.Object
@@ -760,7 +792,7 @@ class QtoCalculator:
area += polygon.area
return area + total_opening_area
def get_gross_side_area(self, obj):
def get_gross_side_area(self, obj: bpy.types.Object) -> float:
if not self.has_openings(obj):
return self.get_net_side_area(obj)
@@ -768,15 +800,15 @@ class QtoCalculator:
return gross_side_area
def get_net_side_area(self, obj):
def get_net_side_area(self, obj: bpy.types.Object) -> float:
net_side_area = self.get_lateral_area(obj, exclude_end_areas=True, main_axis="x") / 2
return net_side_area
def get_outer_surface_area(self, obj):
def get_outer_surface_area(self, obj: bpy.types.Object) -> float:
outer_surface_area = self.get_lateral_area(obj, exclude_end_areas=True, angle_z1=0, angle_z2=360)
return outer_surface_area
def get_end_area(self, obj):
def get_end_area(self, obj: bpy.types.Object) -> float:
element = tool.Ifc.get_entity(obj)
gross_mesh = self.get_gross_element_mesh(element)
gross_obj = bpy.data.objects.new("MyObject", gross_mesh)
@@ -790,7 +822,7 @@ class QtoCalculator:
return end_area
def get_gross_top_area(self, obj, angle: int = 45):
def get_gross_top_area(self, obj: bpy.types.Object, angle: int = 45) -> float:
"""_summary_: Returns the gross top area of the object.
:param blender-object obj: blender object
@@ -827,12 +859,14 @@ class QtoCalculator:
return area + opening_area
# curently net top area is larger then projected area, because its taking into account internal polygons, or window sills
def get_net_top_area(self, obj, angle: int = 45, ignore_internal: bool = True):
def get_net_top_area(self, obj: bpy.types.Object, angle: int = 45, ignore_internal: bool = True) -> float:
"""_summary_: Returns the net top area of the object.
:param blender-object obj: blender object
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area. Values lower than this will be ignored, defaults to 45
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills), defaults to True
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area.
Values lower than this will be ignored, defaults to 45
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills),
defaults to True
:return float: Net Top Area
"""
z_axis = (0, 0, 1)
@@ -852,11 +886,11 @@ class QtoCalculator:
return area
def get_projected_area(self, obj, projection_axis: str = "z", is_gross: bool = True):
def get_projected_area(self, obj, projection_axis: AxisType = "z", is_gross: bool = True) -> float:
"""_summary_: Returns the projected area of the object.
:param blender-object obj: blender object
:param str projection_axis: Axis to project the area onto. Can be "X", "Y" or "Z"
:param str projection_axis: Axis to project the area onto. Can be "x", "y" or "z"
:param bool is_gross: if True, the projected area will include openings, if False, the projected area will exclude openings
:return float: Projected Area
"""
@@ -891,7 +925,7 @@ class QtoCalculator:
return projected_polygon.area + void_area
return projected_polygon.area
def get_OBB_object(self, obj):
def get_OBB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
"""_summary_: Returns the Oriented-Bounding-Box (OBB) of the object.
:param blender-object obj: Blender Object
@@ -932,7 +966,7 @@ class QtoCalculator:
return new_OBB_object
def get_AABB_object(self, obj):
def get_AABB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
"""_summary_: Returns the Axis-Aligned-Bounding-Box (AABB) of the object.
:param blender-object obj: Blender Object
@@ -988,12 +1022,12 @@ class QtoCalculator:
def get_bisected_obj(
self,
obj,
plane_co_pos,
plane_no_pos,
plane_co_neg,
plane_no_neg,
):
obj: bpy.types.Object,
plane_co_pos: VectorTuple,
plane_no_pos: VectorTuple,
plane_co_neg: VectorTuple,
plane_no_neg: VectorTuple,
) -> bpy.types.Object:
"""_summary_: Returns the object bisected by two planes.
:param blender-object obj: Blender Object
@@ -1031,11 +1065,12 @@ class QtoCalculator:
return bis_obj
def get_total_contact_area(self, obj, class_filter: str = ["IfcElement"]):
def get_total_contact_area(self, obj: bpy.types.Object, class_filter: list[str] = ["IfcElement"]) -> float:
"""_summary_: Returns the total contact area of the object with other objects.
:param blender-object obj: Blender Object
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:param list [] class_filter: A list of classes used to filter the objects
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:return float: Total contact area of the object with other objects.
"""
total_contact_area = 0
@@ -1046,11 +1081,12 @@ class QtoCalculator:
return total_contact_area
def get_touching_objects(self, obj, class_filter):
def get_touching_objects(self, obj: bpy.types.Object, class_filter: list[str]) -> list[bpy.types.Object]:
"""_summary_: Returns a list of objects that are touching the object.
:param blender-object obj: Blender Object
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:param list [] class_filter: A list of classes used to filter the objects
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:return list: List of touching objects
"""
# rotate the object ever so slightly, otherwise bvhtree.overlap won't work properly. https://blender.stackexchange.com/a/275244/130742
@@ -1094,7 +1130,7 @@ class QtoCalculator:
return touching_objects
def get_contact_area(self, object1, object2):
def get_contact_area(self, object1: bpy.types.Object, object2: bpy.types.Object) -> float:
"""_summary_: Returns the contact area between two objects.
:param blender-object obj: Blender Object
@@ -1109,7 +1145,13 @@ class QtoCalculator:
total_area += self.get_intersection_between_polygons(object1, poly1, object2, poly2)
return total_area
def get_intersection_between_polygons(self, object1, poly1, object2, poly2):
def get_intersection_between_polygons(
self,
object1: bpy.types.Object,
poly1: bpy.types.MeshPolygon,
object2: bpy.types.Object,
poly2: bpy.types.MeshPolygon,
) -> float:
"""_summary_: Returns the intersection between two polygons.
:param blender-object object1: Blender Object
@@ -1152,7 +1194,9 @@ class QtoCalculator:
# TopologicalError - Generated Geometry might be invalid
return 0
def create_shapely_polygon(self, obj, polygon, trans_matrix):
def create_shapely_polygon(
self, obj: bpy.types.Object, polygon: bpy.types.MeshPolygon, trans_matrix: Matrix
) -> Polygon:
"""_summary_: Create a shapely polygon
:param blender-object obj: Blender Object
@@ -1171,12 +1215,14 @@ class QtoCalculator:
polygon_tuples.append((x, y))
return Polygon(polygon_tuples)
def get_gross_element_mesh(self, element):
def get_gross_element_mesh(self, element: ifcopenshell.entity_instance) -> bpy.types.Mesh:
settings = ifcopenshell.geom.settings()
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
return self.create_mesh_from_shape(element, settings)
def create_mesh_from_shape(self, element, settings=None):
def create_mesh_from_shape(
self, element: ifcopenshell.entity_instance, settings: Optional[ifcopenshell.geom.settings] = None
) -> bpy.types.Mesh:
if settings is None:
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, element)
@@ -1203,12 +1249,12 @@ class QtoCalculator:
mesh.update()
return mesh
def get_bmesh_from_mesh(self, mesh):
def get_bmesh_from_mesh(self, mesh: bpy.types.Mesh) -> bmesh.types.BMesh:
bm = bmesh.new()
bm.from_mesh(mesh)
return bm
def get_object_main_axis(self, o):
def get_object_main_axis(self, o: bpy.types.Object) -> AxisType:
"""_summary_: Returns the main object axis. Useful for profile-defined objects.
:param blender-object o: Blender Object
@@ -1227,18 +1273,18 @@ class QtoCalculator:
else:
return "x"
def is_opening_horizontal(self, o):
def is_opening_horizontal(self, o: bpy.types.Object) -> bool:
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return z < x and z < y
def delete_mesh(self, mesh):
def delete_mesh(self, mesh: bpy.types.Mesh) -> None:
mesh.user_clear()
bpy.data.meshes.remove(mesh)
def delete_obj(self, obj):
def delete_obj(self, obj: bpy.types.Object) -> None:
bpy.data.objects.remove(obj, do_unlink=True)
+24 -14
View File
@@ -33,7 +33,6 @@ from blenderbim.bim.module.pset.data import (
GroupPsetData,
ProfilePsetsData,
WorkSchedulePsetsData,
)
@@ -91,7 +90,7 @@ def get_active_pset_obj_name(context, obj_type):
return ""
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_removing=True):
box = layout.box()
row = box.row(align=True)
if "is_expanded" not in pset:
@@ -115,16 +114,20 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
op.pset_id = pset_id
op.obj = obj_name
op.obj_type = obj_type
op = row.operator("bim.remove_pset", icon="X", text="")
remove_pset_row = row.row(align=True)
op = remove_pset_row.operator("bim.remove_pset", icon="X", text="")
op.pset_id = pset_id
op.obj = obj_name
op.obj_type = obj_type
remove_pset_row.enabled = allow_removing
elif props.active_pset_id != pset_id:
row.label(text=pset["Name"], icon="COPY_ID")
op = row.operator("bim.remove_pset", icon="X", text="")
remove_pset_row = row.row(align=True)
op = remove_pset_row.operator("bim.remove_pset", icon="X", text="")
op.pset_id = pset_id
op.obj = obj_name
op.obj_type = obj_type
remove_pset_row.enabled = allow_removing
if pset["is_expanded"]:
if props.active_pset_id == pset_id:
for prop in props.properties:
@@ -172,7 +175,7 @@ def draw_psetqto_editable_ui(box, props, prop):
class BIM_PT_object_psets(Panel):
bl_label = "Object Property Sets"
bl_label = "Property Sets"
bl_idname = "BIM_PT_object_psets"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
@@ -204,17 +207,19 @@ class BIM_PT_object_psets(Panel):
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET":
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
for pset in ObjectPsetsData.data["psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
if ObjectPsetsData.data["psets"]:
self.layout.label(text="Instance:")
for pset in ObjectPsetsData.data["psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
if ObjectPsetsData.data["inherited_psets"]:
self.layout.label(text="Inherited Psets:", icon="FILE_PARENT")
self.layout.label(text="Type:")
for pset in ObjectPsetsData.data["inherited_psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object", allow_removing=False)
class BIM_PT_object_qtos(Panel):
bl_label = "Object Quantity Sets"
bl_label = "Quantity Sets"
bl_idname = "BIM_PT_object_qtos"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
@@ -246,8 +251,15 @@ class BIM_PT_object_qtos(Panel):
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "QTO":
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
for qto in ObjectQtosData.data["qtos"]:
draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object")
if ObjectQtosData.data["qtos"]:
self.layout.label(text="Instance:")
for qto in ObjectQtosData.data["qtos"]:
draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object")
if ObjectQtosData.data["inherited_qsets"]:
self.layout.label(text="Type:")
for qset in ObjectQtosData.data["inherited_qsets"]:
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object", allow_removing=False)
class BIM_PT_material_psets(Panel):
@@ -541,8 +553,6 @@ class BIM_PT_group_psets(Panel):
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Group")
class BIM_PT_profile_psets(Panel):
bl_label = "Profile Property Sets"
bl_idname = "BIM_PT_profile_psets"
@@ -34,6 +34,7 @@ classes = (
operator.RemovePropTemplate,
operator.RemovePsetTemplate,
operator.RemovePsetTemplateFile,
operator.SavePsetTemplateFile,
prop.PsetTemplate,
prop.EnumerationValues,
prop.PropTemplate,
@@ -215,7 +215,7 @@ class UnlinkObject(bpy.types.Operator):
bl_idname = "bim.unlink_object"
bl_label = "Unlink Object"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj: bpy.props.StringProperty(name="Object Name")
should_delete: bpy.props.BoolProperty(name="Delete IFC Element", default=True)
def execute(self, context):
@@ -227,6 +227,7 @@ class UnlinkObject(bpy.types.Operator):
else:
objects = context.selected_objects
objects: list[bpy.types.Object]
for obj in objects:
was_active_object = obj == context.active_object
@@ -242,6 +243,27 @@ class UnlinkObject(bpy.types.Operator):
if obj.data:
obj_copy.data = obj.data.copy()
# prevent unlinking materials that might be used elsewhere
replacements: dict[bpy.types.Material, bpy.types.Material] = dict()
for material_slot in obj_copy.material_slots:
material = material_slot.material
if material is None:
continue
if material in replacements:
material_replacement = replacements[material]
# no need to copy non-ifc materials as unlinking won't do anything to them
elif tool.Ifc.get_entity(material) is None and tool.Style.get_style(material) is None:
replacements[material] = material
continue
else:
material_replacement = material.copy()
replacements[material] = material_replacement
material_slot.material = material_replacement
tool.Geometry.delete_ifc_object(obj)
obj = obj_copy
@@ -24,21 +24,15 @@ classes = (
operator.ActivateIfcClassFilter,
operator.AddFilter,
operator.AddFilterGroup,
operator.AddToIfcGroup,
operator.ColourByProperty,
operator.EditFilterQuery,
operator.FilterModelElements,
operator.IfcSelector,
operator.LoadColourscheme,
operator.LoadQuery,
operator.LoadSearch,
operator.OpenQueryLibrary,
operator.RemoveFilter,
operator.RemoveFilterGroup,
operator.ResetObjectColours,
operator.SaveColourscheme,
operator.SaveSearch,
operator.SaveSelectorQuery,
operator.Search,
operator.SelectByProperty,
operator.SelectFilterElements,
@@ -51,11 +45,6 @@ classes = (
prop.BIMFilterClasses,
prop.BIMFilterBuildingStoreys,
prop.BIMSearchProperties,
prop.SearchCollection,
prop.SearchQueryFilter,
prop.SearchQuery,
prop.SearchQueryGroup,
prop.IfcSelectorProperties,
ui.BIM_PT_search,
ui.BIM_PT_filter,
ui.BIM_PT_colour_by_property,
@@ -63,15 +52,12 @@ classes = (
ui.BIM_UL_colourscheme,
ui.BIM_UL_ifc_class_filter,
ui.BIM_UL_ifc_building_storey_filter,
ui.BIM_PT_IFCSelector,
)
def register():
bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties)
bpy.types.Scene.IfcSelectorProperties = bpy.props.PointerProperty(type=prop.IfcSelectorProperties)
def unregister():
del bpy.types.Scene.BIMSearchProperties
del bpy.types.Scene.IfcSelectorProperties
@@ -131,6 +131,7 @@ class SelectFilterElements(bpy.types.Operator):
text_data = bpy.data.texts.new(name)
text_data.from_string(",".join(global_ids))
filter_groups[self.group_index].filters[self.index].value = f"bpy.data.texts['{name}']"
self.report({"INFO"}, f'List of Global Ids was saved to the text file "{name}" in the current .blend file')
else:
filter_groups[self.group_index].filters[self.index].value = ",".join(global_ids)
return {"FINISHED"}
@@ -270,25 +271,29 @@ class ColourByProperty(Operator):
colourscheme = {}
if len(props.colourscheme):
colourscheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme}
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": 0} for cs in props.colourscheme}
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
value = str(ifcopenshell.util.selector.get_element_value(element, query))
if value not in colourscheme:
colourscheme[value] = next(colours)[0:3]
obj.color = (*colourscheme[value], 1)
if value in colourscheme:
colourscheme[value]["total"] += 1
else:
colourscheme[value] = {"colour": next(colours)[0:3], "total": 1}
obj.color = (*colourscheme[value]["colour"], 1)
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
if areas:
areas[0].spaces[0].shading.color_type = "OBJECT"
props.colourscheme.clear()
for value, colour in colourscheme.items():
for value in sorted(colourscheme.keys()):
data = colourscheme[value]
new = props.colourscheme.add()
new.name = str(value)
new.colour = colour[0:3]
new.total = data["total"]
new.colour = data["colour"][0:3]
return {"FINISHED"}
def store_state(self, context):
@@ -350,16 +355,16 @@ class SaveColourscheme(Operator, tool.Ifc.Operator):
query = props.colourscheme_query
group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name]
coulour_scheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme}
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": cs.total} for cs in props.colourscheme}
if group:
group = group[0]
description = json.loads(group.Description)
description["colourscheme"] = coulour_scheme
description["colourscheme"] = colourscheme
description["colourscheme_query"] = query
group.Description = json.dumps(description)
else:
description = json.dumps(
{"type": "BBIM_Search", "colourscheme": coulour_scheme, "colourscheme_query": query}
{"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query}
)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
@@ -379,10 +384,11 @@ class LoadColourscheme(Operator, tool.Ifc.Operator):
description = json.loads(group.Description)
props.colourscheme_query = description.get("colourscheme_query")
props.colourscheme.clear()
for name, colour in description.get("colourscheme", {}).items():
for name, data in description.get("colourscheme", {}).items():
new = props.colourscheme.add()
new.name = name
new.colour = colour
new.total = data["total"]
new.colour = data["colour"]
def draw(self, context):
props = context.scene.BIMSearchProperties
@@ -538,9 +544,11 @@ class ActivateIfcClassFilter(Operator):
"filter_classes",
context.scene.BIMSearchProperties,
"filter_classes_index",
rows=20
if len(bpy.context.scene.BIMSearchProperties.filter_classes) > 20
else len(bpy.context.scene.BIMSearchProperties.filter_classes),
rows=(
20
if len(bpy.context.scene.BIMSearchProperties.filter_classes) > 20
else len(bpy.context.scene.BIMSearchProperties.filter_classes)
),
)
row = self.layout.row(align=True)
row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT"
@@ -595,9 +603,11 @@ class ActivateContainerFilter(Operator):
"filter_container",
context.scene.BIMSearchProperties,
"filter_container_index",
rows=20
if len(bpy.context.scene.BIMSearchProperties.filter_container) > 20
else len(bpy.context.scene.BIMSearchProperties.filter_container),
rows=(
20
if len(bpy.context.scene.BIMSearchProperties.filter_container) > 20
else len(bpy.context.scene.BIMSearchProperties.filter_container)
),
)
row = self.layout.row(align=True)
row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT"
@@ -618,223 +628,6 @@ class ShowAllElements(Operator):
return {"FINISHED"}
class FilterModelElements(Operator):
"""Filter model elements based on selection"""
bl_idname = "bim.filter_model_elements"
bl_label = "Filter Model Elements"
option: StringProperty("select|isolate|hide")
def execute(self, context):
selection_props = context.scene.IfcSelectorProperties
selection = (
selection_props.selector_query_syntax
if selection_props.manual_override
else self.add_groups(selection_props)
)
selection_props.selector_query_syntax = selection
r = core.search(tool.Search, tool.Spatial, query=selection, action=self.option)
if isinstance(r, str):
self.report({"WARNING"}, r)
return {"FINISHED"}
def add_groups(self, selector: str) -> str:
selection = ""
for group_index, group in enumerate(selector.groups):
if group_index != 0:
selection += " | "
selection += "(" if len(selector.groups) > 1 else ""
selection = self.add_queries(selection, group)
selection += ")" if len(selector.groups) > 1 else ""
return selection
def add_queries(self, selection: str, group: str) -> str:
for query_index, query in enumerate(group.queries):
if query_index != 0:
selection += " & " if query.and_or == "and" else " | "
if query.selector == "IFC Class":
active_option = query.active_option.split(": ")[1]
selection += f".{active_option}"
selection = self.add_filters(selection, query)
elif query.selector == "GlobalId":
selection += f"#{query.value}"
elif query.selector == "IfcElementType":
index = int(query.active_sub_option.split(":")[0])
selection += f"* #{query.sub_options[index].global_id}"
elif query.selector == "IfcSpatialElement":
index = int(query.active_sub_option.split(":")[0])
selection += f"@ #{query.sub_options[index].global_id}"
return selection
def add_filters(self, selection: str, query: str) -> str:
for f_index, f in enumerate(query.filters):
if f_index != 0:
selection += " & " if f.and_or == "and" else " | "
selection += f".{query.active_option}"
if f.value in ("True", "False"):
value = f.value
elif f.value.isnumeric() and str(float(f.value))[0] == f.value[0]:
value = f.value
else:
value = f'"{f.value}"'
selection += "["
if f.selector == "IfcPropertySet":
selection += f'{f.active_option.split(": ")[1]}.{f.active_sub_option.split(": ")[1]} {"!" if f.negation else ""}{f.comparison} {value}'
elif f.selector == "Attribute":
# we're using the prop_search functionality in blender which returns the index of the option. Sometimes the user can override this and enter a value that doesn't exist in the list. In this case there is no index and we need to handle it. @vulevukusej
pattern = re.compile(r"^[0-9]+:")
match = pattern.search(f.active_option)
selection += f'{f.active_option.split(": ")[1] if match else f.active_option} {"!" if f.negation else ""}{f.comparison} {value}'
selection += "]"
return selection
# This needs to be moved into ui code, I know ;) - vulevukusej
class IfcSelector(Operator):
"""Select elements in model with IFC Selector"""
bl_idname = "bim.ifc_selector"
bl_label = "Select elements with IFC Selector"
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=800)
@classmethod
def poll(cls, context):
return IfcStore.get_file()
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
from . import ui
ui.IfcSelectorUI.draw(context, self.layout)
class SaveSelectorQuery(Operator):
bl_idname = "bim.save_selector_query"
bl_label = "Save Selector Query"
save_name: StringProperty()
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=400)
def draw(self, context):
layout = self.layout
layout.prop(self, "save_name")
def execute(self, context):
ifc_selector = context.scene.IfcSelectorProperties
new = ifc_selector.query_library.add()
new.name = self.save_name
new.query = ifc_selector.selector_query_syntax
return {"FINISHED"}
class OpenQueryLibrary(Operator):
"""Open Query Library"""
bl_idname = "bim.open_query_library"
bl_label = "Open Query Library"
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=400)
def draw(self, context):
layout = self.layout
ifc_selector = context.scene.IfcSelectorProperties
for index, query in enumerate(ifc_selector.query_library):
row = layout.row(align=True)
row.prop(query, "query", text=query.name)
op = row.operator("bim.load_query", icon="SORT_ASC", text="")
op.index = index
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
op.option = "remove"
op.collection = "query_library"
op.index = index
def execute(self, context):
return {"FINISHED"}
class LoadQuery(Operator):
bl_idname = "bim.load_query"
bl_label = "Load Query"
index: IntProperty()
def invoke(self, context, event):
close_operator_panel(event)
return self.execute(context)
def execute(self, context):
ifc_selector = context.scene.IfcSelectorProperties
ifc_selector.selector_query_syntax = ifc_selector.query_library[self.index].query
return {"FINISHED"}
class AddToIfcGroup(Operator):
bl_idname = "bim.add_to_ifc_group"
bl_label = "Add to IFC Group"
group_name: StringProperty(name="Group Name")
def invoke(self, context, event):
bpy.ops.bim.load_groups()
return context.window_manager.invoke_props_dialog(self, width=400)
def draw(self, context):
self.props = context.scene.BIMGroupProperties
row = self.layout.row()
row.operator("bim.add_group")
self.layout.template_list(
"BIM_UL_groups",
"",
self.props,
"groups",
self.props,
"active_group_index",
)
if self.props.active_group_id:
for attribute in self.props.group_attributes:
if attribute.name in ["Name", "Description"]:
row = self.layout.row(align=True)
row.prop(attribute, "string_value", text=attribute.name)
def execute(self, context):
active_group_index = self.props.active_group_index
ifc_definition_id = self.props.groups[active_group_index].ifc_definition_id
bpy.ops.bim.enable_editing_group(group=ifc_definition_id)
selector_query_syntax = context.scene.IfcSelectorProperties.selector_query_syntax
for attribute in self.props.group_attributes:
if attribute.name == "Description":
if "*selector*" not in attribute.string_value:
attribute.string_value += f" *selector*{selector_query_syntax}*selector*"
else:
new_description = attribute.string_value.split("*selector*")
new_description[1] = selector_query_syntax
attribute.string_value = "*selector*".join(new_description)
bpy.ops.bim.edit_group()
bpy.ops.bim.disable_group_editing_ui()
return {"FINISHED"}
class SelectSimilar(Operator, tool.Ifc.Operator):
bl_idname = "bim.select_similar"
bl_label = "Select Similar"
@@ -78,7 +78,7 @@ def update_is_container_selected(self, context):
else:
for obj in context.selected_objects:
container = tool.Spatial.get_container(tool.Ifc.get_entity(obj))
if (container and container.Name == self.name) or (not container and self.name== "None"):
if (container and container.Name == self.name) or (not container and self.name == "None"):
obj.select_set(False)
new = self.unselected_objects.add()
new.obj = obj
@@ -100,6 +100,7 @@ class BIMFilterBuildingStoreys(PropertyGroup):
class BIMColour(PropertyGroup):
name: StringProperty(name="Name")
total: IntProperty(name="Total")
colour: FloatVectorProperty(name="Colour", subtype="COLOR", default=(1, 0, 0), min=0.0, max=1.0)
@@ -138,139 +139,3 @@ def get_classes(self, ifc_product):
declarations = util.schema.get_subtypes(declaration)
names = [d.name() for d in declarations]
return [(c, c, "") for c in sorted(names)]
def load_sub_options(self, context):
if self.selector not in ["IfcClass"]:
self.load_option = "sub_options"
load_selection_options(self, context)
def load_selection_options(self, context):
ifc = IfcStore.file
load_option = self.load_option
op = getattr(self, load_option)
op.clear()
options = []
if load_option == "options":
self.sub_options.clear()
if self.selector == "IFC Class":
options = get_classes(self, "IfcElement")
options.append(("IfcSpace", "IfcSpace", ""))
elif self.selector == "IfcSpatialElement":
options = get_classes(self, "IfcSpatialElement")
elif self.selector == "IfcElementType":
options = get_classes(self, "IfcElementType")
elif self.selector == "GlobalId":
return
elif self.selector == "IfcPropertySet":
psets = Selector.parse(ifc, ".IfcPropertySet")
options = set([o.Name for o in psets])
elif self.selector == "Attribute":
attributes = ["GlobalId", "Name", "Description", "ObjectType", "Tag", "PredefinedType"]
for a in attributes:
options.append((a, a, ""))
elif load_option == "sub_options":
if self.selector in ["IfcSpatialElement", "IfcElementType"]:
active_option = self.active_option.split(": ")[1]
options = Selector.parse(ifc, f".{active_option}")
elif self.selector == "IfcPropertySet":
options = set()
active_pset = self.active_option.split(": ")[1]
psets_in_file = Selector.parse(ifc, f'.IfcPropertySet[Name="{active_pset}"]')
for pset in psets_in_file:
for prop in pset.HasProperties:
options.add(prop.Name)
for index, option in enumerate(options):
new = op.add()
if self.selector in ["IfcSpatialElement", "IfcElementType"]:
if self.load_option == "sub_options":
new.name = f"{index}: {option.Name}"
new.global_id = option.GlobalId
else:
new.name = f"{index}: {option[0]}"
elif self.selector == "IfcPropertySet":
new.name = f"{index}: {option}"
else:
new.name = f"{index}: {option[0]}"
self.load_option = "options"
def load_spatial_elements(self, context):
ifc = IfcStore.file
col_items = Selector.parse(ifc, f".{self.selected_spatial_element}")
collection = getattr(self, "sub_spatial_elements", None)
collection.clear()
for index, c in enumerate(col_items):
new = collection.add()
new.name = f"{index}-{c.Name}"
class SearchCollection(PropertyGroup):
name: StringProperty()
long_name: StringProperty()
global_id: StringProperty()
query: StringProperty()
class IfcSelector:
and_or: EnumProperty(
items=[(i, i, i) for i in ["and", "or"]],
)
negation: BoolProperty(name="not")
comparison: EnumProperty(
items=[
("=", "equal to", ""),
("*=", "contains", ""),
(">=", "greater than or equal to", ""),
("<=", "lesser than or equal to", ""),
(">", "greater than", ""),
("<", "less than", ""),
],
)
load_option: StringProperty(
default="options", description="controls whether or not options or sub_options are loaded"
)
options: CollectionProperty(type=SearchCollection)
active_option: StringProperty(update=load_sub_options)
sub_options: CollectionProperty(type=SearchCollection)
active_sub_option: StringProperty()
value: StringProperty(description="generic 'value' that can be used in multiple scenarios")
class SearchQueryFilter(PropertyGroup, IfcSelector):
selector: EnumProperty(
items=[(i, i, i) for i in ["-", "IfcPropertySet", "Attribute"]],
name="Filter selection by",
update=load_selection_options,
default="-",
)
class SearchQuery(PropertyGroup, IfcSelector):
filters: CollectionProperty(type=SearchQueryFilter)
selector: EnumProperty(
items=[(i, i, i) for i in ["-", "IFC Class", "IfcSpatialElement", "IfcElementType", "GlobalId"]],
name="Selector type",
update=load_selection_options,
default="-",
)
class SearchQueryGroup(PropertyGroup, IfcSelector):
queries: CollectionProperty(type=SearchQuery)
class IfcSelectorProperties(PropertyGroup, IfcSelector):
groups: CollectionProperty(type=SearchQueryGroup)
selector_query_syntax: StringProperty()
query_library: CollectionProperty(type=SearchCollection)
active_query: StringProperty()
active_query: StringProperty()
manual_override: BoolProperty(default=False, description="Toggle to allow manual typing of query-syntax")
@@ -125,7 +125,7 @@ class BIM_UL_colourscheme(bpy.types.UIList):
if not item:
return
row = layout.row(align=True)
row.label(text=item.name)
row.label(text=f"{item.name} ({item.total})")
row.prop(item, "colour", text="")
@@ -159,153 +159,3 @@ class BIM_UL_ifc_building_storey_filter(bpy.types.UIList):
split = split.column()
split.scale_x = 0.5
split.label(text=str(item.total))
class BIM_PT_IFCSelector(Panel):
bl_label = "Selector"
bl_idname = "BIM_PT_ifc_selector"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_sandbox"
def draw(self, context):
layout = self.layout
layout.operator("bim.ifc_selector")
# this doesn't inherit from Panel since it's just a class to abstract away UI code from the IfcSelector Operator
class IfcSelectorUI:
@classmethod
def draw(self, context, layout):
ifc_selector = context.scene.IfcSelectorProperties
row = layout.row()
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
op = row.operator("bim.edit_blender_collection", text="Add selection group")
op.option = "add"
op.collection = "groups"
layout.separator()
self.draw_query_group_ui(self, ifc_selector, layout)
if len(ifc_selector.groups) != 0:
row = layout.row(align=True)
row.prop(ifc_selector, "selector_query_syntax", text="Query Syntax")
row.prop(ifc_selector, "manual_override", text="")
select = row.operator("bim.filter_model_elements", text="", icon="RESTRICT_SELECT_OFF")
select.option = "select"
isolate = row.operator("bim.filter_model_elements", text="", icon="ZOOM_SELECTED")
isolate.option = "isolate"
hide = row.operator("bim.filter_model_elements", text="", icon="HIDE_ON")
hide.option = "hide"
unhide = row.operator("bim.filter_model_elements", text="", icon="HIDE_OFF")
unhide.option = "unhide"
row = layout.row(align=True)
row.alignment = "CENTER"
row.operator("bim.save_selector_query", text="Save Query")
op = row.operator("bim.open_query_library", text="Load Query")
row.operator("bim.add_to_ifc_group", text="Add to IFC Group")
row = layout.row(align=True)
row.operator("bim.show_scene_elements", text="Show All Elements", icon="HIDE_OFF")
def draw_query_group_ui(self, ifc_selector, layout):
for index, group in enumerate(ifc_selector.groups):
row = layout.row()
row.alignment = "CENTER"
row.label(text="or") if index != 0 else None
box = layout.box()
row = box.row(align=True)
row.alignment = "CENTER"
row.label(text=f"Group #{str(index+1)}")
row.context_pointer_set(name="bim_prop_group", data=group)
op = row.operator("bim.edit_blender_collection", text="Add query", icon="PLUS")
op.option = "add"
op.collection = "queries"
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
op = row.operator("bim.edit_blender_collection", text="Remove selection group")
op.option = "remove"
op.collection = "groups"
op.index = index
self.draw_query_ui(self, group, box)
def draw_query_ui(self, group, box):
for index, query in enumerate(group.queries):
row = box.row()
row.alignment = "LEFT"
row.prop(query, "and_or", text="") if index != 0 else None
if query.and_or == "or":
row = box.row()
row.alignment = "LEFT"
row.prop(query, "selector", text="")
if query.selector in ["IFC Class", "IfcSpatialElement", "IfcElementType"]:
row.label(text="Equals")
row.prop_search(query, "active_option", query, "options", text="")
row.prop_search(query, "active_sub_option", query, "sub_options", text="") if len(
query.sub_options
) != 0 else None
self.draw_filter_ui(self, box, query)
elif query.selector in ["GlobalId", "Attribute"]:
row.prop(query, "negation", text="")
row.prop(query, "comparison", text="")
row.prop(query, "value", text="")
row.context_pointer_set(name="bim_prop_group", data=group)
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
op.option = "remove"
op.collection = "queries"
op.index = index
row.context_pointer_set(name="bim_prop_group", data=query)
op = (
row.operator("bim.edit_blender_collection", text="Add filter")
if query.selector == "IFC Class"
else None
)
if op:
op.option = "add"
op.collection = "filters"
def draw_filter_ui(self, box, query):
for filter_index, f in enumerate(query.filters):
row = box.row()
row.alignment = "LEFT"
row.label(text="")
row.prop(f, "and_or", text="") if filter_index != 0 else None
if f.and_or == "or":
row = box.row()
row.alignment = "LEFT"
row.label(text="")
row.prop(f, "selector", text="")
if f.selector == "Attribute":
row.prop_search(f, "active_option", f, "options", text="", results_are_suggestions=True)
row.prop(
f,
"negation",
)
row.prop(f, "comparison", text="")
row.prop(f, "value", text="")
elif f.selector == "IfcPropertySet":
row.prop_search(f, "active_option", f, "options", text="")
row.prop_search(f, "active_sub_option", f, "sub_options", text="")
row.prop(f, "negation")
row.prop(f, "comparison", text="")
row.prop(f, "value", text="")
row.context_pointer_set(name="bim_prop_group", data=query)
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
op.option = "remove"
op.collection = "filters"
op.index = filter_index
@@ -473,7 +473,7 @@ class AssignStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator):
self.file,
**{
"relating_object": self.file.by_id(self.work_plan),
"product": self.file.by_id(self.load_case),
"products": [self.file.by_id(self.load_case)],
},
)
return {"FINISHED"}
@@ -492,7 +492,7 @@ class UnassignStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator):
self.file,
**{
"relating_object": self.file.by_id(self.work_plan),
"product": self.file.by_id(self.load_case),
"products": [self.file.by_id(self.load_case)],
},
)
return {"FINISHED"}
@@ -330,16 +330,21 @@ class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
style_path = Path(tool.Ifc.resolve_uri(external_style.Location))
if style_path.suffix != ".blend":
self.report({"ERROR"}, f"Error loading external style - only Blender external styles are supported")
self.report(
{"ERROR"},
f"Error loading external style for \"{material.name}\" - only Blender external styles are supported",
)
return {"CANCELLED"}
if not style_path.exists():
self.report({"ERROR"}, f"Error loading external style - cannot find file: '{style_path}'")
self.report(
{"ERROR"}, f"Error loading external style for \"{material.name}\" - cannot find file: '{style_path}'"
)
return {"CANCELLED"}
db = tool.Blender.append_data_block(str(style_path), data_block_type, data_block)
if not db["data_block"]:
self.report({"ERROR"}, f"Error loading external style - {db['msg']}")
self.report({"ERROR"}, f"Error loading external style for \"{material.name}\" - {db['msg']}")
return {"CANCELLED"}
self.copy_material_attributes(db["data_block"], material)
@@ -259,7 +259,7 @@ def update_shading_style(self, context):
try:
bpy.ops.bim.activate_external_style(material_name=blender_material.name)
except RuntimeError as error:
if str(error).startswith("Error: Error loading external style - "):
if str(error).startswith("Error: Error loading external style for "):
return
raise error
@@ -18,7 +18,9 @@
import bpy
import bmesh
import ifcopenshell.util.element
import ifcopenshell.util.schema
import ifcopenshell.util.representation
import ifcopenshell.util.type
import ifcopenshell.api
import blenderbim.tool as tool
@@ -76,7 +78,7 @@ class UnassignType(bpy.types.Operator):
element = tool.Ifc.get_entity(obj)
if not element or element.is_a("IfcElementType"):
continue
ifcopenshell.api.run("type.unassign_type", self.file, related_object=element)
ifcopenshell.api.run("type.unassign_type", self.file, related_objects=[element])
active_representation = tool.Geometry.get_active_representation(obj)
active_context = active_representation.ContextOfItems
@@ -171,15 +173,22 @@ class SelectSimilarType(bpy.types.Operator):
def execute(self, context):
self.file = IfcStore.get_file()
objects = bpy.context.selected_objects
# store relating types to avoid selecting same elements multiple times
relating_types = set()
for related_object in objects:
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(related_object))
if not relating_type:
related_object.select_set(False)
continue
relating_types.add(relating_type)
for relating_type in relating_types:
related_objects = ifcopenshell.util.element.get_types(relating_type)
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if element and element in related_objects:
for element in related_objects:
obj = tool.Ifc.get_object(element)
if obj and obj in context.visible_objects:
obj.select_set(True)
return {"FINISHED"}
@@ -274,7 +283,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
else:
material = self.add_default_material()
rel = ifcopenshell.api.run(
"material.assign_material", ifc_file, product=element, type="IfcMaterialLayerSet"
"material.assign_material", ifc_file, products=[element], type="IfcMaterialLayerSet"
)
layer_set = rel.RelatingMaterial
layer = ifcopenshell.api.run("material.add_layer", ifc_file, layer_set=layer_set, material=material)
@@ -352,7 +361,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
)
rel = ifcopenshell.api.run(
"material.assign_material", ifc_file, product=element, type="IfcMaterialProfileSet"
"material.assign_material", ifc_file, products=[element], type="IfcMaterialProfileSet"
)
profile_set = rel.RelatingMaterial
material_profile = ifcopenshell.api.run(
+25
View File
@@ -936,6 +936,31 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
)
class BIM_OT_select_object(bpy.types.Operator):
bl_idname = "bim.select_object"
bl_label = "Select Object"
bl_options = {"REGISTER", "UNDO"}
obj_name: bpy.props.StringProperty(description="Object Name To Select")
def execute(self, context):
obj = bpy.data.objects[self.obj_name]
tool.Blender.set_objects_selection(context, obj, [obj], clear_previous_selection=True)
return {"FINISHED"}
class BIM_OT_delete_object(bpy.types.Operator):
bl_idname = "bim.delete_object"
bl_label = "Delete Object"
bl_options = {"REGISTER", "UNDO"}
obj_name: bpy.props.StringProperty(description="Object Name To Delete")
def execute(self, context):
obj = bpy.data.objects[self.obj_name]
with context.temp_override(selected_objects=[obj], active_object=obj):
bpy.ops.bim.override_object_delete(is_batch=False)
return {"FINISHED"}
class EditBlenderCollection(bpy.types.Operator):
bl_idname = "bim.edit_blender_collection"
bl_label = "Add or Remove Blender Collection Item"
+2
View File
@@ -273,6 +273,8 @@ class Attribute(PropertyGroup):
def get_value(self):
if self.is_optional and self.is_null:
return None
if self.data_type == "string":
return self.string_value.replace("\\n", "\n")
return getattr(self, str(self.get_value_name()), None)
def get_value_default(self):
File diff suppressed because it is too large Load Diff
+42 -1
View File
@@ -127,6 +127,40 @@ class BIM_PT_section_with_cappings(Panel):
row.operator("bim.clipping_plane_cut_with_cappings", icon="XRAY", text="Cut")
row.operator("bim.revert_clipping_plane_cut", icon="FILE_REFRESH", text="Revert Cut")
props = context.scene.BIMProjectProperties
box = layout.box()
header = box.row(align=True)
header.label(text="Clipping Planes")
header.operator("bim.create_clipping_plane", text="", icon="ADD")
box.template_list(
"BIM_UL_clipping_plane",
"",
props,
"clipping_planes",
props,
"clipping_planes_active",
)
if props.clipping_planes_active < len(props.clipping_planes):
active_clipping_plane_obj = props.clipping_planes[props.clipping_planes_active].obj
box.prop(active_clipping_plane_obj, "location")
box.prop(active_clipping_plane_obj, "rotation_euler")
class BIM_UL_clipping_plane(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
obj = item.obj
row = layout.row(align=True)
row.prop(obj, "name", text="", emboss=False)
row.operator("bim.select_object", text="", icon="RESTRICT_SELECT_OFF").obj_name = obj.name
row.context_pointer_set("active_object", obj)
row.operator("bim.flip_clipping_plane", text="", icon="PASTEFLIPDOWN")
row.operator("bim.delete_object", text="", icon="TRASH").obj_name = obj.name
else:
layout.label(text="", translate=False)
class BIM_UL_generic(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
@@ -432,7 +466,7 @@ class BIM_PT_tab_grouping_and_filtering(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_options = {"DEFAULT_CLOSED","HEADER_LAYOUT_EXPAND"}
@classmethod
def poll(cls, context):
@@ -441,6 +475,13 @@ class BIM_PT_tab_grouping_and_filtering(Panel):
def draw(self, context):
pass
def draw_header(self, context):
# Draws help button on the right
row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row
row.operator("bim.open_uri", text="", icon="HELP").uri = \
"https://docs.ifcopenshell.org/ifcopenshell-python/selector_syntax.html"
class BIM_PT_tab_geometry(Panel):
bl_label = "Geometry"
+3 -3
View File
@@ -29,7 +29,7 @@ def assign_object(ifc, aggregator, collector, relating_obj=None, related_obj=Non
if not aggregator.can_aggregate(relating_obj, related_obj):
return
rel = ifc.run(
"aggregate.assign_object", product=ifc.get_entity(related_obj), relating_object=ifc.get_entity(relating_obj)
"aggregate.assign_object", products=[ifc.get_entity(related_obj)], relating_object=ifc.get_entity(relating_obj)
)
collector.assign(relating_obj)
collector.assign(related_obj)
@@ -45,9 +45,9 @@ def unassign_object(ifc, aggregate, collector, relating_obj=None, related_obj=No
if related_element:
relating_obj = ifc.get_object(relating_element)
if relating_obj:
ifc.run("aggregate.unassign_object", product=related_element)
ifc.run("aggregate.unassign_object", products=[related_element])
if container:
ifc.run("spatial.assign_container", product=related_element, relating_structure=container)
ifc.run("spatial.assign_container", products=[related_element], relating_structure=container)
collector.assign(relating_obj)
collector.assign(related_obj)
+1 -2
View File
@@ -47,7 +47,6 @@ def add_instance_flooring_covering_from_cursor(ifc, spatial, model, Type, geomet
obj = spatial.get_named_obj_from_mesh(name, mesh)
spatial.set_obj_origin_to_cursor_position(obj)
# spatial.traslate_obj_to_z_location(obj, z)
spatial.link_obj_to_active_collection(obj)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
@@ -153,7 +152,7 @@ def add_instance_flooring_coverings_from_walls(ifc, spatial, collector, geometry
spatial.regen_obj_representation(ifc, geometry, obj, body)
def add_instance_ceiling_coverings_from_walls(ifc, spatial, collector, geometry, covering):
z = covering.get_z_from_ceiling_height()
z = covering.get_z_from_ceiling_height() + spatial.get_active_obj_z()
union = spatial.get_union_shape_from_selected_objects()
for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
+5 -5
View File
@@ -251,7 +251,7 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati
drawing_tool.copy_representation(drawing, new_drawing)
drawing_tool.set_name(new_drawing, drawing_name)
group = drawing_tool.get_drawing_group(new_drawing)
ifc.run("group.unassign_group", group=group, product=new_drawing)
ifc.run("group.unassign_group", group=group, products=[new_drawing])
new_group = ifc.run("group.add_group")
ifc.run("group.edit_group", group=new_group, attributes={"Name": drawing_name, "ObjectType": "DRAWING"})
ifc.run("group.assign_group", group=new_group, products=[new_drawing])
@@ -261,7 +261,7 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati
continue
new_annotation = ifc.run("root.copy_class", product=annotation)
drawing_tool.copy_representation(annotation, new_annotation)
ifc.run("group.unassign_group", group=group, product=new_annotation)
ifc.run("group.unassign_group", group=group, products=[new_annotation])
ifc.run("group.assign_group", group=new_group, products=[new_annotation])
old_reference = drawing_tool.get_drawing_document(new_drawing)
@@ -388,13 +388,13 @@ def sync_references(ifc, collector, drawing_tool, drawing=None):
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
elif not reference_obj:
should_delete_existing_annotation = True
if reference_obj and (should_delete_existing_annotation or not annotation):
if should_delete_existing_annotation or not annotation:
should_create_annotation = True
if should_delete_existing_annotation:
+13 -4
View File
@@ -16,10 +16,17 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import blenderbim.core.style
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
import blenderbim.core.tool as tool
if TYPE_CHECKING:
import bpy
def edit_object_placement(ifc, geometry, surveyor, obj=None):
def edit_object_placement(
ifc: tool.Ifc, geometry: tool.Geometry, surveyor: tool.Surveyor, obj: Optional[bpy.types.Object] = None
) -> None:
element = ifc.get_entity(obj)
if not element:
return
@@ -192,23 +199,25 @@ def select_connection(geometry, connection=None):
def remove_connection(geometry, connection=None):
geometry.remove_connection(connection)
def get_similar_openings(ifc, opening):
model = ifc.get()
all_openings = model.by_type("IfcOpeningElement")
similar_openings = [o for o in all_openings if o.ObjectPlacement == opening.ObjectPlacement and o != opening]
return similar_openings
def get_similar_openings_building_objs(ifc, similar_openings):
building_objs = []
for similar_opening in similar_openings:
building_objs.append(ifc.get_object(similar_opening.VoidsElements[0].RelatingBuildingElement))
return building_objs
def edit_similar_opening_placement(geometry, opening = None, similar_openings = None):
def edit_similar_opening_placement(geometry, opening=None, similar_openings=None):
if not opening or not similar_openings:
return
for similar_opening in similar_openings:
old_placement = similar_opening.ObjectPlacement
similar_opening.ObjectPlacement = opening.ObjectPlacement
geometry.delete_opening_object_placement(old_placement)
+4 -4
View File
@@ -102,7 +102,7 @@ def assign_material(ifc, material_tool, material_type, objects):
element = ifc.get_entity(obj)
if not element:
continue
ifc.run("material.assign_material", product=element, type=material_type, material=material)
ifc.run("material.assign_material", products=[element], type=material_type, material=material)
assigned_material = material_tool.get_material(element)
if material_tool.is_a_material_set(assigned_material):
material_tool.add_material_to_set(material_set=assigned_material, material=material)
@@ -116,12 +116,12 @@ def unassign_material(ifc, material_tool, objects):
inherited_material = material_tool.get_material(element, should_inherit=True)
if material and "Usage" in material.is_a():
element_type = material_tool.get_type(element)
ifc.run("material.unassign_material", product=element_type)
ifc.run("material.unassign_material", products=[element_type])
elif not material and inherited_material:
element_type = material_tool.get_type(element)
ifc.run("material.unassign_material", product=element_type)
ifc.run("material.unassign_material", products=[element_type])
elif material:
ifc.run("material.unassign_material", product=element)
ifc.run("material.unassign_material", products=[element])
def patch_non_parametric_mep_segment(ifc, material_tool, profile_tool, obj):
+5 -3
View File
@@ -29,7 +29,9 @@ def assign_object(ifc, nest, collector, relating_obj=None, related_obj=None):
if not nest.can_nest(relating_obj, related_obj):
return
rel = ifc.run(
"nest.assign_object", related_object=ifc.get_entity(related_obj), relating_object=ifc.get_entity(relating_obj)
"nest.assign_object",
related_objects=[ifc.get_entity(related_obj)],
relating_object=ifc.get_entity(relating_obj),
)
collector.assign(relating_obj)
collector.assign(related_obj)
@@ -45,9 +47,9 @@ def unassign_object(ifc, nest, collector, relating_obj=None, related_obj=None):
if related_element:
relating_obj = ifc.get_object(relating_element)
if relating_obj:
ifc.run("nest.unassign_object", related_object=related_element)
ifc.run("nest.unassign_object", related_objects=[related_element])
if container:
ifc.run("spatial.assign_container", product=related_element, relating_structure=container)
ifc.run("spatial.assign_container", products=[related_element], relating_structure=container)
collector.assign(relating_obj)
collector.assign(related_obj)
+2 -2
View File
@@ -61,11 +61,11 @@ def remove_work_schedule(ifc, work_schedule=None):
def assign_work_schedule(ifc, work_plan=None, work_schedule=None):
if work_schedule:
return ifc.run("aggregate.assign_object", relating_object=work_plan, product=work_schedule)
return ifc.run("aggregate.assign_object", relating_object=work_plan, products=[work_schedule])
def unassign_work_schedule(ifc, work_schedule=None):
ifc.run("aggregate.unassign_object", product=work_schedule)
ifc.run("aggregate.unassign_object", products=[work_schedule])
def enable_editing_work_schedule(sequence, work_schedule=None):
+37 -12
View File
@@ -16,23 +16,47 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Union
import blenderbim.core.tool as tool
def reference_structure(ifc, spatial, structure=None, element=None):
if TYPE_CHECKING:
import bpy
import ifcopenshell
def reference_structure(
ifc: tool.Ifc,
spatial: tool.Spatial,
structure: Optional[ifcopenshell.entity_instance] = None,
element: Optional[ifcopenshell.entity_instance] = None,
) -> Union[ifcopenshell.entity_instance, None]:
if spatial.can_reference(structure, element):
return ifc.run("spatial.reference_structure", product=element, relating_structure=structure)
def dereference_structure(ifc, spatial, structure=None, element=None):
def dereference_structure(
ifc: tool.Ifc,
spatial: tool.Spatial,
structure: Optional[ifcopenshell.entity_instance] = None,
element: Optional[ifcopenshell.entity_instance] = None,
) -> None:
if spatial.can_reference(structure, element):
return ifc.run("spatial.dereference_structure", product=element, relating_structure=structure)
def assign_container(ifc, collector, spatial, structure_obj=None, element_obj=None):
def assign_container(
ifc: tool.Ifc,
collector: tool.Collector,
spatial: tool.Spatial,
structure_obj: Optional[bpy.types.Object] = None,
element_obj: Optional[bpy.types.Object] = None,
) -> Union[ifcopenshell.entity_instance, None]:
if not spatial.can_contain(structure_obj, element_obj):
return
rel = ifc.run(
"spatial.assign_container",
product=ifc.get_entity(element_obj),
products=[ifc.get_entity(element_obj)],
relating_structure=ifc.get_entity(structure_obj),
)
spatial.disable_editing(element_obj)
@@ -54,7 +78,7 @@ def change_spatial_level(spatial, parent=None):
def remove_container(ifc, collector, obj=None):
ifc.run("spatial.remove_container", product=ifc.get_entity(obj))
ifc.run("spatial.unassign_container", products=[ifc.get_entity(obj)])
collector.assign(obj)
@@ -126,7 +150,7 @@ def select_decomposed_elements(spatial):
spatial.select_products(spatial.get_decomposed_elements(container))
#HERE STARTS SPATIAL TOOL
# HERE STARTS SPATIAL TOOL
def generate_space(ifc, spatial, model, Type):
active_obj = spatial.get_active_obj()
selected_objects = spatial.get_selected_objects()
@@ -140,21 +164,20 @@ def generate_space(ifc, spatial, model, Type):
relating_type = None
if selected_objects and active_obj:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) ##mat
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) ##mat
element = ifc.get_entity(active_obj)
else:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor() ##mat
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor() ##mat
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
if not space_polygon:
return
bm = spatial.get_bmesh_from_polygon(space_polygon, h=h) ##mat
bm = spatial.get_bmesh_from_polygon(space_polygon, h=h) ##mat
mesh = spatial.get_named_mesh_from_bmesh(name = "Space", bmesh = bm)
mesh = spatial.get_named_mesh_from_bmesh(name="Space", bmesh=bm)
if element and element.is_a("IfcSpace"):
mesh = spatial.get_transformed_mesh_from_local_to_global(mesh)
@@ -175,6 +198,7 @@ def generate_space(ifc, spatial, model, Type):
if relating_type:
spatial.assign_relating_type_to_element(ifc, Type, element, relating_type)
def generate_spaces_from_walls(ifc, spatial, collector):
z = spatial.get_active_obj_z()
h = spatial.get_active_obj_height()
@@ -188,7 +212,7 @@ def generate_spaces_from_walls(ifc, spatial, collector):
name = "Space" + str(i)
obj = spatial.get_named_obj_from_bmesh(name, bmesh = bm)
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter(obj)
spatial.traslate_obj_to_z_location(obj, z)
@@ -206,6 +230,7 @@ def toggle_space_visibility(ifc, spatial):
return
spatial.toggle_spaces_visibility_wired_and_textured(spaces)
def toggle_hide_spaces(ifc, spatial):
model = ifc.get()
spaces = model.by_type("IfcSpace")
+2 -2
View File
@@ -55,11 +55,11 @@ def disable_editing_system(system):
def assign_system(ifc, system=None, product=None):
ifc.run("system.assign_system", product=product, system=system)
ifc.run("system.assign_system", products=[product], system=system)
def unassign_system(ifc, system=None, product=None):
ifc.run("system.unassign_system", product=product, system=system)
ifc.run("system.unassign_system", products=[product], system=system)
def select_system_products(system_tool, system=None):
+13 -2
View File
@@ -76,7 +76,7 @@ class Blender:
def get_name(cls, ifc_class, name): pass
def get_selected_objects(cls): pass
def create_ifc_object(cls, ifc_class: str, name: str = None, data=None): pass
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None): pass
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None, context=None): pass
def is_ifc_object(cls, obj): pass
def is_ifc_class_active(cls, ifc_class): pass
def get_viewport_context(cls): pass
@@ -153,6 +153,17 @@ class Bsdd:
def should_load_preview_domains(cls): pass
@interface
class Clash:
def export_clash_sets(cls): pass
def get_clash(cls, clash_set, a_global_id, b_global_id): pass
def get_clash_set(cls, name): pass
def get_clash_sets(cls): pass
def import_active_clashes(cls): pass
def load_clash_sets(cls, fn): pass
def look_at(cls, target, location): pass
@interface
class Collector:
def assign(cls, obj): pass
@@ -508,7 +519,7 @@ class Misc:
class Model:
def convert_si_to_unit(cls, value): pass
def convert_unit_to_si(cls, value): pass
def export_curve(cls, position, edge_indices, points=None): pass
def export_curve(cls, position, edge_indices): pass
def export_points(cls, position, indices): pass
def export_profile(cls, obj, position=None): pass
def generate_occurrence_name(cls, element_type, ifc_class): pass
+1 -1
View File
@@ -20,7 +20,7 @@ import blenderbim.core.geometry
def assign_type(ifc, type_tool, element=None, type=None):
ifc.run("type.assign_type", related_object=element, relating_type=type)
ifc.run("type.assign_type", related_objects=[element], relating_type=type)
obj = ifc.get_object(element)
if type_tool.has_material_usage(element):
pass # for now, representation regeneration handled by API listeners
@@ -22,6 +22,7 @@ from blenderbim.tool.boundary import Boundary
from blenderbim.tool.brick import Brick
from blenderbim.tool.bsdd import Bsdd
from blenderbim.tool.cad import Cad
from blenderbim.tool.clash import Clash
from blenderbim.tool.collector import Collector
from blenderbim.tool.context import Context
from blenderbim.tool.debug import Debug
+3 -2
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.core.tool
import blenderbim.tool as tool
import ifcopenshell.util.element
@@ -23,7 +24,7 @@ import ifcopenshell.util.element
class Aggregate(blenderbim.core.tool.Aggregate):
@classmethod
def can_aggregate(cls, relating_obj, related_obj):
def can_aggregate(cls, relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool:
relating_object = tool.Ifc.get_entity(relating_obj)
related_object = tool.Ifc.get_entity(related_obj)
if not relating_object or not related_object:
@@ -60,4 +61,4 @@ class Aggregate(blenderbim.core.tool.Aggregate):
def get_relating_object(cls, related_element):
for rel in related_element.Decomposes:
if rel.is_a("IfcRelAggregates"):
return rel.RelatingObject
return rel.RelatingObject
+249 -60
View File
@@ -17,13 +17,17 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bmesh
import json
import ifcopenshell.api
import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.bim
import addon_utils
from mathutils import Vector
from pathlib import Path
import addon_utils
from blenderbim.bim.ifc import IFC_CONNECTED_TYPE
from typing import Any, Optional, Union, Literal, Iterable, Callable
VIEWPORT_ATTRIBUTES = [
@@ -37,13 +41,16 @@ VIEWPORT_ATTRIBUTES = [
"clip_planes",
]
OBJECT_DATA_TYPE = Union[bpy.types.Mesh, bpy.types.Curve, bpy.types.Camera]
class Blender(blenderbim.core.tool.Blender):
OBJECT_TYPES_THAT_SUPPORT_EDIT_MODE = ("MESH", "CURVE", "SURFACE", "META", "FONT", "LATTICE", "ARMATURE")
OBJECT_TYPES_THAT_SUPPORT_EDIT_GPENCIL_MODE = ("GPENCIL",)
TYPE_MANAGER_ICON = "LIGHTPROBE_VOLUME" if bpy.app.version >= (4, 1, 0) else "LIGHTPROBE_GRID"
@classmethod
def get_area_props(cls, context):
def get_area_props(cls, context: bpy.types.Context) -> Any:
try:
if context.screen.name.endswith("-nonnormal"): # Ctrl-space temporary fullscreen
screen = bpy.data.screens[context.screen.name[0 : -len("-nonnormal")]]
@@ -55,12 +62,12 @@ class Blender(blenderbim.core.tool.Blender):
return
@classmethod
def set_active_object(cls, obj):
def set_active_object(cls, obj: bpy.types.Object) -> None:
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
@classmethod
def setup_tabs(cls):
def setup_tabs(cls) -> None:
# https://blender.stackexchange.com/questions/140644/how-can-make-the-state-of-a-boolean-property-relative-to-the-3d-view-area
for screen in bpy.data.screens:
if len(screen.BIMAreaProperties) == 20:
@@ -70,7 +77,7 @@ class Blender(blenderbim.core.tool.Blender):
screen.BIMAreaProperties.add()
@classmethod
def is_tab(cls, context, tab):
def is_tab(cls, context: bpy.types.Context, tab: str) -> bool:
aprops = cls.get_area_props(context)
if not aprops:
return context.screen.BIMTabProperties.tab == tab
@@ -79,7 +86,7 @@ class Blender(blenderbim.core.tool.Blender):
return aprops.tab == tab
@classmethod
def is_default_scene(cls):
def is_default_scene(cls) -> bool:
if len(bpy.context.scene.objects) != 3:
return False
if {obj.type for obj in bpy.context.scene.objects} == {"MESH", "LIGHT", "CAMERA"}:
@@ -87,7 +94,7 @@ class Blender(blenderbim.core.tool.Blender):
return False
@classmethod
def get_name(cls, ifc_class, name):
def get_name(cls, ifc_class: str, name: str) -> str:
if not bpy.data.objects.get(f"{ifc_class}/{name}"):
return name
i = 2
@@ -96,13 +103,15 @@ class Blender(blenderbim.core.tool.Blender):
return f"{name} {i}"
@classmethod
def get_selected_objects(cls):
def get_selected_objects(cls) -> set[bpy.types.Object]:
if bpy.context.selected_objects:
return set(bpy.context.selected_objects + [bpy.context.active_object])
return set([bpy.context.active_object])
@classmethod
def create_ifc_object(cls, ifc_class: str, name: str = None, data=None) -> bpy.types.Object:
def create_ifc_object(
cls, ifc_class: str, name: Optional[str] = None, data: Optional[OBJECT_DATA_TYPE] = None
) -> bpy.types.Object:
name = name or "My " + ifc_class
name = cls.get_name(ifc_class, name)
obj = bpy.data.objects.new(name, data)
@@ -110,7 +119,11 @@ class Blender(blenderbim.core.tool.Blender):
return obj
@classmethod
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None):
def get_obj_ifc_definition_id(
cls, obj: Optional[str] = None, obj_type: Optional[str] = None, context: Optional[bpy.types.Context] = None
) -> Union[int, None]:
if context is None:
context = bpy.context
if obj_type == "Object":
return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "Material":
@@ -122,41 +135,42 @@ class Blender(blenderbim.core.tool.Blender):
elif obj_type == "MaterialSetItem":
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
elif obj_type == "Task":
return bpy.context.scene.BIMTaskTreeProperties.tasks[
bpy.context.scene.BIMWorkScheduleProperties.active_task_index
return context.scene.BIMTaskTreeProperties.tasks[
context.scene.BIMWorkScheduleProperties.active_task_index
].ifc_definition_id
elif obj_type == "Cost":
return bpy.context.scene.BIMCostProperties.cost_items[
bpy.context.scene.BIMCostProperties.active_cost_item_index
return context.scene.BIMCostProperties.cost_items[
context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id
elif obj_type == "Resource":
return bpy.context.scene.BIMResourceTreeProperties.resources[
bpy.context.scene.BIMResourceProperties.active_resource_index
return context.scene.BIMResourceTreeProperties.resources[
context.scene.BIMResourceProperties.active_resource_index
].ifc_definition_id
elif obj_type == "Profile":
return bpy.context.scene.BIMProfileProperties.profiles[
bpy.context.scene.BIMProfileProperties.active_profile_index
return context.scene.BIMProfileProperties.profiles[
context.scene.BIMProfileProperties.active_profile_index
].ifc_definition_id
elif obj_type == "WorkSchedule":
return bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id
return context.scene.BIMWorkScheduleProperties.active_work_schedule_id
elif obj_type == "Group":
prop = bpy.context.scene.BIMGroupProperties
prop = context.scene.BIMGroupProperties
return prop.groups[prop.active_group_index].ifc_definition_id
@classmethod
def is_ifc_object(cls, obj):
def is_ifc_object(cls, obj: bpy.types.Object) -> bool:
return bool(obj.BIMObjectProperties.ifc_definition_id)
@classmethod
def is_ifc_class_active(cls, ifc_class):
if bpy.context.active_object:
if cls.is_ifc_object(bpy.context.active_object):
return tool.Ifc.get_entity(bpy.context.active_object).is_a(ifc_class)
def is_ifc_class_active(cls, ifc_class: str) -> bool:
obj = bpy.context.active_object
if obj:
if cls.is_ifc_object(obj):
return tool.Ifc.get_entity(obj).is_a(ifc_class)
return False
return False
@classmethod
def show_info_message(cls, text, message_type="INFO"):
def show_info_message(cls, text: str, message_type: Literal["INFO", "ERROR"] = "INFO") -> None:
"""useful for showing error messages outside blender operators
Possible `message_type`: `INFO` / `ERROR`"""
@@ -167,14 +181,14 @@ class Blender(blenderbim.core.tool.Blender):
bpy.context.window_manager.popup_menu(message_ui, title=message_type.capitalize(), icon=message_type)
@classmethod
def get_view3d_area(cls):
def get_view3d_area(cls) -> Union[bpy.types.Area, None]:
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == "VIEW_3D":
return area
@classmethod
def get_blender_prop_default_value(cls, props, prop_name):
def get_blender_prop_default_value(cls, props, prop_name: str) -> Any:
prop_bl_rna = props.bl_rna.properties[prop_name]
if getattr(prop_bl_rna, "array_length", 0) > 0:
prop_value = prop_bl_rna.default_array
@@ -183,7 +197,7 @@ class Blender(blenderbim.core.tool.Blender):
return prop_value
@classmethod
def get_viewport_context(cls):
def get_viewport_context(cls) -> dict:
"""Get viewport area context for context overriding.
Useful for calling operators outside viewport context.
@@ -198,25 +212,25 @@ class Blender(blenderbim.core.tool.Blender):
return context_override
@classmethod
def get_viewport_position(cls):
def get_viewport_position(cls) -> dict:
region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d
copy_if_possible = lambda x: x.copy() if hasattr(x, "copy") else x
viewport_data = {attr: copy_if_possible(getattr(region_3d, attr)) for attr in VIEWPORT_ATTRIBUTES}
return viewport_data
@classmethod
def set_viewport_position(cls, data):
def set_viewport_position(cls, data: dict) -> None:
region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d
for attr in VIEWPORT_ATTRIBUTES:
setattr(region_3d, attr, data[attr])
@classmethod
def set_viewport_tool(cls, tool_name):
def set_viewport_tool(cls, tool_name: str) -> None:
with bpy.context.temp_override(**tool.Blender.get_viewport_context()):
bpy.ops.wm.tool_set_by_id(name=tool_name)
@classmethod
def get_shader_editor_context(cls):
def get_shader_editor_context(cls) -> Union[dict, None]:
for screen in bpy.data.screens:
for area in screen.areas:
if area.type == "NODE_EDITOR":
@@ -226,7 +240,7 @@ class Blender(blenderbim.core.tool.Blender):
return context_override
@classmethod
def copy_node_graph(cls, material_to, material_from):
def copy_node_graph(cls, material_to: bpy.types.Material, material_from: bpy.types.Material) -> None:
# https://projects.blender.org/blender/blender/issues/108763
if bpy.app.version[:2] == (4, 0):
print(
@@ -270,7 +284,9 @@ class Blender(blenderbim.core.tool.Blender):
shader_editor.pin = previous_pin_setting
@classmethod
def get_material_node(cls, blender_material, node_type, kwargs={}):
def get_material_node(
cls, blender_material: bpy.types.Material, node_type: str, kwargs: Optional[dict] = {}
) -> Union[bpy.types.ShaderNode, None]:
"""returns first node from the `blender_material` shader graph with type `node_type`"""
if not blender_material.use_nodes:
return
@@ -280,15 +296,15 @@ class Blender(blenderbim.core.tool.Blender):
return node
@classmethod
def update_screen(cls):
def update_screen(cls) -> None:
bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1)
@classmethod
def update_viewport(cls):
def update_viewport(cls) -> None:
tool.Blender.get_viewport_context()["area"].tag_redraw()
@classmethod
def force_depsgraph_update(cls):
def force_depsgraph_update(cls) -> None:
"""useful if you need to trigger callbacks like `depsgraph_update_pre`"""
# blender is requiring some ID to be changed
# to trigger depsgraph update
@@ -297,9 +313,11 @@ class Blender(blenderbim.core.tool.Blender):
bpy.context.view_layer.update()
@classmethod
def ensure_unique_name(cls, name, objects, iteration=0):
def ensure_unique_name(cls, name: str, objects: Iterable[str], iteration=0) -> str:
"""returns a unique name for the given name and dictionary of objects
blender style name with .001, .002, etc. suffix
blender style name with .001, .002, etc. suffix.
objects can be `bpy.data.objects`.
"""
current_iteration = name if not iteration else f"{name}.{iteration:03d}"
if current_iteration not in objects:
@@ -307,7 +325,7 @@ class Blender(blenderbim.core.tool.Blender):
return cls.ensure_unique_name(name, objects, iteration + 1)
@classmethod
def blender_path_to_posix(cls, blender_path):
def blender_path_to_posix(cls, blender_path: str) -> str:
"""Process blender path to be saved as posix.
If path is relative the method will keep it relative to .ifc file
@@ -322,7 +340,7 @@ class Blender(blenderbim.core.tool.Blender):
return path.as_posix()
@classmethod
def get_default_selection_keypmap(cls):
def get_default_selection_keypmap(cls) -> tuple:
"""keymap to replicate default blender selection behaviour with click and box selection"""
# code below comes from blender_default.py which is part of default blender scripts licensed under GPL v2
# https://github.com/blender/blender/blob/master/release/scripts/presets/keyconfig/keymap_data/blender_default.py
@@ -369,7 +387,9 @@ class Blender(blenderbim.core.tool.Blender):
return keymap
@classmethod
def add_layout_hotkey_operator(cls, tool_name, layout, text, hotkey, description):
def add_layout_hotkey_operator(
cls, tool_name: str, layout: bpy.types.UILayout, text: str, hotkey: str, description: str
) -> tuple[bpy.types.OperatorProperties, bpy.types.UILayout]:
modifiers = {
"A": "EVENT_ALT",
"S": "EVENT_SHIFT",
@@ -386,7 +406,7 @@ class Blender(blenderbim.core.tool.Blender):
return op, row
@classmethod
def get_object_bounding_box(cls, obj):
def get_object_bounding_box(cls, obj: bpy.types.Object) -> dict:
"""Returns dict with local min and max x, y, z values for the object.
Careful with using this method for objects in EDIT mode because
@@ -407,14 +427,20 @@ class Blender(blenderbim.core.tool.Blender):
return bbox_dict
@classmethod
def select_and_activate_single_object(cls, context, active_object):
def select_and_activate_single_object(cls, context: bpy.types.Context, active_object: bpy.types.Object) -> None:
for obj in context.selected_objects:
obj.select_set(False)
context.view_layer.objects.active = active_object
active_object.select_set(True)
@classmethod
def set_objects_selection(cls, context, active_object, selected_objects, clear_previous_selection=True):
def set_objects_selection(
cls,
context: bpy.types.Context,
active_object: Optional[bpy.types.Object] = None,
selected_objects: list[bpy.types.Object] = list(),
clear_previous_selection=True,
) -> None:
if clear_previous_selection:
for obj in context.selected_objects:
obj.select_set(False)
@@ -425,7 +451,7 @@ class Blender(blenderbim.core.tool.Blender):
active_object.select_set(True)
@classmethod
def enum_property_has_valid_index(cls, props, prop_name, enum_items):
def enum_property_has_valid_index(cls, props, prop_name: str, enum_items: tuple) -> bool:
"""method created for readibility and to avoid console warnings like
`pyrna_enum_to_py: current value '17' matches no enum in 'BIMModelProperties', '', 'relating_type_id'`
"""
@@ -436,7 +462,7 @@ class Blender(blenderbim.core.tool.Blender):
return current_value_index < len(enum_items)
@classmethod
def append_data_block(cls, filepath, data_block_type, name, link=False, relative=False):
def append_data_block(cls, filepath: str, data_block_type: str, name: str, link=False, relative=False) -> dict:
if Path(filepath) == Path(bpy.data.filepath):
data_block = getattr(bpy.data, data_block_type).get(name, None)
if not data_block:
@@ -450,7 +476,7 @@ class Blender(blenderbim.core.tool.Blender):
return {"data_block": getattr(data_to, data_block_type)[0], "msg": ""}
@classmethod
def remove_data_block(cls, data_block, do_unlink=True):
def remove_data_block(cls, data_block: bpy.types.ID, do_unlink=True) -> None:
"""Removes a datablock (such as a mesh)
See https://projects.blender.org/blender/blender/issues/118787 for more
@@ -472,7 +498,7 @@ class Blender(blenderbim.core.tool.Blender):
## BMESH UTILS ##
@classmethod
def apply_bmesh(cls, mesh, bm, obj=None):
def apply_bmesh(cls, mesh: bpy.types.Mesh, bm: bmesh.types.BMesh, obj: Optional[bpy.types.Object] = None) -> None:
"""`obj` argument is not optional if you plan to update mesh in EDIT mode
and it's possible that that mesh object won't be currenly active."""
import bmesh
@@ -506,7 +532,7 @@ class Blender(blenderbim.core.tool.Blender):
mesh.update()
@classmethod
def get_bmesh_for_mesh(cls, mesh, clean=False):
def get_bmesh_for_mesh(cls, mesh: bpy.types.Mesh, clean=False) -> bmesh.types.BMesh:
import bmesh
if mesh.is_editmode:
@@ -520,7 +546,16 @@ class Blender(blenderbim.core.tool.Blender):
return bm
@classmethod
def bmesh_join(cls, bm_a, bm_b, callback=None):
def bmesh_join(
cls,
bm_a: bmesh.types.BMesh,
bm_b: bmesh.types.BMesh,
callback: Optional[
Callable[
[bmesh.types.BMesh, list[bmesh.types.BMVert], list[bmesh.types.BMEdge], list[bmesh.types.BMFace]], None
]
] = None,
):
"""Join two meshes into single one, store it in `bm_a`"""
import bmesh
@@ -535,7 +570,19 @@ class Blender(blenderbim.core.tool.Blender):
return bm_a
@classmethod
def toggle_edit_mode(cls, context):
def bmesh_check_vertex_in_groups(
cls, vertex: bmesh.types.BMVert, deform_layer: bmesh.types.BMLayerItem, groups: list[int]
) -> Union[tuple[Literal[True], int], tuple[Literal[False], None]]:
"""returns tuple boolean (whether vertex is in any of the groups) and related group index"""
for group_index in vertex[deform_layer].keys():
# ignore vertex groups assignments produced by edge subdivision near arcs
# they usually have weight = 0.5
if group_index in groups and vertex[deform_layer][group_index] == 1.0:
return True, group_index
return False, None
@classmethod
def toggle_edit_mode(cls, context: bpy.types.Context) -> set:
ao = context.active_object
if not ao:
return {"CANCELLED"}
@@ -547,21 +594,21 @@ class Blender(blenderbim.core.tool.Blender):
return {"CANCELLED"}
@classmethod
def is_object_an_ifc_class(cls, obj, classes):
def is_object_an_ifc_class(cls, obj: bpy.types.Object, classes: Iterable[str]) -> bool:
if not tool.Ifc.get():
return False
element = tool.Ifc.get_entity(obj)
return element and element.is_a() in classes
@classmethod
def get_object_from_guid(cls, guid):
def get_object_from_guid(cls, guid: str) -> Union[IFC_CONNECTED_TYPE, None]:
element = tool.Ifc.get().by_guid(guid)
obj = tool.Ifc.get_object(element)
if obj:
return obj
@classmethod
def lock_transform(cls, obj, lock_state=True):
def lock_transform(cls, obj: bpy.types.ObjectBase, lock_state=True) -> None:
for prop in ("lock_location", "lock_rotation", "lock_scale"):
attr = getattr(obj, prop)
for axis_idx in range(3):
@@ -570,7 +617,9 @@ class Blender(blenderbim.core.tool.Blender):
operator_invoke_filepath_hotkeys_description = "Hold Shift to open the file, Alt to browse containing directory"
@classmethod
def operator_invoke_filepath_hotkeys(cls, operator, context, event, filepath: Path):
def operator_invoke_filepath_hotkeys(
cls, operator: bpy.types.Operator, context: bpy.types.Context, event: bpy.types.Event, filepath: Path
) -> Union[set, None]:
if not event.alt and not event.shift:
return
@@ -611,6 +660,43 @@ class Blender(blenderbim.core.tool.Blender):
open_file_or_folder(filepath.as_posix())
return {"PASS_THROUGH"}
@classmethod
def get_layer_collection(
cls, collection: bpy.types.Collection, view_layer: Optional[bpy.types.ViewLayer] = None
) -> Union[bpy.types.LayerCollection, None]:
return cls.get_layer_collections_mapping([collection], view_layer).get(collection)
@classmethod
def get_layer_collections_mapping(
cls, collections: list[bpy.types.Collection], view_layer: Optional[bpy.types.ViewLayer] = None
) -> dict[bpy.types.Collection, bpy.types.LayerCollection]:
if view_layer is None:
view_layer = bpy.context.view_layer
collections = list(collections) # copy to prevent mutation
collections_mapping = dict()
queue = [view_layer.layer_collection]
while queue:
layer = queue.pop()
collection = layer.collection
if collection in collections:
collections_mapping[collection] = layer
collections.remove(collection)
if not collections:
break
queue.extend(list(layer.children))
return collections_mapping
@classmethod
def object_supports_edit_mode(cls, obj):
if not obj.data:
return False, "Can't Edit Empty Object"
if obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
return False, "Can't Edit Clipping Plane Geometry"
return True, ""
class Modifier:
@classmethod
def is_eligible_for_railing_modifier(cls, obj):
@@ -712,8 +798,7 @@ class Blender(blenderbim.core.tool.Blender):
@classmethod
def get_modifiers_data(cls, parent_element):
array_pset = ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array")
for modifier_data in json.loads(array_pset["Data"]):
yield modifier_data
yield from json.loads(array_pset["Data"])
@classmethod
def get_children_objects(cls, modifier_data):
@@ -760,7 +845,9 @@ class Blender(blenderbim.core.tool.Blender):
bpy.utils.register_tool(ws_model.PipeTool, after={"bim.duct_tool"}, separator=False, group=False)
bpy.utils.register_tool(ws_model.BimTool, after={"bim.pipe_tool"}, separator=False, group=False)
bpy.utils.register_tool(ws_drawing.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=False)
bpy.utils.register_tool(ws_spatial.SpatialTool, after={"bim.annotation_tool"}, separator=False, group=False)
bpy.utils.register_tool(
ws_spatial.SpatialTool, after={"bim.annotation_tool"}, separator=False, group=False
)
bpy.utils.register_tool(
ws_structural.StructuralTool, after={"bim.spatial_tool"}, separator=False, group=False
)
@@ -797,3 +884,105 @@ class Blender(blenderbim.core.tool.Blender):
bpy.utils.unregister_tool(ws_covering.CoveringTool)
except:
pass
@classmethod
def get_scene_panels_list(cls) -> tuple[bpy.types.Panel, ...]:
# example default blender scene panels can be found in
# https://projects.blender.org/blender/blender/src/branch/main/scripts/startup/bl_ui/properties_scene.py#L421
scene_panels: list[str] = []
panels_to_parents: dict[str, str] = dict()
for item_name in dir(bpy.types):
item = getattr(bpy.types, item_name)
# filter only panels
if not hasattr(item, "bl_rna") or not isinstance(item.bl_rna, bpy.types.Panel):
continue
# ignore bbim panels
if item.__module__.startswith("blenderbim"):
continue
# filter scene panels
if getattr(item, "bl_context", None) != "scene":
continue
scene_panels.append(item_name)
parent_panel = getattr(item, "bl_parent_id", None)
if parent_panel is not None:
panels_to_parents[item_name] = parent_panel
scene_panels = cls.sort_panels_for_register(scene_panels, panels_to_parents)
final_panels = [getattr(bpy.types, p) for p in scene_panels]
return tuple(final_panels)
@classmethod
def sort_panels_for_register(cls, items: list[str], items_to_parents: dict[str, str]) -> list[str]:
"""sort panels ensuring parents panels will be registered first
as otherwise we'll get errors unregistering them all and registering child panel"""
final_items = []
unsorted = items.copy()
# first, add items without parents
for item in unsorted[:]:
if item not in items_to_parents:
final_items.append(item)
unsorted.remove(item)
# store children for each parent
children: dict[str, list[str]] = dict()
for item in items_to_parents:
children.setdefault(items_to_parents[item], []).append(item)
# add children recursively, ensuring parents are added first
keep_looking = True
while keep_looking:
keep_looking = False
for item in list(children.keys()):
# check if parent panel was already added
if item not in final_items:
continue
final_items.extend(children[item])
del children[item]
keep_looking = True
assert set(items) == set(final_items), "Sorted list doesn't match original"
return final_items
@classmethod
def override_scene_panel(cls, original_panel: bpy.types.Panel) -> None:
@classmethod
def poll_check_blender_tab(cls, context):
return tool.Blender.is_tab(context, "BLENDER")
polls = blenderbim.bim.original_scene_panels_polls
# override poll method
if not hasattr(original_panel, "poll"):
polls[original_panel] = None
original_panel.poll = poll_check_blender_tab
else:
polls[original_panel] = original_panel.poll
@classmethod
def wrapped_poll(cls, context):
return polls[cls](context) and poll_check_blender_tab.__func__(cls, context)
original_panel.poll = wrapped_poll
# reregister to activate new poll
bpy.utils.unregister_class(original_panel)
bpy.utils.register_class(original_panel)
@classmethod
def remove_scene_panel_override(cls, panel: bpy.types.Panel) -> None:
polls = blenderbim.bim.original_scene_panels_polls
poll = polls[panel]
if poll is None:
del panel.poll
else:
panel.poll = poll
# panel might be already unregistered during blender exit
# or if it's addon was disabled
if panel.is_registered:
# reregister to activate new poll
bpy.utils.unregister_class(panel)
bpy.utils.register_class(panel)
del polls[panel]
+117
View File
@@ -0,0 +1,117 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import bpy
import json
import ifcopenshell
import blenderbim.core.tool
import blenderbim.tool as tool
from contextlib import contextmanager
from mathutils import Vector
class Clash(blenderbim.core.tool.Clash):
@classmethod
def export_clash_sets(cls):
clash_sets = []
for clash_set in bpy.context.scene.BIMClashProperties.clash_sets:
a = []
b = []
for ab in ["a", "b"]:
for data in getattr(clash_set, ab):
clash_source = {"file": data.name}
query = tool.Search.export_filter_query(data.filter_groups)
if query and data.mode != "a":
clash_source["selector"] = query
clash_source["mode"] = data.mode
if ab == "a":
a.append(clash_source)
elif ab == "b":
b.append(clash_source)
clash_set_data = {"name": clash_set.name, "mode": clash_set.mode, "a": a, "b": b}
if clash_set.mode == "intersection":
clash_set_data["tolerance"] = clash_set.tolerance
clash_set_data["check_all"] = clash_set.check_all
elif clash_set.mode == "collision":
clash_set_data["allow_touching"] = clash_set.allow_touching
elif clash_set.mode == "clearance":
clash_set_data["clearance"] = clash_set.clearance
clash_set_data["check_all"] = clash_set.check_all
clash_sets.append(clash_set_data)
return clash_sets
@classmethod
def get_clash(cls, clash_set, a_global_id, b_global_id):
clashes = clash_set.get("clashes", None)
if not clashes:
return
return clashes.get(f"{a_global_id}-{b_global_id}", None)
@classmethod
def get_clash_set(cls, name):
for clash_set in ClashStore.clash_sets:
if clash_set["name"] == name:
return clash_set
@classmethod
def get_clash_sets(cls):
return ClashStore.clash_sets
@classmethod
def import_active_clashes(cls):
clash_set = bpy.context.scene.BIMClashProperties.active_clash_set
if not clash_set:
return
clash_set.clashes.clear()
result = tool.Clash.get_clash_set(clash_set.name)
for clash in sorted(result.get("clashes", {}).values(), key=lambda x: x["distance"]):
blender_clash = clash_set.clashes.add()
blender_clash.a_global_id = clash["a_global_id"]
blender_clash.b_global_id = clash["b_global_id"]
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
@classmethod
def load_clash_sets(cls, fn):
with open(fn) as f:
ClashStore.clash_sets = json.load(f)
@classmethod
def look_at(cls, target, location):
camera_location = location
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
region = next(region for region in area.regions if region.type == "WINDOW")
space = next(space for space in area.spaces if space.type == "VIEW_3D")
override = {"area": area, "region": region, "space_data": space}
space.region_3d.view_location = target
space.region_3d.view_rotation = Vector((camera_location - target)).to_track_quat("Z", "Y")
space.region_3d.view_distance = (camera_location - target).length
space.shading.show_xray = True
class ClashStore:
clash_sets = None
path = None
@staticmethod
def purge():
ClashStore.clash_sets = None
ClashStore.path = None

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