Compare commits

...

206 Commits

Author SHA1 Message Date
Andrej730 4fdb78aebc spatial.workspace - add hotkeys using add_layout_hotkey 2024-03-12 15:04:49 +05:00
Andrej730 01724913e1 bim.decorate_boundaries to decorate all boundaries in the scene
It's added as alt+v hotkey in Spatial Tool
Now it's possible to combine it with bim.load_project_space_boundaries to show decorations for all the boundaries or to keep the boundaries after Alt+B and just disable the decorations.

example - https://imgur.com/a/ecbPz13
2024-03-12 15:04:49 +05:00
Andrej730 44dd07547e Fix issue with drawing same boundaries multiple times
If you'd hit alt+b in space tool multiple times it would redraw them again and again instead of switching between hiding and showing.
2024-03-12 12:12:12 +05:00
Andrej730 cc157239a2 model.space.py - small fixes 2024-03-12 12:10:56 +05:00
Andrej730 0576914e07 small fixes after d209cc9ff, added a test 2024-03-11 15:15:17 +05:00
Andrej730 a064173ffe BBIM - option to append occurrences from the other ifc files
Example - https://imgur.com/a/yASjbs8
2024-03-11 15:15:17 +05:00
Andrej730 8e502c9bb7 util.unit.py - black format 2024-03-11 15:15:17 +05:00
Andrej730 4d23706145 more typing 2024-03-11 15:15:17 +05:00
Geert Hesselink 6e8de292c6 Redundant closing symbol ' (#4413)
* scale placeangleunit

* add radian si_conversions

* return conversion value of planeangleunit

* Update unit.py

* redundant closing

* redundant closing
2024-03-11 11:12:23 +01:00
Geert Hesselink d209cc9ff4 Scale placeangelunit unit type (#4406)
* scale placeangleunit

* add radian si_conversions

* return conversion value of planeangleunit

* Update unit.py
2024-03-11 08:54:20 +01:00
Andrej730 ddfaf3308b fix error reloading representation for objects without representation 2024-03-11 11:39:18 +05:00
Andrej730 301ddab1ee commented out apil.run settings serialization as it's not used currently 2024-03-11 11:39:18 +05:00
Andrej730 1160d20d33 small fixes 2024-03-11 11:39:18 +05:00
Ryan Schultz 28099ddc8f Add parameter for angle tolerance, and change 'deg' to '°' 2024-03-09 09:53:21 +11:00
Andrej730 c1bca22638 add test for util.unit.calculate_unit_scale 2024-03-08 16:40:43 +05:00
Andrej730 3d5d868812 fix error in typing 2024-03-08 16:40:30 +05:00
fescale b015647543 Replace the specific "libpython3.8" debian package dependency for the generic "libpython3-dev". 2024-03-07 09:59:58 +01:00
Ryan Schultz c54f2131af added python version for Blender 4.1 2024-03-07 09:34:13 +11:00
Andrej730 2a832202fa Fix bug joining multiple extruded profiles in non-meters projects
Demo if someone missed that feature - https://imgur.com/a/JywJHyc
2024-03-06 17:30:55 +05:00
Andrej730 a3d970e014 shape_builder.create_axis2_placement_3d utility method and docs 2024-03-06 17:30:55 +05:00
Andrej730 24912bb827 Fix bug resetting rotations after editing profile axis
Before - https://imgur.com/a/czjAoSb
After - https://imgur.com/a/ajiNcdK
2024-03-06 16:22:15 +05:00
Adrian Insaurralde Avalos 1cd0bc9e7d occt 7.8 compatibility 2024-03-06 09:01:01 +01:00
Dion Moult 8880ab0fe9 Aggressively remove pandas test files 2024-03-06 17:11:33 +11:00
Andrej730 1c052d1417 fix demo library references after 7a7c89e 2024-03-06 10:53:19 +05:00
Dion Moult 20cb6ab007 See #4368. Don't ship with mostly unused and bloaty boto3 dep. Augin users can install boto3 with new debug tool. 2024-03-06 16:46:26 +11:00
Dion Moult e854447714 Little debug tool for people to easily install pip packages graphically
BBIM is being used in companies as ways to automate various BIM processes, and it ends up being such that Blender Python becomes their dev environment.
2024-03-06 16:44:32 +11:00
Dion Moult 7f394a5807 Purge hppfcl in preparation for new clash features 2024-03-05 23:16:47 +11:00
Andrej730 c80b437682 load clipping planes decorator reopening .blend file 2024-03-05 15:01:23 +05:00
Andrej730 b793ab63cc fix poll errors for clipping plane gizmo #4392 2024-03-05 14:37:30 +05:00
Andrej730 8c28f52e6a fix duplicated ifcstyleditems using append_asset
since we don't use `file.add` to add inverse items and we use `file.create_entity` it was duplicating new elements, added optional parameter that makes it possible to track already added relationships between `append_asset`s.
2024-03-05 14:18:18 +05:00
Benoit DAMAY 9d71b7483f Merge pull request #4397 from bdamay/v0.7.0
Fix proposal for # 3515 
OK I've deleted the unnecessary commented lines and left the option that feed only the description tooltip. 

BTW, I have 2 questions i would like to ask you
1. about comment, i noticed there are not many comments in the code. I understand it is deliberate choice of design, is it ? 
I usually comment a lot inside  the code sometimes simply to order my thoughts and remember what the code is intending to do when i come to it later on. 
I understand it is better not to leave unneccessary  lines like here, but in general, do you prefer me to avoid comments when writing in blenderbim ?    

2. As i'm in a discovering phase, I would like to feed some other bl_description operators to provide some additionnal inline information instead (undocumented operator) , is it something you planned or can i go for it. I could simply add the bl_description in each one i find while reading code and testing app.    

Thank you for your kind help. and G'day !
2024-03-05 09:14:06 +01:00
Benoit DAMAY e6e71adba7 Removing unnecessary comment lines 2024-03-05 08:53:56 +01:00
Benoit DAMAY 7ab8a5921b Merge branch 'v0.7.0' of https://github.com/bdamay/IfcOpenShell into v0.7.0 2024-03-04 17:11:54 +01:00
Benoit DAMAY d68d4d54a9 Replaced panel row with operator descriptor 2024-03-04 17:10:19 +01:00
Andrej730 bce0f0d8d4 fix typing loading issues 2024-03-04 18:14:50 +05:00
Andrej730 50648fd350 append_asset - keep preexisting contexts valid #4280
issue occurred with ifcpatch ExtractElements - it was adding project from other ifc file (with the contexts) and then adding some elements from it using `append_asset`. During `append_asset` it was considering existing context with the duplicated one and was partially purging it.
2024-03-04 18:04:24 +05:00
Andrej730 b7e26bacea typing 2024-03-04 18:02:49 +05:00
Andrej730 a2fdd93e6b Optimise patch - lazy import toposort
to fix empty Optimise patch description in BlenderBIM
2024-03-04 18:02:49 +05:00
Dion Moult aef00a4dfc See #4389. Accommodate null pset values when serializing to SVG. 2024-03-04 17:53:53 +11:00
Dion Moult efae8ebdcd Add tree species to tree library 2024-03-04 17:51:48 +11:00
Dion Moult 61403fcf42 See #4389. Accommodate null metadata. 2024-03-04 17:51:07 +11:00
Benoit DAMAY badee8fdc5 Fix proposition to 3515 - extra empty lines removed from previous commit 2024-03-03 19:29:02 +01:00
Benoit DAMAY 36e779e32f Fix proposition to 3515 2024-03-03 19:23:51 +01:00
M!l!nd c50ac3db4f Update Makefile 2024-03-03 23:10:27 +11:00
Dion Moult b17c261344 Minor fix to shape aspects because not everything has a mesh (e.g. text annotations) 2024-03-03 23:09:40 +11:00
Dion Moult 956aae37a8 Fix #4377. Add support for adding filled elements in AXIS3 layered elements. 2024-03-03 23:09:17 +11:00
Dion Moult 48d6cc8a28 Fix #4383. Demo library should use Model/Annotation/MODEL_VIEW for text annotations so that they can be used anywhere, not just on plans. 2024-03-03 16:12:18 +11:00
Dion Moult cf9d3026f4 Fix #4384. Accommodate null values when getting data from pset. 2024-03-03 15:46:55 +11:00
Dion Moult e2f0b1ed5c Fix #4336. Close #4337. Remove rounding for more accurate fills. 2024-03-03 15:36:19 +11:00
Dion Moult b833cffbd3 Minor fixes to IfcCSV documentation 2024-03-03 15:25:15 +11:00
Dion Moult 7a7c89e475 Separate project templates and libraries and provide dropdown to built-in libraries to aid discoverability. 2024-03-03 15:24:46 +11:00
Dion Moult 01b0164e0d Fix #4385. Probably a mistake in the bool argument for removing datablocks. 2024-03-03 14:25:55 +11:00
Dion Moult 13f26d9b70 Tweak generated tree parameters and add manual trees to landscape library thanks to SavyGust27 2024-03-02 22:59:12 +11:00
jgunstone 8a88b0b3a1 🔧update pip install command assuming that this branch will be merged back 2024-03-02 10:56:20 +11:00
jgunstone a15e293db5 update license and version 2024-03-02 10:56:20 +11:00
jgunstone 9509f07da8 📝add docs about so to install and develop this package standalone 2024-03-02 10:56:20 +11:00
jgunstone 37b15de3e4 add simple tests to ensure the package is working as expected 2024-03-02 10:56:20 +11:00
jgunstone 66b4509b06 🐛 add limit and offset kwargs to get_classes. TODO- I think this needs doing throughout... 2024-03-02 10:56:20 +11:00
jgunstone 41dcb83bdb add version 2024-03-02 10:56:20 +11:00
jgunstone 0bda41710c 🔧 add pyproject.toml and __init__.py 2024-03-02 10:56:20 +11:00
Dion Moult cbd85fafda Fix #889. Write docs for how to do translations. 2024-03-01 23:16:05 +11:00
Andrej730 dc7ebab0d1 Clipping planes with cappings
Added new experimental section that allows you to cut objects based on current clipping planes in the scene. The difference from other clipping plane methods - it actually (temporarily) edits meshes geometry and adds cappings at the cut. So, the clipped meshes can be seen both on render and cutted parts won't get in the way as they do with temporary section cutaways (shader based method)

Example - https://imgur.com/a/SOqovOM
2024-03-01 16:22:58 +05:00
Andrej730 0c2a3ff400 remove_data_block - option to skip some checks to speed it up 2024-03-01 16:22:58 +05:00
Dion Moult 6af6fda799 See #4380. Remove support for EOL Python 3.6 2024-03-01 21:04:09 +11:00
Dion Moult 2c9e443425 Fix #4300. You can now select spatial elements by their location too. 2024-03-01 20:02:15 +11:00
Dion Moult 2bede1cdf0 Fix #4340. Indicate user needs to setup IFC2X3 user/app when using API directly. 2024-03-01 19:46:50 +11:00
Dion Moult 4f3397a04e Fix #4333. Ensure order of info is correct for valid IDSes. 2024-03-01 16:42:38 +11:00
Dion Moult 9b8adad8c8 See #4380. Standardise filepaths when linking in IFCs. 2024-03-01 16:32:41 +11:00
Dion Moult c48bccf727 You can now specify a false origin when linking in a model 2024-03-01 15:02:01 +11:00
Dion Moult 5d41336342 Add Python3.12 to installation docs 2024-03-01 12:39:39 +11:00
Dion Moult b8628cc9df See #4380. Bump IOS. 2024-03-01 12:23:42 +11:00
Dion Moult 519cb86163 New cable carrier and cable tool grouped into MEP tool submenu 2024-03-01 12:12:35 +11:00
Dion Moult 0008d8bd2a Add icon and convenience functions for walk and set orbit center in explore tool 2024-03-01 12:12:35 +11:00
Andrej730 bc7946091a bump svgfill 2024-02-29 16:22:08 +05:00
ChristophMellueh ac2db7b4c9 add missing Props request 2024-02-29 21:23:23 +11:00
Dion Moult 0b57d5fc5f You can now flip clipping planes 2024-02-29 20:39:29 +11:00
Dion Moult 82af25d09a New clipping planes feature. 2024-02-29 16:49:23 +11:00
Dion Moult 4a82525bdb You can now query data about a linked object from a SQLite cache. 2024-02-29 16:48:46 +11:00
Dion Moult 002112dd50 Fix #4279. You can now link in IFCs directly with efficient chunking instead of having to first create blend files 2024-02-29 16:42:54 +11:00
Dion Moult 844a9842cf See #4279. Viewport object culling feature to increase FPS when navigating large models. 2024-02-29 16:40:48 +11:00
Dion Moult 3a098e55c4 New IfcPatch recipe to extract flattened properties and relationships to SQLite for simple queries 2024-02-29 16:23:21 +11:00
Dion Moult e186b81f0c Fix #4335. Fix compilation error. 2024-02-29 16:22:39 +11:00
patrick.joerg 527d16c5f0 Small doc change to direct readers to advanced mode 2024-02-28 23:54:05 +11:00
Andrej730 8c1b33e1d5 Fix issues with accessing invalid entities in bim.reassign_class #4371
bim.reassign_class wasn't considering that root.reassign_class will also reassign class for occurrences of the same type and was reaccessing invalid elements
2024-02-28 16:54:11 +05:00
Andrej730 e46f18f686 type.py - some typing 2024-02-28 16:54:11 +05:00
Andrej730 931fd9cc9f reassign_class - cover with tests 2024-02-28 16:54:11 +05:00
Andrej730 aebc073416 fix pass-valid-selected-enumeration.ifc after 5270045
.ifc was failing during test_validate.py
1) Changed schema to rc2 as rc3-4 doesn't have IfcBridgePart
2) PredefinedType and UsageType were mixed up
3) With just `.DECK.` it was failing with error below
```
With attribute:
    <attribute PredefinedType: <select IfcFacilityPartTypeSelect: (<enumeration IfcBridgePartTypeEnum: (ABUTMENT, DECK, DECK_SEGMENT, FOUNDATION, NOTDEFINED, PIER, PIER_SEGMENT, PYLON, SUBSTRUCTURE, SUPERSTRUCTURE, SURFACESTRUCTURE, USERDEFINED)> | <enumeration IfcFacilityPartCommonTypeEnum: (ABOVEGROUND, BELOWGROUND, JUNCTION, LEVELCROSSING, NOTDEFINED, SEGMENT, SUBSTRUCTURE, SUPERSTRUCTURE, TERMINAL, USERDEFINED)> | <enumeration IfcMarinePartTypeEnum: (ABOVEWATERLINE, ANCHORAGE, APPROACHCHANNEL, BELOWWATERLINE, BERTHINGSTRUCTURE, CHAMBER, CILL_LEVEL, COPELEVEL, CORE, CREST, GATEHEAD, GUDINGSTRUCTURE, HIGHWATERLINE, LANDFIELD, LEEWARDSIDE, LOWWATERLINE, MANUFACTURING, NAVIGATIONALAREA, NOTDEFINED, PROTECTION, SHIPTRANSFER, STORAGE, USERDEFINED, VEHICLESERVICING, WATERFIELD, WEATHERSIDE)> | <enumeration IfcRailwayPartTypeEnum: (DILATATIONSUPERSTRUCTURE, LINESIDESTRUCTURE, LINESIDESTRUCTUREPART, NOTDEFINED, PLAINTRACKSUPESTRUCTURE, SUPERSTRUCTURE, TRACKSTRUCTURE, TRACKSTRUCTUREPART, TURNOUTSUPERSTRUCTURE, USERDEFINED)> | <enumeration IfcRoadPartTypeEnum: (BICYCLECROSSING, BUS_STOP, CARRIAGEWAY, CENTRALISLAND, CENTRALRESERVE, HARDSHOULDER, INTERSECTION, LAYBY, NOTDEFINED, PARKINGBAY, PASSINGBAY, PEDESTRIAN_CROSSING, RAILWAYCROSSING, REFUGEISLAND, ROADSEGMENT, ROADSIDE, ROADSIDEPART, ROADWAYPLATEAU, ROUNDABOUT, SHOULDER, SIDEWALK, SOFTSHOULDER, TOLLPLAZA, TRAFFICISLAND, TRAFFICLANE, USERDEFINED)>)>>
Value:
    DECK
Not valid
```
2024-02-28 16:54:11 +05:00
Andrej730 56e00d059b fix error removing pset from material or profile 2024-02-28 16:54:11 +05:00
Andrej730 8a6b4aa5fb fix tests after a971860 2024-02-28 16:54:11 +05:00
Andrej730 46bee8aed9 fix errors in tests
they were failing because import_reference_attributes were failing to get docs for unset schema version since ifc file wasn't attached to Blender
2024-02-28 16:54:11 +05:00
Thomas Krijnen eaa2aa05a4 Add buffer (bytestring) accessors to material colors - plus small fixes 2024-02-28 11:12:17 +01:00
Thomas Krijnen abfa6edc5c Add buffer (bytestring) accessors to geometry data 2024-02-27 16:18:40 +01:00
Andrej730 e3a1c29593 Fix #4365
Allowing to add new properties with None values if should_purge is not set to True (and it's False by default)
2024-02-27 17:33:56 +05:00
Thomas Krijnen e6691989dc #4283 convert closed wires to faces prior to processing in svg serializer 2024-02-26 13:27:36 +01:00
Thomas Krijnen 83845bdd6d Disable building 3.6.x - due to from __future__ import annotations 2024-02-26 13:20:19 +01:00
Andrej730 0f4c19fd2d black format 2024-02-26 11:51:52 +05:00
Andrej730 ce8bdffe57 show occurrence name function in prefs only for custom name style 2024-02-26 11:35:28 +05:00
Andrej730 53e052eb77 Fix #4363 2024-02-26 11:35:21 +05:00
Dion Moult f21b472cfe Update Makefile 2024-02-26 17:10:36 +11:00
Dion Moult 324b440f5c Create blender_manifest.toml 2024-02-26 17:09:31 +11:00
Bruno Perdigão 37a7049a61 UI for linked aggregate (#4271)
* initial ui

* changed the group name

* groups counter in ui

* added operators for selection and breaking link in the ui

* small removal

* changed IFC operators to _execute
2024-02-23 14:56:28 -06:00
Bruno Perdigão 1c1035e251 Linked aggregate preserve names (#4135)
* added simples linked aggregate test

* small fix in remove_old_connections function

* added test for refresh linked aggregate

* refresh linked aggregates now preserves the objects original name

* small fix in recreate_connections function

* tests for Refresh Linked Aggregate

* test for Refresh Linked Aggregate after copying an object

* small deletion

* added custom incremental naming

* small fix in tests

* small fix on the number for renaming
2024-02-23 14:55:18 -06:00
Ryan Schultz d83924b73e Selects all the instances from selected instances that have different types 2024-02-23 12:51:16 -06:00
Massimo Fabbro a04542dfd1 New button hide all spaces in spatial tool 2024-02-23 18:16:15 +01:00
Massimo Fabbro 066616707a Now generate space and generate spaces from walls are always visible in the UI so users understand better that are different procedures 2024-02-23 18:16:15 +01:00
Andrej730 c1cd228911 Fix #4342 2024-02-22 16:19:19 +05:00
Andrej730 515830e52d update representation item style on editing/removing shape aspect
Example - https://imgur.com/a/2SxgSRH
2024-02-22 15:24:09 +05:00
Andrej730 a971860094 style.assign_representation_styles to replace previously assigned styles
as probably in the most cases user would want 1 style per 1 representation item

After this it's finally possible in BBIM to assign styles to representation items through the materials: https://imgur.com/a/cgiJ1KV
2024-02-22 13:41:34 +05:00
Andrej730 f872906e0c MaterialCreator - minor refactor for code readibility 2024-02-22 12:26:12 +05:00
Andrej730 0597100356 fix error in data.py occurring if object has no material 2024-02-21 16:13:56 +05:00
Andrej730 d5feaaa1af show error message removing material that is used in material sets
previously it wasn't showing anything when you would try to remove a material
2024-02-21 15:58:17 +05:00
Andrej730 caca209c8f BBIM - update related representations on assigning style to material 2024-02-21 15:58:17 +05:00
Andrej730 2e45d4a57e unassign_material_style to handle constituents 2024-02-21 15:15:03 +05:00
Andrej730 0d8c86a0f6 api.unassign_representation_styles 2024-02-21 15:14:28 +05:00
Andrej730 7f0df72fec assigning material styles to reuse existing styled item + tests 2024-02-21 15:14:28 +05:00
Andrej730 863a8248af assign_representation_styles to reuse existing styled item + tests 2024-02-21 12:36:06 +05:00
Andrej730 bdc991856c style.assign_material_style small optimization 2024-02-21 11:32:48 +05:00
Andrej730 1a38b7f5c6 set previous active style and context values editing material's style 2024-02-20 16:02:50 +05:00
Andrej730 91199308a9 descriptive error for missing user or application in ifc2x3 #4340 2024-02-20 15:21:48 +05:00
Andrej730 1f7930c5c8 fixed bug migrating attributes to older ifc version (after 8728fa3) 2024-02-20 14:19:10 +05:00
Andrej730 54c7f355ff black format 2024-02-20 12:38:29 +05:00
Andrej730 551f3b0d8d Fix issues editing work time in ifc4x3 #4295 2024-02-20 12:38:29 +05:00
Andrej730 049c796629 IFC4.3 test bootstrap 2024-02-20 12:38:29 +05:00
Andrej730 412700c8fd Default Blender filepath hotkeys for .ifc filepath
Example - https://i.imgur.com/Mk6mkuP.png

E.g. alt-click now opens .ifc directory and shift-click opens .ifc with default software associated with .ifc files.

Should be cross-platform but tested only on Windows

Added this code to tool.Blender so it can be reused for other operators invoking file browsing window.
2024-02-20 12:01:43 +05:00
Andrej730 96a8c1e8ae style.assign_material_style to assign style to related shape aspects 2024-02-19 18:08:18 +05:00
Andrej730 602e18a262 promote get_shape_aspects to util.element method 2024-02-19 18:08:18 +05:00
Andrej730 9e30e1ad31 get material constituents from cache and filter active element material 2024-02-19 18:08:18 +05:00
Andrej730 b6f939f1b1 disable editing rep item style if it comes from material constituent through shape aspect 2024-02-19 18:08:18 +05:00
Thomas Krijnen d964f924d7 CMakeLists.txt pre 2.37 compat 2024-02-19 09:26:18 +01:00
Ryan Schultz 36b702630d Merge pull request #4291 from IfcOpenShell/Assign_Product_In_Any_Order
Select in any order, when assigning relating_product
2024-02-18 13:02:45 -06:00
Thomas Krijnen ccf70c7aad #4012 #2138 #2044 #4032 IfcConvert --keep-bounding-boxes option, default is now exluded. 2024-02-18 14:02:07 +01:00
Andrej730 a2ff63f9ec Fix issue editing object placement that shares it's axis placement with other placements
If multiple object placements were reusing the same IfcAxis2Placement then editing one of them would remove IfcAxis2Placement ignoring it's inverse references making all other object placements invalid.
2024-02-16 18:04:09 +05:00
Andrej730 a82b0f30b0 fix errors getting layer assignments in ifc2x3 #4330 2024-02-16 18:04:09 +05:00
Andrej730 bc2fd232d0 sort representation items in ui based on shape aspect
- display shape aspect along with the repsentation type
- sort representation items based on shape aspects name, items without shape aspects come last
2024-02-15 16:05:14 +05:00
Andrej730 5d7b6ad998 Fix #4319
StringProperty search keyword was introduced only in Blender 3.3
2024-02-15 14:53:11 +05:00
Gorgious56 0e0678b926 Fix section cut planes not working anymore on Blender < 4.0.0 2024-02-15 08:41:42 +01:00
krande 1d4dfd0eb3 add missing typename 2024-02-14 21:49:43 +01:00
Gorgious56 65179db444 #4308 : Fix section cutaway shader links incorrectly linking when removing a section 2024-02-14 14:15:07 +01:00
Gorgious56 5457ee3a40 Fix #4308 : Section cutaway shader : Update API for 4.0 breaking changes + draw line decorator for more than one section cutaway 2024-02-14 13:43:27 +01:00
Andrej730 4f3ba7ae22 some typing 2024-02-14 14:25:55 +05:00
Andrej730 a33a1700dc Fix bug in ExtractElements recipe adding a lot unrelated products
if you'd add a type product and it had material that were used in bunch of ther products
example - https://community.osarch.org/discussion/1965/error-extracting-ifcslab-or-ifccolumn
2024-02-14 14:23:50 +05:00
Massimo Fabbro 8a48769760 Minor fix 2024-02-13 17:15:25 +01:00
Andrej730 7e55872ca8 fix bugs in c5c7363a9 2024-02-13 17:58:12 +05:00
Andrej730 edbe1d80bf fix project name for IfcAdvancedHouse 2024-02-13 13:50:29 +05:00
Andrej730 bd093303c4 UI info message when patch execution was finished 2024-02-13 12:33:36 +05:00
Andrej730 c5c7363a93 ifctester Attribute.filter not to check subtypes attributes
if attribute is already found in the super type
2024-02-13 12:25:32 +05:00
Andrej730 5014fef129 Fix #4314
It was looking for IFC entities with the attribute from Attribute.name but wasn't considering ifc subclasses that inherit from that entity.
2024-02-13 12:24:58 +05:00
Andrej730 d710a01df5 ifctester - some typing 2024-02-13 12:24:58 +05:00
Andrej730 23c65742e1 CMake - disable optimization for RelWithDebInfo with ENABLE_BUILD_OPTIMIZATIONS=Off 2024-02-12 14:23:21 +05:00
Andrej730 d8f52afa6c Fixed #4310 2024-02-12 12:15:27 +05:00
Andrej730 29ce8d5d80 Fix bug removing relating object of IfcRelNests
it was unlinking current collection of the related object and related objects were missing after
2024-02-12 12:01:04 +05:00
Andrej730 5e5de0bbd8 root.remove_product - fix some cases producing invalid ifc #4313
always remove IfcRelNests
1) if product is RelatingObject
2) if product is the only RelatedObject
2024-02-12 11:40:25 +05:00
Ryan Schultz b9b58f5726 proposed fix for #4311 2024-02-11 13:36:23 -06:00
Massimo Fabbro 5c7271ddc8 See #4007. Now clone opening works also when a opening hosts a window or a door 2024-02-11 14:43:12 +01:00
Massimo Fabbro 2db35556e9 Clone opening now deletes also old opening placement 2024-02-11 14:43:12 +01:00
Massimo Fabbro 2449656311 Small refactor clone opening operator in order to better understand and reuse it rebase 2024-02-11 14:43:12 +01:00
Gorgious56 c66cf0a22b Assigning an ifc class to an object now displays a message if the object is not in OBJECT mode 2024-02-09 17:26:48 +01:00
Andrej730 e1a1ba62c5 add fallback to the previous schema versions for docs and mappings
before it was breaking part of BlenderBIM UI if we would open IFC4X1 file
2024-02-09 12:12:02 +05:00
Andrej730 6489f4dc1d bump py version in run-cmake.bat after c0d84713b 2024-02-08 16:39:27 +05:00
Andrej730 beab99bb0d Fix issue building IfcOpenHouse for IFC4 2024-02-07 13:34:19 +01:00
Andrej730 8b50f1bcd6 Fix bug in IfcHierarchyHelper.addRelatedObject
Noticed that `file.addRelatedObject<IfcSchema::IfcRelDefinesByType>(door_style, door);` was resulting in list with `door` assigned as RelatingObject and `door_style` assigned as RelatedObjects.
2024-02-07 17:10:16 +05:00
Andrej730 4617834578 MergeDuplicateTypes - fix error occuring if ifcopenshell.api is not imported
error was:
File "\site-packages\ifcpatch\__init__.py", line 78, in execute
    patcher.patch()
  File "\site-packages\ifcpatch\recipes\MergeDuplicateTypes.py", line 73, in patch
    self.assign_type(element, original_type)
  File "\site-packages\ifcpatch\recipes\MergeDuplicateTypes.py", line 114, in assign_type
    ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": types[0]})
    ^^^^^^^^^^^^^^^^
AttributeError: module 'ifcopenshell' has no attribute 'api'
2024-02-07 12:20:11 +05:00
Andrej730 426dcbee43 ifcopenshell installation notion about compiling only specific schemas 2024-02-07 12:20:11 +05:00
Andrej730 2a12151c07 cmake - clean up trailing spaces 2024-02-07 12:16:55 +05:00
Andrej730 c08958c1bd fix cmake warning with CMP0144 policy
The warning was:
```
CMake Warning (dev) at CMakeLists.txt:299 (find_package):
  Policy CMP0144 is not set: find_package uses upper-case <PACKAGENAME>_ROOT
  variables.  Run "cmake --help-policy CMP0144" for policy details.  Use the
  cmake_policy command to set the policy and suppress this warning.

  CMake variable BOOST_ROOT is set to:

    \IfcOpenShell\_deps\boost_1_74_0

  For compatibility, find_package is ignoring the variable, but code in a
  .cmake module might still use it.
```
2024-02-07 12:16:55 +05:00
Andrej730 e355a33d48 bump required cmake version to remove deprecation warning
deprecation warning:
CMake Deprecation Warning at CMakeLists.txt:20 (cmake_minimum_required):
  Compatibility with CMake < 3.5 will be removed from a future version of
  CMake.

  Update the VERSION argument <min> value or use a ...<max> suffix to tell
  CMake that the project does not need compatibility with older versions.
2024-02-07 12:16:55 +05:00
Andrej730 ece9e2fdd1 build-deps - bump boost version to avoid "missing .b2 issue"
issue was mentioned in #2524 and resulted in error below trying to build boost dependency
```
'.\b2' is not recognized as an internal or external command,
operable program or batch file.
```
2024-02-07 12:16:54 +05:00
Andrej730 c0d84713bc bump python version in windows build dependencies
they seem to switch to .exe from .msi starting from 3.5+, changed amd prefix. Also fixed version parsing line for two digits minor versions
2024-02-07 12:16:54 +05:00
Andrej730 ca9f165b6a ifcpatch.execute - make input attribute optional
e.g. MergeDuplicateTypes  doesn't use it in the code either way
2024-02-07 12:16:54 +05:00
dependabot[bot] 8f9c97a663 Bump fastapi from 0.101.0 to 0.109.1 in /src/opencdeserver/api/app
Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.101.0 to 0.109.1.
- [Release notes](https://github.com/tiangolo/fastapi/releases)
- [Commits](https://github.com/tiangolo/fastapi/compare/0.101.0...0.109.1)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-07 09:31:38 +11:00
marwiss 4ed8310b93 Correct protocol to encrypted HTTPS 2024-02-06 09:55:11 +11:00
marwiss 31be899dc9 Replace existing code in src/opencdeserver with code from the "Kontroll OpenCDE API:s server" POC project on https://github.com/marwiss/Kontroll 2024-02-06 09:55:11 +11:00
Ryan Schultz 4463dd8af9 deleted extraneous variables 2024-02-04 17:35:40 -06:00
Ryan Schultz 95c0ac901f Select in any order, when assigning relating_product 2024-02-04 15:28:18 -06:00
Andrej730 6d7a6eeda7 fixed bug removing representation item
if you'd click on "remove item" button for some item it wouldn't remove the corresponding item but would go for removing active item instead
2024-02-02 14:53:10 +05:00
Andrej730 c520e0eeb5 some refactor for bbim drawings svg serialization
Made some changes to svg serialization code by moving chunks of code so `.generate_linework()` will be easier to read
- moved gathering linework contexts to a separate method
- moved common code for elements serialization to a function
- replaced self.yield_from_iterator(it) with it.__iter__

Functionally code is exactly the same.
2024-02-02 12:56:06 +05:00
Andrej730 73817acdff add a note on get_info_2() #4270 2024-02-01 17:19:35 +05:00
Andrej730 3d7e8d7c6c Fixed #4259
Result of DeepDiff contain ordered sets. Also added fallback for usual sets to lists just in case, should be safe.
2024-01-30 15:41:08 +05:00
Andrej730 a701840826 Fixed error adding a boolean
It occured if active object was IFC object and second selected object was just Blender object.
2024-01-30 14:40:07 +05:00
Andrej730 5de2ac9042 black format 2024-01-30 14:36:35 +05:00
Andrej730 573e2626ad Fix test Model.get_manual_booleans after we added pset to store them 2024-01-30 14:36:35 +05:00
Andrej730 9de59afd49 Fix #4267 and add tests
Crash was caused by unmark_manual_booleans accessing boolean element after they were deleted from IFC
2024-01-30 14:36:35 +05:00
Andrej730 74a9671075 Update ui.py 2024-01-30 14:36:35 +05:00
Andrej730 799658ff41 display active representation presentation layer in UI
example - https://i.imgur.com/mpnoqPH.png
2024-01-30 14:36:35 +05:00
Andrej730 bd64f42a54 remove_representation to consider representation item's layers too
and purge layers if they are out of items as they become invalid
2024-01-30 14:36:35 +05:00
Andrej730 8eab7f155e Small presentation layers UI fixes 2024-01-30 14:36:35 +05:00
Andrej730 5351966430 access layer assignments from inverse attribute instead of going through representation inverses 2024-01-30 14:36:35 +05:00
Andrej730 2663c079f4 test for removing representation items 2024-01-29 16:47:17 +05:00
Andrej730 6747bd9c8f UI to remove representation items
example - https://i.imgur.com/jJDpIQP.png
2024-01-29 16:47:16 +05:00
Andrej730 6ece295be4 tests for creating shape aspects and adding items 2024-01-29 15:52:40 +05:00
Andrej730 64a9f85f25 create new shape aspects from UI
example - https://i.imgur.com/BJJZWrC.png
2024-01-29 15:52:39 +05:00
Andrej730 d8fa853308 edit shape aspect attributes from UI
example - https://i.imgur.com/SzUEn6r.png
2024-01-29 15:52:39 +05:00
Andrej730 ce37c49624 Fix #4262 after 0da3270c7 2024-01-29 11:13:22 +05:00
Andrej730 0da3270c7f UI for changing representation item's shape aspect
Example - https://imgur.com/a/tN7hCYz
2024-01-26 17:32:46 +05:00
Andrej730 fda6fc6c48 geometry.remove_representation documentation 2024-01-26 17:20:39 +05:00
Andrej730 3865f01df9 operator to remove representation item from shape aspect 2024-01-26 17:20:39 +05:00
Andrej730 6bf61d7163 Simple UI to display repr item shape aspect
Example - https://i.imgur.com/YEAE9zU.png
2024-01-26 15:27:05 +05:00
Andrej730 b8ad73668f BBIM to print console errors in case IFC file is failed to load 2024-01-26 14:54:51 +05:00
Andrej730 b7a3dd4164 black format 2024-01-26 14:54:50 +05:00
Bruno Postle 566fd443db IfcGit avoid leaving stray .git/config.lock files 2024-01-25 23:30:00 +00:00
Andrej730 51b5a507cf shape builder docs 2024-01-25 16:46:38 +05:00
Andrej730 a44357dad5 shape builder polyline example and tests 2024-01-25 16:46:34 +05:00
Andrej730 8e792ebb50 UI for changing styles for representation items
Example - https://imgur.com/a/SQh6rR2
2024-01-25 15:17:44 +05:00
Andrej730 352321364c fix couple building docs errors / warnings 2024-01-24 16:38:53 +05:00
Andrej730 2ab32e41b2 shape builder docs fixes 2024-01-24 16:38:53 +05:00
Andrej730 3fbd9d7cdf IFC4 Landscape Library
Created IFC library with simple trees, shrubs and hedges - with both 2d and 3d representations. Example - https://i.imgur.com/pOjh1si.png
Now it's available along with the furniture library when creating new project - https://i.imgur.com/Ho0B5Gn.png
2024-01-24 16:38:52 +05:00
303 changed files with 26607 additions and 4604 deletions
+3
View File
@@ -76,6 +76,9 @@ src/ifcopenshell-python/test/build
# blenderbim libs
src/blenderbim/blenderbim/libs
# blenderbim i18n
src/blenderbim/blenderbim/translations.py
# blenderbim test temp files
src/blenderbim/test/files/temp
src/blenderbim/drawings
+45 -15
View File
@@ -17,7 +17,7 @@
# #
################################################################################
cmake_minimum_required(VERSION 3.1.3)
cmake_minimum_required(VERSION 3.5)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
@@ -27,6 +27,9 @@ cmake_policy(SET CMP0048 NEW)
cmake_policy(SET CMP0074 NEW)
cmake_policy(SET CMP0078 OLD)
cmake_policy(SET CMP0086 NEW)
if (POLICY CMP0144)
cmake_policy(SET CMP0144 NEW) # find_package() uses upper-case <PACKAGENAME>_ROOT variables.
endif()
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release")
@@ -186,7 +189,7 @@ if(GLTF_SUPPORT AND BUILD_CONVERT)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_GLTF)
endif()
# Add USD support to serializers
# Add USD support to serializers
if(USD_SUPPORT)
UNIFY_ENVVARS_AND_CACHE(USD_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(USD_LIBRARY_DIR)
@@ -209,13 +212,13 @@ if(USD_SUPPORT)
endif()
set(USD_LIBRARIES
usd_usd
usd_usd
usd_usdGeom
usd_usdShade
usd_usdLux
usd_vt
usd_sdf
usd_tf
usd_usdShade
usd_usdLux
usd_vt
usd_sdf
usd_tf
usd_gf
)
@@ -373,13 +376,34 @@ if(BUILD_IFCGEOM)
message(STATUS "Looking for Open CASCADE include files in: ${OCC_INCLUDE_DIR}")
endif()
if(OCC_INCLUDE_DIR)
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAJOR
REGEX "#define OCC_VERSION_MAJOR.*"
)
string(REGEX MATCH "[0-9]+" OCC_MAJOR ${OCC_MAJOR})
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MINOR
REGEX "#define OCC_VERSION_MINOR.*"
)
string(REGEX MATCH "[0-9]+" OCC_MINOR ${OCC_MINOR})
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAINT
REGEX "#define OCC_VERSION_MAINTENANCE.*"
)
string(REGEX MATCH "[0-9]+" OCC_MAINT ${OCC_MAINT})
set(OCC_VERSION_STRING "${OCC_MAJOR}.${OCC_MINOR}.${OCC_MAINT}")
endif(OCC_INCLUDE_DIR)
set(OPENCASCADE_LIBRARY_NAMES
TKernel TKMath TKBRep TKGeomBase TKGeomAlgo TKG3d TKG2d TKShHealing TKTopAlgo TKMesh TKPrim TKBool TKBO
TKFillet TKSTEP TKSTEPBase TKSTEPAttr TKXSBase TKSTEP209 TKIGES TKOffset TKHLR
TKFillet TKXSBase TKOffset TKHLR
# @todo investigate the exact conditions when this is necessary
TKBin
)
if(OCC_VERSION_STRING VERSION_LESS 7.8.0)
list(APPEND OPENCASCADE_LIBRARY_NAMES TKIGES TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP)
else(OCC_VERSION_STRING VERSION_LESS 7.8.0)
list(APPEND OPENCASCADE_LIBRARY_NAMES TKDESTEP TKDEIGES)
endif(OCC_VERSION_STRING VERSION_LESS 7.8.0)
if("${OCC_LIBRARY_DIR}" STREQUAL "")
find_library(OCC_LIBRARY TKernel
@@ -435,7 +459,7 @@ if(BUILD_IFCGEOM)
if(OCCT_STATIC)
find_package(Threads)
if(WASM_BUILD)
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
@@ -447,7 +471,7 @@ if(BUILD_IFCGEOM)
set(OPENCASCADE_LIBRARIES -Wl,--start-group ${OPENCASCADE_LIBRARIES} -Wl,--end-group ${CMAKE_THREAD_LIBS_INIT})
endif()
endif()
if (NOT APPLE AND NOT WIN32)
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} "rt")
endif()
@@ -656,6 +680,10 @@ if(ENABLE_BUILD_OPTIMIZATIONS)
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELEASE} -O3")
endif()
else()
if(MSVC)
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /Od")
endif()
endif()
if(MSVC)
@@ -755,6 +783,8 @@ if(NOT SCHEMA_VERSIONS)
endif()
endif()
message(STATUS "IFC SCHEMA_VERSIONS that will be used for the build: ${SCHEMA_VERSIONS}.")
foreach(schema ${SCHEMA_VERSIONS})
add_definitions(-DHAS_SCHEMA_${schema})
endforeach()
@@ -837,7 +867,7 @@ if (BUILD_IFCGEOM)
if (WASM_BUILD)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
else()
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
endif()
endif()
@@ -900,7 +930,7 @@ if(BUILD_IFCGEOM)
add_library(IfcGeom_ifc${schema} STATIC ${IFCGEOM_FILES})
set_target_properties(IfcGeom_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
if (NOT WASM_BUILD)
target_link_libraries(IfcGeom_ifc${schema} IfcParse ${OPENCASCADE_LIBRARIES})
target_link_libraries(IfcGeom_ifc${schema} IfcParse ${OPENCASCADE_LIBRARIES})
endif()
endforeach()
@@ -936,7 +966,7 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
foreach(schema ${SCHEMA_VERSIONS})
add_library(Serializers_ifc${schema} STATIC ${SERIALIZERS_S_FILES})
set_target_properties(Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
if (WASM_BUILD)
target_link_libraries(Serializers_ifc${schema} ${HDF5_LIBRARIES})
else()
@@ -1142,7 +1172,7 @@ endforeach(COMPONENT)
set(CPACK_DEBIAN_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${CPACK_PACKAGE_CONTACT}")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3.8, python3-pytest ${BOOST_DEPS}")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3-dev, python3-pytest ${BOOST_DEPS}")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION_SUMMARY "${CPACK_PACKAGE_DESCRIPTION_SUMMARY}")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "${CPACK_PACKAGE_DESCRIPTION}")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
+1 -1
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.6.14", "3.7.13", "3.8.13", "3.9.11", "3.10.3", "3.11.7", "3.12.1"]
PYTHON_VERSIONS = ["3.7.13", "3.8.13", "3.9.11", "3.10.3", "3.11.7", "3.12.1"]
JSON_VERSION = "v3.6.1"
OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.5.3"
+6 -256
View File
@@ -61,26 +61,6 @@ PYPI_PLATFORM:=--platform manylinux_2_17_x86_64
LXML_ARCH:=manylinux_2_28_x86_64
SHAPELY_ARCH:=manylinux_2_17_x86_64.manylinux2014_x86_64
PILLOW_ARCH:=manylinux_2_28_x86_64
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/linux-64/hpp-fcl-2.4.0-py39h359ec1e_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/linux-64/eigenpy-3.2.0-py39hb1babde_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/linux-64/boost-1.78.0-py39h7c9e3ff_4.tar.bz2
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/linux-64/hpp-fcl-2.4.0-py310hd002f03_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/linux-64/eigenpy-3.2.0-py310hcfe0b05_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/linux-64/boost-1.78.0-py310hc4a4660_4.tar.bz2
endif
ifeq ($(PYVERSION), py311)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/linux-64/hpp-fcl-2.4.0-py311h04f086a_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/linux-64/eigenpy-3.2.0-py311h995a085_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/linux-64/boost-1.78.0-py311h59ea3da_3.tar.bz2
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/linux-64/assimp-5.0.1-hedfc422_6.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/linux-64/qhull-2020.2-h4bd325d_2.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.8/download/linux-64/octomap-1.9.8-h924138e_0.tar.bz2
BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/linux-64/boost-cpp-1.78.0-h6582d0a_3.conda
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/linux-64/zlib-1.2.11-h516909a_1010.tar.bz2
endif
ifeq ($(PLATFORM), macos)
@@ -89,27 +69,13 @@ SHAPELY_ARCH:=macosx_10_9_x86_64
PILLOW_ARCH:=macosx_10_10_x86_64
ifeq ($(PYVERSION), py39)
LXML_ARCH:=macosx_11_0_x86_64
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-64/hpp-fcl-2.4.0-py39h26350b9_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-64/eigenpy-3.2.0-py39h3323527_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-64/boost-1.78.0-py39h953a6b8_4.tar.bz2
endif
ifeq ($(PYVERSION), py310)
LXML_ARCH:=macosx_11_0_x86_64
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-64/hpp-fcl-2.4.0-py310ha216469_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-64/eigenpy-3.2.0-py310h33a8c9b_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-64/boost-1.78.0-py310h3e792ce_4.tar.bz2
endif
ifeq ($(PYVERSION), py311)
LXML_ARCH:=macosx_11_0_universal2
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-64/hpp-fcl-2.4.0-py311hebdd1a2_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-64/eigenpy-3.2.0-py311h62e90b6_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-64/boost-1.78.0-py311h349b758_4.tar.bz2
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-64/assimp-5.0.1-h1224e73_6.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-64/qhull-2020.2-h940c156_2.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.8/download/osx-64/octomap-1.9.8-hb8565cd_0.tar.bz2
BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/osx-64/boost-cpp-1.78.0-hf5ba120_3.conda
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/osx-64/zlib-1.2.11-h7795811_1010.tar.bz2
endif
ifeq ($(PLATFORM), macosm1)
@@ -117,26 +83,6 @@ PYPI_PLATFORM:=--platform macosx_11_0_arm64
LXML_ARCH:=macosx_11_0_universal2
SHAPELY_ARCH:=macosx_11_0_arm64
PILLOW_ARCH:=macosx_11_0_arm64
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-arm64/hpp-fcl-2.4.0-py39h82b2ff2_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-arm64/eigenpy-3.2.0-py39h3a9e884_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py39h99de9ae_4.tar.bz2
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-arm64/hpp-fcl-2.4.0-py310hdb6c3eb_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-arm64/eigenpy-3.2.0-py310h96e9823_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py310h629746b_4.tar.bz2
endif
ifeq ($(PYVERSION), py311)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-arm64/hpp-fcl-2.4.0-py311hc0c29b9_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-arm64/eigenpy-3.2.0-py311h24fa84d_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py311h5c64717_4.tar.bz2
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-arm64/assimp-5.0.1-h0f81e16_7.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-arm64/qhull-2020.2-hc021e02_2.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.8/download/osx-arm64/octomap-1.9.8-hffc8910_0.tar.bz2
BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/osx-arm64/boost-cpp-1.78.0-h9ed8d21_3.conda
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/osx-arm64/zlib-1.2.11-h90dfc92_1014.tar.bz2
endif
ifeq ($(PLATFORM), win)
@@ -144,26 +90,6 @@ PYPI_PLATFORM:=--platform win_amd64
LXML_ARCH:=win_amd64
SHAPELY_ARCH:=win_amd64
PILLOW_ARCH:=win_amd64
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/win-64/hpp-fcl-2.4.0-py39hf3ebc58_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/win-64/eigenpy-3.2.0-py39ha1f6a47_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/win-64/boost-1.78.0-py39hea4d8d1_4.tar.bz2
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/win-64/hpp-fcl-2.4.0-py310h1bf3f0a_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/win-64/eigenpy-3.2.0-py310h7fb871d_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/win-64/boost-1.78.0-py310h220cb41_4.tar.bz2
endif
ifeq ($(PYVERSION), py311)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/win-64/hpp-fcl-2.4.0-py311hd263b2c_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/win-64/eigenpy-3.2.0-py311hd354db1_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/win-64/boost-1.78.0-py311h12feb9d_3.tar.bz2
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.2.5/download/win-64/assimp-5.2.5-h4dcb625_0.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/win-64/qhull-2020.2-h70d2c02_2.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.8/download/win-64/octomap-1.9.8-h91493d7_0.tar.bz2
BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/win-64/boost-cpp-1.78.0-h9f4b32c_3.conda
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/win-64/zlib-1.2.11-h62dcd97_1010.tar.bz2
endif
LXML_NAME:=lxml
@@ -211,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-6c9e130-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-eaa2aa0-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -427,6 +353,8 @@ endif
cd dist/working && mkdir site-packages
# Required by IfcCSV
cd dist/working && . env/bin/activate && $(PIP) install pandas $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --target=./site-packages
# Pandas ships with very heavy tests
cd dist/working/site-packages/pandas && rm -rf tests
cd dist/working && . env/bin/activate && $(PIP) install openpyxl --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install odfpy --target=./site-packages
# Required by IFCCityJSON
@@ -438,157 +366,6 @@ endif
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by IFCClash
mkdir dist/working
cd dist/working && wget $(HPPFCL_URL)
cd dist/working && unzip hpp-fcl*
cd dist/working && tar -I zstd -xvf pkg*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Lib/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(ASSIMP_URL)
cd dist/working && tar -xf assimp*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(QHULL_URL)
cd dist/working && tar -xf qhull*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(EIGENPY_URL)
cd dist/working && unzip eigenpy*
cd dist/working && tar -I zstd -xvf pkg*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(OCTOMAP_URL)
cd dist/working && tar -xf octomap*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(BOOST_URL)
cd dist/working && tar -xf boost*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(BOOSTCPP_URL)
cd dist/working && unzip boost-cpp*
cd dist/working && tar -I zstd -xvf pkg*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/libboost_serialization.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/libboost_serialization.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/libboost_serialization.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/boost_serialization.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(ZLIB_URL)
cd dist/working && tar -xf zlib*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by BIMTester
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/c8/4b/d0a8c23b6c8985e5544ea96d27105a273ea22051317f850c2cdbf2029fe4/behave-1.2.6.tar.gz
@@ -636,34 +413,6 @@ endif
cd dist/working/parse_type-0.5.2/ && cp -r parse_type ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Required by augin
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/76/b4/b7baffbda025efd5dc8fcd8d2e953e3aa939c236a484084fa8f4c3588ee9/boto3-1.17.17.tar.gz
cd dist/working && tar -xzvf boto3*
cd dist/working/boto3-1.17.17/ && cp -r boto3 ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Required by boto3
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/7f/2e/e02fdfd0f0377c4e44e61de27b05d5cfe93575770661ef9ded80ed90fa88/botocore-1.20.17.tar.gz
cd dist/working && tar -xzvf botocore*
cd dist/working/botocore-1.20.17/ && cp -r botocore ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Required by boto3
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/3c/56/3f325b1eef9791759784aa5046a8f6a1aff8f7c898a2e34506771d3b99d8/jmespath-0.10.0.tar.gz
cd dist/working && tar -xzvf jmespath*
cd dist/working/jmespath-0.10.0/ && cp -r jmespath ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Required by boto3
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/08/e1/3ee2096ebaeeb8c186d20ed16c8faf4a503913e5c9a0e14cd6b8ffc405a3/s3transfer-0.3.4.tar.gz
cd dist/working && tar -xzvf s3transfer*
cd dist/working/s3transfer-0.3.4/ && cp -r s3transfer ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Provides IFCJSON functionality
mkdir dist/working
cd dist/working && wget https://github.com/IFCJSON-Team/IFC2JSON_python/archive/master.zip
@@ -696,9 +445,9 @@ endif
# Required for Desktop icon and file association
cp -r blenderbim/libs/desktop dist/blenderbim/libs/
# generate translations module for BBIM build
# Generate translations module for BBIM build
git clone https://github.com/IfcOpenShell/blenderbim-translations.git dist/working
python scripts/bbim_translations.py -i "dist/working" -o "dist/blenderbim"
$(PYTHON) scripts/bbim_translations.py -i "dist/working" -o "dist/blenderbim"
rm -rf dist/working
# Remove dependencies also bundled with Blender
@@ -706,6 +455,7 @@ endif
rm -rf dist/blenderbim/libs/site/packages/numpy.libs
cd dist/blenderbim && $(SED) "s/999999/$(VERSION)/" __init__.py
cd dist/blenderbim && $(SED) "s/999999/$(VERSION)/" blender_manifest.toml
cd dist/blenderbim/bim && $(SED) "s/8888888/$(LAST_COMMIT_HASH)/" __init__.py
cd dist && zip -r blenderbim-$(VERSION)-$(PYVERSION)-$(PLATFORM).zip ./*
rm -rf dist/blenderbim
@@ -99,6 +99,7 @@ classes = [
operator.BIM_OT_open_webbrowser,
operator.BIM_OT_remove_section_plane,
operator.BIM_OT_show_description,
operator.ClippingPlaneCutWithCappings,
operator.EditBlenderCollection,
operator.FileAssociate,
operator.FileUnassociate,
@@ -106,6 +107,7 @@ classes = [
operator.OpenUri,
operator.ReloadIfcFile,
operator.RemoveIfcFile,
operator.RevertClippingPlaneCut,
operator.SelectDataDir,
operator.SelectIfcFile,
operator.SelectSchemaDir,
@@ -175,6 +177,7 @@ classes = [
ui.BIM_PT_tab_sandbox,
# TODO: move this somewhere else and clean it up
ui.BIM_PT_section_plane,
ui.BIM_PT_section_with_cappings,
]
for mod in modules.values():
@@ -1,17 +1,17 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
FILE_NAME('/dev/null','2023-09-07T13:40:06+10:00',(),(),'IfcOpenShell v0.7.0-fbd8ea1ed','IfcOpenShell v0.7.0-fbd8ea1ed','Nobody');
FILE_NAME('/dev/null','2024-03-03T16:03:58+11:00',(),(),'IfcOpenShell v0.7.0-eaa2aa05a','IfcOpenShell v0.7.0-eaa2aa05a','Nobody');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('1arVsMni92iPUaaryGTUz2',$,'BlenderBIM Demo',$,$,$,$,(#12,#16),#7);
#2=IFCPROJECTLIBRARY('1FfDG5b21DuBDOO4eUE9rF',$,'BlenderBIM Demo Library',$,$,$,$,$,$);
#3=IFCRELDECLARES('3tfDIC3Tf9wP8EUOoFeycK',$,$,$,#1,(#2));
#1=IFCPROJECT('3NJUxHyTzEv96oezyDYEqf',$,'BlenderBIM Demo',$,$,$,$,(#12,#16),#7);
#2=IFCPROJECTLIBRARY('0D39k7hbT5ce$jWaCd2niG',$,'BlenderBIM Demo Library',$,$,$,$,$,$);
#3=IFCRELDECLARES('2RWVohyP57PBHT9zCs4H2O',$,$,$,#1,(#2));
#4=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#5=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#6=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#7=IFCUNITASSIGNMENT((#5,#6,#4));
#7=IFCUNITASSIGNMENT((#6,#4,#5));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCDIRECTION((0.,0.,1.));
#10=IFCDIRECTION((1.,0.,0.));
@@ -23,84 +23,84 @@ DATA;
#16=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Plan',2,1.E-05,#15,$);
#17=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#12,$,.MODEL_VIEW.,$);
#18=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Plan',*,*,*,*,#16,$,.PLAN_VIEW.,$);
#19=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#16,$,.PLAN_VIEW.,$);
#19=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#16,$,.MODEL_VIEW.,$);
#20=IFCMATERIAL('Unknown',$,$);
#21=IFCWALLTYPE('2o8NqQQbHA5BeErrgwfr7d',$,'WAL50',$,$,$,$,$,$,.NOTDEFINED.);
#21=IFCWALLTYPE('2F1k78lI53fwS0rg3rqrRb',$,'WAL50',$,$,$,$,$,$,.NOTDEFINED.);
#22=IFCMATERIALLAYERSET((#24),$,$);
#23=IFCRELASSOCIATESMATERIAL('1Q$5DZKr98n82Eitz2dWey',$,$,$,(#21),#22);
#23=IFCRELASSOCIATESMATERIAL('0khP_Smdj12f2JrOTRbRKj',$,$,$,(#21),#22);
#24=IFCMATERIALLAYER(#20,0.05,$,$,$,$,$);
#25=IFCRELDECLARES('1KXKUEn8z0g8lvyEsCTWD3',$,$,$,#2,(#38,#82,#77,#42,#26,#48,#72,#59,#92,#30,#96,#63,#21,#54,#87,#153,#34,#67,#210));
#26=IFCWALLTYPE('3KlIOv_P9A79tkt8D_jq_m',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#26,#48,#72,#59,#92,#30,#96,#63,#21,#54,#87,#210,#153,#34,#67,#38,#82,#77,#42));
#26=IFCWALLTYPE('3R5onkTtX1IxdCMPoThRaw',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
#27=IFCMATERIALLAYERSET((#29),$,$);
#28=IFCRELASSOCIATESMATERIAL('32B9Wx8Z1FxhIN7m77MiHG',$,$,$,(#26),#27);
#28=IFCRELASSOCIATESMATERIAL('0$mXRPCT16nP6Q70XFNvZv',$,$,$,(#26),#27);
#29=IFCMATERIALLAYER(#20,0.1,$,$,$,$,$);
#30=IFCWALLTYPE('2DqrSeel1AGw6h3b$ujafZ',$,'WAL200',$,$,$,$,$,$,.NOTDEFINED.);
#30=IFCWALLTYPE('1juanculbDJPqvqT2FCm8m',$,'WAL200',$,$,$,$,$,$,.NOTDEFINED.);
#31=IFCMATERIALLAYERSET((#33),$,$);
#32=IFCRELASSOCIATESMATERIAL('2FeYUpt3D4tAvwgGmwXWZn',$,$,$,(#30),#31);
#32=IFCRELASSOCIATESMATERIAL('0FsB2A$frFn9AZW2IG38ny',$,$,$,(#30),#31);
#33=IFCMATERIALLAYER(#20,0.2,$,$,$,$,$);
#34=IFCWALLTYPE('2jNdEOFIP1eQb3WZePlFGY',$,'WAL300',$,$,$,$,$,$,.NOTDEFINED.);
#34=IFCWALLTYPE('05UYduR9r1vRxZZgP64Rdw',$,'WAL300',$,$,$,$,$,$,.NOTDEFINED.);
#35=IFCMATERIALLAYERSET((#37),$,$);
#36=IFCRELASSOCIATESMATERIAL('2bFeM6xevAwucuwMlauX77',$,$,$,(#34),#35);
#36=IFCRELASSOCIATESMATERIAL('2FYhYrvmv4SBoZHVWDTTte',$,$,$,(#34),#35);
#37=IFCMATERIALLAYER(#20,0.3,$,$,$,$,$);
#38=IFCCOVERINGTYPE('3310gCgH59w9tu$DfmiCEN',$,'COV10',$,$,$,$,$,$,.NOTDEFINED.);
#38=IFCCOVERINGTYPE('0iLxgfHB9F2hRH2vMcw7Yv',$,'COV10',$,$,$,$,$,$,.NOTDEFINED.);
#39=IFCMATERIALLAYERSET((#41),$,$);
#40=IFCRELASSOCIATESMATERIAL('3pTNmWd_jBoOWyHc8Yf4qI',$,$,$,(#38),#39);
#40=IFCRELASSOCIATESMATERIAL('0yvFX0zgb9uvJ3ARnf4mfe',$,$,$,(#38),#39);
#41=IFCMATERIALLAYER(#20,0.01,$,$,$,$,$);
#42=IFCCOVERINGTYPE('2sksWFuPzCgeNk1ofXS6PZ',$,'COV20',$,$,(#46),$,$,$,.NOTDEFINED.);
#42=IFCCOVERINGTYPE('15Riw2WTrAUPwvfLyKXJ2g',$,'COV20',$,$,(#46),$,$,$,.NOTDEFINED.);
#43=IFCMATERIALLAYERSET((#45),$,$);
#44=IFCRELASSOCIATESMATERIAL('36rl5NddT1ux6ejkQAkT2z',$,$,$,(#42),#43);
#44=IFCRELASSOCIATESMATERIAL('1iAOG6NK9FBxitFtzN9qP5',$,$,$,(#42),#43);
#45=IFCMATERIALLAYER(#20,0.02,$,$,$,$,$);
#46=IFCPROPERTYSET('2VZJ9xfdn1NeOY6uEmnnYW',$,'EPset_Parametric',$,(#47));
#46=IFCPROPERTYSET('18ICCZ0fjDG9UXMyzrsFBA',$,'EPset_Parametric',$,(#47));
#47=IFCPROPERTYSINGLEVALUE('LayerSetDirection',$,IFCLABEL('AXIS2'),$);
#48=IFCCOVERINGTYPE('1uU6rxt95AC91lZ0jTRG6G',$,'COV30',$,$,(#52),$,$,$,.NOTDEFINED.);
#48=IFCCOVERINGTYPE('0gEOtYULD0F9KD9zPoA6cJ',$,'COV30',$,$,(#52),$,$,$,.NOTDEFINED.);
#49=IFCMATERIALLAYERSET((#51),$,$);
#50=IFCRELASSOCIATESMATERIAL('2lFv4xQYr5bwtiSsMTVCgz',$,$,$,(#48),#49);
#50=IFCRELASSOCIATESMATERIAL('0xd6dsjf13JA98q9fnAMpQ',$,$,$,(#48),#49);
#51=IFCMATERIALLAYER(#20,0.03,$,$,$,$,$);
#52=IFCPROPERTYSET('0HnrRhcd16Yfbgtci4sqCG',$,'EPset_Parametric',$,(#53));
#52=IFCPROPERTYSET('3qCbs1tuDCvuOXam5RrA$a',$,'EPset_Parametric',$,(#53));
#53=IFCPROPERTYSINGLEVALUE('LayerSetDirection',$,IFCLABEL('AXIS3'),$);
#54=IFCRAMPTYPE('3nonXCVn58jhVV08N8c97B',$,'RAM200',$,$,$,$,$,$,.NOTDEFINED.);
#54=IFCRAMPTYPE('2yW1ePlyb8cOR6HdEntpRh',$,'RAM200',$,$,$,$,$,$,.NOTDEFINED.);
#55=IFCMATERIALLAYERSET((#57),$,$);
#56=IFCRELASSOCIATESMATERIAL('2NdIrmbUP6bh4x$p_RY31C',$,$,$,(#54),#55);
#56=IFCRELASSOCIATESMATERIAL('1x9ZyVim19AQtJhfEE06XR',$,$,$,(#54),#55);
#57=IFCMATERIALLAYER(#20,0.2,$,$,$,$,$);
#58=IFCCIRCLEPROFILEDEF(.AREA.,$,$,0.3);
#59=IFCPILETYPE('2uuf5kq5TDDfO9jGN7XIHh',$,'P1',$,$,$,$,$,$,.NOTDEFINED.);
#59=IFCPILETYPE('25MTkMtaH5QeCf$gFbvSSw',$,'P1',$,$,$,$,$,$,.NOTDEFINED.);
#60=IFCMATERIALPROFILESET($,$,(#62),$);
#61=IFCRELASSOCIATESMATERIAL('3hi821Ffj8YR9yFjp226LU',$,$,$,(#59),#60);
#61=IFCRELASSOCIATESMATERIAL('09FXUZCnrFEffxBrtg16gx',$,$,$,(#59),#60);
#62=IFCMATERIALPROFILE($,$,#20,#58,$,$);
#63=IFCSLABTYPE('2janmz3nH4P9IHaOVvGY_B',$,'FLR150',$,$,$,$,$,$,.NOTDEFINED.);
#63=IFCSLABTYPE('1Akbal7tP5DAPW0ti$Qp30',$,'FLR150',$,$,$,$,$,$,.NOTDEFINED.);
#64=IFCMATERIALLAYERSET((#66),$,$);
#65=IFCRELASSOCIATESMATERIAL('0og5Zg4ib67g6XWgOgqBWy',$,$,$,(#63),#64);
#65=IFCRELASSOCIATESMATERIAL('0jzkXlkBL4HhmQ_FTWW6Cj',$,$,$,(#63),#64);
#66=IFCMATERIALLAYER(#20,0.2,$,$,$,$,$);
#67=IFCSLABTYPE('26UKpEaTb9fh4qrqi3Ymzj',$,'FLR250',$,$,$,$,$,$,.NOTDEFINED.);
#67=IFCSLABTYPE('0$gpYNi3T08BrxaR67CxgI',$,'FLR250',$,$,$,$,$,$,.NOTDEFINED.);
#68=IFCMATERIALLAYERSET((#70),$,$);
#69=IFCRELASSOCIATESMATERIAL('1BeBXPqen9TQEE42SxzhjF',$,$,$,(#67),#68);
#69=IFCRELASSOCIATESMATERIAL('3a6kjEM29DleYTlSkA8_DK',$,$,$,(#67),#68);
#70=IFCMATERIALLAYER(#20,0.3,$,$,$,$,$);
#71=IFCRECTANGLEPROFILEDEF(.AREA.,'500x600',$,0.5,0.6);
#72=IFCCOLUMNTYPE('1KnWzBBoLCNfJ3_J79LT7d',$,'C1',$,$,$,$,$,$,.NOTDEFINED.);
#72=IFCCOLUMNTYPE('278sjptdDDzgOZseERj390',$,'C1',$,$,$,$,$,$,.NOTDEFINED.);
#73=IFCMATERIALPROFILESET($,$,(#75),$);
#74=IFCRELASSOCIATESMATERIAL('1W8OWQjEL05gH5hEpve6Lh',$,$,$,(#72),#73);
#74=IFCRELASSOCIATESMATERIAL('1ppYXw39v6kBwi3rrIm4ZE',$,$,$,(#72),#73);
#75=IFCMATERIALPROFILE($,$,#20,#71,$,$);
#76=IFCCIRCLEHOLLOWPROFILEDEF(.AREA.,'500.0x5.0 CHS',$,0.25,0.005);
#77=IFCCOLUMNTYPE('33jPFERfL9bfTeWfTL5kZ7',$,'C2',$,$,$,$,$,$,.NOTDEFINED.);
#77=IFCCOLUMNTYPE('3aSZOvGmr7jggSNwsq5PJE',$,'C2',$,$,$,$,$,$,.NOTDEFINED.);
#78=IFCMATERIALPROFILESET($,$,(#80),$);
#79=IFCRELASSOCIATESMATERIAL('3czOrQw2v9BfS0PeQCrzWq',$,$,$,(#77),#78);
#79=IFCRELASSOCIATESMATERIAL('1AtNJPaD14DgINbAjXvbU4',$,$,$,(#77),#78);
#80=IFCMATERIALPROFILE($,$,#20,#76,$,$);
#81=IFCRECTANGLEHOLLOWPROFILEDEF(.AREA.,'150x75x2.0 RHS',$,0.075,0.15,0.002,0.005,0.005);
#82=IFCCOLUMNTYPE('21YcNdKNj2$95U55yNx1Nw',$,'C3',$,$,$,$,$,$,.NOTDEFINED.);
#82=IFCCOLUMNTYPE('28iv3Kru12yQ7R7RRwNjvD',$,'C3',$,$,$,$,$,$,.NOTDEFINED.);
#83=IFCMATERIALPROFILESET($,$,(#85),$);
#84=IFCRELASSOCIATESMATERIAL('3n6Cru3xr6jvDptU9u7QEF',$,$,$,(#82),#83);
#84=IFCRELASSOCIATESMATERIAL('2M_oL$n3j6rBRnOW5RSk87',$,$,$,(#82),#83);
#85=IFCMATERIALPROFILE($,$,#20,#81,$,$);
#86=IFCISHAPEPROFILEDEF(.AREA.,'DEMO-I',$,0.1,0.2,0.005,0.01,0.005,$,$);
#87=IFCBEAMTYPE('2CsGpD$6nDCxWv9jXTtvq9',$,'B1',$,$,$,$,$,$,.NOTDEFINED.);
#87=IFCBEAMTYPE('2E$V5l4b54dxuToJ1A6IHp',$,'B1',$,$,$,$,$,$,.NOTDEFINED.);
#88=IFCMATERIALPROFILESET($,$,(#90),$);
#89=IFCRELASSOCIATESMATERIAL('2LJExc74j9rgBSBx51HDPK',$,$,$,(#87),#88);
#89=IFCRELASSOCIATESMATERIAL('3PUNnY7cj8LhyXbHKVqZnf',$,$,$,(#87),#88);
#90=IFCMATERIALPROFILE($,$,#20,#86,$,$);
#91=IFCCSHAPEPROFILEDEF(.AREA.,'DEMO-C',$,0.2,0.1,0.0015,0.03,0.005);
#92=IFCBEAMTYPE('32pHN2b0P0awGw$U8PpneO',$,'B2',$,$,$,$,$,$,.NOTDEFINED.);
#92=IFCBEAMTYPE('3qutoZTvP0lvLWNXyjPhPm',$,'B2',$,$,$,$,$,$,.NOTDEFINED.);
#93=IFCMATERIALPROFILESET($,$,(#95),$);
#94=IFCRELASSOCIATESMATERIAL('2wg8R1Drb2xhQ2Y_pRSh3c',$,$,$,(#92),#93);
#94=IFCRELASSOCIATESMATERIAL('3dGyQf42H2ahch2bSYPPHP',$,$,$,(#92),#93);
#95=IFCMATERIALPROFILE($,$,#20,#91,$,$);
#96=IFCWINDOWTYPE('1Gg9HfZEr69u8SdWfDs2J3',$,'WT01',$,$,$,(#135,#152),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
#96=IFCWINDOWTYPE('0bK3c4PWL3eOMNwkPN$rlg',$,'WT01',$,$,$,(#135,#152),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
#97=IFCINDEXEDPOLYGONALFACE((13,17,18,14));
#98=IFCINDEXEDPOLYGONALFACE((5,6,3,4));
#99=IFCINDEXEDPOLYGONALFACE((7,8,2,1));
@@ -157,7 +157,7 @@ DATA;
#150=IFCDIRECTION((0.,0.,1.));
#151=IFCAXIS2PLACEMENT3D(#148,#150,#149);
#152=IFCREPRESENTATIONMAP(#151,#147);
#153=IFCDOORTYPE('0NBUmPKyT9WecsIeYJrEqg',$,'DT01',$,$,$,(#196,#209),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
#153=IFCDOORTYPE('2j0vIBgkH0ERnBCP0cpcCz',$,'DT01',$,$,$,(#196,#209),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
#154=IFCINDEXEDPOLYGONALFACE((17,16,15,14));
#155=IFCINDEXEDPOLYGONALFACE((2,3,29,28));
#156=IFCINDEXEDPOLYGONALFACE((27,28,29,30,32,31));
@@ -214,7 +214,7 @@ DATA;
#207=IFCDIRECTION((0.,0.,1.));
#208=IFCAXIS2PLACEMENT3D(#205,#207,#206);
#209=IFCREPRESENTATIONMAP(#208,#204);
#210=IFCFURNITURETYPE('3Kyc6IyarAUw3_8fkNtXIg',$,'BUN01',$,$,$,(#931,#952),$,$,.NOTDEFINED.,.NOTDEFINED.);
#210=IFCFURNITURETYPE('02XxQ_3oT0SPrmFPATrt7o',$,'BUN01',$,$,$,(#931,#952),$,$,.NOTDEFINED.,.NOTDEFINED.);
#211=IFCINDEXEDPOLYGONALFACE((187,278,44));
#212=IFCINDEXEDPOLYGONALFACE((21,52,60));
#213=IFCINDEXEDPOLYGONALFACE((91,100,31));
@@ -957,35 +957,35 @@ DATA;
#950=IFCDIRECTION((0.,0.,1.));
#951=IFCAXIS2PLACEMENT3D(#948,#950,#949);
#952=IFCREPRESENTATIONMAP(#951,#947);
#953=IFCTYPEPRODUCT('0TBMBnD_b66QLUWKD9HLsc',$,'SETOUT-POINT',$,'IfcAnnotation/SYMBOL',(#954),$,$);
#954=IFCPROPERTYSET('2dQzX_K4r1Xet6dz9zBlgs',$,'EPset_Annotation',$,(#955));
#953=IFCTYPEPRODUCT('0UrP0fLdD5OwzwD41aRKao',$,'SETOUT-POINT',$,'IfcAnnotation/SYMBOL',(#954),$,$);
#954=IFCPROPERTYSET('27lmSbeAXC08EWkEq8XdUG',$,'EPset_Annotation',$,(#955));
#955=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('setout-point'),$);
#956=IFCTYPEPRODUCT('0vo_7PU3H9ygTnrIipqPRI',$,'CONTROL-POINT',$,'IfcAnnotation/SYMBOL',(#957),$,$);
#957=IFCPROPERTYSET('3APQOw$FP1ivxp08UxHsl6',$,'EPset_Annotation',$,(#958));
#956=IFCTYPEPRODUCT('3IQrQOSFP0WfkuLk0ak7_0',$,'CONTROL-POINT',$,'IfcAnnotation/SYMBOL',(#957),$,$);
#957=IFCPROPERTYSET('23aN9DsdjFcfWq8KGSIVoN',$,'EPset_Annotation',$,(#958));
#958=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('control-point'),$);
#959=IFCTYPEPRODUCT('32V27G3$T1OO3TbXg6948F',$,'TRAVERSE-POINT',$,'IfcAnnotation/SYMBOL',(#960),$,$);
#960=IFCPROPERTYSET('0cTSYXMc9B$PTXVDrhBYYW',$,'EPset_Annotation',$,(#961));
#959=IFCTYPEPRODUCT('3wvbaiaIz8agQgDRzt01vz',$,'TRAVERSE-POINT',$,'IfcAnnotation/SYMBOL',(#960),$,$);
#960=IFCPROPERTYSET('1j7aFM1Rb9BRuKgUEn1U10',$,'EPset_Annotation',$,(#961));
#961=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('traverse-point'),$);
#962=IFCTYPEPRODUCT('2PXIC7Bg914gJhRh2XeGnN',$,'DASHED',$,'IfcAnnotation/LINEWORK',(#963),$,$);
#963=IFCPROPERTYSET('0RiYsOxp529gXnCkw44wF8',$,'EPset_Annotation',$,(#964));
#962=IFCTYPEPRODUCT('0p5ZTfTnX9ZOywroa7Ffql',$,'DASHED',$,'IfcAnnotation/LINEWORK',(#963),$,$);
#963=IFCPROPERTYSET('27smSHyiv39vhBRSwfDVCD',$,'EPset_Annotation',$,(#964));
#964=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('dashed'),$);
#965=IFCTYPEPRODUCT('1Ou1kA3Vb4HhuNBvM0uGe0',$,'FINE',$,'IfcAnnotation/LINEWORK',(#966),$,$);
#966=IFCPROPERTYSET('2gzly9D0L1qPK2MExeXgRO',$,'EPset_Annotation',$,(#967));
#965=IFCTYPEPRODUCT('2EvrG9Vuf6t9etgMeWFuJ2',$,'FINE',$,'IfcAnnotation/LINEWORK',(#966),$,$);
#966=IFCPROPERTYSET('0gVTuDcZ5ByRdR3sZnEZUR',$,'EPset_Annotation',$,(#967));
#967=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('fine'),$);
#968=IFCTYPEPRODUCT('3lx$KQPRbEZwbQ7Xdfm5gw',$,'THIN',$,'IfcAnnotation/LINEWORK',(#969),$,$);
#969=IFCPROPERTYSET('0vsVpqs6zArvA2bfBtvQew',$,'EPset_Annotation',$,(#970));
#968=IFCTYPEPRODUCT('0IeM1ywXn1qhR_6NhB6N4s',$,'THIN',$,'IfcAnnotation/LINEWORK',(#969),$,$);
#969=IFCPROPERTYSET('0t6c$uCeT9092GYkbm8hDS',$,'EPset_Annotation',$,(#970));
#970=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('thin'),$);
#971=IFCTYPEPRODUCT('00j$y97p903w2HOb35lAQ2',$,'MEDIUM',$,'IfcAnnotation/LINEWORK',(#972),$,$);
#972=IFCPROPERTYSET('240rEBDOn8PAvfnm_43s55',$,'EPset_Annotation',$,(#973));
#971=IFCTYPEPRODUCT('1jZVbCwrTCGhZdKbH4uqTP',$,'MEDIUM',$,'IfcAnnotation/LINEWORK',(#972),$,$);
#972=IFCPROPERTYSET('2uTJ11lF98GeN8pPIf340N',$,'EPset_Annotation',$,(#973));
#973=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('medium'),$);
#974=IFCTYPEPRODUCT('2tVdFGorj6dfrL4uA1kyW8',$,'THICK',$,'IfcAnnotation/LINEWORK',(#975),$,$);
#975=IFCPROPERTYSET('2IEGHncr1D$R8fKA9zCEJP',$,'EPset_Annotation',$,(#976));
#974=IFCTYPEPRODUCT('38zW9E1uH2ae$zat9KrieS',$,'THICK',$,'IfcAnnotation/LINEWORK',(#975),$,$);
#975=IFCPROPERTYSET('2XQxgO16v9vxjvuIJpaPpG',$,'EPset_Annotation',$,(#976));
#976=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('thick'),$);
#977=IFCTYPEPRODUCT('1T5C$$ONTBB8A9a7vv0Gn6',$,'STRONG',$,'IfcAnnotation/LINEWORK',(#978),$,$);
#978=IFCPROPERTYSET('1gRomAS1L2WguA51ZI0E40',$,'EPset_Annotation',$,(#979));
#977=IFCTYPEPRODUCT('3G2s7ZLzfDJh5J$2iIzHw9',$,'STRONG',$,'IfcAnnotation/LINEWORK',(#978),$,$);
#978=IFCPROPERTYSET('0TcB8Gal96vebTrLWa5CEw',$,'EPset_Annotation',$,(#979));
#979=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('strong'),$);
#980=IFCTYPEPRODUCT('2LmKYrAEr2K8EwJyCAsyc4',$,'SETOUT-TAG',$,'IfcAnnotation/TEXT',(#981),(#1000),$);
#981=IFCPROPERTYSET('0nF9du8qDAF9aldqjCUsbX',$,'EPset_Annotation',$,(#982));
#980=IFCTYPEPRODUCT('3pq2B$0k52ZOv2bFuRYkxO',$,'SETOUT-TAG',$,'IfcAnnotation/TEXT',(#981),(#1000),$);
#981=IFCPROPERTYSET('39$oNFI052cBhtpZCVVLfj',$,'EPset_Annotation',$,(#982));
#982=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('setout-tag'),$);
#983=IFCCARTESIANPOINT((0.,0.,0.));
#984=IFCDIRECTION((0.,0.,1.));
@@ -1005,8 +1005,8 @@ DATA;
#998=IFCDIRECTION((0.,0.,1.));
#999=IFCAXIS2PLACEMENT3D(#996,#998,#997);
#1000=IFCREPRESENTATIONMAP(#999,#995);
#1001=IFCTYPEPRODUCT('1cdhVmqEPF6e13_TFOdlQw',$,'DOOR-TAG',$,'IfcAnnotation/TEXT',(#1002),(#1021),$);
#1002=IFCPROPERTYSET('1MUPGQolnDEgp0NxcMN56E',$,'EPset_Annotation',$,(#1003));
#1001=IFCTYPEPRODUCT('11M3ahhrr9NBdB29YEazzw',$,'DOOR-TAG',$,'IfcAnnotation/TEXT',(#1002),(#1021),$);
#1002=IFCPROPERTYSET('3xUNoLKPT9kB4LMdoctm5k',$,'EPset_Annotation',$,(#1003));
#1003=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('door-tag'),$);
#1004=IFCCARTESIANPOINT((0.,0.,0.));
#1005=IFCDIRECTION((0.,0.,1.));
@@ -1026,8 +1026,8 @@ DATA;
#1019=IFCDIRECTION((0.,0.,1.));
#1020=IFCAXIS2PLACEMENT3D(#1017,#1019,#1018);
#1021=IFCREPRESENTATIONMAP(#1020,#1016);
#1022=IFCTYPEPRODUCT('0OuDi3gRH2CxW9mrtE0vXw',$,'WINDOW-TAG',$,'IfcAnnotation/TEXT',(#1023),(#1036),$);
#1023=IFCPROPERTYSET('2X7dAo_1P5ruRnlKA4kIl6',$,'EPset_Annotation',$,(#1024));
#1022=IFCTYPEPRODUCT('1eE8Y$BVDFDgG8Fj6d9wiV',$,'WINDOW-TAG',$,'IfcAnnotation/TEXT',(#1023),(#1036),$);
#1023=IFCPROPERTYSET('2MqzSGkXDBcPSolaAQvYO4',$,'EPset_Annotation',$,(#1024));
#1024=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('window-tag'),$);
#1025=IFCCARTESIANPOINT((0.,0.,0.));
#1026=IFCDIRECTION((0.,0.,1.));
@@ -1041,8 +1041,8 @@ DATA;
#1034=IFCDIRECTION((0.,0.,1.));
#1035=IFCAXIS2PLACEMENT3D(#1032,#1034,#1033);
#1036=IFCREPRESENTATIONMAP(#1035,#1031);
#1037=IFCTYPEPRODUCT('3WEV_9wQn6AQTESgjW36PH',$,'SPACE-TAG',$,'IfcAnnotation/TEXT',(#1038),(#1063),$);
#1038=IFCPROPERTYSET('02FAX8dIzAlunWD5ak$6sU',$,'EPset_Annotation',$,(#1039));
#1037=IFCTYPEPRODUCT('0vvfHSiaPBxewA$32C4LdW',$,'SPACE-TAG',$,'IfcAnnotation/TEXT',(#1038),(#1063),$);
#1038=IFCPROPERTYSET('2zKuFdTPj3DhNdQ0U2kd5u',$,'EPset_Annotation',$,(#1039));
#1039=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('space-tag'),$);
#1040=IFCCARTESIANPOINT((0.,0.,0.));
#1041=IFCDIRECTION((0.,0.,1.));
@@ -1068,8 +1068,8 @@ DATA;
#1061=IFCDIRECTION((0.,0.,1.));
#1062=IFCAXIS2PLACEMENT3D(#1059,#1061,#1060);
#1063=IFCREPRESENTATIONMAP(#1062,#1058);
#1064=IFCTYPEPRODUCT('1evqvQLLL1zxdsIWqEn0lK',$,'MATERIAL-TAG',$,'IfcAnnotation/TEXT',(#1065),(#1078),$);
#1065=IFCPROPERTYSET('2KPJlGyer0UBmphfR7952k',$,'EPset_Annotation',$,(#1066));
#1064=IFCTYPEPRODUCT('36lTcd9yT8UBNDejQUNWrq',$,'MATERIAL-TAG',$,'IfcAnnotation/TEXT',(#1065),(#1078),$);
#1065=IFCPROPERTYSET('1x86VNVk1Drwe5XOj$GZX3',$,'EPset_Annotation',$,(#1066));
#1066=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('rectangle-tag'),$);
#1067=IFCCARTESIANPOINT((0.,0.,0.));
#1068=IFCDIRECTION((0.,0.,1.));
@@ -1083,8 +1083,8 @@ DATA;
#1076=IFCDIRECTION((0.,0.,1.));
#1077=IFCAXIS2PLACEMENT3D(#1074,#1076,#1075);
#1078=IFCREPRESENTATIONMAP(#1077,#1073);
#1079=IFCTYPEPRODUCT('3Nem6d4xX87O3deyWDi3AW',$,'TYPE-TAG',$,'IfcAnnotation/TEXT',(#1080),(#1093),$);
#1080=IFCPROPERTYSET('15bspgnA9CEuFox6xFjoTL',$,'EPset_Annotation',$,(#1081));
#1079=IFCTYPEPRODUCT('1rxhEX6I16oPqglXaGZ7Sw',$,'TYPE-TAG',$,'IfcAnnotation/TEXT',(#1080),(#1093),$);
#1080=IFCPROPERTYSET('3BeqEbDzX0xORamqqSx_kZ',$,'EPset_Annotation',$,(#1081));
#1081=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('capsule-tag'),$);
#1082=IFCCARTESIANPOINT((0.,0.,0.));
#1083=IFCDIRECTION((0.,0.,1.));
@@ -1098,8 +1098,8 @@ DATA;
#1091=IFCDIRECTION((0.,0.,1.));
#1092=IFCAXIS2PLACEMENT3D(#1089,#1091,#1090);
#1093=IFCREPRESENTATIONMAP(#1092,#1088);
#1094=IFCTYPEPRODUCT('0klFX9AjnEnPBkdIURv8XD',$,'NAME-TAG',$,'IfcAnnotation/TEXT',(#1095),(#1108),$);
#1095=IFCPROPERTYSET('1d53tifbv2rwcoFFJosiHf',$,'EPset_Annotation',$,(#1096));
#1094=IFCTYPEPRODUCT('0sws1hxNb2Og1etXqyoEh1',$,'NAME-TAG',$,'IfcAnnotation/TEXT',(#1095),(#1108),$);
#1095=IFCPROPERTYSET('11L$PtdnX8LfYigzRZ$g0a',$,'EPset_Annotation',$,(#1096));
#1096=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('capsule-tag'),$);
#1097=IFCCARTESIANPOINT((0.,0.,0.));
#1098=IFCDIRECTION((0.,0.,1.));
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -27,5 +27,6 @@ DATA;
#20=IFCSIMPLEPROPERTYTEMPLATE('0joEq0Rd10cxweEh0NeHT6',$,'JoinCriteria','Comma separated selection keys which determine what cut objects are to be joined.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#21=IFCSIMPLEPROPERTYTEMPLATE('0nYMT3OSj5gArVniCWZRtv',$,'ShadingStyles','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#22=IFCSIMPLEPROPERTYTEMPLATE('3VWG22eZXBdQwdKlzMeVQH',$,'CurrentShadingStyle','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#23=IFCSIMPLEPROPERTYTEMPLATE('28mJ$GDxb7kBbKFH_rACMQ',$,'AngleDecimalPlaces','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
File diff suppressed because one or more lines are too long
+33 -27
View File
@@ -23,33 +23,39 @@ import hashlib
import zipfile
import tempfile
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper
import blenderbim.bim.handler
import blenderbim.tool as tool
from pathlib import Path
from blenderbim.tool.brick import BrickStore
from typing import Set, Union
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
class IfcStore:
path = ""
file = None
schema = None
cache = None
cache_path = None
id_map = {}
guid_map = {}
edited_objs = set()
file: ifcopenshell.file = None
schema: ifcopenshell.ifcopenshell_wrapper.schema_definition = None
cache: ifcopenshell.ifcopenshell_wrapper.HdfSerializer = None
cache_path: str = None
id_map: dict[int, IFC_CONNECTED_TYPE] = {}
guid_map: dict[str, IFC_CONNECTED_TYPE] = {}
edited_objs: Set[bpy.types.Object] = set()
pset_template_path = ""
pset_template_file = None
pset_template_file: ifcopenshell.file = None
classification_path = ""
classification_file = None
classification_file: ifcopenshell.file = None
library_path = ""
library_file = None
library_file: ifcopenshell.file = None
current_transaction = ""
last_transaction = ""
history = []
future = []
schema_identifiers = ["IFC4", "IFC2X3", "IFC4X3"]
session_files = {}
session_files: dict[str, ifcopenshell.file] = {}
@staticmethod
def purge():
@@ -80,8 +86,8 @@ class IfcStore:
if IfcStore.path:
try:
IfcStore.load_file(IfcStore.path)
except:
pass
except Exception as e:
print(f"Failed to load file {IfcStore.path}. Error details: {e}")
return IfcStore.file
@staticmethod
@@ -117,7 +123,7 @@ class IfcStore:
IfcStore.get_cache()
@staticmethod
def load_file(path):
def load_file(path) -> None:
if not os.path.isfile(path):
return
extension = path.split(".")[-1]
@@ -146,7 +152,7 @@ class IfcStore:
return IfcStore.schema
@staticmethod
def get_element(id_or_guid):
def get_element(id_or_guid: Union[int, str]) -> IFC_CONNECTED_TYPE:
if isinstance(id_or_guid, int):
map_object = IfcStore.id_map
else:
@@ -159,7 +165,7 @@ class IfcStore:
return obj
@staticmethod
def relink_all_objects():
def relink_all_objects() -> None:
if not IfcStore.get_file():
return
for obj in bpy.data.objects:
@@ -172,7 +178,7 @@ class IfcStore:
IfcStore.relink_object(obj)
@staticmethod
def relink_object(obj):
def relink_object(obj: IFC_CONNECTED_TYPE) -> None:
if not obj:
return
if obj.BIMObjectProperties.ifc_definition_id:
@@ -193,7 +199,7 @@ class IfcStore:
IfcStore.commit_link_element(data)
@staticmethod
def link_element(element, obj):
def link_element(element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
# Please use tool.Ifc.link() instead of this method. We want to
# refactor this class and deprecate usage of IfcStore in favour of
# tools.
@@ -259,7 +265,7 @@ class IfcStore:
# TODO We're handling id_map and guid_map, but what about edited_objs? This might cause big problems.
@staticmethod
def rollback_unlink_element(data):
def rollback_unlink_element(data) -> None:
if "id" not in data or "obj" not in data:
return
obj = bpy.data.objects.get(data["obj"])
@@ -268,13 +274,13 @@ class IfcStore:
IfcStore.guid_map[data["guid"]] = obj
@staticmethod
def commit_unlink_element(data):
def commit_unlink_element(data) -> None:
del IfcStore.id_map[data["id"]]
if data["guid"]:
del IfcStore.guid_map[data["guid"]]
@staticmethod
def unlink_element(element=None, obj=None):
def unlink_element(element: ifcopenshell.entity_instance = None, obj: IFC_CONNECTED_TYPE = None) -> None:
if element is None:
try:
element = tool.Ifc.get_entity(obj)
@@ -320,7 +326,7 @@ class IfcStore:
)
@staticmethod
def execute_ifc_operator(operator, context, is_invoke=False):
def execute_ifc_operator(operator: bpy.types.Operator, context: bpy.types.Context, is_invoke=False):
bpy.context.scene.BIMProperties.is_dirty = True
is_top_level_operator = not bool(IfcStore.current_transaction)
@@ -354,17 +360,17 @@ class IfcStore:
return result
@staticmethod
def begin_transaction(operator):
def begin_transaction(operator: bpy.types.Operator) -> None:
IfcStore.current_transaction = str(uuid.uuid4())
operator.transaction_key = IfcStore.current_transaction
@staticmethod
def end_transaction(operator):
def end_transaction(operator: bpy.types.Operator) -> None:
IfcStore.current_transaction = ""
operator.transaction_key = ""
@staticmethod
def add_transaction_operation(operator, rollback=None, commit=None):
def add_transaction_operation(operator: bpy.types.Operator, rollback=None, commit=None) -> None:
key = getattr(operator, "transaction_key", None)
data = getattr(operator, "transaction_data", None)
bpy.context.scene.BIMProperties.last_transaction = key
@@ -380,7 +386,7 @@ class IfcStore:
IfcStore.future = []
@staticmethod
def undo(until_key=None):
def undo(until_key=None) -> None:
BrickStore.undo()
if not IfcStore.history:
return
@@ -395,7 +401,7 @@ class IfcStore:
IfcStore.future.append(event)
@staticmethod
def redo(until_key=None):
def redo(until_key=None) -> None:
BrickStore.redo()
if not IfcStore.future:
+5 -4
View File
@@ -36,14 +36,15 @@ import blenderbim.tool as tool
from itertools import chain, accumulate
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
from typing import Dict
class MaterialCreator:
def __init__(self, ifc_import_settings: IfcImportSettings, ifc_importer: IfcImporter):
self.mesh = None
self.obj = None
self.materials = {}
self.styles = {}
self.materials: Dict[int, bpy.types.Material] = {}
self.styles: Dict[int, bpy.types.Material] = {}
self.parsed_meshes = set()
self.ifc_import_settings = ifc_import_settings
self.ifc_importer = ifc_importer
@@ -156,8 +157,8 @@ class MaterialCreator:
material_to_slot = {}
if len(self.mesh.polygons) == len(self.mesh["ios_material_ids"]):
for i, material in enumerate(self.mesh["ios_materials"]):
slot_index = self.mesh.materials.find(self.styles[material].name)
for i, style_id in enumerate(self.mesh["ios_materials"]):
slot_index = self.mesh.materials.find(self.styles[style_id].name)
material_to_slot[i] = slot_index
material_index = [
@@ -28,8 +28,11 @@ classes = (
operator.BIM_OT_select_aggregate,
operator.BIM_OT_select_parts,
operator.BIM_OT_aggregate_unassign_object,
operator.BIM_OT_break_link_to_other_aggregates,
operator.BIM_OT_select_linked_aggregates,
prop.BIMObjectAggregateProperties,
ui.BIM_PT_aggregate,
ui.BIM_PT_linked_aggregate,
)
@@ -37,6 +37,7 @@ class AggregateData:
"has_related_objects": cls.has_related_objects(),
"total_parts": cls.total_parts(),
"ifc_class": cls.ifc_class(),
"total_linked_aggregate": cls.total_linked_aggregate(),
}
cls.is_loaded = True
@@ -72,3 +73,26 @@ class AggregateData:
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
return element.is_a()
@classmethod
def total_linked_aggregate(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
return []
if not element:
return []
product_linked_agg_group = [
r
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
if not product_linked_agg_group:
return []
total = len(product_linked_agg_group[0].RelatedObjects)
return total
@@ -89,6 +89,12 @@ 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')
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
@@ -230,3 +236,63 @@ class BIM_OT_add_part_to_object(bpy.types.Operator, Operator):
part_class=self.part_class,
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"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
element = tool.Ifc.get_entity(bpy.context.active_object)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
return []
if not element:
return []
parts = ifcopenshell.util.element.get_parts(aggregate)
for part in parts:
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)
return {"FINISHED"}
class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator):
bl_idname = "bim.select_linked_aggregates"
bl_label = "Select linked aggregates"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
element = tool.Ifc.get_entity(bpy.context.active_object)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
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)
obj.select_set(True)
return {"FINISHED"}
@@ -18,6 +18,7 @@
from bpy.types import Panel
from blenderbim.bim.module.aggregate.data import AggregateData
from blenderbim.bim.module.group.data import GroupsData, ObjectGroupsData
from blenderbim.bim.ifc import IfcStore
@@ -96,3 +97,49 @@ class BIM_PT_aggregate(Panel):
op = layout.operator("bim.add_part_to_object", text="Add " + part_class.lstrip("Ifc"))
op.part_class = part_class
op.obj = context.active_object.name
class BIM_PT_linked_aggregate(Panel):
bl_label = "Linked Aggregates"
bl_idname = "BIM_PT_linked_aggregate"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 2
bl_parent_id = "BIM_PT_aggregate"
@classmethod
def poll(cls, context):
if not context.active_object:
return False
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
if not IfcStore.get_element(props.ifc_definition_id):
return False
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False
return True
def draw(self, context):
layout = self.layout
if not AggregateData.is_loaded:
AggregateData.load()
props = context.active_object.BIMObjectAggregateProperties
row = layout.row(align=True)
row.label(text="Advanced Users Only", icon="ERROR")
row = layout.row(align=True)
if type(AggregateData.data['total_linked_aggregate']) is int:
if AggregateData.data['total_linked_aggregate'] > 0:
row.label(text=f"{AggregateData.data['total_linked_aggregate']} Linked Aggregates")
row.operator("bim.select_linked_aggregates", text="", icon="RESTRICT_SELECT_OFF")
row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH")
op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X")
else:
row.label(text="No Linked Aggregates")
@@ -22,6 +22,7 @@ from . import ui, operator, prop
classes = (
operator.AddBoundary,
operator.ColourByRelatedBuildingElement,
operator.DecorateBoundaries,
operator.DisableEditingBoundary,
operator.DisableEditingBoundaryGeometry,
operator.EditBoundaryAttributes,
@@ -24,6 +24,7 @@ import mathutils
import numpy as np
import multiprocessing
import ifcopenshell.api
import ifcopenshell.geom
import ifcopenshell.util.unit
import ifcopenshell.util.shape
import ifcopenshell.util.element
@@ -37,6 +38,7 @@ from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.decorator import ProfileDecorator
from blenderbim.bim.module.boundary.decorator import BoundaryDecorator
import blenderbim.core
import blenderbim.core.geometry
def get_boundaries_collection(blender_space):
@@ -147,7 +149,9 @@ class Loader:
self.ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
self.ifc_importer.file = self.ifc_file
def load_boundary(self, boundary, blender_space=None):
def load_boundary(
self, boundary: ifcopenshell.entity_instance, blender_space: bpy.types.Object = None
) -> bpy.types.Object:
if not blender_space:
if not boundary.RelatingSpace:
self.operator.report(
@@ -509,10 +513,10 @@ class DisableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
class ShowBoundaries(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.show_boundaries"
bl_label = "Show Boundaries"
bl_description = "Load selected objects boundaries if they are not loaded"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMBoundaryProperties
loader = Loader(self)
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -524,7 +528,7 @@ class ShowBoundaries(bpy.types.Operator, tool.Ifc.Operator):
for rel in element.BoundedBy or []:
boundary_obj = loader.load_boundary(rel, obj)
tool.Boundary.decorate_boundary(boundary_obj)
BoundaryDecorator.install(bpy.context)
BoundaryDecorator.install(context)
return {"FINISHED"}
@@ -557,6 +561,36 @@ class HideBoundaries(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.decorate_boundaries"
bl_label = "Toggle Boundaries Decorations"
bl_description = "Add special decorations for all boundaries in the scene or hide them if they are already added"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMBoundaryProperties
# filter not decorated boundaries and add decorations for them
decorated_boundaries = set([i.obj for i in props.boundaries])
active_boundaries = set()
for element in tool.Ifc.get().by_type("IfcRelSpaceBoundary"):
obj = tool.Ifc.get_object(element)
if obj is not None:
active_boundaries.add(obj)
boundaries_to_decorate = active_boundaries - decorated_boundaries
if boundaries_to_decorate:
for obj in boundaries_to_decorate:
tool.Boundary.decorate_boundary(obj)
BoundaryDecorator.install(context)
else:
for obj in decorated_boundaries:
tool.Boundary.undecorate_boundary(obj)
props.boundaries.clear()
BoundaryDecorator.uninstall()
tool.Blender.update_viewport()
return {"FINISHED"}
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_boundary"
bl_label = "Add Boundary"
@@ -37,7 +37,7 @@ classes = (
def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.CadTool, after={"builtin.transform"}, separator=True, group=True)
bpy.utils.register_tool(workspace.CadTool, after={"builtin.transform"}, separator=True, group=False)
bpy.types.Scene.BIMCadProperties = bpy.props.PointerProperty(type=prop.BIMCadProperties)
@@ -28,18 +28,19 @@ classes = (
operator.InspectFromStepId,
operator.OverrideDisplayType,
operator.ParseExpress,
operator.PipInstall,
operator.PrintIfcFile,
operator.PrintObjectPlacement,
operator.PrintUnusedElementStats,
operator.ProfileImportIFC,
operator.PurgeHdf5Cache,
operator.PurgeIfcLinks,
operator.PurgeUnusedElementsByClass,
operator.RewindInspector,
operator.SelectExpressFile,
operator.SelectHighPolygonMeshes,
operator.SelectHighestPolygonMeshes,
operator.ValidateIfcFile,
operator.PrintUnusedElementStats,
operator.PurgeUnusedElementsByClass,
prop.BIMDebugProperties,
ui.BIM_PT_debug,
)
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import bpy
import time
import logging
@@ -623,3 +624,39 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
print("Finished 20 batches. Manually stopping in case of infinite loop.")
self.report({"INFO"}, f"Auto purged {total_purged} orphaned elements")
tool.Ifc.get().write(self.filepath)
class PipInstall(bpy.types.Operator):
bl_idname = "bim.pip_install"
bl_label = "Pip Install"
bl_description = "Installs a package from PyPI"
name: bpy.props.StringProperty()
def execute(self, context):
import blenderbim.bim
target = [p for p in sys.path if p.endswith("packages") and "addons" in p][0]
py_exec = str(sys.executable)
print("Detected executable:", py_exec)
print("Installing to:", target)
subprocess.call([py_exec, "-m", "ensurepip", "--user"])
subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pip"])
# Trusted host to make things simpler due to some enterprise network restrictions
subprocess.call(
[
py_exec,
"-m",
"pip",
"install",
f"--target={target}",
"--upgrade",
self.name,
"--trusted-host",
"pypi.org",
"--trusted-host",
"files.pythonhosted.org",
]
)
return {"FINISHED"}
@@ -51,3 +51,4 @@ class BIMDebugProperties(PropertyGroup):
default="BOUNDS",
)
ifc_class_purge: StringProperty(name="Unused Elements IFC Class", default="")
package_name: StringProperty(name="Package Name", default="")
@@ -44,6 +44,10 @@ class BIM_PT_debug(Panel):
row.operator("bim.parse_express", icon="IMPORT", text="")
row.operator("bim.select_express_file", icon="FILE_FOLDER", text="")
row = self.layout.row(align=True)
row.prop(props, "package_name", text="")
row.operator("bim.pip_install", icon="EVENT_PAGEDOWN").name = props.package_name
row = layout.row()
row.operator("bim.reload_ifc_file", text="Incrementally Reload Changes")
@@ -812,8 +812,9 @@ class AngleDecorator(BaseDecorator):
base_edge = edge0 if tool.Cad.is_counter_clockwise_order(p0, p1, p2) else edge1
text_offset = (Matrix.Rotation(-angle_rad / 2, 2) @ base_edge).normalized() * (radius + ANGLE_LABEL_OFFSET)
label_position = p1 + text_offset
text = f"{int(angle)}deg"
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
decimal_places = drawing_pset_data.get("AngleDecimalPlaces", None)
text = f"{round(angle, decimal_places)}°"
label_dir = Vector((1, 0))
self.draw_label(context, text, label_position, label_dir, box_alignment="center")
@@ -44,6 +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 lxml import etree
from mathutils import Vector, Color, Matrix
from timeit import default_timer as timer
@@ -77,6 +78,11 @@ class Operator:
return {"FINISHED"}
class LineworkContexts(NamedTuple):
body: List[List[int]]
annotation: List[List[int]]
class AddAnnotationType(bpy.types.Operator, Operator):
bl_idname = "bim.add_annotation_type"
bl_label = "Add Annotation Type"
@@ -367,6 +373,105 @@ class CreateDrawing(bpy.types.Operator):
self.svg_writer.create_blank_svg(svg_path).draw_underlay(context.scene.render.filepath).save()
return svg_path
def get_linework_contexts(self, ifc, target_view) -> LineworkContexts:
plan_body_target_contexts = []
plan_body_model_contexts = []
model_body_target_contexts = []
model_body_model_contexts = []
plan_annotation_target_contexts = []
plan_annotation_model_contexts = []
model_annotation_target_contexts = []
model_annotation_model_contexts = []
for rep_context in ifc.by_type("IfcGeometricRepresentationContext"):
if rep_context.is_a("IfcGeometricRepresentationSubContext"):
if rep_context.ContextType == "Plan":
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
if rep_context.TargetView == target_view:
plan_body_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
plan_body_model_contexts.append(rep_context.id())
elif rep_context.ContextIdentifier == "Annotation":
if rep_context.TargetView == target_view:
plan_annotation_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
plan_annotation_model_contexts.append(rep_context.id())
elif rep_context.ContextType == "Model":
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
if rep_context.TargetView == target_view:
model_body_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
model_body_model_contexts.append(rep_context.id())
elif rep_context.ContextIdentifier == "Annotation":
if rep_context.TargetView == target_view:
model_annotation_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
model_annotation_model_contexts.append(rep_context.id())
elif rep_context.ContextType == "Model":
# You should never purely assign to a "Model" context, but
# if you do, this is what we assume your intention is.
model_body_model_contexts.append(rep_context.id())
continue
body_contexts = (
[
plan_body_target_contexts,
plan_body_model_contexts,
model_body_target_contexts,
model_body_model_contexts,
]
if target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]
else [
model_body_target_contexts,
model_body_model_contexts,
]
)
annotation_contexts = (
[
plan_annotation_target_contexts,
plan_annotation_model_contexts,
model_annotation_target_contexts,
model_annotation_model_contexts,
]
if target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]
else [
model_annotation_target_contexts,
model_annotation_model_contexts,
]
)
return LineworkContexts(body_contexts, annotation_contexts)
def serialize_contexts_elements(
self, ifc, tree: ifcopenshell.geom.tree, contexts: LineworkContexts, context_type, drawing_elements, target_view
):
drawing_elements = drawing_elements.copy()
contexts = getattr(contexts, context_type)
for context in contexts:
with profile(f"Processing {context_type} context"):
if not context or not drawing_elements:
continue
geom_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
if ifc.by_id(context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
# A 2mm Z offset to combat Z-fighting in plan or RCPs
offset[2] = 0.002 if target_view == "PLAN_VIEW" else -0.002
geom_settings.offset = offset
geom_settings.set_context_ids(context)
it = ifcopenshell.geom.iterator(
geom_settings, ifc, multiprocessing.cpu_count(), include=drawing_elements
)
processed = set()
for elem in it:
processed.add(ifc.by_id(elem.id))
self.serialiser.write(elem)
tree.add_element(elem)
drawing_elements -= processed
def generate_linework(self, context):
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
@@ -408,77 +513,8 @@ class CreateDrawing(bpy.types.Operator):
# Drawings only draw bodies and annotations (and facetation, due to a Revit bug).
# A drawing prioritises a target view context first, followed by a model view context as a fallback.
# Specifically for PLAN_VIEW and REFLECTED_PLAN_VIEW, any Plan context is also prioritised.
plan_body_target_contexts = []
plan_body_model_contexts = []
model_body_target_contexts = []
model_body_model_contexts = []
plan_annotation_target_contexts = []
plan_annotation_model_contexts = []
model_annotation_target_contexts = []
model_annotation_model_contexts = []
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
for rep_context in ifc.by_type("IfcGeometricRepresentationContext"):
if rep_context.is_a("IfcGeometricRepresentationSubContext"):
if rep_context.ContextType == "Plan":
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
if rep_context.TargetView == target_view:
plan_body_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
plan_body_model_contexts.append(rep_context.id())
elif rep_context.ContextIdentifier == "Annotation":
if rep_context.TargetView == target_view:
plan_annotation_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
plan_annotation_model_contexts.append(rep_context.id())
elif rep_context.ContextType == "Model":
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
if rep_context.TargetView == target_view:
model_body_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
model_body_model_contexts.append(rep_context.id())
elif rep_context.ContextIdentifier == "Annotation":
if rep_context.TargetView == target_view:
model_annotation_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
model_annotation_model_contexts.append(rep_context.id())
elif rep_context.ContextType == "Model":
# You should never purely assign to a "Model" context, but
# if you do, this is what we assume your intention is.
model_body_model_contexts.append(rep_context.id())
continue
body_contexts = (
[
plan_body_target_contexts,
plan_body_model_contexts,
model_body_target_contexts,
model_body_model_contexts,
]
if target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]
else [
model_body_target_contexts,
model_body_model_contexts,
]
)
annotation_contexts = (
[
plan_annotation_target_contexts,
plan_annotation_model_contexts,
model_annotation_target_contexts,
model_annotation_model_contexts,
]
if target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]
else [
model_annotation_target_contexts,
model_annotation_model_contexts,
]
)
contexts = self.get_linework_contexts(ifc, target_view)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element)
self.setup_serialiser(ifc, target_view)
@@ -487,60 +523,15 @@ class CreateDrawing(bpy.types.Operator):
tree = ifcopenshell.geom.tree()
tree.enable_face_styles(True)
elements = drawing_elements.copy()
for body_context in body_contexts:
with profile("Processing body context"):
if body_context and elements:
geom_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
if ifc.by_id(body_context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
# A 2mm Z offset to combat Z-fighting in plan or RCPs
offset[2] = 0.002 if target_view == "PLAN_VIEW" else -0.002
geom_settings.offset = offset
geom_settings.set_context_ids(body_context)
it = ifcopenshell.geom.iterator(
geom_settings, ifc, multiprocessing.cpu_count(), include=elements
)
it.set_cache(cache)
processed = set()
for elem in self.yield_from_iterator(it):
processed.add(ifc.by_id(elem.id))
self.serialiser.write(elem)
tree.add_element(elem)
elements -= processed
elements = drawing_elements.copy()
for annotation_context in annotation_contexts:
with profile("Processing annotation context"):
if annotation_context and elements:
geom_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
if ifc.by_id(annotation_context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
# A 2mm Z offset to combat Z-fighting in plan or RCPs
offset[2] = 0.002 if target_view == "PLAN_VIEW" else -0.002
geom_settings.offset = offset
geom_settings.set_context_ids(annotation_context)
it = ifcopenshell.geom.iterator(
geom_settings, ifc, multiprocessing.cpu_count(), include=elements
)
it.set_cache(cache)
processed = set()
for elem in self.yield_from_iterator(it):
processed.add(ifc.by_id(elem.id))
self.serialiser.write(elem)
tree.add_element(elem)
elements -= processed
self.serialize_contexts_elements(ifc, tree, contexts, "body", drawing_elements, target_view)
self.serialize_contexts_elements(ifc, tree, contexts, "annotation", drawing_elements, target_view)
if self.camera_element not in drawing_elements:
with profile("Camera element"):
# The camera must always be included, regardless of any include/exclude filters.
geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True)
it = ifcopenshell.geom.iterator(geom_settings, ifc, include=[self.camera_element])
for elem in self.yield_from_iterator(it):
for elem in it:
self.serialiser.write(elem)
with profile("Finalizing"):
@@ -588,8 +579,7 @@ class CreateDrawing(bpy.types.Operator):
if global_id not in elements_with_faces:
continue
for path in projection.findall("./{http://www.w3.org/2000/svg}path"):
# Rounding is necessary to ensure coincident points are coincident
start, end = [[round(float(o), 1) for o in co[1:].split(",")] for co in path.attrib["d"].split()]
start, end = [[float(o) for o in co[1:].split(",")] for co in path.attrib["d"].split()]
if start == end:
continue
# Extension by 0.5mm is necessary to ensure lines overlap with other diagonal lines
@@ -884,13 +874,6 @@ class CreateDrawing(bpy.types.Operator):
# the style of the face when running tree.select_ray()
# tree.enable_face_styles(True)
def yield_from_iterator(self, it):
if it.initialize():
while True:
yield it.get()
if not it.next():
break
def get_svg_classes(self, element):
classes = [element.is_a()]
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
@@ -970,7 +953,7 @@ class CreateDrawing(bpy.types.Operator):
for path in el.findall("{http://www.w3.org/2000/svg}path"):
for subpath in path.attrib["d"].split("M")[1:]:
subpath = "M" + subpath.strip()
coords = [[round(float(o), 1) for o in co[1:].split(",")] for co in subpath.split()]
coords = [[float(o) for o in co[1:].split(",")] for co in subpath.split()]
if coords[0] != coords[-1]:
has_open_paths = True
line_strings.append(shapely.LineString(coords))
@@ -1034,7 +1017,7 @@ class CreateDrawing(bpy.types.Operator):
for path in el.findall("{http://www.w3.org/2000/svg}path"):
for subpath in path.attrib["d"].split("M")[1:]:
subpath_co = "M" + subpath.strip(" Z")
coords = [[round(float(o), 1) for o in co[1:].split(",")] for co in subpath_co.split()]
coords = [[float(o) for o in co[1:].split(",")] for co in subpath_co.split()]
if subpath.strip().lower().endswith("z"):
coords.append(coords[0])
if len(coords) > 2 and coords[0] == coords[-1]:
@@ -2299,8 +2282,16 @@ class AssignSelectedObjectAsProduct(bpy.types.Operator):
def execute(self, context):
objs = context.selected_objects[:]
objs.remove(context.active_object)
other_selected_object = next(obj for obj in context.selected_objects if obj != context.active_object)
obj1 = objs[0]
element1 = tool.Ifc.get_entity(obj1)
obj2 = objs[1]
element2 = tool.Ifc.get_entity(obj2)
if element1.is_a("IfcAnnotation"):
other_selected_object = obj2
bpy.context.view_layer.objects.active = obj1
elif element2.is_a("IfcAnnotation"):
other_selected_object = obj1
bpy.context.view_layer.objects.active = obj2
context.active_object.BIMAssignedProductProperties.relating_product = other_selected_object
bpy.ops.bim.edit_assigned_product()
return {"FINISHED"}
@@ -32,6 +32,7 @@ import ifcopenshell.util.representation
import blenderbim.tool as tool
import blenderbim.bim.module.drawing.helper as helper
import blenderbim.bim.module.drawing.annotation as annotation
from blenderbim.bim.module.drawing.data import DrawingsData
from blenderbim.bim.module.drawing.data import DecoratorData
from blenderbim.bim.ifc import IfcStore
@@ -1066,7 +1067,9 @@ class SvgWriter:
# calculating text parameters and adding text
text_position = arc_mid_point + arc_mid_dir * 5
text_style = SvgWriter.get_box_alignment_parameters("center")
angle_text = f"{int(angle)}deg"
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
decimal_places = drawing_pset_data.get("AngleDecimalPlaces", None)
angle_text = f"{round(angle, decimal_places)}°"
self.svg.add(self.svg.text(angle_text, insert=text_position, class_="ANGLE", **text_style))
# Draw SVG arc, see for details: http://xahlee.info/js/svg_circle_arc.html
@@ -49,7 +49,17 @@ classes = (
operator.SwitchRepresentation,
operator.UpdateParametricRepresentation,
operator.UpdateRepresentation,
operator.RemoveRepresentationItem,
operator.EnableEditingRepresentationItemStyle,
operator.EditRepresentationItemStyle,
operator.DisableEditingRepresentationItemStyle,
operator.UnassignRepresentationItemStyle,
operator.EnableEditingRepresentationItemShapeAspect,
operator.EditRepresentationItemShapeAspect,
operator.DisableEditingRepresentationItemShapeAspect,
operator.RemoveRepresentationItemFromShapeAspect,
prop.RepresentationItem,
prop.ShapeAspect,
prop.BIMObjectGeometryProperties,
prop.BIMGeometryProperties,
ui.BIM_PT_placement,
@@ -93,7 +103,9 @@ def register():
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked_macro", "D", "PRESS", alt=True)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.object_duplicate_move_linked_aggregate_macro", "D", "PRESS", ctrl=True, shift=True)
kmi = km.keymap_items.new(
"bim.object_duplicate_move_linked_aggregate_macro", "D", "PRESS", ctrl=True, shift=True
)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
addon_keymaps.append((km, kmi))
@@ -38,6 +38,7 @@ class RepresentationsData:
def load(cls):
cls.data = {"representations": cls.representations()}
cls.data["contexts"] = cls.contexts()
cls.data["shape_aspects"] = cls.shape_aspects()
cls.is_loaded = True
@classmethod
@@ -92,6 +93,26 @@ class RepresentationsData:
)
return results
@classmethod
def shape_aspects(cls):
obj = bpy.context.active_object
if not obj.data:
return []
element = tool.Ifc.get_entity(obj)
active_representation_id = obj.data.BIMMeshProperties.ifc_definition_id
base_representation = tool.Ifc.get().by_id(active_representation_id)
# shape aspects matching context of the active representation
matching_shape_aspects = []
for shape_aspect in ifcopenshell.util.element.get_shape_aspects(element):
matching_representation = tool.Geometry.get_shape_aspect_representation(shape_aspect, base_representation)
if matching_representation:
matching_shape_aspects.append(shape_aspect)
# blender enum items
new_shape_aspect = [("NEW", "Create A New Shape Aspect", "")]
return new_shape_aspect + [(str(s.id()), s.Name or "Unnamed", "") for s in matching_shape_aspects]
class RepresentationItemsData:
data = {}
@@ -21,10 +21,12 @@ import bmesh
import logging
import numpy as np
import json
import re
import ifcopenshell
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.placement
import ifcopenshell.api
import blenderbim.core.geometry as core
import blenderbim.core.aggregate
@@ -36,6 +38,7 @@ import blenderbim.bim.handler
from mathutils import Vector, Matrix
from time import time
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.util.shape_builder import ShapeBuilder
class Operator:
@@ -767,11 +770,13 @@ class OverrideDuplicateMove(bpy.types.Operator):
element = tool.Ifc.get_entity(obj)
if element:
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
self.report({'INFO'}, "Did not duplicate. Duplicate drawings through the Drawings and Documents UI.")
self.report(
{"INFO"}, "Did not duplicate. Duplicate drawings through the Drawings and Documents UI."
)
obj.select_set(False)
continue # For now, don't copy drawings until we stabilise a bit more. It's tricky.
elif element.is_a("IfcProject"):
self.report({'INFO'}, "Did not duplicate. IFC can only have one project.")
self.report({"INFO"}, "Did not duplicate. IFC can only have one project.")
obj.select_set(False)
continue # Only one IfcProject is allowed.
@@ -838,6 +843,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
# Recreate decompositions
tool.Root.recreate_decompositions(decomposition_relationships, old_to_new)
OverrideDuplicateMove.handle_linked_aggregates(old_to_new)
blenderbim.bim.handler.refresh_ui_data()
return old_to_new
@@ -890,6 +896,26 @@ class OverrideDuplicateMove(bpy.types.Operator):
if entity in old_to_new.keys():
core.remove_connection(tool.Geometry, connection=connection)
@staticmethod
def handle_linked_aggregates(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
if pset:
old_aggregate = ifcopenshell.util.element.get_aggregate(old)
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
if old_aggregate == new_aggregate:
parts = ifcopenshell.util.element.get_parts(new_aggregate)
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
index += 1
pset = tool.Ifc.get().by_id(pset['id'])
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_linked_macro"
@@ -948,16 +974,38 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
obj.select_set(True)
parts = ifcopenshell.util.element.get_parts(element)
if parts:
index = 0
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
add_linked_aggregate_pset(element, index)
index +=1
for part in parts:
if part.is_a("IfcElementAssembly"):
select_objects_and_add_data(part)
add_linked_aggregate_pset(part, index)
index += 1
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
)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
else:
pass
return index
def add_linked_aggregate_group(element):
linked_aggregate_group = None
@@ -972,23 +1020,30 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
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)
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)
else:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
return index
def custom_incremental_naming_for_element_assembly(old_to_new):
for new in old_to_new.values():
if new[0].is_a("IfcElementAssembly"):
group_elements = [
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
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'
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}'
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"}
@@ -1009,6 +1064,8 @@ 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"):
@@ -1018,6 +1075,18 @@ 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]
if len(index) > 0:
index = max(index)
return index
else:
return 0
class RefreshLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.refresh_linked_aggregate"
@@ -1042,6 +1111,7 @@ class RefreshLinkedAggregate(bpy.types.Operator):
self.pset_name = "BBIM_Linked_Aggregate"
refresh_start_time = time()
old_to_new = {}
original_names = {}
def delete_objects(element):
parts = ifcopenshell.util.element.get_parts(element)
@@ -1054,6 +1124,61 @@ class RefreshLinkedAggregate(bpy.types.Operator):
tool.Geometry.delete_ifc_object(tool.Ifc.get_object(element))
def get_original_names(element):
group = [
r.RelatingGroup
for r in getattr(element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name
][0].id()
original_names[group] = {}
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
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:
if part.is_a("IfcElementAssembly"):
original_names | get_original_names(part)
else:
try:
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
except:
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
aggregate = element
group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name
]
if not group:
return
group = group[0].id()
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset['Index']
try:
obj.name = original_names[group][index]
except:
return
def get_element_assembly(element):
if element.is_a("IfcElementAssembly"):
return element
@@ -1138,7 +1263,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:
@@ -1153,19 +1280,24 @@ class RefreshLinkedAggregate(bpy.types.Operator):
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
@@ -1198,18 +1330,23 @@ class OverrideJoin(bpy.types.Operator, Operator):
return self.join_blender_obj()
def join_ifc_obj(self):
representation = tool.Ifc.get().by_id(self.target.data.BIMMeshProperties.ifc_definition_id)
ifc_file = tool.Ifc.get()
builder = ShapeBuilder(ifc_file)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
representation = ifc_file.by_id(self.target.data.BIMMeshProperties.ifc_definition_id)
if representation.RepresentationType in ("Tessellation", "Brep"):
for obj in bpy.context.selected_objects:
if obj == self.target:
continue
element = tool.Ifc.get_entity(obj)
if element:
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
ifcopenshell.api.run("root.remove_product", ifc_file, product=element)
bpy.ops.object.join()
bpy.ops.bim.update_representation(obj=self.target.name, ifc_representation_class="")
elif representation.RepresentationType == "SweptSolid":
target_placement = np.array(self.target.matrix_world)
target_placement[:, 3][:3] /= si_conversion
items = list(representation.Items)
for obj in bpy.context.selected_objects:
if obj == self.target:
@@ -1222,31 +1359,28 @@ class OverrideJoin(bpy.types.Operator, Operator):
continue
# Only objects of the same representation type can be joined
obj_rep = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
obj_rep = ifc_file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if obj_rep.RepresentationType != "SweptSolid":
obj.select_set(False)
continue
placement = np.array(obj.matrix_world)
placement[:, 3][:3] /= si_conversion
for item in obj_rep.Items:
copied_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item)
copied_item = ifcopenshell.util.element.copy_deep(ifc_file, item)
for style in item.StyledByItem:
copied_style = ifcopenshell.util.element.copy(tool.Ifc.get(), style)
copied_style = ifcopenshell.util.element.copy(ifc_file, style)
copied_style.Item = copied_item
if copied_item.Position:
position = ifcopenshell.util.placement.get_axis2placement(copied_item.Position)
else:
position = np.eye(4)
position = np.eye(4, dtype=float)
position = placement @ position
position = np.linalg.inv(target_placement) @ position
copied_item.Position = tool.Ifc.get().createIfcAxis2Placement3D(
tool.Ifc.get().createIfcCartesianPoint([float(n) for n in position[:, 3][:3]]),
tool.Ifc.get().createIfcDirection([float(n) for n in position[:, 2][:3]]),
tool.Ifc.get().createIfcDirection([float(n) for n in position[:, 0][:3]]),
)
copied_item.Position = builder.create_axis2_placement_3d_from_matrix(position)
items.append(copied_item)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
ifcopenshell.api.run("root.remove_product", ifc_file, product=element)
representation.Items = items
bpy.ops.object.join()
core.switch_representation(
@@ -1590,6 +1724,16 @@ class EnableEditingRepresentationItems(bpy.types.Operator, Operator):
new.surface_style = style.Name or "Unnamed"
elif inverse.is_a("IfcPresentationLayerAssignment"):
new.layer = inverse.Name or "Unnamed"
elif inverse.is_a("IfcShapeRepresentation"):
if inverse.OfShapeAspect:
shape_aspect = inverse.OfShapeAspect[0]
new.shape_aspect = shape_aspect.Name
new.shape_aspect_id = shape_aspect.id()
# sort created items
sorted_items = sorted(props.items[:], key=lambda i: (not i.shape_aspect, i.shape_aspect))
for i, item in enumerate(sorted_items[:-1]): # last item is sorted automatically
props.items.move(props.items[:].index(item), i)
class DisableEditingRepresentationItems(bpy.types.Operator, Operator):
@@ -1600,3 +1744,270 @@ class DisableEditingRepresentationItems(bpy.types.Operator, Operator):
def _execute(self, context):
obj = context.active_object
obj.BIMGeometryProperties.is_editing = False
class RemoveRepresentationItem(bpy.types.Operator, Operator):
bl_idname = "bim.remove_representation_item"
bl_label = "Remove Representation Item"
bl_options = {"REGISTER", "UNDO"}
representation_item_id: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
if context.active_object is None or len(context.active_object.BIMGeometryProperties.items) <= 1:
cls.poll_message_set(
"Active object need to have more than 1 representation items to keep representation valid"
)
return False
return True
def _execute(self, context):
obj = context.active_object
props = obj.BIMGeometryProperties
ifc_file = tool.Ifc.get()
representation_item = ifc_file.by_id(self.representation_item_id)
tool.Geometry.remove_representation_item(representation_item)
tool.Geometry.reload_representation(obj)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
def poll_editing_representaiton_item_style(cls, context):
if not (obj := getattr(context, "active_object", None)):
return False
props = obj.BIMGeometryProperties
if not props.is_editing:
return False
if not 0 <= props.active_item_index < len(props.items):
return False
item = props.items[props.active_item_index]
shape_aspect = item.shape_aspect
if shape_aspect == "":
return True
from blenderbim.bim.module.material.data import ObjectMaterialData
if not ObjectMaterialData.is_loaded:
ObjectMaterialData.load()
constituents = ObjectMaterialData.data["active_material_constituents"]
if any(c for c in constituents if c == shape_aspect):
cls.poll_message_set(
"Style comes from item's shape aspect related to the material constituent "
"with the same name and cannot be edited on representation item directly - "
"you can change it from the material constituent"
)
return False
return True
class EnableEditingRepresentationItemStyle(bpy.types.Operator, Operator):
bl_idname = "bim.enable_editing_representation_item_style"
bl_label = "Enable Editing Representation Item Style"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return poll_editing_representaiton_item_style(cls, context)
def _execute(self, context):
props = context.active_object.BIMGeometryProperties
props.is_editing_item_style = True
ifc_file = tool.Ifc.get()
# set dropdown to currently active style
representation_item_id = props.items[props.active_item_index].ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
style = tool.Style.get_representation_item_style(representation_item)
if style:
props.representation_item_style = str(style.id())
class EditRepresentationItemStyle(bpy.types.Operator, Operator):
bl_idname = "bim.edit_representation_item_style"
bl_label = "Edit Representation Item Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMGeometryProperties
props.is_editing_item_style = False
ifc_file = tool.Ifc.get()
surface_style = ifc_file.by_id(int(props.representation_item_style))
representation_item_id = props.items[props.active_item_index].ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
tool.Style.assign_style_to_representation_item(representation_item, surface_style)
tool.Geometry.reload_representation(obj)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
class DisableEditingRepresentationItemStyle(bpy.types.Operator, Operator):
bl_idname = "bim.disable_editing_representation_item_style"
bl_label = "Disable Editing Representation Item Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.active_object.BIMGeometryProperties
props.is_editing_item_style = False
class UnassignRepresentationItemStyle(bpy.types.Operator, Operator):
bl_idname = "bim.unassign_representation_item_style"
bl_label = "Unassign Representation Item Style"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return poll_editing_representaiton_item_style(cls, context)
def _execute(self, context):
obj = context.active_object
props = obj.BIMGeometryProperties
props.is_editing_item_style = False
# get active representation item
representation_item_id = props.items[props.active_item_index].ifc_definition_id
representation_item = tool.Ifc.get_entity_by_id(representation_item_id)
if not representation_item:
self.report({"ERROR"}, f"Couldn't find representation item by id {representation_item_id}.")
return {"CANCELLED"}
tool.Style.assign_style_to_representation_item(representation_item, None)
tool.Geometry.reload_representation(obj)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
class EnableEditingRepresentationItemShapeAspect(bpy.types.Operator, Operator):
bl_idname = "bim.enable_editing_representation_item_shape_aspect"
bl_label = "Enable Editing Representation Item Shape Aspect"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.active_object.BIMGeometryProperties
props.is_editing_item_shape_aspect = True
# set dropdown to currently active shape aspect
shape_aspect_id = props.items[props.active_item_index].shape_aspect_id
if shape_aspect_id != 0:
props.representation_item_shape_aspect = str(shape_aspect_id)
class EditRepresentationItemShapeAspect(bpy.types.Operator, Operator):
bl_idname = "bim.edit_representation_item_shape_aspect"
bl_label = "Edit Representation Item Shape Aspect"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMGeometryProperties
props.is_editing_item_shape_aspect = False
ifc_file = tool.Ifc.get()
representation_item_id = props.items[props.active_item_index].ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
if props.representation_item_shape_aspect == "NEW":
active_representation = tool.Geometry.get_active_representation(obj)
# find IfcProductRepresentationSelect based on current representation
if hasattr(element, "Representation"): # IfcProduct
product_shape = element.Representation
else: # IfcTypeProduct
for representation_map in element.RepresentationMaps:
if representation_map.MappedRepresentation == active_representation:
product_shape = representation_map
previous_shape_aspect_id = props.items[props.active_item_index].shape_aspect_id
# will be None if item didn't had a shape aspect
previous_shape_aspect = tool.Ifc.get_entity_by_id(previous_shape_aspect_id)
shape_aspect = tool.Geometry.create_shape_aspect(
product_shape, active_representation, [representation_item], previous_shape_aspect
)
else:
shape_aspect = ifc_file.by_id(int(props.representation_item_shape_aspect))
tool.Geometry.add_representation_item_to_shape_aspect([representation_item], shape_aspect)
# set attributes from UI
shape_aspect_attrs = props.shape_aspect_attrs
shape_aspect.Name = shape_aspect_attrs.name
shape_aspect.Description = shape_aspect_attrs.description
shape_aspect_representation = tool.Geometry.get_shape_aspect_representation_for_item(
shape_aspect, representation_item
)
styles = tool.Geometry.get_shape_aspect_styles(element, shape_aspect, representation_item)
tool.Ifc.run(
"style.assign_representation_styles",
shape_representation=shape_aspect_representation,
styles=styles,
)
tool.Geometry.get_shape_aspect_styles(element, shape_aspect, representation_item)
tool.Geometry.reload_representation(obj)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
class DisableEditingRepresentationItemShapeAspect(bpy.types.Operator, Operator):
bl_idname = "bim.disable_editing_representation_item_shape_aspect"
bl_label = "Disable Editing Representation Item Shape Aspect"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.active_object.BIMGeometryProperties
props.is_editing_item_shape_aspect = False
class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, Operator):
bl_idname = "bim.remove_representation_item_from_shape_aspect"
bl_label = "Remove Representation Item From Shape Aspect"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMGeometryProperties
ifc_file = tool.Ifc.get()
representation_item_id = props.items[props.active_item_index].ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
shape_aspect = ifc_file.by_id(props.items[props.active_item_index].shape_aspect_id)
# unassign items before removing items as removing items
# might remove shape aspect
if representation_item.StyledByItem:
styles = tool.Geometry.get_shape_aspect_styles(element, shape_aspect, representation_item)
self.remove_styles_from_item(representation_item, styles)
tool.Geometry.reload_representation(obj)
tool.Geometry.remove_representation_items_from_shape_aspect([representation_item], shape_aspect)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
def remove_styles_from_item(self, representation_item, styles):
ifc_file = tool.Ifc.get()
styled_item = representation_item.StyledByItem[0]
new_styles = [s for s in styled_item.Styles if s not in styles]
styled_item.Styles = new_styles
for style_ in new_styles:
if style_.is_a("IfcPresentationStyleAssignment"):
new_assignment_styles = [s for s in styled_item.Styles if s not in styles]
if not new_assignment_styles:
ifc_file.remove(style_)
else:
style_.Styles = new_assignment_styles
if not styled_item.Styles:
ifc_file.remove(styled_item)
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.tool as tool
from blenderbim.bim.prop import StrProperty, Attribute
from blenderbim.bim.module.geometry.data import RepresentationsData
from bpy.types import PropertyGroup
@@ -47,11 +48,63 @@ def update_mode(self, context):
bpy.ops.bim.override_mode_set_edit("INVOKE_DEFAULT")
def get_styles(self, context):
# postponed import to avoid circular import
from blenderbim.bim.module.material.data import MaterialsData
if not MaterialsData.is_loaded:
MaterialsData.load()
return MaterialsData.data["styles"]
def get_shape_aspects(self, context):
if not RepresentationsData.is_loaded:
RepresentationsData.load()
return RepresentationsData.data["shape_aspects"]
def get_material_constituents(self, context, edit_text):
from blenderbim.bim.module.material.data import ObjectMaterialData
if not ObjectMaterialData.is_loaded:
ObjectMaterialData.load()
return ObjectMaterialData.data["active_material_constituents"]
def update_shape_aspect(self, context):
shape_aspect_id = self.representation_item_shape_aspect
attrs = self.shape_aspect_attrs
if shape_aspect_id == "NEW": # new shape aspect
attrs.name = tool.Blender.get_blender_prop_default_value(attrs, "name")
attrs.description = tool.Blender.get_blender_prop_default_value(attrs, "description")
else:
shape_aspect = tool.Ifc.get().by_id(int(shape_aspect_id))
attrs.name = shape_aspect.Name or ""
attrs.description = shape_aspect.Description or ""
class RepresentationItem(PropertyGroup):
name: StringProperty(name="Name")
surface_style: StringProperty(name="Surface Style")
layer: StringProperty(name="Layer")
ifc_definition_id: IntProperty(name="IFC Definition ID")
shape_aspect: StringProperty(name="Shape Aspect")
shape_aspect_id: IntProperty(name="Shape Aspect IFC ID")
class ShapeAspect(PropertyGroup):
name: StringProperty(
name="Name",
description=(
"Note that IfcMaterialConstituent is applied based on shape aspects using the same name as material constituent.\n"
"In dropdown suggestions you can see names of existing material constituents."
),
**({} if bpy.app.version < (3, 3, 0) else {"search": get_material_constituents}),
)
description: StringProperty(
name="Description",
)
class BIMObjectGeometryProperties(PropertyGroup):
@@ -59,6 +112,13 @@ class BIMObjectGeometryProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
items: CollectionProperty(name="Representation Items", type=RepresentationItem)
active_item_index: IntProperty(name="Active Representation Item Index")
is_editing_item_style: BoolProperty(name="Is Editing Item's Style", default=False)
representation_item_style: EnumProperty(items=get_styles, name="Representation Item Style")
is_editing_item_shape_aspect: BoolProperty(name="Is Editing Item's Shape Aspect", default=False)
representation_item_shape_aspect: EnumProperty(
items=get_shape_aspects, name="Representation Item Shape Aspect", update=update_shape_aspect
)
shape_aspect_attrs: PointerProperty(type=ShapeAspect)
class BIMGeometryProperties(PropertyGroup):
@@ -29,6 +29,7 @@ from blenderbim.bim.module.geometry.data import (
PlacementData,
DerivedCoordinatesData,
)
from blenderbim.bim.module.layer.data import LayersData
def mode_menu(self, context):
@@ -133,6 +134,7 @@ class BIM_PT_representations(Panel):
if not RepresentationsData.data["representations"]:
layout.label(text="No Representations Found")
return
for representation in RepresentationsData.data["representations"]:
row = self.layout.row(align=True)
@@ -150,6 +152,18 @@ class BIM_PT_representations(Panel):
op.ifc_definition_id = representation["id"]
op.disable_opening_subtractions = False
row.operator("bim.remove_representation", icon="X", text="").representation_id = representation["id"]
if representation["is_active"]:
active_representation = representation
layout.separator()
if not LayersData.is_loaded:
LayersData.load()
if LayersData.data["active_layers"]:
layout.label(text="Representation Presentation Layers:")
for layer_name in LayersData.data["active_layers"].values():
layout.label(text=layer_name, icon="STICKY_UVS_LOC")
else:
layout.label(text="Representation Has No Presentation Layers", icon="STICKY_UVS_LOC")
class BIM_PT_representation_items(Panel):
@@ -187,20 +201,47 @@ class BIM_PT_representation_items(Panel):
self.layout.template_list("BIM_UL_representation_items", "", props, "items", props, "active_item_index")
item_is_active = props.active_item_index < len(props.items)
if item_is_active:
surface_style = props.items[props.active_item_index].surface_style
presentation_layer = props.items[props.active_item_index].layer
if not item_is_active:
return
active_item = props.items[props.active_item_index]
surface_style = active_item.surface_style
shape_aspect = active_item.shape_aspect
row = self.layout.row(align=True)
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="")
row.operator("bim.edit_representation_item_style", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_representation_item_style", icon="CANCEL", text="")
else:
surface_style, presentation_layer = None, None
if surface_style:
row.label(text=surface_style, icon="SHADING_RENDERED")
else:
row.label(text="No Surface Style", icon="MESH_UVSPHERE")
row.operator("bim.enable_editing_representation_item_style", icon="GREASEPENCIL", text="")
if surface_style:
row.operator("bim.unassign_representation_item_style", icon="X", text="")
row = self.layout.row()
if surface_style:
row.label(text=surface_style, icon="SHADING_RENDERED")
else:
row.label(text="No Surface Style", icon="MESH_UVSPHERE")
row.label(text=active_item.layer or "No Presentation Layer", icon="STICKY_UVS_LOC")
row = self.layout.row()
row.label(text=presentation_layer or "No Presentation Layer", icon="STICKY_UVS_LOC")
row = self.layout.row(align=True)
if props.is_editing_item_shape_aspect:
row.prop(props, "representation_item_shape_aspect", icon="SHAPEKEY_DATA", text="")
row.operator("bim.edit_representation_item_shape_aspect", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_representation_item_shape_aspect", icon="CANCEL", text="")
shape_aspect_attrs = props.shape_aspect_attrs
self.layout.label(text="Shape Aspect Attributes:")
self.layout.prop(shape_aspect_attrs, "name")
self.layout.prop(shape_aspect_attrs, "description")
else:
row.label(text=shape_aspect or "No Shape Aspect", icon="SHAPEKEY_DATA")
row.operator("bim.enable_editing_representation_item_shape_aspect", icon="GREASEPENCIL", text="")
if shape_aspect:
row.operator("bim.remove_representation_item_from_shape_aspect", icon="X", text="")
class BIM_PT_connections(Panel):
@@ -466,6 +507,11 @@ class BIM_UL_representation_items(UIList):
if item:
icon = "MATERIAL" if item.surface_style else "MESH_UVSPHERE"
row = layout.row(align=True)
row.label(text=item.name, icon=icon)
item_name = item.name
if item.shape_aspect:
item_name = f"{item.shape_aspect} {item_name}"
row.label(text=item_name, icon=icon)
if item.layer:
row.label(text="", icon="STICKY_UVS_LOC")
op = row.operator("bim.remove_representation_item", icon="X", text="")
op.representation_item_id = item.ifc_definition_id
@@ -40,14 +40,15 @@ class LayersData:
@classmethod
def active_layers(cls):
if not bpy.context.active_object:
return []
data = bpy.context.active_object.data
if not data:
return []
if not isinstance(data, bpy.types.Mesh) or not data.BIMMeshProperties.ifc_definition_id:
return []
results = []
results = dict()
shape = tool.Ifc.get().by_id(data.BIMMeshProperties.ifc_definition_id)
for inverse in tool.Ifc.get().get_inverse(shape):
if inverse.is_a("IfcPresentationLayerAssignment"):
results.append(inverse.id())
for inverse in shape.LayerAssignments:
results[inverse.id()] = inverse.Name or "Unnamed"
return results
@@ -137,6 +137,7 @@ class RemovePresentationLayer(bpy.types.Operator):
class AssignPresentationLayer(bpy.types.Operator):
bl_idname = "bim.assign_presentation_layer"
bl_label = "Assign Presentation Layer"
bl_description = "Assign presentation layer to the active representation of the active object"
bl_options = {"REGISTER", "UNDO"}
item: bpy.props.StringProperty()
layer: bpy.props.IntProperty()
@@ -158,6 +159,7 @@ class AssignPresentationLayer(bpy.types.Operator):
class UnassignPresentationLayer(bpy.types.Operator):
bl_idname = "bim.unassign_presentation_layer"
bl_label = "Unassign Presentation Layer"
bl_description = "Unassign presentation layer from the active representation of the active object"
bl_options = {"REGISTER", "UNDO"}
item: bpy.props.StringProperty()
layer: bpy.props.IntProperty()
@@ -84,6 +84,7 @@ class BIM_UL_layers(UIList):
op = row.operator("bim.assign_presentation_layer", text="", icon="KEYFRAME", emboss=False)
op.layer = item.ifc_definition_id
# TODO: replace placeholder UI for hiding presentation layers
row.operator("bim.disable_editing_layer", text="", icon="HIDE_OFF", emboss=False)
row.operator("bim.disable_editing_layer", text="", icon="FREEZE", emboss=False)
@@ -152,6 +152,7 @@ class ObjectMaterialData:
cls.data["materials"] = cls.materials()
cls.data["type_material"] = cls.type_material()
cls.data["material_type"] = cls.material_type()
cls.data["active_material_constituents"] = cls.active_material_constituents()
cls.is_loaded = True
@classmethod
@@ -318,3 +319,10 @@ class ObjectMaterialData:
if version == "IFC2X3":
material_types = ["IfcMaterial", "IfcMaterialLayerSet", "IfcMaterialList"]
return [(m, m, ifcopenshell.util.doc.get_entity_doc(version, m).get("description", "")) for m in material_types]
@classmethod
def active_material_constituents(cls):
material = cls.material
if not cls.material or not material.is_a("IfcMaterialConstituentSet"):
return []
return [m.Name for m in material.MaterialConstituents if m.Name]
@@ -34,6 +34,7 @@ from blenderbim.bim.ifc import IfcStore
class LoadMaterials(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.load_materials"
bl_label = "Load Materials"
bl_description = "Display list of named materials"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
@@ -151,7 +152,10 @@ class RemoveMaterial(bpy.types.Operator, tool.Ifc.Operator):
material: bpy.props.IntProperty()
def _execute(self, context):
core.remove_material(tool.Ifc, tool.Material, tool.Style, material=tool.Ifc.get().by_id(self.material))
res = core.remove_material(tool.Ifc, tool.Material, tool.Style, material=tool.Ifc.get().by_id(self.material))
if not res:
self.report({"ERROR"}, "Material is used in material sets and cannot be removed.")
return {"CANCELLED"}
class RemoveMaterialSet(bpy.types.Operator, tool.Ifc.Operator):
@@ -270,6 +274,7 @@ class RemoveProfile(bpy.types.Operator, tool.Ifc.Operator):
class AddLayer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_layer"
bl_label = "Add Layer"
bl_description = "The List is Ordered From Origin Point"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
layer_set: bpy.props.IntProperty()
@@ -294,6 +299,7 @@ class AddLayer(bpy.types.Operator, tool.Ifc.Operator):
class ReorderMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.reorder_material_set_item"
bl_label = "Reorder Material Set Item"
bl_description = "The List is Ordered From Origin Point"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
old_index: bpy.props.IntProperty()
@@ -728,6 +734,20 @@ class EnableEditingMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
props.active_material_id = self.material
props.editing_material_type = "STYLE"
# set already applied style and context if possible
if not 0 <= props.active_material_index < len(props.materials):
return
material = tool.Ifc.get().by_id(props.materials[props.active_material_index].ifc_definition_id)
if not material.HasRepresentation:
return
rep = material.HasRepresentation[0].Representations[0] # IfcStyledRepresentation
props.contexts = str(rep.ContextOfItems.id())
style = rep.Items[0].Styles[0]
if style.Name: # props.styles only has named styles
props.styles = str(rep.Items[0].Styles[0].id())
class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_material_style"
@@ -736,14 +756,19 @@ class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = bpy.context.scene.BIMMaterialProperties
material = tool.Ifc.get().by_id(props.active_material_id)
style = tool.Ifc.get().by_id(int(props.styles))
context = tool.Ifc.get().by_id(int(props.contexts))
ifcopenshell.api.run(
"style.assign_material_style", tool.Ifc.get(), material=material, style=style, context=context
)
ifc_file = tool.Ifc.get()
material = ifc_file.by_id(props.active_material_id)
style = ifc_file.by_id(int(props.styles))
context = ifc_file.by_id(int(props.contexts))
ifcopenshell.api.run("style.assign_material_style", ifc_file, material=material, style=style, context=context)
tool.Material.disable_editing_material()
core.load_materials(tool.Material, props.material_type)
# NOTE: Update all elements that has this material and
# not just the ones that are using it in IfcMaterialConstituent,
# to handle cases where Style is connected to geometry implicitly:
# e.g. RepItem->IfcElement->IfcMaterial->Style
# instead of RepItem->ShapeAspect->Constituent->Style.
tool.Material.update_elements_using_material(material)
class UnassignMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
@@ -762,3 +787,4 @@ class UnassignMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
"style.unassign_material_style", tool.Ifc.get(), material=material, style=style, context=context
)
core.load_materials(tool.Material, props.material_type)
tool.Material.update_elements_using_material(material)
@@ -81,6 +81,8 @@ class BIM_PT_materials(Panel):
self.layout.template_list("BIM_UL_materials", "", self.props, "materials", self.props, "active_material_index")
# TODO: data.py is not updated on changing active_material_index
# so the active material styles go out of sync with the active material
for style in MaterialsData.data["active_styles"]:
row = self.layout.row(align=True)
row.label(text="", icon="SHADING_RENDERED")
@@ -228,7 +230,7 @@ class BIM_PT_object_material(Panel):
row = self.layout.row(align=True)
row.label(text="No Profiles Available")
row.operator("bim.add_profile_def", icon="ADD", text="")
else:
else:
row = self.layout.row(align=True)
if ObjectMaterialData.data["set_item_name"] == "profile":
prop_with_search(row, self.mprops, "profiles", icon="ITALIC", text="")
@@ -113,6 +113,7 @@ classes = (
covering.AddInstanceCeilingCoveringFromCursor,
covering.RegenSelectedCoveringObject,
space.ToggleSpaceVisibility,
space.ToggleHideSpaces,
mep.FitFlowSegments,
mep.RegenerateDistributionElement,
prop.BIMModelProperties,
@@ -193,15 +194,18 @@ addon_keymaps = []
def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.WallTool, after={"builtin.transform"}, separator=True, group=False)
bpy.utils.register_tool(workspace.WallTool, after={"bim.explore_tool"}, separator=True, group=False)
bpy.utils.register_tool(workspace.SlabTool, after={"bim.wall_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.DoorTool, after={"bim.slab_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.WindowTool, after={"bim.door_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.ColumnTool, after={"bim.window_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.BeamTool, after={"bim.column_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.DuctTool, after={"bim.beam_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.BimTool, after={"bim.beam_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.DuctTool, after={"bim.beam_tool"}, separator=False, group=True)
bpy.utils.register_tool(workspace.PipeTool, after={"bim.duct_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.BimTool, after={"bim.pipe_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.CableCarrierTool, after={"bim.pipe_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.CableTool, after={"bim.cable_carrier_tool"}, separator=False, group=False)
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties)
bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties)
@@ -210,6 +214,7 @@ def register():
bpy.types.Object.BIMDoorProperties = bpy.props.PointerProperty(type=prop.BIMDoorProperties)
bpy.types.Object.BIMRailingProperties = bpy.props.PointerProperty(type=prop.BIMRailingProperties)
bpy.types.Object.BIMRoofProperties = bpy.props.PointerProperty(type=prop.BIMRoofProperties)
bpy.types.VIEW3D_MT_mesh_add.append(ui.add_mesh_object_menu)
bpy.types.VIEW3D_MT_add.append(ui.add_menu)
bpy.app.handlers.load_post.append(handler.load_post)
@@ -231,6 +236,8 @@ def unregister():
bpy.utils.unregister_tool(workspace.BeamTool)
bpy.utils.unregister_tool(workspace.DuctTool)
bpy.utils.unregister_tool(workspace.PipeTool)
bpy.utils.unregister_tool(workspace.CableCarrierTool)
bpy.utils.unregister_tool(workspace.CableTool)
bpy.utils.unregister_tool(workspace.BimTool)
del bpy.types.Scene.BIMModelProperties
del bpy.types.Object.BIMArrayProperties
@@ -33,7 +33,7 @@ import blenderbim.core.geometry
import blenderbim.bim.import_ifc as import_ifc
import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore
from math import pi
from math import pi, radians
from mathutils import Vector, Matrix
from bpy.types import Operator
from bpy.types import SpaceView3D
@@ -87,20 +87,28 @@ class FilledOpeningGenerator:
# In this prototype, we assume openings are only added to axis-based elements
layers = tool.Model.get_material_layer_parameters(element)
axis = tool.Model.get_wall_axis(voided_obj, layers=layers)["base"]
if layers["layer_set_direction"] == "AXIS2":
axis = tool.Model.get_wall_axis(voided_obj, layers=layers)["base"]
new_matrix = voided_obj.matrix_world.copy()
point_on_axis = tool.Cad.point_on_edge(target, axis)
new_matrix.translation.x = point_on_axis.x
new_matrix.translation.y = point_on_axis.y
new_matrix = voided_obj.matrix_world.copy()
point_on_axis = tool.Cad.point_on_edge(target, axis)
new_matrix.translation.x = point_on_axis.x
new_matrix.translation.y = point_on_axis.y
if should_set_z_level:
if filling.is_a("IfcDoor"):
new_matrix.translation.z = voided_obj.matrix_world.translation.z
if should_set_z_level:
if filling.is_a("IfcDoor"):
new_matrix.translation.z = voided_obj.matrix_world.translation.z
else:
new_matrix.translation.z = voided_obj.matrix_world.translation.z + props.rl2
else:
new_matrix.translation.z = voided_obj.matrix_world.translation.z + props.rl2
else:
new_matrix.translation.z = filling_obj.matrix_world.copy().translation.z
new_matrix.translation.z = filling_obj.matrix_world.copy().translation.z
elif layers["layer_set_direction"] == "AXIS3":
new_matrix = voided_obj.matrix_world.copy()
local_position_on_voided_obj = raycast[1]
# 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')
new_matrix @= rotation_matrix
filling_obj.matrix_world = new_matrix
bpy.context.view_layer.update()
@@ -521,6 +529,7 @@ class AddBoolean(Operator, tool.Ifc.Operator):
return {"FINISHED"}
if element2 and not element1:
obj1, obj2 = obj2, obj1
element1, element2 = element2, element1
if not obj1.data or not hasattr(obj1.data, "BIMMeshProperties"):
return {"FINISHED"}
representation = tool.Ifc.get().by_id(obj1.data.BIMMeshProperties.ifc_definition_id)
@@ -668,17 +677,17 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
except:
continue
boolean = None
boolean_id = None
for inverse in tool.Ifc.get().get_inverse(item):
if inverse.is_a("IfcBooleanResult"):
boolean = inverse
boolean_id = inverse.id()
break
ifcopenshell.api.run("geometry.remove_boolean", tool.Ifc.get(), item=item)
if obj.data.BIMMeshProperties.obj:
upstream_obj = obj.data.BIMMeshProperties.obj
element = tool.Ifc.get_entity(upstream_obj)
bbim_boolean_updates.setdefault(element, []).append(boolean)
bbim_boolean_updates.setdefault(element, []).append(boolean_id)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
blenderbim.core.geometry.switch_representation(
@@ -692,8 +701,8 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
)
bpy.data.objects.remove(obj)
for element, booleans in bbim_boolean_updates.items():
tool.Model.unmark_manual_booleans(element, booleans)
for element, boolean_ids in bbim_boolean_updates.items():
tool.Model.unmark_manual_booleans(element, boolean_ids)
tool.Blender.set_active_object(upstream_obj)
return {"FINISHED"}
@@ -761,8 +770,6 @@ class EditOpenings(Operator, tool.Ifc.Operator):
def _execute(self, context):
props = bpy.context.scene.BIMModelProperties
building_objs = set()
model = tool.Ifc.get()
all_openings = model.by_type("IfcOpeningElement")
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -773,23 +780,32 @@ class EditOpenings(Operator, tool.Ifc.Operator):
obj = tool.Ifc.get_object(element)
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
for opening in openings:
similar_openings = [o for o in all_openings if o.ObjectPlacement == opening.ObjectPlacement]
opening_obj = tool.Ifc.get_object(opening)
building_objs.add(obj)
similar_openings = blenderbim.core.geometry.get_similar_openings(tool.Ifc, opening)
similar_openings_building_objs = blenderbim.core.geometry.get_similar_openings_building_objs(
tool.Ifc, similar_openings
)
building_objs.update(similar_openings_building_objs)
if opening_obj:
if tool.Ifc.is_edited(opening_obj):
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
blenderbim.core.geometry.edit_similar_opening_placement(
tool.Geometry, opening, similar_openings
)
elif tool.Ifc.is_moved(opening_obj):
blenderbim.core.geometry.edit_object_placement(
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
)
for similar_opening in similar_openings:
similar_opening.ObjectPlacement = opening.ObjectPlacement
element = similar_opening.VoidsElements[0].RelatingBuildingElement
obj = tool.Ifc.get_object(element)
building_objs.add(obj)
blenderbim.core.geometry.edit_similar_opening_placement(
tool.Geometry, opening, similar_openings
)
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
building_objs.update(
self.get_all_building_objects_of_similar_openings(opening)
) # NB this has nothing to do with clone similar_opening
tool.Ifc.unlink(element=opening, obj=opening_obj)
bpy.data.objects.remove(opening_obj)
@@ -1013,17 +1013,30 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
start = obj.matrix_world @ obj.data.vertices[0].co.copy()
end = obj.matrix_world @ obj.data.vertices[1].co.copy()
matrix = obj.matrix_world
previous_z_axis = matrix.col[2].to_3d().normalized()
start = matrix @ obj.data.vertices[0].co
end = matrix @ obj.data.vertices[1].co
depth = (end - start).length
z_axis = (end - start).normalized()
y_axis = Vector((0, 0, 1))
# making sure z_axis != y_axis
if z_axis == y_axis:
y_axis = Vector((0, 1, 0))
x_axis = y_axis.cross(z_axis).normalized()
y_axis = z_axis.cross(x_axis).normalized()
# if z-axis didn't changed we can just reuse the previous rotation
if not tool.Cad.are_vectors_equal(previous_z_axis, z_axis):
y_axis = Vector((0, 0, 1))
# making sure z_axis != y_axis
if z_axis == y_axis:
y_axis = Vector((0, 1, 0))
x_axis = y_axis.cross(z_axis).normalized()
y_axis = z_axis.cross(x_axis).normalized()
# update basises
matrix.col[0].xyz = x_axis
matrix.col[1].xyz = y_axis
matrix.col[2].xyz = z_axis
matrix.translation = start
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
@@ -1036,16 +1049,6 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
should_sync_changes_first=False,
)
matrix = Matrix(
(
[x_axis[0], y_axis[0], z_axis[0], start.x],
[x_axis[1], y_axis[1], z_axis[1], start.y],
[x_axis[2], y_axis[2], z_axis[2], start.z],
[0, 0, 0, 1],
)
)
obj.matrix_world = matrix
bpy.context.view_layer.update()
joiner = DumbProfileJoiner()
@@ -99,7 +99,10 @@ class BIMModelProperties(PropertyGroup):
items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")],
name="Occurrence Name Style",
)
occurrence_name_function: bpy.props.StringProperty(name="Occurrence Name Function")
occurrence_name_function: bpy.props.StringProperty(
name="Occurrence Name Function",
description="Code that will be evaluated to generate occurrence name for CUSTOM occurrence name style",
)
getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type_id}
extrusion_depth: bpy.props.FloatProperty(min=0.001, default=42.0, subtype="DISTANCE")
cardinal_point: bpy.props.EnumProperty(
@@ -234,7 +237,6 @@ class BIMStairProperties(PropertyGroup):
unit="LENGTH",
size=2,
)
# TODO: need to clamp at zero for non WOOD/STEEL
nosing_length: bpy.props.FloatProperty(
name="Nosing Length",
description=(
@@ -712,8 +714,15 @@ class BIMRoofProperties(PropertyGroup):
height: bpy.props.FloatProperty(
name="Height", default=1.0, description="Maximum height of the roof to be generated.", subtype="DISTANCE"
)
angle: bpy.props.FloatProperty(name="Slope Angle", default=pi / 18, subtype="ANGLE", update=lambda self, context: self.update_percentage())
percentage: bpy.props.FloatProperty(name="Slope %", default=17.63269754733197, subtype="PERCENTAGE", update=lambda self, context: self.update_angle())
angle: bpy.props.FloatProperty(
name="Slope Angle", default=pi / 18, subtype="ANGLE", update=lambda self, context: self.update_percentage()
)
percentage: bpy.props.FloatProperty(
name="Slope %",
default=math.tan(pi / 18) * 100,
subtype="PERCENTAGE",
update=lambda self, context: self.update_angle(),
)
roof_thickness: bpy.props.FloatProperty(name="Roof Thickness", default=0.1, subtype="DISTANCE")
rafter_edge_angle: bpy.props.FloatProperty(name="Rafter Edge Angle", min=0, max=pi, default=pi / 2, subtype="ANGLE")
@@ -745,4 +754,4 @@ class BIMRoofProperties(PropertyGroup):
self.angle = math.atan(self.percentage / 100)
def update_percentage(self):
self.percentage = math.tan(self.angle)*100
self.percentage = math.tan(self.angle) * 100
@@ -29,13 +29,19 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_space"
bl_label = "Generate Space"
bl_options = {"REGISTER"}
bl_description = "Create a space from the cursor position. Move the cursor position into the desired position, select the right space collection and run the operator"
bl_description = (
"Create a space from the cursor position. "
"Move the cursor position into the desired position, "
"select the right space collection and run the operator"
)
@classmethod
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
return tool.Ifc.get_entity(collection_obj)
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
return tool.Ifc.get_entity(collection_obj) and not element.is_a("IfcWall")
def _execute(self, context):
# This works as a 2.5 extruded polygon based on a cutting plane. Note
@@ -45,16 +51,14 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
# props = context.scene.BIMModelProperties
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
if not collection_obj:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
core.generate_space(tool.Ifc, tool.Spatial, tool.Model, tool.Type)
@@ -68,7 +72,7 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
active_obj = bpy.context.active_object
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
if element:
return context.selected_objects and element.is_a("IfcWall")
@@ -79,7 +83,7 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
# In order to run, the active object must be a wall and
# there must be selected walls
active_obj = bpy.context.active_object
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
container = tool.Spatial.get_container(element)
@@ -94,19 +98,30 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
if not container:
self.report({"ERROR"}, "The wall is not contained.")
if not bpy.context.selected_objects:
if not context.selected_objects:
self.report({"ERROR"}, "No selected objects found. Please select walls.")
return
core.generate_spaces_from_walls(tool.Ifc, tool.Spatial, tool.Collector)
class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.toggle_space_visibility"
bl_label = "Toggle Space Visibility"
bl_options = {"REGISTER"}
bl_description = "Change the space visibility"
def execute(cls, context):
def execute(self, context):
core.toggle_space_visibility(tool.Ifc, tool.Spatial)
return {"FINISHED"}
class ToggleHideSpaces(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.toggle_hide_spaces"
bl_label = "Toggle Hide Spaces"
bl_options = {"REGISTER"}
bl_description = "Hide or Unhide all spaces"
def execute(self, context):
core.toggle_hide_spaces(tool.Ifc, tool.Spatial)
return {"FINISHED"}
@@ -1120,11 +1120,11 @@ 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,
obj,
other,
connection,
rel.RelatedConnectionType,
is_relating=True,
description=rel.Description
)
for rel in element.ConnectedFrom:
@@ -1226,6 +1226,7 @@ class DumbWallJoiner:
# If opening has filling then stick to the filling's position
# We're applying new openings position only after wall position is applied
for opening in [r.RelatedOpeningElement for r in element.HasOpenings if r.RelatedOpeningElement.HasFillings]:
similar_openings = blenderbim.core.geometry.get_similar_openings(tool.Ifc, opening)
filling_obj = tool.Ifc.get_object(opening.HasFillings[0].RelatedBuildingElement)
filling_moved = tool.Ifc.is_moved(filling_obj)
if filling_moved:
@@ -1234,6 +1235,7 @@ class DumbWallJoiner:
ifcopenshell.api.run(
"geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=filling_obj.matrix_world
)
blenderbim.core.geometry.edit_similar_opening_placement(tool.Geometry, opening, similar_openings)
blenderbim.core.geometry.switch_representation(
tool.Ifc,
@@ -176,6 +176,21 @@ class DuctTool(BimTool):
BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
class CableCarrierTool(BimTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "bim.cable_carrier_tool"
bl_label = "Create Cable Carrier"
bl_description = "Create and edit cable carriers"
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.cablecarrier")
bl_widget = None
ifc_element_type = "IfcCableCarrierSegmentType"
@classmethod
def draw_settings(cls, context, layout, ws_tool):
BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
class PipeTool(BimTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
@@ -191,6 +206,21 @@ class PipeTool(BimTool):
BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
class CableTool(BimTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "bim.cable_tool"
bl_label = "Create Cable"
bl_description = "Create and edit cables"
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.cable")
bl_widget = None
ifc_element_type = "IfcCableSegmentType"
@classmethod
def draw_settings(cls, context, layout, ws_tool):
BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
def add_layout_hotkey_operator(layout, text, hotkey, description):
modifiers = {
"A": "EVENT_ALT",
@@ -100,6 +100,7 @@ class ExecuteIfcPatch(bpy.types.Operator):
}
)
ifcpatch.write(output, ifc_patch_output)
self.report({"INFO"}, f"{props.ifc_patch_recipes} patch executed successfully")
return {"FINISHED"}
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from . import ui, prop, operator
from . import ui, prop, operator, workspace, gizmo, decorator
classes = (
operator.AppendEntireLibrary,
@@ -25,18 +25,26 @@ classes = (
operator.AppendLibraryElementByQuery,
operator.AssignLibraryDeclaration,
operator.ChangeLibraryElement,
operator.CreateClippingPlane,
operator.CreateProject,
operator.DisableCulling,
operator.DisableEditingHeader,
operator.EditHeader,
operator.EnableCulling,
operator.EnableEditingHeader,
operator.ExportIFC,
operator.FlipClippingPlane,
operator.ImportIFC,
operator.LinkIfc,
operator.LoadLink,
operator.LoadLinkedProject,
operator.LoadProject,
operator.LoadProjectElements,
operator.NewProject,
operator.QueryLinkedElement,
operator.RefreshClippingPlanes,
operator.RefreshLibrary,
operator.ReloadLink,
operator.RevertProject,
operator.RewindLibrary,
operator.SaveLibraryFile,
@@ -48,7 +56,7 @@ classes = (
operator.UnlinkIfc,
operator.UnloadLink,
operator.UnloadProject,
operator.ReloadLink,
workspace.ExploreHotkey,
prop.LibraryElement,
prop.FilterCategory,
prop.Link,
@@ -62,6 +70,7 @@ classes = (
ui.BIM_UL_library,
ui.BIM_UL_filter_categories,
ui.BIM_UL_links,
gizmo.ClippingPlane,
)
@@ -69,7 +78,10 @@ addon_keymaps = []
def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.ExploreTool, after={"builtin.transform"}, separator=True, group=False)
bpy.types.Scene.BIMProjectProperties = bpy.props.PointerProperty(type=prop.BIMProjectProperties)
bpy.app.handlers.load_post.append(decorator.toggle_decorations_on_load)
bpy.types.TOPBAR_MT_file.prepend(ui.file_menu)
bpy.types.TOPBAR_MT_file_context_menu.prepend(ui.file_menu)
wm = bpy.context.window_manager
@@ -88,9 +100,12 @@ def register():
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.ExploreTool)
del bpy.types.Scene.BIMProjectProperties
bpy.app.handlers.load_post.remove(decorator.toggle_decorations_on_load)
bpy.types.TOPBAR_MT_file.remove(ui.file_menu)
bpy.types.TOPBAR_MT_file_context_menu.remove(ui.file_menu)
del bpy.types.Scene.BIMProjectProperties
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
@@ -24,6 +24,7 @@ from blenderbim.bim.ifc import IfcStore
def refresh():
ProjectData.is_loaded = False
LinksData.is_loaded = False
class ProjectData:
@@ -34,6 +35,7 @@ class ProjectData:
def load(cls):
cls.data = {
"export_schema": cls.get_export_schema(),
"library_file": cls.library_file(),
"template_file": cls.template_file(),
"last_saved": cls.last_saved(),
}
@@ -44,10 +46,17 @@ class ProjectData:
return [(s, s, "") for s in IfcStore.schema_identifiers]
@classmethod
def template_file(cls):
def library_file(cls):
files = os.listdir(os.path.join(bpy.context.scene.BIMProperties.data_dir, "libraries"))
results = [("0", "Custom Library", "")]
results.extend([(f, os.path.splitext(f)[0], "") for f in files if ".ifc" in f])
return results
@classmethod
def template_file(cls):
files = os.listdir(os.path.join(bpy.context.scene.BIMProperties.data_dir, "templates", "projects"))
results = [("0", "Blank Project", "")]
results.extend([(f, f.replace(".ifc", ""), "") for f in files if ".ifc" in f])
results.extend([(f, os.path.splitext(f)[0], "") for f in files if ".ifc" in f])
return results
@classmethod
@@ -61,3 +70,9 @@ class ProjectData:
return f"{save_date} {':'.join(save_time.split(':')[0:2])}"
except:
return ""
class LinksData:
linked_data = {}
enable_culling = False
is_loaded = False
@@ -0,0 +1,203 @@
# 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 gpu
import bpy
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.app.handlers import persistent
@persistent
def toggle_decorations_on_load(*args):
if bpy.context.scene.BIMProjectProperties.clipping_planes:
ClippingPlaneDecorator.install(bpy.context)
else:
ClippingPlaneDecorator.uninstall()
# NOTE: ProjectDecorator cannot be loaded at reopening .blend file
# since selected_vertices and other data is stored in queried object's
# custom attributes and they get purged after Blender session is closed
# as queried object is linked from separate .blend file.
class ProjectDecorator:
installed = None
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
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 __call__(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
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
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 = []
selected_edges = []
selected_tris = []
try:
obj = context.scene.BIMProjectProperties.queried_obj
selected_vertices = obj["selected_vertices"]
selected_edges = obj["selected_edges"]
selected_tris = obj["selected_tris"]
except:
return
if selected_edges:
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
class ClippingPlaneDecorator:
installed = None
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
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 __call__(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
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
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 = []
selected_edges = []
selected_tris = []
unselected_vertices = []
unselected_edges = []
unselected_tris = []
for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
obj = clipping_plane.obj
if not obj or not obj.data:
continue
if obj.mode == "EDIT":
continue # A profile decorator or something else is used here.
bm = bmesh.new()
bm.from_mesh(obj.data)
obj.data.calc_loop_triangles()
if obj.select_get():
offset = len(selected_vertices)
selected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
selected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
selected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
else:
offset = len(unselected_vertices)
unselected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
unselected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
unselected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
verts = [
tuple(obj.matrix_world @ Vector((0, 0, 0))),
tuple(obj.matrix_world @ Vector((0, 0, -0.5))),
tuple(obj.matrix_world @ Vector((-0.05, 0, -0.45))),
tuple(obj.matrix_world @ Vector((0.05, 0, -0.45))),
tuple(obj.matrix_world @ Vector((0, -0.05, -0.45))),
tuple(obj.matrix_world @ Vector((0, 0.05, -0.45))),
]
edges = [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
color = selected_elements_color if obj in context.selected_objects else special_elements_color
self.draw_batch("LINES", verts, color, edges)
if obj.mode != "EDIT":
bm.free()
if unselected_edges:
self.draw_batch("LINES", unselected_vertices, special_elements_color, unselected_edges)
self.draw_batch("TRIS", unselected_vertices, transparent_color(special_elements_color), unselected_tris)
if selected_edges:
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
@@ -0,0 +1,68 @@
# 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
from bpy.types import GizmoGroup
from mathutils import Matrix
class ClippingPlane(GizmoGroup):
bl_idname = "OBJECT_GGT_bim_clipping_plane"
bl_label = "Clipping Plane"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
@classmethod
def poll(cls, context):
obj = context.object
return (
context.selected_objects
and obj
and obj.name.startswith("ClippingPlane")
and obj in [sp.obj for sp in context.scene.BIMProjectProperties.clipping_planes]
)
def setup(self, context):
self.obj = None
self.offset = 0
self.mw = Matrix()
self.last_mw = Matrix()
self.gizmo = self.gizmos.new("GIZMO_GT_arrow_3d")
def move_get_x():
return self.offset
def move_set_x(value):
self.obj.matrix_world.col[3] = self.mw.col[3] + (self.mw.col[2] * self.offset)
self.last_mw = self.obj.matrix_world.copy()
self.offset = value
self.gizmo.target_set_handler("offset", get=move_get_x, set=move_set_x)
def refresh(self, context):
if self.obj != context.object or self.last_mw != context.object.matrix_world:
self.obj = context.object
self.offset = 0
self.mw = context.object.matrix_world.copy()
obj = context.object
mw = context.object.matrix_world.normalized()
mw.col[3] -= mw.col[2] * self.offset
self.gizmo.matrix_basis = mw
@@ -21,10 +21,14 @@ import bpy
import time
import logging
import tempfile
import subprocess
import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.selector
import ifcopenshell.util.geolocation
import ifcopenshell.util.representation
import ifcopenshell.util.element
import blenderbim.bim.handler
import blenderbim.bim.schema
import blenderbim.tool as tool
@@ -35,8 +39,12 @@ from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.ui import IFCFileSelector
from blenderbim.bim import import_ifc
from blenderbim.bim import export_ifc
from math import radians
from pathlib import Path
from mathutils import Vector, Matrix
from bpy.app.handlers import persistent
from blenderbim.bim.module.project.data import LinksData
from blenderbim.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator
class NewProject(bpy.types.Operator):
@@ -76,7 +84,7 @@ class NewProject(bpy.types.Operator):
bpy.context.scene.unit_settings.length_unit = "MILLIMETERS"
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
bpy.context.scene.BIMProjectProperties.template_file = "IFC4 Demo Library.ifc"
bpy.context.scene.BIMProjectProperties.template_file = "IFC4 Demo Template.ifc"
if self.preset != "wizard":
bpy.ops.bim.create_project()
@@ -178,7 +186,11 @@ class RefreshLibrary(bpy.types.Operator):
self.props.active_library_element = ""
types = IfcStore.library_file.wrapped_data.types_with_super()
for importable_type in sorted(["IfcTypeProduct", "IfcMaterial", "IfcCostSchedule", "IfcProfileDef"]):
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()
new.name = importable_type
@@ -197,11 +209,15 @@ class ChangeLibraryElement(bpy.types.Operator):
self.library_file = IfcStore.library_file
ifc_classes = set()
self.props.active_library_element = self.element_name
crumb = self.props.library_breadcrumb.add()
crumb.name = self.element_name
elements = self.library_file.by_type(self.element_name)
[ifc_classes.add(e.is_a()) for e in elements]
self.props.library_elements.clear()
if len(ifc_classes) == 1 and list(ifc_classes)[0] == self.element_name:
for name, ifc_definition_id in sorted([(self.get_name(e), e.id()) for e in elements]):
self.add_library_asset(name, ifc_definition_id)
@@ -423,6 +439,11 @@ class AppendLibraryElement(bpy.types.Operator):
self.import_type_from_ifc(element, context)
elif element.is_a("IfcProduct"):
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)
elif element.is_a("IfcMaterial"):
self.import_material_from_ifc(element, context)
try:
@@ -811,27 +832,30 @@ class ToggleFilterCategories(bpy.types.Operator):
class LinkIfc(bpy.types.Operator):
bl_idname = "bim.link_ifc"
bl_label = "Link Blend/IFC File"
bl_label = "Link IFC"
bl_options = {"REGISTER", "UNDO"}
bl_description = "This will link the Blender file that is synced with the IFC file"
bl_description = "Reference in a read-only IFC model in the background"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement)
directory: bpy.props.StringProperty(subtype="DIR_PATH")
filter_glob: bpy.props.StringProperty(default="*.blend;*.blend1", options={"HIDDEN"})
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
false_origin: bpy.props.StringProperty(name="False Origin", default="0,0,0")
def execute(self, context):
files = [f.name for f in self.files] if self.files else [self.filepath]
start = time.time()
files = [f.name.replace("\\", "/") for f in self.files] if self.files else [self.filepath.replace("\\", "/")]
for filename in files:
filepath = os.path.join(self.directory, filename)
filepath = os.path.join(self.directory, filename).replace("\\", "/")
if bpy.data.filepath and Path(filepath).samefile(bpy.data.filepath):
self.report({"INFO"}, "Can't link the current .blend file")
continue
new = context.scene.BIMProjectProperties.links.add()
if self.use_relative_path:
filepath = os.path.relpath(filepath, bpy.path.abspath("//"))
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
new.name = filepath
bpy.ops.bim.load_link(filepath=filepath)
bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
print(f"Finished linking {len(files)} IFCs", time.time() - start)
return {"FINISHED"}
def invoke(self, context, event):
@@ -847,6 +871,7 @@ class UnlinkIfc(bpy.types.Operator):
filepath: bpy.props.StringProperty()
def execute(self, context):
self.filepath = self.filepath.replace("\\", "/")
bpy.ops.bim.unload_link(filepath=self.filepath)
index = context.scene.BIMProjectProperties.links.find(self.filepath)
if index != -1:
@@ -862,14 +887,15 @@ class UnloadLink(bpy.types.Operator):
filepath: bpy.props.StringProperty()
def execute(self, context):
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))
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 == filepath:
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 == filepath:
if scene.library and scene.library.filepath.replace("\\", "/") == filepath:
bpy.data.scenes.remove(scene)
link = context.scene.BIMProjectProperties.links.get(self.filepath)
link.is_loaded = False
@@ -882,23 +908,59 @@ class LoadLink(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load the selected file"
filepath: bpy.props.StringProperty()
false_origin: bpy.props.StringProperty(name="False Origin", default="0,0,0")
def execute(self, context):
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))
filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), filepath)).replace("\\", "/")
if self.filepath.lower().endswith(".blend"):
self.link_blend(filepath)
elif self.filepath.lower().endswith(".ifc"):
self.link_ifc()
return {"FINISHED"}
def link_blend(self, filepath):
with bpy.data.libraries.load(filepath, link=True) as (data_from, data_to):
data_to.scenes = data_from.scenes
for scene in bpy.data.scenes:
if not scene.library or scene.library.filepath != filepath:
if not scene.library or scene.library.filepath.replace("\\", "/") != filepath:
continue
for child in scene.collection.children:
if "IfcProject" not in child.name:
continue
bpy.data.scenes[0].collection.children.link(child)
link = context.scene.BIMProjectProperties.links.get(self.filepath)
link = bpy.context.scene.BIMProjectProperties.links.get(self.filepath)
link.is_loaded = True
return {"FINISHED"}
def link_ifc(self):
blend_filepath = self.filepath + ".cache.blend"
h5_filepath = self.filepath + ".cache.h5"
if not os.path.exists(blend_filepath):
code = f"""
import bpy
def run():
bpy.ops.bim.load_linked_project(filepath="{self.filepath}", false_origin="{self.false_origin}")
bpy.ops.wm.save_as_mainfile(filepath="{blend_filepath}")
try:
run()
except Exception as e:
import traceback
traceback.print_exc()
exit(1)
"""
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
temp_file.write(code)
run = subprocess.run([bpy.app.binary_path, "-b", "--python", temp_file.name, "--python-exit-code", "1"])
if run.returncode == 1:
print("An error occurred while processing your IFC.")
self.link_blend(blend_filepath)
class ReloadLink(bpy.types.Operator):
@@ -910,7 +972,7 @@ class ReloadLink(bpy.types.Operator):
def execute(self, context):
def get_linked_ifcs():
selected_filename = os.path.basename(self.filepath)
selected_filename = os.path.basename(self.filepath.replace("\\", "/"))
return [
c.library
for c in bpy.data.collections
@@ -934,7 +996,7 @@ class ToggleLinkSelectability(bpy.types.Operator):
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))
self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath)).replace("\\", "/")
for collection in self.get_linked_collections():
collection.hide_select = not collection.hide_select
link.is_selectable = not collection.hide_select
@@ -944,7 +1006,7 @@ class ToggleLinkSelectability(bpy.types.Operator):
return [
c
for c in bpy.data.collections
if "IfcProject" in c.name and c.library and c.library.filepath == self.filepath
if "IfcProject" in c.name and c.library and c.library.filepath.replace("\\", "/") == self.filepath
]
@@ -961,7 +1023,7 @@ class ToggleLinkVisibility(bpy.types.Operator):
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))
self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath)).replace("\\", "/")
if self.mode == "WIREFRAME":
self.toggle_wireframe(link)
elif self.mode == "VISIBLE":
@@ -1000,7 +1062,7 @@ class ToggleLinkVisibility(bpy.types.Operator):
return [
c
for c in bpy.data.collections
if "IfcProject" in c.name and c.library and c.library.filepath == self.filepath
if "IfcProject" in c.name and c.library and c.library.filepath.replace("\\", "/") == self.filepath
]
@@ -1130,3 +1192,747 @@ class ImportIFC(bpy.types.Operator):
def execute(self, context):
bpy.ops.bim.load_project("INVOKE_DEFAULT")
return {"FINISHED"}
class LoadLinkedProject(bpy.types.Operator):
bl_idname = "bim.load_linked_project"
bl_label = "Load a project for viewing only."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty()
false_origin: bpy.props.StringProperty(name="False Origin", default="0,0,0")
def execute(self, context):
import ifcpatch
import multiprocessing
import ifcopenshell.geom
start = time.time()
self.filepath = self.filepath.replace("\\", "/")
print("Processing", self.filepath)
self.collection = bpy.data.collections.new("IfcProject/" + os.path.basename(self.filepath))
self.file = ifcopenshell.open(self.filepath)
print("Finished opening")
self.db_filepath = self.filepath + ".cache.sqlite"
db = ifcpatch.execute(
{"input": self.filepath, "file": self.file, "recipe": "ExtractPropertiesToSQLite", "arguments": []}
)
ifcpatch.write(db, self.db_filepath)
print("Finished writing property database")
logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
ifc_importer.file = self.file
ifc_importer.process_context_filter()
self.elements = set(self.file.by_type("IfcElement"))
if self.file.schema in ("IFC2X3", "IFC4"):
self.elements |= set(self.file.by_type("IfcProxy"))
self.elements |= set(self.file.by_type("IfcSite"))
self.elements -= set(self.file.by_type("IfcFeatureElement"))
self.elements = list(self.elements)
model_origin = np.array(ifcopenshell.util.geolocation.auto_xyz2enh(self.file, 0, 0, 0))
false_origin = np.array([float(o.strip()) for o in self.false_origin.split(",")])
model_offset = model_origin - false_origin
zero_origin = np.array((0, 0, 0))
has_model_offset = not np.allclose(model_offset, zero_origin)
for settings in ifc_importer.context_settings:
settings = ifcopenshell.geom.settings()
if has_model_offset:
offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
offset[0], offset[1], offset[2] = model_offset
settings.offset = offset
iterator = ifcopenshell.geom.iterator(
settings, self.file, multiprocessing.cpu_count(), include=self.elements
)
self.meshes = {}
self.blender_mats = {}
blender_mats = {}
default_mat = np.array([[1, 1, 1, 1]], dtype=np.float32)
chunked_guids = []
chunked_guid_ids = []
chunked_verts = []
chunked_faces = []
chunked_materials = []
chunked_material_ids = []
material_offset = 0
max_slot_index = 0
chunk_size = 10000
r4 = np.array([[0, 0, 0, 1]])
offset = 0
ci = 0
if iterator.initialize():
while True:
shape = iterator.get()
if len(shape.geometry.faces) > 1000 and self.is_local(shape): # 333 tris
self.process_occurrence(shape)
if not iterator.next():
mats = np.concatenate(chunked_materials)
midx = np.concatenate(chunked_material_ids)
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
midx = mapping[midx]
mat_results = []
for mat in mats:
mat = tuple(mat)
blender_mat = blender_mats.get(mat, None)
if not blender_mat:
blender_mat = bpy.data.materials.new("Chunk")
blender_mat.diffuse_color = mat
blender_mats[mat] = blender_mat
mat_results.append(blender_mat)
# The left over chunk
self.create_object(
np.concatenate(chunked_verts),
np.concatenate(chunked_faces),
mat_results,
midx,
chunked_guids,
chunked_guid_ids,
)
break
continue
ci += 1
if ci % 50 == 0:
print("Doing chunk", ci)
has_processed_chunk = False
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)
chunked_materials.append(ms)
chunked_material_ids.append(mi + material_offset + 1)
material_offset += len(ms)
M4 = np.frombuffer(shape.transformation_buffer).reshape((4, 3))
M4 = np.concatenate((M4.T, r4))
vs = np.frombuffer(shape.geometry.verts_buffer).reshape((-1, 3))
vs = np.hstack((vs, np.ones((len(vs), 1))))
vs = (np.asmatrix(M4) * np.asmatrix(vs).T).T.A
vs = vs[:, :3].flatten()
fs = np.frombuffer(shape.geometry.faces_buffer, dtype=np.int32)
chunked_verts.append(vs)
chunked_faces.append(fs + offset)
offset += len(vs) // 3
chunked_guids.append(shape.guid)
if chunked_guid_ids:
chunked_guid_ids.append((len(fs) // 3) + chunked_guid_ids[-1])
else:
chunked_guid_ids.append(len(fs) // 3)
if offset > chunk_size:
has_processed_chunk = True
mats = np.concatenate(chunked_materials)
midx = np.concatenate(chunked_material_ids)
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
midx = mapping[midx]
mat_results = []
for mat in mats:
mat = tuple(mat)
blender_mat = blender_mats.get(mat, None)
if not blender_mat:
blender_mat = bpy.data.materials.new("Chunk")
blender_mat.diffuse_color = mat
blender_mats[mat] = blender_mat
mat_results.append(blender_mat)
self.create_object(
np.concatenate(chunked_verts),
np.concatenate(chunked_faces),
mat_results,
midx,
chunked_guids,
chunked_guid_ids,
)
chunked_guids = []
chunked_guid_ids = []
chunked_verts = []
chunked_faces = []
chunked_materials = []
chunked_material_ids = []
material_offset = 0
max_slot_index = 0
offset = 0
if not iterator.next():
if not has_processed_chunk:
mats = np.concatenate(chunked_materials)
midx = np.concatenate(chunked_material_ids)
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
midx = mapping[midx]
mat_results = []
for mat in mats:
mat = tuple(mat)
blender_mat = blender_mats.get(mat, None)
if not blender_mat:
blender_mat = bpy.data.materials.new("Chunk")
blender_mat.diffuse_color = mat
blender_mats[mat] = blender_mat
mat_results.append(blender_mat)
# The left over chunk
self.create_object(
np.concatenate(chunked_verts),
np.concatenate(chunked_faces),
mat_results,
midx,
chunked_guids,
chunked_guid_ids,
)
break
bpy.context.scene.collection.children.link(self.collection)
print("Finished", time.time() - start)
break
return {"FINISHED"}
def is_local(self, shape):
m = shape.transformation.matrix.data
if max([abs(co) for co in (m[9], m[10], m[11])]) > 1000:
return False
elif max([abs(co) for co in shape.geometry.verts[0:3]]) > 1000:
return False
return True
def process_occurrence(self, shape):
element = self.file.by_id(shape.id)
matrix = shape.transformation.matrix.data
faces = shape.geometry.faces
verts = shape.geometry.verts
materials = shape.geometry.materials
material_ids = shape.geometry.material_ids
m = shape.transformation.matrix.data
mat = np.array(([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]))
mesh = self.meshes.get(shape.geometry.id, None)
if not mesh:
mesh = bpy.data.meshes.new("Mesh")
material_to_slot = {}
max_slot_index = 0
for i, material in enumerate(materials):
alpha = 1.0
if material.has_transparency and material.transparency > 0:
alpha = 1.0 - material.transparency
diffuse = material.diffuse + (alpha,)
material_name = f"{diffuse[0]}-{diffuse[1]}-{diffuse[2]}-{diffuse[3]}"
blender_mat = self.blender_mats.get(material_name, None)
if not blender_mat:
blender_mat = bpy.data.materials.new(material_name)
blender_mat.diffuse_color = diffuse
self.blender_mats[material_name] = blender_mat
slot_index = mesh.materials.find(material.name)
if slot_index == -1:
mesh.materials.append(blender_mat)
slot_index = max_slot_index
max_slot_index += 1
material_to_slot[i] = slot_index
material_index = [(material_to_slot[i] if i != -1 else 0) for i in material_ids]
num_vertices = len(verts) // 3
total_faces = len(faces)
loop_start = range(0, total_faces, 3)
num_loops = total_faces // 3
loop_total = [3] * num_loops
num_vertex_indices = len(faces)
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
mesh.loops.add(num_vertex_indices)
mesh.loops.foreach_set("vertex_index", faces)
mesh.polygons.add(num_loops)
mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
mesh.polygons.foreach_set("material_index", material_index)
mesh.update()
self.meshes[shape.geometry.id] = mesh
obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh)
obj.matrix_world = Matrix(mat.tolist())
obj["guids"] = [shape.guid]
obj["guid_ids"] = [len(mesh.polygons)]
obj["db"] = self.db_filepath
self.collection.objects.link(obj)
def create_object(self, verts, faces, materials, material_ids, guids, guid_ids):
num_vertices = len(verts) // 3
if not num_vertices:
return
total_faces = len(faces)
loop_start = range(0, total_faces, 3)
num_loops = total_faces // 3
loop_total = [3] * num_loops
num_vertex_indices = len(faces)
mesh = bpy.data.meshes.new("Mesh")
for material in materials:
mesh.materials.append(material)
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
mesh.loops.add(num_vertex_indices)
mesh.loops.foreach_set("vertex_index", faces)
mesh.polygons.add(num_loops)
mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
if material_ids.size > 0:
mesh.polygons.foreach_set("material_index", material_ids)
mesh.update()
obj = bpy.data.objects.new("Chunk", mesh)
obj["guids"] = list(guids)
obj["guid_ids"] = list(guid_ids)
obj["db"] = self.db_filepath
self.collection.objects.link(obj)
class QueryLinkedElement(bpy.types.Operator):
bl_idname = "bim.query_linked_element"
bl_label = "Query Linked Element"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.area.type == "VIEW_3D"
def execute(self, context):
import sqlite3
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
LinksData.linked_data = {}
bpy.context.scene.BIMProjectProperties.queried_obj = None
for area in bpy.context.screen.areas:
if area.type == "PROPERTIES":
for region in area.regions:
if region.type == "WINDOW":
region.tag_redraw()
elif 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 "guids" not in obj:
self.report({"INFO"}, "Object is not a linked IFC element.")
return {"FINISHED"}
guid = None
guid_start_index = 0
for i, guid_end_index in enumerate(obj["guid_ids"]):
if face_index < guid_end_index:
guid = obj["guids"][i]
bpy.context.scene.BIMProjectProperties.queried_obj = obj
selected_tris = []
selected_edges = []
vert_indices = set()
for polygon in obj.data.polygons[guid_start_index:guid_end_index]:
vert_indices.update(polygon.vertices)
vert_indices = list(vert_indices)
vert_map = {k: v for v, k in enumerate(vert_indices)}
selected_vertices = [tuple(obj.matrix_world @ obj.data.vertices[vi].co) for vi in vert_indices]
for polygon in obj.data.polygons[guid_start_index:guid_end_index]:
selected_tris.append(tuple(vert_map[v] for v in polygon.vertices))
selected_edges.extend(tuple([vert_map[vi] for vi in e] for e in polygon.edge_keys))
obj["selected_vertices"] = selected_vertices
obj["selected_edges"] = selected_edges
obj["selected_tris"] = selected_tris
break
guid_start_index = guid_end_index
self.db = sqlite3.connect(obj["db"])
self.c = self.db.cursor()
self.c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1")
element = self.c.fetchone()
attributes = {}
for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]):
if element[i + 1] is not None:
attributes[attr] = element[i + 1]
self.c.execute("SELECT * FROM properties WHERE element_id = ?", (element[0],))
rows = self.c.fetchall()
properties = {}
for row in rows:
properties.setdefault(row[1], {})[row[2]] = row[3]
self.c.execute("SELECT * FROM relationships WHERE from_id = ?", (element[0],))
relationships = self.c.fetchall()
relating_type_id = None
for relationship in relationships:
if relationship[1] == "IfcRelDefinesByType":
relating_type_id = relationship[2]
type_properties = {}
if relating_type_id is not None:
self.c.execute("SELECT * FROM properties WHERE element_id = ?", (relating_type_id,))
rows = self.c.fetchall()
for row in rows:
type_properties.setdefault(row[1], {})[row[2]] = row[3]
LinksData.linked_data = {
"attributes": attributes,
"properties": [(k, properties[k]) for k in sorted(properties.keys())],
"type_properties": [(k, type_properties[k]) for k in sorted(type_properties.keys())],
}
for area in bpy.context.screen.areas:
if area.type == "PROPERTIES":
for region in area.regions:
if region.type == "WINDOW":
region.tag_redraw()
elif area.type == "VIEW_3D":
area.tag_redraw()
self.report({"INFO"}, f"Loaded data for {guid}")
ProjectDecorator.install(bpy.context)
return {"FINISHED"}
def ray_cast(self, context, origin, direction):
depsgraph = context.evaluated_depsgraph_get()
result = context.scene.ray_cast(depsgraph, origin, direction)
return result
def invoke(self, context, event):
self.mouse_x = event.mouse_region_x
self.mouse_y = event.mouse_region_y
return self.execute(context)
class EnableCulling(bpy.types.Operator):
bl_idname = "bim.enable_culling"
bl_label = "Enable Culling"
bl_options = {"REGISTER"}
def __init__(self):
self.last_view_corners = None
self.total_mousemoves = 0
self.cullable_objects = []
def modal(self, context, event):
if not LinksData.enable_culling:
for obj in bpy.context.visible_objects:
if obj.type == "MESH" and obj.name.startswith("Ifc"):
obj.display_type = "SOLID"
self.cullable_objects = []
return {"CANCELLED"}
# Even if the view is changing, there are certain scenarios where we
# don't want to apply culling. For example, if we scroll to zoom but
# are simultaneously moving our mouse, or if we `Zoom to Selected`. A
# dumb but seemingly effective way is to count MOUSEMOVE events. If at
# least 3 consecutive events occur, you're probably not doing some
# other navigational thing.
if event.type == "MOUSEMOVE":
self.total_mousemoves += 1
else:
self.total_mousemoves = 0
if self.total_mousemoves > 2 and self.is_view_changed(context):
self.total_mousemoves = 0
camera_position = context.region_data.view_matrix.inverted().translation
for obj in self.cullable_objects:
if obj.type == "MESH" and obj.name.startswith("Ifc"):
if self.is_object_in_view(obj, context, camera_position):
obj.display_type = "SOLID"
elif obj.display_type != "BOUNDS":
obj.display_type = "BOUNDS"
return {"PASS_THROUGH"}
def is_view_changed(self, context):
view_matrix = context.region_data.view_matrix
projection_matrix = context.region_data.window_matrix
vp_matrix = projection_matrix @ view_matrix
# Get NDC coordinates of the viewport corners
viewport_corners = [Vector((-1, -1, 0)), Vector((1, -1, 0)), Vector((1, 1, 0)), Vector((-1, 1, 0))]
ndc_corners = [vp_matrix @ Vector((corner.x, corner.y, -1, 1)) for corner in viewport_corners]
ndc_corners = [(corner / corner.w).xy for corner in ndc_corners]
if self.last_view_corners != ndc_corners:
self.last_view_corners = ndc_corners
return True
return False
def is_object_in_view(self, obj, context, camera_position):
# Get the view matrix and the projection matrix from the active viewport
view_matrix = context.region_data.view_matrix
projection_matrix = context.region_data.window_matrix
# Calculate the combined view projection matrix
vp_matrix = projection_matrix @ view_matrix
obj_matrix_world = obj.matrix_world
# Transform each corner of the bounding box using the view projection matrix
# and check if it's inside the normalized device coordinates (NDC) space
for corner in [obj_matrix_world @ Vector(corner) for corner in obj.bound_box]:
ndc = vp_matrix @ corner.to_4d()
ndc /= ndc.w
if -1 <= ndc.x <= 1 and -1 <= ndc.y <= 1 and 0 <= ndc.z <= 1:
# At least one corner is inside the view, so the object is visible
break
else:
return False
# Check if the object is too far away from the camera
object_center = obj.matrix_world.translation
distance_threshold = 900 # 30m squared
if (camera_position - object_center).length_squared > distance_threshold:
# The object is too far away, so consider it not visible
return False
return True
def invoke(self, context, event):
LinksData.enable_culling = True
self.cullable_objects = []
for obj in bpy.context.visible_objects:
if obj.type == "MESH" and obj.name.startswith("Ifc") and max(obj.dimensions) < 0.6:
self.cullable_objects.append(obj)
camera_position = context.region_data.view_matrix.inverted().translation
if not self.is_object_in_view(obj, context, camera_position):
obj.display_type = "BOUNDS"
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
class DisableCulling(bpy.types.Operator):
bl_idname = "bim.disable_culling"
bl_label = "Disable Culling"
bl_options = {"REGISTER"}
def execute(self, context):
LinksData.enable_culling = False
return {"FINISHED"}
class RefreshClippingPlanes(bpy.types.Operator):
bl_idname = "bim.refresh_clipping_planes"
bl_label = "Refresh Clipping Planes"
bl_options = {"REGISTER"}
def __init__(self):
self.total_planes = 0
def invoke(self, context, event):
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
def modal(self, context, event):
should_refresh = False
self.clean_deleted_planes(context)
for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
if clipping_plane.obj and self.is_moved(clipping_plane.obj):
should_refresh = True
break
total_planes = len(context.scene.BIMProjectProperties.clipping_planes)
if should_refresh or total_planes != self.total_planes:
self.refresh_clipping_planes(context)
for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
if clipping_plane.obj:
tool.Geometry.record_object_position(clipping_plane.obj)
self.total_planes = total_planes
return {"PASS_THROUGH"}
def clean_deleted_planes(self, context):
while True:
for i, clipping_plane in enumerate(context.scene.BIMProjectProperties.clipping_planes):
if clipping_plane.obj:
try:
clipping_plane.obj.name
except:
context.scene.BIMProjectProperties.clipping_planes.remove(i)
break
else:
context.scene.BIMProjectProperties.clipping_planes.remove(i)
break
else:
break
def is_moved(self, obj):
if not obj.BIMObjectProperties.location_checksum:
return True # Let's be conservative
loc_check = np.frombuffer(eval(obj.BIMObjectProperties.location_checksum))
rot_check = np.frombuffer(eval(obj.BIMObjectProperties.rotation_checksum))
loc_real = np.array(obj.matrix_world.translation).flatten()
rot_real = np.array(obj.matrix_world.to_3x3()).flatten()
if np.allclose(loc_check, loc_real, atol=1e-4) and np.allclose(rot_check, rot_real, atol=1e-2):
return False
return True
def refresh_clipping_planes(self, context):
import bmesh
from itertools import cycle
area = next(a for a in bpy.context.screen.areas if a.type == "VIEW_3D")
region = next(r for r in area.regions if r.type == "WINDOW")
data = region.data
if not len(context.scene.BIMProjectProperties.clipping_planes):
data.use_clip_planes = False
else:
with bpy.context.temp_override(area=area, region=region):
bpy.ops.view3d.clip_border()
clip_planes = []
for clipping_plane in bpy.context.scene.BIMProjectProperties.clipping_planes:
obj = clipping_plane.obj
if not obj:
continue
print("doing", obj)
bm = bmesh.new()
bm.from_mesh(obj.data)
world_matrix = obj.matrix_world
bm.faces.ensure_lookup_table()
face = bm.faces[0]
center = world_matrix @ face.calc_center_median()
print("center", center)
normal = world_matrix.to_3x3() @ face.normal * -1
center += normal * -0.01
print("normal", normal)
print("new center", center)
normal.normalize()
distance = -center.dot(normal)
clip_plane = (normal.x, normal.y, normal.z, distance)
clip_planes.append(clip_plane)
bm.free()
clip_planes = cycle(clip_planes)
data.clip_planes = [tuple(next(clip_planes)) for i in range(0, 6)]
data.update()
region.tag_redraw()
[a.tag_redraw() for a in bpy.context.screen.areas]
return {"FINISHED"}
class CreateClippingPlane(bpy.types.Operator):
bl_idname = "bim.create_clipping_plane"
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:
self.report({"INFO"}, "Maximum of six clipping planes allowed.")
return {"FINISHED"}
for area in bpy.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"}
vertices = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (0.5, 0.5, 0), (-0.5, 0.5, 0)]
faces = [(0, 1, 2, 3)]
mesh = bpy.data.meshes.new(name="ClippingPlane")
mesh.from_pydata(vertices, [], faces)
mesh.update()
plane_obj = bpy.data.objects.new("ClippingPlane", mesh)
bpy.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
new = bpy.context.scene.BIMProjectProperties.clipping_planes.add()
new.obj = plane_obj
tool.Blender.set_active_object(plane_obj)
ClippingPlaneDecorator.install(bpy.context)
bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
return {"FINISHED"}
def ray_cast(self, context, origin, direction):
depsgraph = context.evaluated_depsgraph_get()
result = context.scene.ray_cast(depsgraph, origin, direction)
return result
def invoke(self, context, event):
self.mouse_x = event.mouse_region_x
self.mouse_y = event.mouse_region_y
return self.execute(context)
class FlipClippingPlane(bpy.types.Operator):
bl_idname = "bim.flip_clipping_plane"
bl_label = "Flip Clipping Plane"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.area.type == "VIEW_3D"
def execute(self, context):
obj = bpy.context.active_object
if obj in [cp.obj for cp in bpy.context.scene.BIMProjectProperties.clipping_planes]:
obj.rotation_euler[0] += radians(180)
bpy.context.view_layer.update()
return {"FINISHED"}
@@ -16,11 +16,12 @@
# 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 ifcopenshell.util.placement
from blenderbim.bim.module.project.data import ProjectData
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.prop import StrProperty
from blenderbim.bim.prop import StrProperty, ObjProperty
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -45,6 +46,23 @@ def get_template_file(self, context):
return ProjectData.data["template_file"]
def get_library_file(self, context):
if not ProjectData.is_loaded:
ProjectData.load()
return ProjectData.data["library_file"]
def update_library_file(self, context):
if self.library_file != "0":
bpy.ops.bim.select_library_file(
filepath=os.path.join(bpy.context.scene.BIMProperties.data_dir, "libraries", self.library_file)
)
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":
@@ -107,6 +125,11 @@ 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)
@@ -169,7 +192,10 @@ class BIMProjectProperties(PropertyGroup):
active_link_index: IntProperty(name="Active Link Index")
export_schema: EnumProperty(items=get_export_schema, name="IFC Schema")
template_file: EnumProperty(items=get_template_file, name="Template File")
library_file: EnumProperty(items=get_library_file, name="Library File", update=update_library_file)
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)
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))
@@ -21,7 +21,7 @@ import blenderbim.bim
from blenderbim.bim.helper import prop_with_search
from bpy.types import Panel, Menu, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.project.data import ProjectData
from blenderbim.bim.module.project.data import ProjectData, LinksData
class BIM_MT_project(Menu):
@@ -255,16 +255,26 @@ class BIM_PT_project_library(Panel):
self.layout.use_property_split = True
self.props = context.scene.BIMProjectProperties
row = self.layout.row(align=True)
row.label(text=IfcStore.library_path or "No Library Loaded", icon="ASSET_MANAGER")
row.prop(self.props, "library_file", text="")
if self.props.library_file == "0":
row.operator("bim.select_library_file", icon="FILE_FOLDER", text="")
row = self.layout.row(align=True)
if IfcStore.library_file:
row.label(text=IfcStore.library_file.schema)
row.label(
text=os.path.splitext(os.path.basename(IfcStore.library_path))[0]
+ f" - {IfcStore.library_file.schema}",
icon="ASSET_MANAGER",
)
row.operator("bim.append_library_element_by_query", text="", icon="APPEND_BLEND")
row.operator("bim.save_library_file", text="", icon="EXPORT")
row.operator("bim.select_library_file", icon="FILE_FOLDER", text="")
if IfcStore.library_file:
self.draw_library_ul()
else:
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")
@@ -307,6 +317,41 @@ class BIM_PT_links(Panel):
"active_link_index",
)
if LinksData.enable_culling:
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")
return
for name, value in LinksData.linked_data["attributes"].items():
row = self.layout.row(align=True)
row.label(text=name)
row.label(text=value)
for pset in LinksData.linked_data["properties"]:
box = self.layout.box()
row = box.row(align=True)
row.label(text=pset[0], icon="COPY_ID")
for name, value in pset[1].items():
row = box.row(align=True)
row.label(text=name)
row.label(text=value)
if LinksData.linked_data["type_properties"]:
row = self.layout.row()
row.label(icon="LINKED", text="Type Properties")
for pset in LinksData.linked_data["type_properties"]:
box = self.layout.box()
row = box.row(align=True)
row.label(text=pset[0], icon="COPY_ID")
for name, value in pset[1].items():
row = box.row(align=True)
row.label(text=name)
row.label(text=value)
class BIM_UL_library(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
@@ -0,0 +1,90 @@
# 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 blenderbim.tool as tool
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.project.data import LinksData
class ExploreTool(bpy.types.WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "bim.explore_tool"
bl_label = "Explore Tool"
bl_description = "Fetch data about a linked IFC element"
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.explore")
bl_widget = None
bl_keymap = (
("bim.query_linked_element", {"type": "RIGHTMOUSE", "value": "PRESS"}, None),
("bim.explore_hotkey", {"type": "W", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_W")]}),
("bim.explore_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
("bim.explore_hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
("bim.explore_hotkey", {"type": "C", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_C")]}),
)
def draw_settings(context, layout, ws_tool):
row = layout.row(align=True)
row.label(text="Query Object", icon="MOUSE_RMB")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Walk Mode", icon="EVENT_W")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Add Clipping Plane", icon="EVENT_C")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Flip Clipping Plane", icon="EVENT_F")
row = layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="Set Orbit Center", icon="MOUSE_MMB")
row = layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="Disable Culling" if LinksData.enable_culling else "Enable Culling", icon="EVENT_C")
class ExploreHotkey(bpy.types.Operator):
bl_idname = "bim.explore_hotkey"
bl_label = "Explore Hotkey"
bl_options = {"REGISTER", "UNDO"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@classmethod
def description(cls, context, operator):
return operator.description or ""
def execute(self, context):
getattr(self, f"hotkey_{self.hotkey}")()
return {"FINISHED"}
def hotkey_S_W(self):
bpy.ops.view3d.walk("INVOKE_DEFAULT")
def hotkey_S_C(self):
bpy.ops.bim.create_clipping_plane("INVOKE_DEFAULT")
def hotkey_S_F(self):
bpy.ops.bim.flip_clipping_plane("INVOKE_DEFAULT")
def hotkey_A_C(self):
if LinksData.enable_culling:
bpy.ops.bim.disable_culling()
else:
bpy.ops.bim.enable_culling("INVOKE_DEFAULT")
@@ -21,6 +21,7 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.schema
import ifcopenshell.util.element
import ifcopenshell.util.type
import blenderbim.bim.handler
import blenderbim.core.geometry
import blenderbim.core.material
@@ -103,33 +104,68 @@ class ReassignClass(bpy.types.Operator):
predefined_type = context.scene.BIMRootProperties.ifc_predefined_type
if predefined_type == "USERDEFINED":
predefined_type = context.scene.BIMRootProperties.ifc_userdefined_type
reassigned_elements = set()
# NOTE: root.reassign_class
# automatically will reassign class for other occurrences of the type
# so we need to run it only for the types or non-typed elements
elements_to_reassign = set()
# need to update blender object name
# for all elements that were changed in the process
elements_to_update = set()
for obj in objects:
product = ifcopenshell.api.run(
obj.BIMObjectProperties.is_reassigning_class = False
element = tool.Ifc.get_entity(obj)
if element.is_a("IfcTypeObject"):
elements_to_reassign.add(element)
elements_to_update.update(ifcopenshell.util.element.get_types(element))
continue
# check if element is typed
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
elements_to_reassign.add(element_type)
elements_to_update.update(ifcopenshell.util.element.get_types(element_type))
continue
# non-typed element
elements_to_reassign.add(element)
# store elements to objects to update later as elements will get invalid
# after class reassignment
elements_to_update = elements_to_update | elements_to_reassign
objects_to_update = set(o for e in elements_to_update if (o := tool.Ifc.get_object(e)))
base_class = context.scene.BIMRootProperties.ifc_class
if context.scene.BIMRootProperties.ifc_product == "IfcElementType":
type_class = base_class
occurrence_classes = ifcopenshell.util.type.get_applicable_entities(type_class)
occurrence_class = None if len(occurrence_classes) == 0 else occurrence_classes[0]
else:
occurrence_class = base_class
type_classes = ifcopenshell.util.type.get_applicable_types(occurrence_class)
type_class = None if len(type_classes) == 0 else type_classes[0]
reassigned_elements = set()
for element in elements_to_reassign:
ifc_class = type_class if element.is_a("IfcTypeObject") else occurrence_class
if ifc_class is None:
self.report(
{"ERROR"},
f"Couldn't find valid class for reassigning element of class {element.is_a()} based on class {base_class}",
)
return {"CANCELLED"}
element = ifcopenshell.api.run(
"root.reassign_class",
self.file,
product=tool.Ifc.get_entity(obj),
ifc_class=context.scene.BIMRootProperties.ifc_class,
product=element,
ifc_class=ifc_class,
predefined_type=predefined_type,
)
reassigned_elements.add(product)
obj.name = tool.Loader.get_name(product)
obj.BIMObjectProperties.is_reassigning_class = False
reassigned_elements.add(element)
dependent_elements = set()
for reassigned_element in reassigned_elements:
if reassigned_element.is_a("IfcTypeObject"):
dependent_elements.update(ifcopenshell.util.element.get_types(product))
else:
element_type = ifcopenshell.util.element.get_type(product)
if element_type:
dependent_elements.add(element_type)
dependent_elements.update(ifcopenshell.util.element.get_types(element_type))
for dependent_element in dependent_elements:
obj = tool.Ifc.get_object(dependent_element)
if obj:
obj.name = tool.Loader.get_name(dependent_element)
for obj in objects_to_update:
obj.name = tool.Loader.get_name(tool.Ifc.get_entity(obj))
return {"FINISHED"}
@@ -158,6 +194,9 @@ class AssignClass(bpy.types.Operator, Operator):
ifc_context = tool.Ifc.get().by_id(ifc_context)
active_object = context.active_object
for obj in objects:
if obj.mode != "OBJECT":
self.report({"ERROR"}, "Object must be in OBJECT mode to assign class")
continue
core.assign_class(
tool.Ifc,
tool.Collector,
@@ -128,8 +128,12 @@ class SequenceData:
cls.data["work_times"] = {}
for work_time in tool.Ifc.get().by_type("IfcWorkTime"):
data = work_time.get_info()
data["Start"] = ifcopenshell.util.date.ifc2datetime(data["Start"]) if data["Start"] else None
data["Finish"] = ifcopenshell.util.date.ifc2datetime(data["Finish"]) if data["Finish"] else None
if tool.Ifc.get_schema() == "IFC4X3":
start_date, finish_date = data["StartDate"], data["FinishDate"]
else:
start_date, finish_date = data["Start"], data["Finish"]
data["Start"] = ifcopenshell.util.date.ifc2datetime(start_date) if start_date else None
data["Finish"] = ifcopenshell.util.date.ifc2datetime(finish_date) if finish_date else None
data["RecurrencePattern"] = work_time.RecurrencePattern.id() if work_time.RecurrencePattern else None
cls.data["work_times"][work_time.id()] = data
@@ -48,8 +48,10 @@ class SpatialTool(WorkSpaceTool):
("bim.spatial_hotkey", {"type": "B", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_B")]}),
("bim.spatial_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
("bim.spatial_hotkey", {"type": "B", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_B")]}),
("bim.spatial_hotkey", {"type": "V", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_V")]}),
("bim.spatial_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
("bim.spatial_hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
("bim.spatial_hotkey", {"type": "H", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_H")]}),
)
def draw_settings(context, layout, ws_tool):
@@ -91,31 +93,34 @@ class SpatialToolUI:
if context.active_object and context.selected_objects:
cls.draw_selected_object_interface(context)
add_layout_hotkey(cls.layout, "Decorate Boundaries", "A_V", bpy.ops.bim.decorate_boundaries.__doc__)
@classmethod
def draw_default_interface(cls, context):
row = cls.layout.row(align=True)
row.prop(data=cls.model_props, property="rl3", text="RL")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
if context.selected_objects:
row.operator("bim.generate_spaces_from_walls")
else:
row.operator("bim.generate_space")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_T")
row.operator("bim.toggle_space_visibility")
op_name = lambda op: op.get_rna_type().name
add_layout_hotkey(
cls.layout,
op_name(bpy.ops.bim.generate_spaces_from_walls),
"S_A",
bpy.ops.bim.generate_spaces_from_walls.__doc__,
)
add_layout_hotkey(cls.layout, op_name(bpy.ops.bim.generate_space), "S_A", bpy.ops.bim.generate_space.__doc__)
add_layout_hotkey(
cls.layout, op_name(bpy.ops.bim.toggle_space_visibility), "S_T", bpy.ops.bim.toggle_space_visibility.__doc__
)
add_layout_hotkey(
cls.layout, op_name(bpy.ops.bim.toggle_hide_spaces), "S_H", bpy.ops.bim.toggle_hide_spaces.__doc__
)
@classmethod
def draw_selected_object_interface(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if element and bpy.context.selected_objects and element.is_a("IfcSpace"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
row.operator("bim.generate_space", text="Regen")
add_layout_hotkey(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -123,7 +128,7 @@ class SpatialToolUI:
row.prop(cls.model_props, "boundary_class", text="")
row.operator("bim.add_boundary", text="Add Boundary")
add_layout_hotkey(cls.layout, "Boundaries", "A_B", "Toggle boundaries")
add_layout_hotkey(cls.layout, "Boundaries", "A_B", "Load/unload boundaries on selected objects")
@classmethod
def draw_type_selection_interface(cls, context):
@@ -169,6 +174,8 @@ class Hotkey(bpy.types.Operator, Operator):
bpy.ops.bim.add_boundary()
def hotkey_A_B(self):
if not AuthoringData.is_loaded:
AuthoringData.load()
if not bpy.context.selected_objects:
return
if AuthoringData.data["has_visible_boundaries"]:
@@ -176,8 +183,14 @@ class Hotkey(bpy.types.Operator, Operator):
else:
bpy.ops.bim.show_boundaries()
def hotkey_A_V(self):
bpy.ops.bim.decorate_boundaries()
def hotkey_S_T(self):
bpy.ops.bim.toggle_space_visibility()
def hotkey_S_H(self):
bpy.ops.bim.toggle_hide_spaces()
def hotkey_S_G(self):
bpy.ops.bim.generate_space()
@@ -49,8 +49,12 @@ class AssignType(bpy.types.Operator, Operator):
if self.related_object
else context.selected_objects or [context.active_object]
)
model_props = context.scene.BIMModelProperties
for obj in related_objects:
core.assign_type(tool.Ifc, tool.Type, element=tool.Ifc.get_entity(obj), type=type)
element = tool.Ifc.get_entity(obj)
core.assign_type(tool.Ifc, tool.Type, element=element, type=type)
if model_props.occurrence_name_style == "TYPE":
obj.name = tool.Model.generate_occurrence_name(type, element.is_a())
class UnassignType(bpy.types.Operator):
@@ -166,15 +170,17 @@ class SelectSimilarType(bpy.types.Operator):
def execute(self, context):
self.file = IfcStore.get_file()
related_object = bpy.data.objects.get(self.related_object) if self.related_object else context.active_object
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(related_object))
if not relating_type:
return {"FINISHED"}
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:
obj.select_set(True)
objects = bpy.context.selected_objects
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
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:
obj.select_set(True)
return {"FINISHED"}
@@ -29,8 +29,9 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_opening"
bl_label = "Add Opening"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Adds an opening to an element.\n" \
"Need to select two elements, order of selection is not important"
bl_description = (
"Adds an opening to an element.\n" "Need to select two elements, order of selection is not important"
)
@classmethod
def poll(cls, context):
@@ -41,12 +42,10 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMModelProperties
selected_objects = context.selected_objects
target_object = selected_objects[0]
target_object = selected_objects[0]
opening_objects = [obj for obj in selected_objects if obj != target_object]
for opening_obj in opening_objects:
element1 = tool.Ifc.get_entity(target_object)
obj1 = target_object
@@ -58,7 +57,7 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
if element1 and element2:
if element1.is_a("IfcOpeningElement") and element2.is_a("IfcOpeningElement"):
self.report({'INFO'}, "You can't add an opening to another opening.")
self.report({"INFO"}, "You can't add an opening to another opening.")
continue
elif not element1.is_a("IfcOpeningElement") and not element2.is_a("IfcOpeningElement"):
if element1.is_a("IfcWindow") or element1.is_a("IfcDoor"): # Add a fill to an element.
@@ -75,7 +74,7 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
element1, element2 = element2, element1
if element1.is_a("IfcOpeningElement"):
self.report({'INFO'}, "You can't add an opening to another opening.")
self.report({"INFO"}, "You can't add an opening to another opening.")
continue
if tool.Ifc.is_moved(obj1):
@@ -259,8 +258,10 @@ class BooleansMarkAsManual(bpy.types.Operator):
if self.mark_as_manual:
tool.Model.mark_manual_booleans(element, booleans)
else:
tool.Model.unmark_manual_booleans(element, booleans)
tool.Model.unmark_manual_booleans(element, [b.id() for b in booleans])
self.report({"INFO"}, f"{len(booleans)} were marked as {'manual' if self.mark_as_manual else 'automatic'}")
self.report(
{"INFO"}, f"{len(booleans)} booleans were marked as {'manual' if self.mark_as_manual else 'automatic'}"
)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
+186 -13
View File
@@ -18,6 +18,7 @@
import os
import bpy
import bmesh
import json
import time
import logging
@@ -38,6 +39,9 @@ from blenderbim.bim.ui import IFCFileSelector
from blenderbim.bim.helper import get_enum_items
from mathutils import Vector, Matrix, Euler
from math import radians
from pathlib import Path
from collections import namedtuple
from typing import List
class SetTab(bpy.types.Operator):
@@ -127,7 +131,7 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector):
bl_idname = "bim.select_ifc_file"
bl_label = "Select IFC File"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select a different IFC file"
bl_description = f"Select a different IFC file.\n{tool.Blender.operator_invoke_filepath_hotkeys_description}"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
@@ -138,6 +142,11 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector):
return {"FINISHED"}
def invoke(self, context, event):
filepath = Path(context.scene.BIMProperties.ifc_file)
res = tool.Blender.operator_invoke_filepath_hotkeys(self, context, event, filepath)
if res is not None:
return res
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
@@ -404,11 +413,21 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
def create_section_compare_node(self):
group = bpy.data.node_groups.new("Section Compare", type="ShaderNodeTree")
group.inputs.new("NodeSocketFloat", "Value")
group.inputs["Value"].default_value = 1.0 # Mandatory multiplier for the last node group
group.inputs.new("NodeSocketVector", "Vector")
group.outputs.new("NodeSocketFloat", "Value")
group.outputs.new("NodeSocketFloat", "Line Decorator")
if bpy.app.version >= (4, 0, 0):
input_value = group.interface.new_socket(name="Value", in_out="INPUT", socket_type="NodeSocketFloat")
input_value.default_value = 1.0 # Mandatory multiplier for the last node group
group.interface.new_socket(name="Vector", in_out="INPUT", socket_type="NodeSocketVector")
group.interface.new_socket(name="Line Decorator", in_out="INPUT", socket_type="NodeSocketFloat")
group.interface.new_socket(name="Value", in_out="OUTPUT", socket_type="NodeSocketFloat")
group.interface.new_socket(name="Line Decorator", in_out="OUTPUT", socket_type="NodeSocketFloat")
else:
group.inputs.new("NodeSocketFloat", "Value")
group.inputs["Value"].default_value = 1.0 # Mandatory multiplier for the last node group
group.inputs.new("NodeSocketVector", "Vector")
group.inputs.new("NodeSocketFloat", "Line Decorator")
group.outputs.new("NodeSocketFloat", "Value")
group.outputs.new("NodeSocketFloat", "Line Decorator")
group_input = group.nodes.new(type="NodeGroupInput")
group_input.location = 0, 50
@@ -431,8 +450,16 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
multiply.inputs[0].default_value = 1
multiply.location = 600, 150
add_line_decorator = group.nodes.new(type="ShaderNodeMath")
add_line_decorator.operation = "ADD"
add_line_decorator.location = 600, -200
multiply_line_decorator = group.nodes.new(type="ShaderNodeMath")
multiply_line_decorator.operation = "MULTIPLY"
multiply_line_decorator.location = 800, -200
group_output = group.nodes.new(type="NodeGroupOutput")
group_output.location = 800, 0
group_output.location = 1000, 0
group.links.new(group_input.outputs["Value"], multiply.inputs[0])
group.links.new(group_input.outputs["Vector"], separate_xyz.inputs[0])
@@ -440,12 +467,20 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
group.links.new(greater.outputs[0], multiply.inputs[1])
group.links.new(multiply.outputs[0], group_output.inputs["Value"])
group.links.new(separate_xyz.outputs[2], compare.inputs[0])
group.links.new(compare.outputs[0], group_output.inputs["Line Decorator"])
group.links.new(compare.outputs[0], add_line_decorator.inputs[0])
group.links.new(group_input.outputs["Line Decorator"], add_line_decorator.inputs[1])
group.links.new(multiply.outputs[0], multiply_line_decorator.inputs[0])
group.links.new(add_line_decorator.outputs[0], multiply_line_decorator.inputs[1])
group.links.new(multiply_line_decorator.outputs[0], group_output.inputs["Line Decorator"])
def create_section_override_node(self, obj, context):
group = bpy.data.node_groups.new("Section Override", type="ShaderNodeTree")
group.inputs.new("NodeSocketShader", "Shader")
group.outputs.new("NodeSocketShader", "Shader")
if bpy.app.version >= (4, 0, 0):
group.interface.new_socket(name="Shader", in_out="INPUT", socket_type="NodeSocketShader")
group.interface.new_socket(name="Shader", in_out="OUTPUT", socket_type="NodeSocketShader")
else:
group.inputs.new("NodeSocketShader", "Shader")
group.outputs.new("NodeSocketShader", "Shader")
links = group.links
nodes = group.nodes
@@ -455,6 +490,7 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
mix_decorator = group.nodes.new(type="ShaderNodeMixShader")
mix_decorator.name = "Line Decorator Mix"
mix_decorator.inputs[0].default_value = 0 # Directly pass input shader when there is no cutaway
mix_decorator.location = group_output.location - Vector((200, 0))
mix_section = group.nodes.new(type="ShaderNodeMixShader")
@@ -519,7 +555,15 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
cut_obj.object = obj
cut_obj.location = last_section_node.location - Vector((400, 150)) - offset
group.links.new(section_compare.outputs[0], last_section_node.inputs[0])
group.links.new(section_compare.outputs["Value"], last_section_node.inputs[0])
group.links.new(
section_compare.outputs["Line Decorator"],
(
last_section_node.inputs["Line Decorator"]
if "Line Decorator" in last_section_node.inputs
else group.nodes.get("Line Decorator Mix").inputs[0]
),
)
group.links.new(cut_obj.outputs["Object"], section_compare.inputs[1])
section_compare.name = "Last Section Compare"
@@ -594,6 +638,14 @@ class BIM_OT_remove_section_plane(bpy.types.Operator):
previous_section_compare = section_compare.inputs[0].links[0].from_node
next_section_compare = section_compare.outputs[0].links[0].to_node
section_override.links.new(previous_section_compare.outputs[0], next_section_compare.inputs[0])
section_override.links.new(
previous_section_compare.outputs[1],
(
next_section_compare.inputs["Line Decorator"]
if "Line Decorator" in next_section_compare.inputs
else next_section_compare.inputs[0]
),
)
self.offset_previous_nodes(section_compare, offset_x=200)
section_override.nodes.remove(section_compare)
section_override.nodes.remove(tex_coords)
@@ -605,8 +657,8 @@ class BIM_OT_remove_section_plane(bpy.types.Operator):
if section_compare.inputs[0].links:
previous_section_compare = section_compare.inputs[0].links[0].from_node
previous_section_compare.location += Vector((offset_x, offset_y))
if previous_section_compare.inputs[1].links:
previous_section_compare.inputs[1].links[0].from_node.location += Vector((offset_x, offset_y))
if previous_section_compare.inputs["Vector"].links:
previous_section_compare.inputs["Vector"].links[0].from_node.location += Vector((offset_x, offset_y))
self.offset_previous_nodes(previous_section_compare, offset_x, offset_y)
def purge_all_section_data(self, context):
@@ -926,3 +978,124 @@ class BIM_OT_show_description(bpy.types.Operator):
@classmethod
def description(cls, context, properties):
return properties.description
CuttingPlaneData = namedtuple("CuttingPlaneData", ["co", "normal"])
class ClippingPlaneCutWithCappings(bpy.types.Operator):
bl_idname = "bim.clipping_plane_cut_with_cappings"
bl_label = "Cut With Clipping Planes"
bl_description = "Cut selected objects with clipping planes and create cappings"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
cutting_planes = [p.obj for p in context.scene.BIMProjectProperties.clipping_planes]
if not cutting_planes:
self.report({"INFO"}, "No cutting planes found.")
return {"FINISHED"}
cutting_planes_data = self.get_cutting_plane_data(cutting_planes)
wm = context.window_manager
objects_processed, t0 = 0, time.time()
wm.progress_begin(0, len(context.selected_objects))
for obj_i, obj in enumerate(context.selected_objects):
if obj.type != "MESH":
continue
if obj in cutting_planes:
continue
RevertClippingPlaneCut.revert_object_mesh(self, obj)
# localize matrix to consider object's transform
ws_to_ls = obj.matrix_world.inverted()
rotation = ws_to_ls.to_quaternion()
mesh = obj.data
bm = tool.Blender.get_bmesh_for_mesh(mesh)
object_changed = False
for plane_data in cutting_planes_data:
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
plane_co = ws_to_ls @ plane_data.co
plane_no = rotation @ plane_data.normal
# clear_outer -> remove everything in direction of the normal
results = bmesh.ops.bisect_plane(
bm,
geom=geom,
dist=10e-4,
plane_co=plane_co,
plane_no=plane_no,
clear_outer=True,
)
edges_to_fill = [e for e in results["geom_cut"] if isinstance(e, bmesh.types.BMEdge)]
object_changed = object_changed or edges_to_fill
bmesh.ops.contextual_create(bm, geom=edges_to_fill)
# don't swap mesh if it wasn't affected by any of the cutting planes
if object_changed:
temp_mesh = bpy.data.meshes.new("temp_cut")
temp_mesh.BIMMeshProperties.replaced_mesh = mesh
for material in mesh.materials:
temp_mesh.materials.append(material)
obj.data = temp_mesh
tool.Blender.apply_bmesh(temp_mesh, bm, obj)
objects_processed += 1
wm.progress_update(obj_i)
self.report({"INFO"}, f"{objects_processed} processed - {time.time()-t0:.3f} sec")
return {"FINISHED"}
def get_cutting_plane_data(self, cutting_planes: List[bpy.types.Object]) -> List[CuttingPlaneData]:
cutting_planes_data = []
for obj in cutting_planes:
cutting_matrix = obj.matrix_world
plane_data = CuttingPlaneData(cutting_matrix.translation, cutting_matrix.col[2].to_3d())
cutting_planes_data.append(plane_data)
return cutting_planes_data
# NOTE: unused, will be used later for cutting boxes support
def get_box_cutting_plane_data(self, obj: bpy.types.Object) -> List[CuttingPlaneData]:
matrix_world = obj.matrix_world
rotation = matrix_world.to_quaternion() # avoid scale for normals
cutting_planes_data = []
for p in obj.data.polygons:
plane_data = CuttingPlaneData(matrix_world @ p.center, rotation @ p.normal)
cutting_planes_data.append(plane_data)
return cutting_planes_data
class RevertClippingPlaneCut(bpy.types.Operator):
bl_idname = "bim.revert_clipping_plane_cut"
bl_label = "Revert Clipping Plane Cut"
bl_description = "Revert clipping plane cut (switch back to the original mesh)"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
wm = context.window_manager
objects_processed, t0 = 0, time.time()
wm.progress_begin(0, len(context.selected_objects))
for obj_i, obj in enumerate(context.selected_objects):
if obj.type != "MESH":
continue
self.revert_object_mesh(obj)
objects_processed += 1
wm.progress_update(obj_i)
wm.progress_end()
self.report({"INFO"}, f"{objects_processed} processed - {time.time()-t0:.3f} sec")
return {"FINISHED"}
def revert_object_mesh(self, obj):
mesh = obj.data
replaced_mesh = mesh.BIMMeshProperties.replaced_mesh
if replaced_mesh:
obj.data = replaced_mesh
tool.Blender.remove_data_block(mesh, do_unlink=False)
+1
View File
@@ -475,6 +475,7 @@ class BIMMeshProperties(PropertyGroup):
ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter)
material_checksum: StringProperty(name="Material Checksum", default="[]")
mesh_checksum: StringProperty(name="Mesh Checksum", default="")
replaced_mesh: PointerProperty(type=bpy.types.Mesh, description="Original mesh to revert section cutaway")
class BIMFacet(PropertyGroup):
+20 -2
View File
@@ -111,6 +111,23 @@ class BIM_PT_section_plane(Panel):
row.operator("bim.remove_section_plane")
class BIM_PT_section_with_cappings(Panel):
bl_label = "Section Cutaways With Cappings"
bl_idname = "BIM_PT_section_with_cappings"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "output"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "BIM_PT_tab_sandbox"
def draw(self, context):
layout = self.layout
wm = context.window_manager
row = layout.row(align=True)
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")
class BIM_UL_generic(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
@@ -240,8 +257,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row = layout.row()
row.prop(context.scene.BIMModelProperties, "occurrence_name_style")
row = layout.row()
row.prop(context.scene.BIMModelProperties, "occurrence_name_function")
if context.scene.BIMModelProperties == "CUSTOM":
row = layout.row()
row.prop(context.scene.BIMModelProperties, "occurrence_name_function")
row = self.layout.row()
row.prop(self, "decorations_colour")
@@ -0,0 +1,12 @@
schema_version = "1.0.0"
id = "blenderbim"
version = "0.0.999999"
name = "BlenderBIM"
tagline = "Transforms Blender into a native Building Information Model authoring platform using IFC."
maintainer = "Dion Moult <dion@thinkmoult.com>"
type = "add-on"
permissions = ["files"]
website = "https://blenderbim.org/"
tags = ["System"]
blender_version_min = "3.5.0"
license = ["SPDX:GPL-3.0-or-later"]
@@ -191,3 +191,24 @@ 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):
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)
+5 -2
View File
@@ -43,9 +43,11 @@ def add_material_set(ifc, material, set_type=None):
return ifc_material
def remove_material(ifc, material_tool, style, material=None):
def remove_material(ifc, material_tool, style, material=None) -> bool:
"""returns True after deleting False,\n
returns False if material used in material sets and cannot be removed"""
if material_tool.is_material_used_in_sets(material):
return
return False
obj = ifc.get_object(material)
ifc.unlink(element=material)
ifc.run("material.remove_material", material=material)
@@ -53,6 +55,7 @@ def remove_material(ifc, material_tool, style, material=None):
material_tool.delete_object(obj)
if material_tool.is_editing_materials():
material_tool.import_material_definitions(material_tool.get_active_material_type())
return True
def remove_material_set(ifc, material_tool, material=None):
@@ -205,3 +205,10 @@ def toggle_space_visibility(ifc, spatial):
if not spaces:
return
spatial.toggle_spaces_visibility_wired_and_textured(spaces)
def toggle_hide_spaces(ifc, spatial):
model = ifc.get()
spaces = model.by_type("IfcSpace")
if not spaces:
return
spatial.toggle_hide_spaces(spaces)
+2
View File
@@ -397,6 +397,7 @@ class Geometry:
def should_generate_uvs(cls, obj): pass
def should_use_presentation_style_assignment(cls): pass
def unresolve_type_representation(cls, representation, element): pass
def delete_opening_object_placement(cls, opening): pass
@interface
@@ -876,6 +877,7 @@ class Spatial:
def assign_relating_type_to_element(cls, ifc, Type, element, relating_type): pass
def regen_obj_representation(cls, ifc, geometry, obj, body): pass
def toggle_spaces_visibility_wired_and_textured(cls, spaces): pass
def toggle_hide_spaces(cls, spaces): pass
@interface
class Covering:
+62 -2
View File
@@ -450,9 +450,25 @@ 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):
def remove_data_block(cls, data_block, do_unlink=True):
"""Removes a datablock (such as a mesh)
See https://projects.blender.org/blender/blender/issues/118787 for more
details about do_unlink.
:param data_block: The bpy.data datablock to delete.
:param do_unlink: Whether or not to unlink the datablock first. This
defaults to true, which is Blender's default behaviour. If you are
sure that the data block has zero users, then you can set this
to False, which will make datablock deletion significantly faster
by avoiding unnecessary Blender data checks.
:return: None
:rtype: None
"""
collection_name = repr(data_block).split(".", 2)[-1].split("[", 1)[0]
getattr(bpy.data, collection_name).remove(data_block)
getattr(bpy.data, collection_name).remove(
data_block, do_unlink=do_unlink, do_id_user=do_unlink, do_ui_user=do_unlink
)
## BMESH UTILS ##
@classmethod
@@ -551,6 +567,50 @@ class Blender(blenderbim.core.tool.Blender):
for axis_idx in range(3):
attr[axis_idx] = lock_state
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):
if not event.alt and not event.shift:
return
import platform
import os
import subprocess
def open_file_or_folder(path):
if platform.system() == "Windows":
os.startfile(path)
elif platform.system() == "Darwin":
subprocess.Popen(["open", path])
else:
subprocess.Popen(["xdg-open", path])
# resolve relative filepaths with .blend path by default
if not filepath.is_absolute():
if bpy.data.filepath:
filepath = Path(bpy.data.filepath).parent / filepath
else:
operator.report({"ERROR"}, f'Couldn\'t resolve relative filepath "{filepath.as_posix()}"')
return {"CANCELLED"}
# holding ALT - open file directory
if event.alt == True:
# open directory
filepath = filepath.parent
if not filepath.exists():
operator.report({"ERROR"}, f'Cannot open non-existing directory: "{filepath.as_posix()}"')
return {"CANCELLED"}
open_file_or_folder(filepath.as_posix())
return {"PASS_THROUGH"}
# holding sHIFT - open file
if not filepath.exists():
operator.report({"ERROR"}, f'Cannot open non-existing file: "{filepath.as_posix()}"')
return {"CANCELLED"}
open_file_or_folder(filepath.as_posix())
return {"PASS_THROUGH"}
class Modifier:
@classmethod
def is_eligible_for_railing_modifier(cls, obj):
+5 -1
View File
@@ -72,7 +72,11 @@ class Boundary(blenderbim.core.tool.Boundary):
obj.matrix_world = space.matrix_world
@classmethod
def decorate_boundary(cls, obj):
def decorate_boundary(cls, obj: bpy.types.Object) -> None:
new = bpy.context.scene.BIMBoundaryProperties.boundaries.add()
new.obj = obj
obj.show_in_front = True
@classmethod
def undecorate_boundary(cls, obj: bpy.types.Object) -> None:
obj.show_in_front = False
+13 -8
View File
@@ -676,13 +676,13 @@ class Drawing(blenderbim.core.tool.Drawing):
camera.BIMCameraProperties.custom_scale_numerator = numerator
camera.BIMCameraProperties.custom_scale_denominator = denominator
if "HasUnderlay" in pset:
camera.BIMCameraProperties.has_underlay = pset["HasUnderlay"]
camera.BIMCameraProperties.has_underlay = bool(pset["HasUnderlay"])
if "HasLinework" in pset:
camera.BIMCameraProperties.has_linework = pset["HasLinework"]
camera.BIMCameraProperties.has_linework = bool(pset["HasLinework"])
if "HasAnnotation" in pset:
camera.BIMCameraProperties.has_annotation = pset["HasAnnotation"]
camera.BIMCameraProperties.has_annotation = bool(pset["HasAnnotation"])
if "IsNTS" in pset:
camera.BIMCameraProperties.is_nts = pset["IsNTS"]
camera.BIMCameraProperties.is_nts = bool(pset["IsNTS"])
tool.Loader.link_mesh(shape, camera)
@@ -837,7 +837,7 @@ class Drawing(blenderbim.core.tool.Drawing):
if platform.system() == "Darwin":
subprocess.call(("open", path))
elif platform.system() == "Windows":
os.startfile(path)
os.startfile(os.path.normpath(path))
else:
subprocess.call(("xdg-open", path))
@@ -1504,7 +1504,7 @@ class Drawing(blenderbim.core.tool.Drawing):
def get_drawing_metadata(cls, drawing):
return [
v.strip()
for v in ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"].get("Metadata", "").split(",")
for v in (ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"].get("Metadata", "") or "").split(",")
if v
]
@@ -1613,9 +1613,14 @@ class Drawing(blenderbim.core.tool.Drawing):
area = tool.Blender.get_view3d_area()
is_local_view = area.spaces[0].local_view is not None
if is_local_view:
bpy.ops.view3d.localview()
#turn off local view before activating drawing, and then turn it on again.
for a in bpy.context.screen.areas:
if a.type == 'VIEW_3D':
override = bpy.context.copy()
override['area'] = a
bpy.ops.view3d.localview(override)
bpy.context.scene.camera = camera
bpy.ops.view3d.localview()
bpy.ops.view3d.localview(override)
else:
bpy.context.scene.camera = camera
area.spaces[0].region_3d.view_perspective = "CAMERA"
+212 -1
View File
@@ -26,11 +26,13 @@ import ifcopenshell
import blenderbim.core.tool
import blenderbim.core.style
import blenderbim.core.spatial
import blenderbim.core.geometry
import blenderbim.tool as tool
import blenderbim.bim.import_ifc
from math import radians, pi
from mathutils import Vector, Matrix
from blenderbim.bim.ifc import IfcStore
from typing import List
class Geometry(blenderbim.core.tool.Geometry):
@@ -101,6 +103,8 @@ class Geometry(blenderbim.core.tool.Geometry):
parent_collection = parent_obj.BIMObjectProperties.collection
for child in collection.children:
parent_collection.children.link(child)
for child_object in collection.objects:
parent_collection.objects.link(child_object)
bpy.data.collections.remove(collection)
if getattr(element, "FillsVoids", None):
bpy.ops.bim.remove_filling(filling=element.id())
@@ -299,6 +303,7 @@ class Geometry(blenderbim.core.tool.Geometry):
@classmethod
def get_active_representation(cls, obj):
"""< IfcShapeRepresentation or None"""
if obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.ifc_definition_id:
return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
@@ -441,7 +446,7 @@ class Geometry(blenderbim.core.tool.Geometry):
return styles
usage_count = [0] * len(obj.material_slots)
if not usage_count: # if there are no materials, polygons will still use index 0
if not usage_count: # if there are no materials, polygons will still use index 0
return []
for poly in obj.data.polygons:
usage_count[poly.material_index] += 1
@@ -716,3 +721,209 @@ class Geometry(blenderbim.core.tool.Geometry):
obj.matrix_world.translation += original_min_point - new_max_point
bpy.context.view_layer.update()
@classmethod
def reload_representation(cls, obj):
"""reload `obj` active representation"""
if not obj.data:
return
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
@classmethod
def remove_representation_item(cls, representation_item):
# NOTE: we assume it's not the last representation item
# otherwise we probably would need to remove representation too
# NOTE: a lot of shared code with `geometry.remove_representation`
ifc_file = tool.Ifc.get()
shape_aspects = []
consider_inverses = []
styled_item, colour, texture, layer = None, None, None, None
[consider_inverses.append(styled_item := t) for t in representation_item.StyledByItem]
# IFC2X3 is using LayerAssignments
for t in (
representation_item.LayerAssignment
if hasattr(representation_item, "LayerAssignment")
else representation_item.LayerAssignments
):
consider_inverses.append(layer := t)
# IfcTessellatedFaceSet
[consider_inverses.append(colour := t) for t in getattr(representation_item, "HasColours", [])]
[consider_inverses.append(texture := t) for t in getattr(representation_item, "HasTextures", [])]
for inverse in ifc_file.get_inverse(representation_item):
if inverse.is_a("IfcShapeRepresentation"):
if inverse.OfShapeAspect:
shape_aspects.append(inverse.OfShapeAspect[0])
else:
representation = inverse
if styled_item:
ifc_file.remove(styled_item)
if layer and len(layer.Items) == 1:
ifc_file.remove(styled_item)
if colour:
ifcopenshell.util.element.remove_deep2(ifc_file, colour)
if texture:
ifcopenshell.util.element.remove_deep2(ifc_file, texture)
for shape_aspect in shape_aspects:
cls.remove_representation_items_from_shape_aspect([representation_item], shape_aspect)
representation.Items = tuple(set(representation.Items) - {representation_item})
also_consider = list(consider_inverses)
ifcopenshell.util.element.remove_deep2(ifc_file, representation_item, also_consider=also_consider)
@classmethod
def create_shape_aspect(cls, product_shape, base_representation, items, previous_shape_aspect=None):
"""
> `product_shape` - IfcProductDefinitionShape or IfcRepresentationMap\n
> `base_representation` - base representation to get context attributes from\n
> `items` - representation items\n
> `previous_shape_aspect` - (optional) previous shape aspect, if provided\n
items will be removed the previous shape aspect first\n
< IfcShapeAspect
"""
if previous_shape_aspect is not None:
cls.remove_representation_items_from_shape_aspect(items, previous_shape_aspect)
shape_aspect = tool.Ifc.get().createIfcShapeAspect(
PartOfProductDefinitionShape=product_shape, ShapeRepresentations=()
)
# keep IfcShapeAspect and IfcShapeRepresentation valid
rep = tool.Geometry.add_shape_aspect_representation(shape_aspect, base_representation)
rep.Items = items
return shape_aspect
@classmethod
def remove_representation_items_from_shape_aspect(cls, representation_items, shape_aspect):
ifc_file = tool.Ifc.get()
# as shape aspect might have multiple representations
# it's easier to find it from the item
for inverse in ifc_file.get_inverse(representation_items[0]):
if inverse.is_a("IfcShapeRepresentation") and shape_aspect in inverse.OfShapeAspect:
representation = inverse
break
# removing last item would make representation invalid
if len(representation.Items) == len(representation_items):
# removing last representation would make shape aspect invalid.
# remove shape aspect first otherwise remove_representation won't remove it because of the inverse
if len(shape_aspect.ShapeRepresentations) == 1:
ifc_file.remove(shape_aspect)
tool.Ifc.run("geometry.remove_representation", representation=representation)
else:
items = set(representation.Items) - set(representation_items)
representation.Items = tuple(items)
@classmethod
def add_representation_item_to_shape_aspect(cls, representation_items, shape_aspect):
"""NOTE: we assume that all items belonged to the same representation and to the same shape aspect"""
ifc_file = tool.Ifc.get()
previous_shape_aspect = None
for inverse in ifc_file.get_inverse(representation_items[0]):
if inverse.is_a("IfcShapeRepresentation"):
if inverse.OfShapeAspect:
# item is already added to the shape aspect
if inverse.OfShapeAspect[0] == shape_aspect:
return
previous_shape_aspect = inverse.OfShapeAspect[0]
else:
base_representation = inverse
# remove item from previous shape aspect
if previous_shape_aspect:
cls.remove_representation_items_from_shape_aspect(representation_items, previous_shape_aspect)
shape_aspect_representation = cls.get_shape_aspect_representation(
shape_aspect, base_representation, create_new=True
)
shape_aspect_representation.Items = shape_aspect_representation.Items + tuple(representation_items)
@classmethod
def get_shape_aspect_representation(cls, shape_aspect, base_representation, create_new=False):
for representation in shape_aspect.ShapeRepresentations:
if (
representation.ContextOfItems == base_representation.ContextOfItems
and representation.RepresentationIdentifier == base_representation.RepresentationIdentifier
and representation.RepresentationType == base_representation.RepresentationType
):
return representation
if not create_new:
return None
return cls.add_shape_aspect_representation(shape_aspect, base_representation)
@classmethod
def add_shape_aspect_representation(cls, shape_aspect, base_representation):
shape_aspect_representation = tool.Ifc.get().createIfcShapeRepresentation(
ContextOfItems=base_representation.ContextOfItems,
RepresentationIdentifier=base_representation.RepresentationIdentifier,
RepresentationType=base_representation.RepresentationType,
)
shape_aspect.ShapeRepresentations = shape_aspect.ShapeRepresentations + (shape_aspect_representation,)
return shape_aspect_representation
@classmethod
def get_shape_aspect_representation_for_item(cls, shape_aspect, representation_item):
ifc_file = tool.Ifc.get()
for inverse in ifc_file.get_inverse(representation_item):
if inverse.is_a("IfcShapeRepresentation"):
if inverse.OfShapeAspect:
if inverse.OfShapeAspect[0] == shape_aspect:
return inverse
@classmethod
def get_shape_aspect_styles(cls, element, shape_aspect, representation_item) -> List[ifcopenshell.entity_instance]:
"""update `representation_item` style based on styles connected to the `shape_aspect`
through material constituents with the same name
"""
if not shape_aspect.Name:
return []
# get material connected to the shape aspect with material constituent name
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
if not material or not material.is_a("IfcMaterialConstituentSet") or not material.MaterialConstituents:
return []
matching_constituent = next((c for c in material.MaterialConstituents if c.Name == shape_aspect.Name), None)
if matching_constituent is None:
return []
constituent_material = matching_constituent.Material
if not constituent_material.HasRepresentation:
return []
# get shape aspect representation for item
shape_aspect_representation = cls.get_shape_aspect_representation_for_item(shape_aspect, representation_item)
# get the styles for this context
material_representation = None
for r in constituent_material.HasRepresentation[0].Representations:
if r.ContextOfItems == shape_aspect_representation.ContextOfItems:
material_representation = r
break
if material_representation is None:
return []
styles = [s for s in tool.Ifc.get().traverse(material_representation) if s.is_a("IfcPresentationStyle")]
return styles
@classmethod
def delete_opening_object_placement(cls, placement):
model = tool.Ifc.get()
ifcopenshell.util.element.remove_deep2(model, placement)
+9 -8
View File
@@ -415,24 +415,25 @@ class IfcGit:
@classmethod
def config_ifcmerge(cls):
config_reader = IfcGitRepo.repo.config_reader()
config_writer = IfcGitRepo.repo.config_writer()
section = 'mergetool "ifcmerge"'
if not config_reader.has_section(section):
config_writer.set_value(section, "cmd", "ifcmerge $BASE $LOCAL $REMOTE $MERGED")
config_writer.set_value(section, "trustExitCode", True)
with IfcGitRepo.repo.config_writer() as config_writer:
config_writer.set_value(section, "cmd", "ifcmerge $BASE $LOCAL $REMOTE $MERGED")
config_writer.set_value(section, "trustExitCode", True)
section = 'mergetool "ifcmerge-forward"'
if not config_reader.has_section(section):
config_writer.set_value(section, "cmd", "ifcmerge $BASE $REMOTE $LOCAL $MERGED")
config_writer.set_value(section, "trustExitCode", True)
with IfcGitRepo.repo.config_writer() as config_writer:
config_writer.set_value(section, "cmd", "ifcmerge $BASE $REMOTE $LOCAL $MERGED")
config_writer.set_value(section, "trustExitCode", True)
@classmethod
def config_push(cls, repo):
"""Set push.autoSetupRemote"""
config_reader = repo.config_reader()
config_writer = repo.config_writer()
if not config_reader.has_section("push"):
config_writer.set_value("push", "default", "current")
config_writer.set_value("push", "autoSetupRemote", True)
with repo.config_writer() as config_writer:
config_writer.set_value("push", "default", "current")
config_writer.set_value("push", "autoSetupRemote", True)
@classmethod
def config_info_attributes(cls, repo):
@@ -223,3 +223,16 @@ class Material(blenderbim.core.tool.Material):
assinged_material = cls.get_material(element)
material_profile = tool.Ifc.run("material.add_profile", profile_set=assinged_material, material=material)
return material_profile
@classmethod
def update_elements_using_material(cls, material):
# update elements that are using this material
elements = ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material)
# filter only unique representations to avoid reloading same representations multiple times
meshes_to_objects = dict()
for e in elements:
obj = tool.Ifc.get_object(e)
mesh = obj.data
meshes_to_objects[mesh] = obj
for obj in meshes_to_objects.values():
tool.Geometry.reload_representation(obj)
+17 -9
View File
@@ -202,7 +202,7 @@ class Model(blenderbim.core.tool.Model):
return surface
@classmethod
def generate_occurrence_name(cls, element_type, ifc_class):
def generate_occurrence_name(cls, element_type: ifcopenshell.entity_instance, ifc_class: str):
props = bpy.context.scene.BIMModelProperties
if props.occurrence_name_style == "CLASS":
return ifc_class[3:]
@@ -461,12 +461,14 @@ class Model(blenderbim.core.tool.Model):
@classmethod
def get_material_layer_parameters(cls, element):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
layer_set_direction = "AXIS2"
offset = 0.0
thickness = 0.0
direction_sense = "POSITIVE"
material = ifcopenshell.util.element.get_material(element)
if material:
if material.is_a("IfcMaterialLayerSetUsage"):
layer_set_direction = material.LayerSetDirection
offset = material.OffsetFromReferenceLine * unit_scale
direction_sense = material.DirectionSense
material = material.ForLayerSet
@@ -475,10 +477,15 @@ class Model(blenderbim.core.tool.Model):
if direction_sense == "NEGATIVE":
thickness *= -1
offset *= -1
return {"thickness": thickness, "offset": offset, "direction_sense": direction_sense}
return {
"layer_set_direction": layer_set_direction,
"thickness": thickness,
"offset": offset,
"direction_sense": direction_sense,
}
@classmethod
def get_booleans(cls, element=None, representation=None):
def get_booleans(cls, element=None, representation=None) -> list[ifcopenshell.entity_instance]:
if representation is None:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
@@ -493,7 +500,7 @@ class Model(blenderbim.core.tool.Model):
return booleans
@classmethod
def get_manual_booleans(cls, element, representation=None):
def get_manual_booleans(cls, element, representation=None) -> list[ifcopenshell.entity_instance]:
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Boolean")
if not pset:
return []
@@ -502,7 +509,7 @@ class Model(blenderbim.core.tool.Model):
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return []
booleans = [b for b in cls.get_booleans(element) if b.id() in boolean_ids]
booleans = [b for b in cls.get_booleans(element, representation) if b.id() in boolean_ids]
return booleans
@classmethod
@@ -521,11 +528,12 @@ class Model(blenderbim.core.tool.Model):
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
@classmethod
def unmark_manual_booleans(cls, element, booleans):
def unmark_manual_booleans(cls, element, boolean_ids):
# NOTE: we use use boolean_ids instead of boolean entities
# so it will be possible to unmark manual booleans after they already was deleted
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Boolean")
if not pset:
return
boolean_ids = [b.id() for b in booleans]
data = set(json.loads(pset["Data"]))
data -= set(boolean_ids)
data = list(data)
@@ -978,8 +986,8 @@ class Model(blenderbim.core.tool.Model):
if nosing_length < 0: # tread gaps
length += abs(nosing_length) * number_of_treads
calculated_params["Length"] = round(length, 5)
pitch = height/length
pitch_formatted = str(round(pitch*100, 1)) + ' % / ' + str(round(degrees(atan(pitch)), 1)) + ' deg'
pitch = height / length
pitch_formatted = str(round(pitch * 100, 1)) + " % / " + str(round(degrees(atan(pitch)), 1)) + " deg"
calculated_params["Pitch"] = str(pitch_formatted)
return calculated_params
+1 -1
View File
@@ -31,7 +31,7 @@ class Project(blenderbim.core.tool.Project):
@classmethod
def append_all_types_from_template(cls, template):
# TODO refactor
filepath = os.path.join(bpy.context.scene.BIMProperties.data_dir, "libraries", template)
filepath = os.path.join(bpy.context.scene.BIMProperties.data_dir, "templates", "projects", template)
bpy.ops.bim.select_library_file(filepath=filepath)
if IfcStore.library_file.schema != tool.Ifc.get().schema:
return
+2 -1
View File
@@ -48,7 +48,8 @@ class Pset(blenderbim.core.tool.Pset):
return bpy.context.scene.ProfilePsetProperties
elif obj_type == "WorkSchedule":
return bpy.context.scene.WorkSchedulePsetProperties
elif obj_type == "Group":
return bpy.context.scene.GroupPsetProperties
@classmethod
def get_pset_name(cls, obj, obj_type):
pset = cls.get_pset_props(obj, obj_type)
+1 -1
View File
@@ -181,7 +181,7 @@ class Qto(blenderbim.core.tool.Qto):
"quantity_value" : XX,
"quantity_type" : XX
}]
:rtype list
:rtype: list
Example:
+5 -2
View File
@@ -220,8 +220,11 @@ class Root(blenderbim.core.tool.Root):
@classmethod
def recreate_connections(cls, relationship, old_to_new):
for element, data in relationship.items():
new_relating_element = old_to_new.get(data["relating_element"])[0]
new_related_element = old_to_new.get(data["related_element"])[0]
try:
new_relating_element = old_to_new.get(data["relating_element"])[0]
new_related_element = old_to_new.get(data["related_element"])[0]
except:
continue
ifcopenshell.api.run(
"geometry.connect_path",
tool.Ifc.get(),
+14
View File
@@ -733,3 +733,17 @@ class Spatial(blenderbim.core.tool.Spatial):
bpy.data.objects[obj.name].show_wire = False
bpy.data.objects[obj.name].display_type = "TEXTURED"
return
@classmethod
def toggle_hide_spaces(cls, spaces):
first_obj = tool.Ifc.get_object(spaces[0])
if first_obj.hide_get() == False:
for space in spaces:
obj = tool.Ifc.get_object(space)
obj.hide_set(True)
return
elif first_obj.hide_get() == True:
for space in spaces:
obj = tool.Ifc.get_object(space)
obj.hide_set(False)
+21
View File
@@ -549,6 +549,27 @@ class Style(blenderbim.core.tool.Style):
representation = tool.Geometry.get_active_representation(obj)
tool.Ifc.run("style.assign_representation_styles", shape_representation=representation, styles=[style])
@classmethod
def assign_style_to_representation_item(cls, representation_item, style=None):
ifc_file = tool.Ifc.get()
if not representation_item.StyledByItem:
if style is None:
return
ifc_file.createIfcStyledItem(representation_item, (style,))
styled_item = representation_item.StyledByItem[0]
if style is None:
ifc_file.remove(styled_item)
return
styled_item.Styles = (style,)
@classmethod
def get_representation_item_style(cls, representation_item):
for inverse in tool.Ifc.get().get_inverse(representation_item):
if inverse.is_a("IfcStyledItem"):
for style in inverse.Styles:
return style
@classmethod
def reload_material_from_ifc(cls, blender_material):
blender_material.BIMStyleProperties.active_style_type = blender_material.BIMStyleProperties.active_style_type
+3 -2
View File
@@ -63,8 +63,9 @@ class Type(blenderbim.core.tool.Type):
types = ifc_file.by_type("IfcElementType")
# exclude IfcSpatialElementType
types += ifc_file.by_type("IfcTypeProduct", include_subtypes=False)
types += ifc_file.by_type("IfcWindowStyle")
types += ifc_file.by_type("IfcDoorStyle")
if not tool.Ifc.get_schema().startswith("IFC4X3"):
types += ifc_file.by_type("IfcWindowStyle")
types += ifc_file.by_type("IfcDoorStyle")
return types
@classmethod
@@ -0,0 +1,3 @@
# This file will be autogenerated and rewritten when packaged.
translations_dict = {}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+2 -1
View File
@@ -19,7 +19,8 @@ instructions as the **Stable installation**.
You will need to choose which build to download.
- If you are on Blender >=3.1, choose py310
- If you are on Blender >=4.1, choose py311
- If you are on Blender >=3.1 and <=4.0, choose py10
- If you are on Blender >=2.93 and <3.1, choose py39
- Choose ``linux``, ``macos``, ``macosm1`` (for Apple M1 devices), or ``win``
depending on your operating system
+76
View File
@@ -0,0 +1,76 @@
Translations
============
The BlenderBIM Add-on supports translations to all languages that Blender
supports. We'll describe how you can help translate the add-on as a translator,
or how you can ensure your strings are translatable as a developer.
Translations are managed using a separate add-on built for this purpose.
1. Clone the `blenderbim-translations
<https://github.com/IfcOpenShell/blenderbim-translations>`_ repository. This
repository holds all the core translation strings in ``.po`` format.
2. Download the `blenderbim-translations add-on
<https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/src/blenderbim/scripts/bbim_translations.py>`_
and install it as a Blender add-on. This add-on lets you convert translation
data to and from the ``.po`` files to the BlenderBIM Add-on.
3. In the **Render Properties** tab, find the **BlenderBIM Translations**
panel, and browse to where you have closed the ``blenderbim-translations``
repository, and click on **Setup Translation UI**.
You should now see a new interface with two buttons.
.. image:: images/translation-panel.png
How to translate the add-on
---------------------------
Before beginning, look inside the ``blenderbim-translations`` repository for a
``.po`` file for the language you want to translate to (e.g. ``de_DE.po`` for
German). If this file does not yet exist, congratulations! You are the first
person to translate to this language! Create a folder in
``blenderbim-translations`` with your language code and copy the
``blenderbim.pot`` language template file to that folder and rename it
according to your language (e.g. for German you would copy ``blenderbim.pot`` to
``de_DE/de_DE.po``).
Supported language codes are:
.. code-block::
'ca_AD', 'en_US', 'es', 'fr_FR', 'ja_JP', 'sk_SK', 'cs_CZ', 'de_DE',
'it_IT', 'ka', 'ko_KR', 'pt_BR', 'pt_PT', 'ru_RU', 'uk_UA', 'vi_VN',
'zh_HANS', 'zh_HANT', 'ab', 'ar_EG', 'bg_BG', 'el_GR', 'eo', 'eu_EU',
'fa_IR', 'fi_FI', 'ha', 'he_IL', 'hi_IN', 'hr_HR', 'hu_HU', 'id_ID',
'ky_KG', 'ne_NP', 'nl_NL', 'pl_PL', 'sr_RS', 'sr_RS@latin', 'sv_SE', 'sw',
'ta', 'th_TH', 'tr_TR'.
As a translator, it is recommended to translate text in bulk by modifying the
``.po`` files directly. We recommend installing software such as `Poedit
<https://poedit.net/>`_. These translation software offer features such as auto
translation, suggestions, and tracking. Alternatively, you may edit the ``.po``
file as a text file.
Once you have edited the relevant language's ``.po`` file, click on the
**Update Translations From .po** button in the **BlenderBIM Translations**
panel.
How to add new translation strings
----------------------------------
When you have new strings to translate, press the **Parse BlenderBIM strings to
.pot** button. This detects strings in the source code using regex patterns and
writes out to the ``blenderbim.pot`` language template file. You may then diff
this file and propagate changes manually to all translated ``.po`` files.
The ``.pot`` file is only used as a blank template for users to create or
compare ``.po`` files. The ``.po`` files are the source of truth for
translation strings. Blender does not read from the ``.pot`` or ``.po`` files.
Instead, Blender reads from ``blenderbim/translations.py`` which contains a
dictionary of strings formatted specifically for Blender. The
``translations.py`` file is generated from the ``.po`` files. This is generated
when we distribute installable packages, or when translators manually press the
**Update Translations From .po** button.
.. warning::
Do not commit the ``translations.py`` file as it is auto-generated.
+1
View File
@@ -42,6 +42,7 @@ Learn how to model a small building and create simple architectural plans, secti
devs/hello_world
devs/running_tests
devs/writing_docs
devs/translations
devs/undo_system
devs/system_support
devs/ux_guidelines
@@ -61,8 +61,8 @@ include:
Filtered model loading
----------------------
You may filter elements and only load a portion of the model. Click on **Enable
Advanced Mode** when loading a model.
You may filter elements and only load a portion of the model. Click on
:ref:`users/general_usage/ifc_project:Advanced mode` when loading a model.
.. image:: images/advanced-mode.png
@@ -41,7 +41,7 @@ project, or view high level project metadata about the currently loaded project.
Choose between starting a completely blank project with no objects, or with
preloaded object assets (such as wall types, beam types, etc) that you can
use immediately. The blank project is recommended for users who want to
curate assets, whereas the IFC4 Demo Library is recommended for users wanting
curate assets, whereas the IFC4 Demo Template is recommended for users wanting
to experiment and learn with a basic set of objects.
This also shows other larger object libraries for steel and countries
@@ -102,6 +102,9 @@ with basic features to save and unload the project.
Advanced mode
-------------
The advanced mode will not load the model right away, but preload it and
provide additional loading possibilities like :ref:`users/dealing_with_large_models:Filtered model loading`.
.. image:: images/ifc-project-load.png
.. |FILE| image:: /images/icon-FILE.svg
@@ -57,12 +57,12 @@ class LibraryGenerator:
target_view="PLAN_VIEW",
parent=plan,
),
"plan_annotation": ifcopenshell.api.run(
"model_annotation": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Plan",
context_type="Model",
context_identifier="Annotation",
target_view="PLAN_VIEW",
target_view="MODEL_VIEW",
parent=plan,
),
}
@@ -194,8 +194,8 @@ class LibraryGenerator:
))
representation = self.file.createIfcShapeRepresentation(
self.representations["plan_annotation"],
self.representations["plan_annotation"].ContextIdentifier,
self.representations["model_annotation"],
self.representations["model_annotation"].ContextIdentifier,
"Annotation2D",
items,
)
@@ -25,18 +25,16 @@ from ifcopenshell.util.shape_builder import ShapeBuilder, V
class LibraryGenerator:
def generate(self, output_filename="IFC4 EU Steel.ifc"):
def generate(self, library_name, output_filename="IFC4 EU Steel.ifc"):
ifcopenshell.api.pre_listeners = {}
ifcopenshell.api.post_listeners = {}
self.materials = {}
self.file = ifcopenshell.api.run("project.create_file")
self.project = ifcopenshell.api.run(
"root.create_entity", self.file, ifc_class="IfcProject", name=f"Non-structural assets library"
)
self.project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject", name=library_name)
self.library = ifcopenshell.api.run(
"root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=f"Non-structural assets library"
"root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=library_name
)
ifcopenshell.api.run(
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
@@ -1876,4 +1874,6 @@ class LibraryGenerator:
if __name__ == "__main__":
path = Path(__file__).parents[1] / "blenderbim/bim/data/libraries"
LibraryGenerator().generate(output_filename=str(path / "IFC4 Furniture Library.ifc"))
LibraryGenerator().generate(
"Non-structural assets library", output_filename=str(path / "IFC4 Furniture Library.ifc")
)
@@ -0,0 +1,488 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 @Andrej730
#
# 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 csv
import random
import ifcopenshell
import ifcopenshell.api
import blenderbim.tool as tool
from math import cos, sin, tan, pi
from pathlib import Path
from itertools import chain
from ifcopenshell.util.shape_builder import ShapeBuilder, V
from collections import namedtuple
from mathutils import Vector, Matrix
from random import uniform
# When run from Blender
BLEND_DIR = os.path.dirname(bpy.data.filepath)
OUT_PATH = os.path.join(BLEND_DIR, "..", "blenderbim", "bim", "data", "libraries", "IFC4 Landscape Library.ifc")
SimpleTreeParams = namedtuple("SimpleTreeParams", "plant_height crown_diameter trunk_diameter")
LowPolyTreeParams = namedtuple(
"LowPolyTreeParams", "plant_height crown_diameter crown_max_loc crown_taper trunk_height trunk_diameter random_seed"
)
PalmTreeParams = namedtuple("PalmTreeParams", "plant_height crown_diameter trunk_diameter")
TreePresetData = namedtuple("TreePresetData", "preset_class generator")
GeneratedGeometry = namedtuple("GeneratedGeometry", "items_2d items_3d")
def generate_low_poly_tree(
builder: ShapeBuilder,
crown_taper=0.5,
crown_max_loc=0.3,
plant_height=12.0,
trunk_height=2.0,
crown_diameter=4,
trunk_diameter=0.3,
random_seed=10,
) -> GeneratedGeometry:
unit_conversion = 1 / ifcopenshell.util.unit.calculate_unit_scale(builder.file)
random.seed(random_seed)
res = 6
plant_height *= unit_conversion
trunk_height *= unit_conversion
crown_diameter *= unit_conversion
trunk_diameter *= unit_conversion
crown_height = plant_height - trunk_height
def circle(center, r, res):
points = []
for i in range(res):
# fmt: off
vert = (
cos(i * 2 * pi / res) * r + center[0],
sin(i * 2 * pi / res) * r + center[1],
0.0 + center[2]
)
# fmt: on
points.append(vert)
return points
def merge_lists(lists):
merged_list = []
for i in range(len(lists)):
merged_list = merged_list + lists[i]
return merged_list
def XY_scale(vert, scale):
vert_scaled = (vert[0] * scale, vert[1] * scale, vert[2])
return vert_scaled
def rand_add(vert, amp):
# fmt: off
vert_new = (
vert[0] + uniform(-amp, amp),
vert[1] + uniform(-amp, amp),
vert[2] + uniform(-amp, amp)
)
# fmt: on
return vert_new
height_segments_trunk = 2
height_segments_crown = 7
height_segments_total = height_segments_trunk + height_segments_crown
verts = []
# Trunk
for i in range(height_segments_trunk):
points = circle((0.0, 0.0, i * trunk_height), trunk_diameter / 2, res)
verts.append(points)
# Crown
h_crown_max_loc = int((height_segments_crown - 1) * crown_max_loc)
crown_indices = []
for i in range(0, h_crown_max_loc):
crown_indices.append(i)
for i in range(0, height_segments_crown - h_crown_max_loc):
crown_indices.append(h_crown_max_loc - i)
my_min_val = min(crown_indices)
my_max_val = max(crown_indices)
n_crown_taper = crown_taper * 0.8
crown_diameters = []
for x in crown_indices:
crown_diameters.append(((x - my_min_val) / (my_max_val - my_min_val)) * n_crown_taper + (1 - n_crown_taper))
randomize_amplitude = plant_height / 50
crown_segment_height = crown_height / height_segments_crown
for i in range(0, height_segments_crown - 1):
points = circle(
(0.0, 0.0, trunk_height + (i + 1) * crown_segment_height),
crown_diameter * crown_diameters[i] / 2,
res,
)
for i in range(0, len(points)):
points[i] = rand_add(points[i], randomize_amplitude)
verts.append(points)
# Top
verts.append(circle((0.0, 0.0, plant_height), 0.05, res))
edge_loops = []
for i in range(height_segments_total):
edge_loops.append(range(i * res, (i + 1) * res))
verts = merge_lists(verts)
faces = []
bottom = [*edge_loops[0]]
faces.append(bottom)
for i in range(height_segments_total - 1):
for j in range(res):
face = (edge_loops[i][j - 1], edge_loops[i][j], edge_loops[i + 1][j], edge_loops[i + 1][j - 1])
faces.append(face)
top = [*edge_loops[-1]]
faces.append(top)
verts_3D = [verts]
faces_3D = [faces]
verts_2D = [
[
(-0.5865642428398132, 0.8647078275680542, 0.0),
(-0.7583824992179871, 0.6572927236557007, 0.0),
(-0.7234422564506531, 0.5865941047668457, 0.0),
(-0.8714070916175842, 0.46631893515586853, 0.0),
(-0.9203394055366516, 0.15792329609394073, 0.0),
(-0.6487269997596741, 0.11522211134433746, 0.0),
(-0.9273074269294739, 0.0903107076883316, 0.0),
(-0.9606701731681824, -0.2852219343185425, 0.0),
(-0.8060722351074219, -0.35055163502693176, 0.0),
(-0.8271104693412781, -0.4677186608314514, 0.0),
(-0.6899875998497009, -0.6695915460586548, 0.0),
(-0.6215555667877197, -0.6767659187316895, 0.0),
(-0.4315463900566101, -0.8824808597564697, 0.0),
(-0.30316293239593506, -0.9023936986923218, 0.0),
(-0.32218849658966064, -0.8037619590759277, 0.0),
(-0.21792533993721008, -0.9530860185623169, 0.0),
(0.36917445063591003, -0.877334475517273, 0.0),
(0.30831706523895264, -0.685136079788208, 0.0),
(0.4778200685977936, -0.8094909191131592, 0.0),
(0.9163193106651306, -0.368840754032135, 0.0),
(0.827597439289093, -0.29377281665802, 0.0),
(0.9478662610054016, -0.245104119181633, 0.0),
(0.9667968153953552, 0.08458180725574493, 0.0),
(0.7529001832008362, 0.05212751030921936, 0.0),
(0.9102436900138855, 0.19098728895187378, 0.0),
(0.8279229998588562, 0.5522171258926392, 0.0),
(0.6635335087776184, 0.6461355686187744, 0.0),
(0.5753684639930725, 0.8170149326324463, 0.0),
(0.3976244330406189, 0.8779193162918091, 0.0),
(0.18733686208724976, 0.7513588666915894, 0.0),
(0.338344544172287, 0.910933256149292, 0.0),
(-0.013130240142345428, 0.9798095226287842, 0.0),
(-0.18649885058403015, 0.9414206743240356, 0.0),
(-0.3193224370479584, 0.8835403919219971, 0.0),
]
]
edges_2D = []
faces_2D = []
verts_2D_new = []
for vert in verts_2D[0]:
verts_2D_new.append(XY_scale(vert, crown_diameter / 2))
range_of_verts = range(0, len(verts_2D[0]))
for i in range_of_verts:
edges_2D.append([range_of_verts[i - 1], i])
faces_2D.append(i)
verts_2D[0] = verts_2D_new
edges_2D = [edges_2D]
faces_2D = [[faces_2D]]
verts_2D = [Vector(v).to_2d() for v in verts_2D[0]]
shape_2d = builder.polyline(points=verts_2D, closed=True)
shape_3d = builder.polygonal_face_set(points=verts_3D[0], faces=faces_3D[0])
return GeneratedGeometry((shape_2d,), (shape_3d,))
def generate_simple_tree(
builder: ShapeBuilder, plant_height=2, crown_diameter=2, trunk_diameter=0.1
) -> GeneratedGeometry:
unit_conversion = 1 / ifcopenshell.util.unit.calculate_unit_scale(builder.file)
plant_height *= unit_conversion
crown_diameter *= unit_conversion
trunk_diameter *= unit_conversion
crown_radius = crown_diameter / 2
# 3d shape
trunk_base = builder.circle(radius=trunk_diameter / 2)
trunk = builder.extrude(trunk_base, magnitude=plant_height - crown_diameter)
crown_position = (0.0, 0.0, (plant_height - crown_radius))
crown = builder.sphere(radius=crown_radius, center=crown_position)
# 2d shape
shape_2D = builder.circle(radius=crown_radius)
return GeneratedGeometry((shape_2D,), (trunk, crown))
def generate_palm_tree(
builder: ShapeBuilder, plant_height=2, crown_diameter=5.8, trunk_diameter=0.1
) -> GeneratedGeometry:
unit_conversion = 1 / ifcopenshell.util.unit.calculate_unit_scale(builder.file)
palm_verts = [
Vector((-3.6415634155273438, -3.8275668621063232, -1.0948246717453003)),
Vector((-0.3698049783706665, -1.805966854095459, 0.493910551071167)),
Vector((-1.6397790908813477, -0.5056067705154419, 0.04353363811969757)),
Vector((0.0, 0.0, 0.0)),
Vector((5.023162364959717, -1.6531240940093994, -1.0948246717453003)),
Vector((1.8090602159500122, 0.4589407444000244, 0.493910551071167)),
Vector((1.1666079759597778, -1.2413609027862549, 0.04353363811969757)),
Vector((1.6942416429519653, 5.038582801818848, -1.0948246717453003)),
Vector((-0.43215513229370117, 1.833944320678711, 0.493910551071167)),
Vector((1.2652605772018433, 1.1839056015014648, 0.04353363811969757)),
Vector((-4.577561378479004, 2.689336061477661, -1.0948246717453003)),
Vector((-1.8646581172943115, -0.0367276668548584, 0.493910551071167)),
Vector((-0.8873085975646973, 1.4957730770111084, 0.04353363811969757)),
]
palm_faces = [(2, 1, 0), (2, 3, 1), (6, 5, 4), (6, 3, 5), (9, 8, 7), (9, 3, 8), (12, 11, 10), (12, 3, 11)]
# each segment starts with the last point from the previous
palm_segments_2d_ifc = [(3, 11, 10, 12, 3), (3, 1, 0, 2, 3), (3, 5, 4, 6, 3), (3, 8, 7, 9, 3)]
palm_segments_2d_ifc = [[i + 1 for i in segment] for segment in palm_segments_2d_ifc] # IFC indices start with 1
scale = Vector()
scale.xyz = crown_diameter / 11.6
leaves_height = 0.493910551071167 * scale.z
plant_height -= leaves_height
for v in palm_verts:
v *= scale
v.z += plant_height
v *= unit_conversion
plant_height *= unit_conversion
crown_diameter *= unit_conversion
trunk_diameter *= unit_conversion
# 3d
leaves = builder.polygonal_face_set(palm_verts, palm_faces)
trunk_base = builder.circle(radius=trunk_diameter / 2)
trunk = builder.extrude(trunk_base, magnitude=plant_height)
# 2d
remove_z = Vector((1, 1, 0))
verts_2D = [v * remove_z for v in palm_verts]
ifc_points = builder.file.createIfcCartesianPointList2D(verts_2D)
ifc_segments = []
for segment in palm_segments_2d_ifc:
ifc_segments.append(builder.file.createIfcLineIndex(segment))
shape_2d = builder.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
return GeneratedGeometry((shape_2d,), (leaves, trunk))
tree_presets = {
"simple": TreePresetData(SimpleTreeParams, generate_simple_tree),
"low_poly": TreePresetData(LowPolyTreeParams, generate_low_poly_tree),
"palm_tree": TreePresetData(PalmTreeParams, generate_palm_tree),
}
class LibraryGenerator:
def generate(self, library_name, output_filename):
ifcopenshell.api.pre_listeners = {}
ifcopenshell.api.post_listeners = {}
self.materials = {}
self.file = ifcopenshell.api.run("project.create_file")
self.project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject", name=library_name)
self.library = ifcopenshell.api.run(
"root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=library_name
)
ifcopenshell.api.run(
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
)
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
plan = ifcopenshell.api.run("context.add_context", self.file, context_type="Plan")
self.representations = {
"Model/Body/MODEL_VIEW": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=model,
),
"Plan/Body/PLAN_VIEW": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Plan",
context_identifier="Body",
target_view="PLAN_VIEW",
parent=plan,
),
"Model/Body/PLAN_VIEW": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Model",
context_identifier="Body",
target_view="PLAN_VIEW",
parent=model,
),
"Model/Body/SECTION_VIEW": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Model",
context_identifier="Body",
target_view="SECTION_VIEW",
parent=model,
),
}
# Manually modeled trees
for obj in bpy.data.objects:
if not obj.type == "MESH":
continue
if "Plan/" in obj.name or "Model/" in obj.name:
continue
representations = {"Model/Body/MODEL_VIEW": obj.name}
for rep_key in self.representations.keys():
rep_obj = bpy.data.objects.get(obj.name + " " + rep_key)
if rep_obj:
representations[rep_key] = rep_obj.name
self.create_type("IfcGeographicElementType", obj.name, representations)
# Auto generated generic trees
self.builder = ShapeBuilder(self.file)
builder = self.builder
TreeData = namedtuple("TreeData", "tree_name preset_name preset_data")
trees = [
TreeData("Conical Tree Small (5m)", "low_poly", (5.0, 1.13, 0.0, 1.0, 0.28, 0.16, 5)),
TreeData("Conical Tree Medium (10m)", "low_poly", (10.0, 2.7, 0.0, 1.0, 0.32, 0.22, 4)),
TreeData("Conical Tree Big (15m)", "low_poly", (15.0, 4.37, 0.0, 1.0, 0.74, 0.44, 3)),
TreeData("Palm Tree (15m)", "palm_tree", (15.0, 16.64, 0.4)),
TreeData("Shrub Small (0.4m)", "low_poly", (0.4, 0.4, 0.535, 0.524, 0.0, 0.0, 10)),
TreeData("Shrub Big (0.8m)", "low_poly", (0.8, 0.8, 0.535, 0.524, 0.0, 0.0, 10)),
TreeData("Lollipop Tree (4m)", "simple", (4.0, 2.5, 0.2)),
TreeData("Lollipop Tree (6m)", "simple", (6.0, 3.5, 0.2)),
TreeData("Lollipop Tree (8m)", "simple", (8.0, 4.5, 0.2)),
TreeData("Lollipop Tree (10m)", "simple", (10.0, 5.5, 0.44)),
TreeData("Lollipop Tree (14m)", "simple", (14.0, 7.5, 0.44)),
TreeData("Lollipop Tree (18m)", "simple", (18.0, 9.5, 0.44)),
TreeData("Lollipop Tree (25m)", "simple", (25.0, 13.5, 0.50)),
TreeData("Tree Small (8m)", "low_poly", (8.0, 6.0, 0.0, .8, 1.0, 0.2, 10)),
TreeData("Tree Medium (12m)", "low_poly", (12.0, 10.0, 0.0, .8, 2.0, 0.4, 20)),
TreeData("Tree Big (20m)", "low_poly", (20.0, 15.0, 0.0, .8, 4.0, 0.6, 30)),
]
for tree_data in trees:
preset = tree_presets.get(tree_data.preset_name)
preset_data = preset.preset_class(*tree_data.preset_data)._asdict()
tree_geometry = preset.generator(builder, **preset_data)
self.create_explicit_type(
"IfcGeographicElementType", tree_data.tree_name, **self.get_representations(tree_geometry)
)
# From tree species table
with open(os.path.join(BLEND_DIR, "tree_species.csv"), 'r') as csvfile:
reader = csv.reader(csvfile)
for i, row in enumerate(reader):
if i == 0:
continue
preset = tree_presets.get(row[0])
data = [float(x) for x in row[2:]]
preset_data = preset.preset_class(*data)._asdict()
tree_geometry = preset.generator(builder, **preset_data)
self.create_explicit_type(
"IfcGeographicElementType", row[1], **self.get_representations(tree_geometry)
)
self.file.write(output_filename)
def get_representations(self, generated_geometry: GeneratedGeometry):
representation_2d = self.builder.get_representation(
self.representations["Plan/Body/PLAN_VIEW"], items=generated_geometry.items_2d
)
representation_3d = self.builder.get_representation(
self.representations["Model/Body/MODEL_VIEW"], items=generated_geometry.items_3d
)
return {
"representation_3d": representation_3d,
"representation_2d": representation_2d,
}
def create_explicit_type(self, ifc_class, name, representation_3d, representation_2d, **params):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
for param, value in params.items():
setattr(element, param, value)
ifcopenshell.api.run(
"geometry.assign_representation", self.file, product=element, representation=representation_3d
)
ifcopenshell.api.run(
"geometry.assign_representation", self.file, product=element, representation=representation_2d
)
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
return element
def create_type(self, ifc_class, name, representations):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
for rep_name, obj_name in representations.items():
obj = bpy.data.objects.get(obj_name)
representation = ifcopenshell.api.run(
"geometry.add_representation",
self.file,
context=self.representations[rep_name],
blender_object=obj,
geometry=obj.data,
total_items=max(1, len(obj.material_slots)),
)
styles = []
for slot in obj.material_slots:
style = ifcopenshell.api.run("style.add_style", self.file, name=slot.material.name)
ifcopenshell.api.run(
"style.add_surface_style",
self.file,
style=style,
ifc_class="IfcSurfaceStyleShading",
attributes=tool.Style.get_surface_shading_attributes(slot.material),
)
styles.append(style)
if styles:
ifcopenshell.api.run(
"style.assign_representation_styles", self.file, shape_representation=representation, styles=styles
)
ifcopenshell.api.run(
"geometry.assign_representation", self.file, product=element, representation=representation
)
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
if __name__ == "__main__":
LibraryGenerator().generate("Landscape Assets Library", output_filename=OUT_PATH)
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
"f","name","PlantHeight","CrownDiameter","CrownMaxLoc","CrownTaper","TrunkHeight","TrunkDiameter","RandomSeed"
"low_poly","Apple",8,9,0.5,0.2,1.5,0.2,1
"low_poly","Beech",30,20,0,0.4,3,0.75,2
"low_poly","Cedar",25,17,0,1,1,0.625,3
"low_poly","Dogwood",8,6,1,0.9,0.3,0.2,4
"low_poly","Elm",17,12,0.6,0.7,2,0.425,5
"low_poly","Fir",27,15,0.1,1,0.1,0.675,6
"low_poly","Ginkgo",25,9,0.5,0.6,7,0.625,7
"low_poly","Horsechesnut",30,23,0.5,0.2,3,0.75,8
"low_poly","Ilex",26,13,0.4,0.3,12,0.65,9
"low_poly","Juniper",30,15,0.7,1,1,0.75,10
"low_poly","Kentucky Coffeetree",20,10,0.5,0,6,0.5,11
"low_poly","Linden",28,18,0.3,0.2,3,0.7,12
"low_poly","Mulberry",15,12,0.5,0.5,2,0.375,13
"low_poly","Nannybery",5,4,0.7,0.8,1,0.125,14
"low_poly","Oak",25,13,0.5,0.5,6,0.625,15
"low_poly","Pine",27,9,0.5,0.2,14,0.675,16
"low_poly","Quaking Aspen",13,4,0.5,0.3,4,0.325,17
"low_poly","Redwood",90,24,0.1,0.8,8,2.25,18
"low_poly","Sycamore",35,22,0.2,0.6,5,0.875,19
"low_poly","Tree of Heaven",20,15,0.2,0.8,3,0.5,20
"low_poly","Umbrella Tree",8,5,0.4,0.5,2,0.2,21
"low_poly","Viburnum",4,3,0.8,0.7,0.2,0.1,22
"low_poly","Weeping Willow",13,12,0.9,0.3,1,0.325,23
"low_poly","Zanthoxylum",10,8,0.4,0.6,2,0.25,24
"low_poly","Yellowwood",25,20,0.2,0.3,5,0.625,25
"low_poly","Zelkova",12,8,0.4,0.4,2,0.3,26
1 f name PlantHeight CrownDiameter CrownMaxLoc CrownTaper TrunkHeight TrunkDiameter RandomSeed
2 low_poly Apple 8 9 0.5 0.2 1.5 0.2 1
3 low_poly Beech 30 20 0 0.4 3 0.75 2
4 low_poly Cedar 25 17 0 1 1 0.625 3
5 low_poly Dogwood 8 6 1 0.9 0.3 0.2 4
6 low_poly Elm 17 12 0.6 0.7 2 0.425 5
7 low_poly Fir 27 15 0.1 1 0.1 0.675 6
8 low_poly Ginkgo 25 9 0.5 0.6 7 0.625 7
9 low_poly Horsechesnut 30 23 0.5 0.2 3 0.75 8
10 low_poly Ilex 26 13 0.4 0.3 12 0.65 9
11 low_poly Juniper 30 15 0.7 1 1 0.75 10
12 low_poly Kentucky Coffeetree 20 10 0.5 0 6 0.5 11
13 low_poly Linden 28 18 0.3 0.2 3 0.7 12
14 low_poly Mulberry 15 12 0.5 0.5 2 0.375 13
15 low_poly Nannybery 5 4 0.7 0.8 1 0.125 14
16 low_poly Oak 25 13 0.5 0.5 6 0.625 15
17 low_poly Pine 27 9 0.5 0.2 14 0.675 16
18 low_poly Quaking Aspen 13 4 0.5 0.3 4 0.325 17
19 low_poly Redwood 90 24 0.1 0.8 8 2.25 18
20 low_poly Sycamore 35 22 0.2 0.6 5 0.875 19
21 low_poly Tree of Heaven 20 15 0.2 0.8 3 0.5 20
22 low_poly Umbrella Tree 8 5 0.4 0.5 2 0.2 21
23 low_poly Viburnum 4 3 0.8 0.7 0.2 0.1 22
24 low_poly Weeping Willow 13 12 0.9 0.3 1 0.325 23
25 low_poly Zanthoxylum 10 8 0.4 0.6 2 0.25 24
26 low_poly Yellowwood 25 20 0.2 0.3 5 0.625 25
27 low_poly Zelkova 12 8 0.4 0.4 2 0.3 26
@@ -552,3 +552,58 @@ Scenario: Override paste buffer - with active IFC data
And the object "IfcWall/Cube.001" has a "Tessellation" representation of "Model/Body/MODEL_VIEW"
And the object "IfcBuildingStorey/My Storey.001" exists
And the object "IfcBuildingStorey/My Storey.001" is an "IfcBuildingStorey"
Scenario: Duplicate linked aggregate
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
And the object "IfcWall/Wall_01" is selected
When I duplicate linked aggregate the selected objects
Then the object "IfcWall/Wall_01.001" exists
And the object "IfcWall/Wall_02.001" exists
And the object "IfcElementAssembly/Assembly_02" exists
Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_02" belong to the same Linked Aggregate Group
Scenario: Refresh linked aggregate
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
And the object "IfcWall/Wall_01" is selected
When I duplicate linked aggregate the selected objects
Then the object "IfcWall/Wall_01.001" exists
When I deselect all objects
And the object "IfcWall/Wall_01.001" is selected
When the object layer length is set to "3"
Then the object "IfcWall/Wall_01.001" dimensions are "3,0.1,3"
When I refresh linked aggregate the selected object
Then the object "IfcWall/Wall_01" exists
And the object "IfcWall/Wall_01" dimensions are "3,0.1,3"
Scenario: Refresh linked aggregate - after deleting an object
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
And the object "IfcWall/Wall_01" is selected
When I duplicate linked aggregate the selected objects
Then the object "IfcWall/Wall_01.001" exists
When I deselect all objects
And the object "IfcWall/Wall_01.001" is selected
And I delete the selected objects
When I deselect all objects
And the object "IfcWall/Wall_02.001" is selected
When I refresh linked aggregate the selected object
Then the object "IfcWall/Wall_01" does not exist
And the object "IfcWall/Wall_02" exists
Scenario: Refresh linked aggregate - after duplicating an object
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
And the object "IfcWall/Wall_01" is selected
When I duplicate linked aggregate the selected objects
Then the object "IfcWall/Wall_01.001" exists
And the object "IfcWall/Wall_02.001" exists
When I deselect all objects
And the object "IfcWall/Wall_01" is selected
When I duplicate the selected objects
Then the object "IfcWall/Wall_01.002" exists
When I rename the object "IfcWall/Wall_01.002" to "IfcWall/Wall_03"
And I deselect all objects
And the object "IfcWall/Wall_03" is selected
When I refresh linked aggregate the selected object
Then the object "IfcWall/Wall_01.001" exists
And the object "IfcWall/Wall_02.001" exists
And the object "IfcWall/Wall_03.001" exists
+79 -23
View File
@@ -233,6 +233,16 @@ def i_evaluate_expression(expression):
def i_duplicate_the_selected_objects():
bpy.ops.bim.override_object_duplicate_move()
blenderbim.bim.handler.active_object_callback()
@when("I duplicate linked aggregate the selected objects")
def i_duplicate_the_selected_objects():
bpy.ops.bim.object_duplicate_move_linked_aggregate()
blenderbim.bim.handler.active_object_callback()
@when("I refresh linked aggregate the selected object")
def i_refresh_the_selected_objects():
bpy.ops.bim.refresh_linked_aggregate()
blenderbim.bim.handler.active_object_callback()
@when("I deselect all objects")
@@ -360,6 +370,13 @@ def the_object_name_exists(name) -> bpy.types.Object:
return obj
@then(parsers.parse('the object "{name}" does not exist'))
def the_object_name_does_not_exist(name) -> bpy.types.Object:
obj = bpy.data.objects.get(name)
if not obj:
assert True, f'The object "{name}" exists'
return obj
@given(parsers.parse('the collection "{name}" exists'))
@when(parsers.parse('the collection "{name}" exists'))
@then(parsers.parse('the collection "{name}" exists'))
@@ -774,6 +791,10 @@ def the_file_name_should_contain_value(name, value):
def the_object_name_has_no_modifiers(name):
assert len(the_object_name_exists(name).modifiers) == 0
@given(parsers.parse('I load the IFC test file "{filepath}"'))
def i_load_the_ifc_test_file(filepath):
filepath = f"{variables['cwd']}{filepath}"
bpy.ops.bim.load_project(filepath=filepath, use_relative_path=True)
@given("I load the demo construction library")
@when("I load the demo construction library")
@@ -826,6 +847,64 @@ def hit_undo():
bpy.ops.ed.undo_push(message="UNDO STEP")
bpy.ops.ed.undo()
@then(parsers.parse('the object "{obj_name1}" has a connection with "{obj_name2}"'))
def the_obj1_has_a_connection_with_obj2(obj_name1, obj_name2):
element1 = replace_variables(obj_name1)
element1 = tool.Ifc.get_entity(the_object_name_exists(element1))
element2 = replace_variables(obj_name2)
element2 = tool.Ifc.get_entity(the_object_name_exists(element2))
connections = []
if hasattr(element1, "ConnectedTo") and element1.ConnectedTo:
connections = [connection for connection in element1.ConnectedTo]
elif hasattr(element1, "ConnectedFrom") and element1.ConnectedFrom:
connections = [connection for connection in element1.ConnectedFrom]
else:
assert False, f'Object "{obj_name1}" has no connections'
relationships = {}
for conn in connections:
relationships[conn.RelatedElement] = conn.RelatingElement
for key, value in relationships.items():
assert (key.id() == element1.id() and value.id() == element2.id()) or (key.id() == element2.id() and value.id() == element1.id()), f"The object {obj_name1} is connected to {obj_name2}"
@then(parsers.parse('the object "{obj_name1}" and "{obj_name2}" belong to the same Linked Aggregate Group'))
def the_obj1_and_obj2_belong_the_same_linked_aggregate_group(obj_name1, obj_name2):
element1 = replace_variables(obj_name1)
element1 = tool.Ifc.get_entity(the_object_name_exists(element1))
element2 = replace_variables(obj_name2)
element2 = tool.Ifc.get_entity(the_object_name_exists(element2))
elements = [element1, element2]
groups = []
for element in elements:
product_linked_agg_group = [
r.RelatingGroup
for r in getattr(element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
try:
groups.append(product_linked_agg_group[0].id())
except:
assert False, "Object is not part of a Linked Aggregate."
assert groups[0] == groups[1], "Objects do not belong to the same Linked Aggregate group"
@when(parsers.parse('the object layer length is set to "{value}"'))
def the_obj_layer_lenght_is_set_to(value):
value = float(value)
try:
eval(f"bpy.context.scene.BIMModelProperties.length")
except:
assert False, f"Property BIMModelProperties.length does not exist when trying to set to value {value}"
print(50*"@", bpy.context.selected_objects)
bpy.context.scene.BIMModelProperties.length = value
bpy.ops.bim.change_layer_length(length=value)
# These definitions are not to be used in tests but simply in debugging failing tests
@@ -875,27 +954,4 @@ def run_pdb():
pdb.set_trace()
@then(parsers.parse('the object "{obj_name1}" has a connection with "{obj_name2}"'))
def the_obj1_has_a_connection_with_obj2(obj_name1, obj_name2):
element1 = replace_variables(obj_name1)
element1 = tool.Ifc.get_entity(the_object_name_exists(element1))
element2 = replace_variables(obj_name2)
element2 = tool.Ifc.get_entity(the_object_name_exists(element2))
connections = []
if hasattr(element1, "ConnectedTo") and element1.ConnectedTo:
connections = [connection for connection in element1.ConnectedTo]
elif hasattr(element1, "ConnectedFrom") and element1.ConnectedFrom:
connections = [connection for connection in element1.ConnectedFrom]
else:
assert False, f'Object "{obj_name1}" has no connections'
relationships = {}
for conn in connections:
relationships[conn.RelatedElement] = conn.RelatingElement
for key, value in relationships.items():
print(key, value)
# assert False, f"1-{key} and {element1.id()} and {key.id()}"
assert (key.id() == element1.id() and value.id() == element2.id()) or (key.id() == element2.id() and value.id() == element1.id()), f"The object {obj_name1} is connected to {obj_name2}"
File diff suppressed because one or more lines are too long
+97
View File
@@ -575,3 +575,100 @@ class TestGetIfcRepresentationClass(NewFile):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
assert subject.get_ifc_representation_class(ifc.createIfcColumn(), ifc.createIfcShapeRepresentation()) is None
class TestRemoveRepresentationItem(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
context = ifc.createIfcGeometricRepresentationContext()
element = ifc.createIfcWall()
items = [ifc.createIfcExtrudedAreaSolid(), ifc.createIfcExtrudedAreaSolid()]
representation = ifc.createIfcShapeRepresentation(Items=items, ContextOfItems=context)
tool.Ifc.run("geometry.assign_representation", product=element, representation=representation)
product_shape = element.Representation
shape_aspect = subject.create_shape_aspect(product_shape, representation, items[:1], None)
shape_aspect_id = shape_aspect.id()
subject.remove_representation_item(items[0])
assert tool.Ifc.get_entity_by_id(shape_aspect_id) is None
assert set(representation.Items) == {items[1]}
class TestCreateShapeAspect(NewFile):
def test_run(self):
self.create_shape_aspect(use_element_type=False)
self.create_shape_aspect(use_element_type=True)
def create_shape_aspect(self, use_element_type):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
context = ifc.createIfcGeometricRepresentationContext()
element = ifc.createIfcWallType() if use_element_type else ifc.createIfcWall()
item = ifc.createIfcExtrudedAreaSolid()
items = [item]
representation = ifc.createIfcShapeRepresentation(Items=items, ContextOfItems=context)
tool.Ifc.run("geometry.assign_representation", product=element, representation=representation)
product_shape = element.RepresentationMaps[0] if use_element_type else element.Representation
subject.create_shape_aspect(product_shape, representation, items, None)
assert len(product_shape.HasShapeAspects) == 1
representation = product_shape.HasShapeAspects[0].ShapeRepresentations[0]
assert set(representation.Items) == set(items)
class TestAddRepresentationItemToShapeAspect(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
context = ifc.createIfcGeometricRepresentationContext()
element = ifc.createIfcWall()
items = [ifc.createIfcExtrudedAreaSolid(), ifc.createIfcExtrudedAreaSolid()]
representation = ifc.createIfcShapeRepresentation(Items=items, ContextOfItems=context)
tool.Ifc.run("geometry.assign_representation", product=element, representation=representation)
product_shape = element.Representation
shape_aspect0 = subject.create_shape_aspect(product_shape, representation, [items[0]], None)
previous_shape_aspect_id = shape_aspect0.id()
shape_aspect1 = subject.create_shape_aspect(product_shape, representation, [items[1]], None)
subject.add_representation_item_to_shape_aspect([items[0]], shape_aspect1)
# previous shape aspect removed as there won't be any items in it
assert tool.Ifc.get_entity_by_id(previous_shape_aspect_id) is None
representation = shape_aspect1.ShapeRepresentations[0]
assert set(representation.Items) == set(items)
class TestRemoveRepresentationItemFromShapeAspect(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
context = ifc.createIfcGeometricRepresentationContext()
element = ifc.createIfcWall()
items = [ifc.createIfcExtrudedAreaSolid(), ifc.createIfcExtrudedAreaSolid()]
representation = ifc.createIfcShapeRepresentation(Items=items, ContextOfItems=context)
tool.Ifc.run("geometry.assign_representation", product=element, representation=representation)
product_shape = element.Representation
shape_aspect0 = subject.create_shape_aspect(product_shape, representation, [items[0]], None)
previous_shape_aspect_id = shape_aspect0.id()
previous_shape_aspect_rep_id = shape_aspect0.ShapeRepresentations[0].id()
shape_aspect1 = subject.create_shape_aspect(product_shape, representation, [items[1]], None)
subject.remove_representation_items_from_shape_aspect([items[0]], shape_aspect0)
# previous shape aspect and representation are removed as there won't be any items in them
assert tool.Ifc.get_entity_by_id(previous_shape_aspect_id) is None
assert tool.Ifc.get_entity_by_id(previous_shape_aspect_rep_id) is None
# items is removed from the representaiton
subject.add_representation_item_to_shape_aspect([items[0]], shape_aspect1)
subject.remove_representation_items_from_shape_aspect([items[1]], shape_aspect1)
representation = shape_aspect1.ShapeRepresentations[0]
assert set(representation.Items) == {items[0]}
+2 -2
View File
@@ -75,7 +75,7 @@ class TestGetActiveReference(NewFile):
class TestImportLibraryAttributes(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc := ifcopenshell.file())
library = ifc.createIfcLibraryInformation("Name", "Version", None, "VersionDate", "Location", "Description")
subject.import_library_attributes(library)
props = bpy.context.scene.BIMLibraryProperties
@@ -88,7 +88,7 @@ class TestImportLibraryAttributes(NewFile):
class TestImportReferenceAttributes(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc := ifcopenshell.file())
reference = ifc.createIfcLibraryReference("Location", "Identification", "Name", "Description", "Language")
subject.import_reference_attributes(reference)
props = bpy.context.scene.BIMLibraryProperties

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