Compare commits

...

647 Commits

Author SHA1 Message Date
Dion Moult e74eb0cc53 Fix bug where unlinking operations were not stored in the undo stack 2022-10-04 13:41:27 +11:00
Dion Moult 008d6bed5c PIP publish now only happens once a month, on a full moon 2022-10-04 10:41:25 +11:00
Dion Moult d0985a23d0 Fix critical bug where default context was not set on project load 2022-10-04 10:03:59 +11:00
Massimo Fabbro a5d49e81a0 Fix little bug in qto_calculator about net footprint area (#2470) 2022-10-04 09:59:50 +11:00
Dion Moult bfcc086aad Attempt to change PyPI name to ifcopenshell 2022-10-03 23:40:29 +11:00
Dion Moult 0eae3de6a2 Openings now move with the host by default. Small change, big impact. 2022-10-03 23:30:19 +11:00
Thomas Krijnen 1d39413717 Test cases for previous commits 2022-10-03 14:29:41 +02:00
Thomas Krijnen c57e5e2100 Capture lower level errors in validate.py reported in C++ 2022-10-03 14:19:29 +02:00
Thomas Krijnen ca5249a179 More resiliance against wrong attribute counts 2022-10-03 14:05:08 +02:00
Thomas Krijnen 698c708bf6 Reduce heap allocations during parse by reusing vector for top level instances 2022-10-03 14:04:42 +02:00
Thomas Krijnen 084151ed4c Augment invalid entity message with offset in file 2022-10-03 14:03:53 +02:00
Thomas Krijnen c56c7288e3 Clarify wrong attribute count message 2022-10-03 14:03:04 +02:00
Thomas Krijnen f5398c77d5 Clarify invalid entity message 2022-10-03 14:02:28 +02:00
Thomas Krijnen a724b5cb24 Clarify character offset in exception 2022-10-03 14:02:04 +02:00
Thomas Krijnen f3a15953b2 Missing include 2022-10-03 14:01:43 +02:00
Dion Moult 567548c7f3 Fix bug where adding an opening didn't create a relative placement 2022-10-03 22:35:53 +11:00
Dion Moult 8c25349b49 Add new opening functions to BIM tool workspace UI 2022-10-03 20:50:33 +11:00
Amoki 49cd966d43 remove-yacc 2022-10-03 11:16:42 +02:00
Dion Moult b552ea1376 Openings now can be explicitly edited and saved and use IOS instead of Blender modifiers 2022-10-03 01:02:08 +11:00
Dion Moult 9c91ee31e5 New booleans UI panel to manage representation boolean stack 2022-10-02 23:48:19 +11:00
Carlos Villagrasa e9a063ccbd Construction Types Browser - Multipreview now runs smoothly (#2463)
* Construction types multipreview now runs smoothly

* Minor fix

* Minor fix
2022-10-02 16:46:14 +11:00
Dion Moult e4e9db0b3e Implement show and hide openings and hide booleans operator 2022-10-01 23:45:49 +10:00
Dion Moult 2ec41bf3a0 Add support for processing rectangular profiles in the profile editor 2022-10-01 23:27:05 +10:00
Dion Moult 944cbeea1a Handle both 2D and 3D placement matrixes in IfcOpenShell utils 2022-10-01 23:25:34 +10:00
Dion Moult 4b8238a602 Openings are no longer loaded when loading fresh IFC projects 2022-10-01 20:32:40 +10:00
Dion Moult 8789a19f57 Basic show and remove representation boolean operators 2022-10-01 20:31:46 +10:00
Kristoffer Andersen 8c8e83d98f update ci-daily build use win2019 for upload 2022-10-01 10:04:24 +02:00
Dion Moult 527d26c1c6 Openings are now specially shaded, and you can now add half space clipping booleans
# Conflicts:
#	src/blenderbim/blenderbim/bim/module/model/ui.py
2022-10-01 14:33:33 +10:00
Massimo Fabbro e2a5b09d77 Add calculate all qtos button: initial fixing (#2456) 2022-10-01 10:55:42 +10:00
Kristoffer Andersen 98b6edf119 Merge pull request #2461 from Krande/fix-daily-conda-build
fix: daily builds of conda ifcopenshell
2022-09-30 19:42:34 +02:00
Carlos Villagrasa 0a05197d3e Construction Types Browser: Solution to most of the current issues (#2460)
* Delay asset generation

* bpy.app.is_job_running only in Blender >= 3.3

* First time preview on sidebar works again

* Avoid flickering

* Show multiple previews on the first run

* Preserve mouse position with hacky operator
2022-10-01 02:01:26 +10:00
krande 75645a837a fix: daily builds of conda
Ran into a lot of `build not compiled for windows` errors on windows. So using the host and dependencies from conda-forge meta.yaml and pin boost-cpp to <=1.74 to at least allow windows builds of ifcopenshell to be created while we figure out how to support boost-cpp >=1.78
2022-09-30 17:49:55 +02:00
Thomas Krijnen 40e463a55f #2457 uplift curves to wires in geometric sets 2022-09-30 13:59:31 +02:00
Dion Moult 7b817f7359 Minor fix 2022-09-30 18:53:14 +10:00
Dion Moult 22e69a606d CAD sketcher constraints can now be saved and loaded from IFC 2022-09-30 16:04:16 +10:00
Dion Moult 8c7b2ab2a1 Slab depth auto-updates on type/layer changes with new slab system 2022-09-30 13:43:23 +10:00
Dion Moult 3512a2ce36 Add undo support to profile and wall element operators 2022-09-30 13:08:39 +10:00
Dion Moult 9e76b2b9eb Footprints are now supported in the new profile editor 2022-09-30 13:00:40 +10:00
Dion Moult a08a78b3c9 Slabs are now first created directly in IFC instead of reverse engineering meshes 2022-09-30 12:08:57 +10:00
Dion Moult 9fc5f72831 Minor fix 2022-09-29 23:51:25 +10:00
Dion Moult 391e09c16c Convenience operator to draw rectangles when editing profiles 2022-09-29 23:31:11 +10:00
Dion Moult 60e4de5ffa You can now align the view to the edited profile, for angled profiles 2022-09-29 22:33:32 +10:00
Vukas Pajic a248f16cf8 indentation 2022-09-29 14:30:25 +02:00
Vukas Pajic 9240a10fa8 no need for Shapely.libs when using macos 2022-09-29 14:25:42 +02:00
Vukas Pajic 19d3208744 add Shapely.libs 2022-09-29 14:08:24 +02:00
Vukas Pajic c080567f53 update makefile for shapely dependancy (#2452)
* initial

* unzip instead of tar

Co-authored-by: Dion Moult <dion@thinkmoult.com>
2022-09-29 22:01:54 +10:00
Dion Moult ac72f41051 You can now change the radius of existing arcs. Minor interface cleanup. 2022-09-29 17:24:00 +10:00
Dion Moult fc9273c1d3 You can now change the radius of existing circles. Radius settings are remembered. 2022-09-29 16:47:40 +10:00
Dion Moult 43dc2da05f Minor fix 2022-09-29 15:43:19 +10:00
Dion Moult 121190c9b9 Vertex group indices in repeated operations do crazy stuff. This is more robust. 2022-09-29 15:41:06 +10:00
Dion Moult 0f2f6e2721 More robust arcindex generation and let user specify radius for circles 2022-09-29 11:41:52 +10:00
Carlos Villagrasa 05070c3758 Construction Types - Attempt to solve repeated object creation (#2450)
* Constr types dicts to Blender props

* Reduce update interval

* Base for not linking dummy asset objects to collections
2022-09-29 10:00:39 +10:00
Dion Moult d4d923e51b New feature to fillet profiles with true arcs 2022-09-28 23:35:29 +10:00
Dion Moult 766ffa71cd Operator to clear parametric vertices when editing profile 2022-09-28 23:04:07 +10:00
Dion Moult cbf213952b Minor fix 2022-09-28 22:45:03 +10:00
Vukas Pajic b7c76048ab comment out tests 2022-09-28 14:24:17 +02:00
Kristoffer Andersen 20fdb09a0c Update meta.yaml
Do not force boost version 1.80.0. It fails horribly
2022-09-28 14:21:47 +02:00
Vukas Pajic 1943d59339 Add additional quantity calculations to QtoCalculator (#2405) 2022-09-28 14:20:10 +02:00
Dion Moult d2836e07d5 Minor fix 2022-09-28 22:18:54 +10:00
Dion Moult 90a788082d New 2D CAD offset operator 2022-09-28 21:07:28 +10:00
Kristoffer Andersen eafad2862f Update meta.yaml
test using boost 1.80.0
2022-09-28 09:21:39 +02:00
Dion Moult 1c0fa13f39 Fix CAD arc bug where sometimes arc went the wrong way, and now auto-clean verts when creating arc 2022-09-28 11:02:49 +10:00
Carlos Villagrasa 2e5ce6b862 Fix bim tool crash (#2446)
* Attempt to wait for assets (triggers infinite loop)

* Using timer instead of sleep

* remove sleep import

* Reduce asset update interval

Co-authored-by: Dion Moult <dion@thinkmoult.com>
2022-09-28 09:30:52 +10:00
Ryan Schultz f63a33647b relocate remove drawing button per https://community.osarch.org/discussion/comment/12744/#Comment_12744 (#2444)
* added 3"=1'-0" scale (a common scale), and corrected 6"=1'-0" scale

* relocate remove drawing button per
https://community.osarch.org/discussion/comment/12744/#Comment_12744
2022-09-28 01:54:11 +10:00
Carlos Villagrasa 2f84c188f5 Fix bim tool crash (#2445)
* Attempt to wait for assets (triggers infinite loop)

* Using timer instead of sleep

* remove sleep import
2022-09-28 01:31:59 +10:00
Dion Moult 01b1b57f71 Mitre joints now auto merge and are accessible in the new profile editor 2022-09-27 23:35:40 +10:00
Dion Moult 1ad0ef82d4 Fix bug where CAD filleting sometimes led to crashes 2022-09-27 23:34:08 +10:00
Dion Moult 23212003e9 Add support for circles in profile editing 2022-09-27 17:16:26 +10:00
Dion Moult aff281d00b Add unit support in new profile editing tool 2022-09-26 22:40:33 +10:00
Dion Moult 4bbcf3fa1e Set and unset 3-point arcs only for profiles 2022-09-26 22:06:11 +10:00
Dion Moult 5e50a15b41 New smart profile editor for extruded elements inspired by CAD Sketcher 2022-09-26 18:00:05 +10:00
Massimo Fabbro b673fc94a5 Add calculate all qtos button (#2441)
* Add button to calculate all object qtos in qto n-panel

* Calculate all qtos button: now the schema is readed from the file

* Calculate all qtos button: now edit_qto runs only one time every object

* Add button to assign pset qto to selected objects

* Calculate all qtos button: small fixing and unable to execute if no selected objects

* Calculate all qtos button: first tool test

* Calculate all qtos button: use better built-in function to get ifc obj entity
2022-09-26 16:05:08 +10:00
Dion Moult 4c7a5e8a1a Experimental CAD Sketcher integration for simple arbitary polyline extrusions 2022-09-25 12:27:10 +10:00
Dion Moult 33ebcd7664 New convenient modifier to rotate beam/wall/column etc by 90 degrees 2022-09-24 21:46:32 +10:00
Dion Moult 76ce726f1d Implement refresh profiles on type and profile changes 2022-09-24 20:19:01 +10:00
Ryan Schultz 54da579891 Ifc text tweaks (#2431)
* added 3"=1'-0" scale (a common scale), and corrected 6"=1'-0" scale

* Allow float point values in smart tags, and avoid breaking when no assigned product See #2340, #2349
2022-09-24 20:07:32 +10:00
Dion Moult e024e9e622 You can now set cardinal point on creation and bulk change for profile elements 2022-09-24 16:21:20 +10:00
Dion Moult 56f4b72161 Reimplement profile cardinal points 1-9 with new profile engine 2022-09-24 15:42:50 +10:00
Dion Moult 7545313dee Removing an element now also purges connection relationships 2022-09-24 14:12:45 +10:00
Dion Moult 48eb6b70e3 You can now manually set the length/height of profiled extrusions 2022-09-23 23:53:12 +10:00
Dion Moult 5f2acb43b1 You can now recalculate connections manually for profiles 2022-09-23 23:41:37 +10:00
Dion Moult b7eaefee50 Fix bug when deleting elements may possibly create an invalid containment relationship 2022-09-23 23:26:29 +10:00
Dion Moult 13a387cc73 Add support for descriptions in path connection relationships 2022-09-23 23:00:27 +10:00
Dion Moult f139f2ba1e Fix bug where removing an element in an aggregation could leave the file in an invalid state 2022-09-23 23:00:07 +10:00
Dion Moult 1d8085f6c9 New profile engine now supports mitred profile joints. 2022-09-23 19:23:22 +10:00
Dion Moult 6699bfeb54 Mitred wall joints rebuilt on the new wall engine 2022-09-23 15:42:05 +10:00
Dion Moult 7cee0a85c6 3D axis representation contexts are now created by default for all new projects 2022-09-23 12:39:23 +10:00
Dion Moult 351dcfa241 Implement profile unjoin and start cleaning up legacy architectural tools panel 2022-09-23 12:38:49 +10:00
Dion Moult 28022581a7 You can now have connection aware L joints for profiled elements 2022-09-23 00:09:56 +10:00
Dion Moult 748aaa671d Extending profiled elements (e.g. beams and columns) now supports axis connectivity and uses new profile engine 2022-09-22 23:32:22 +10:00
Thomas Krijnen 95b474c37d Delete HdfSerializer.h~ 2022-09-22 13:13:57 +02:00
Thomas Krijnen 9bb9f91f57 Delete HdfSerializer.cpp~ 2022-09-22 13:13:47 +02:00
Dion Moult 7db967583b Profile extensions now use the new engine and support 3D axis representations 2022-09-22 16:05:35 +10:00
Dion Moult 5d43f6db37 Adding a profile now uses the new profile engine 2022-09-21 23:28:03 +10:00
Dion Moult 4cfa327159 New API functions for adding arbitrary profile and profile-based representations 2022-09-21 23:27:24 +10:00
Dion Moult 920177d8c6 Minor fix. See #2416. 2022-09-21 10:50:02 +10:00
Dion Moult 5f902600e3 Minor fix. See #2416 2022-09-21 00:04:02 +10:00
Dion Moult db9af13843 Fix #2427. Don't treat missing mandatory attributes as fatal in IFC version migration 2022-09-20 22:46:15 +10:00
Thomas Krijnen dd49c092c6 #2419 remove map erasure from destructor
de initialization order can result in map already destroyed when unique_ptr schemas are still being deleted
2022-09-20 11:35:08 +02:00
Thomas Krijnen d347a7f893 Don't run choco from other repos 2022-09-20 11:33:07 +02:00
Dion Moult 3c61804e8b Flipping walls now preserves connections, and flips around the reference axis 2022-09-20 17:21:11 +10:00
Dion Moult e179070978 New interface to show, select, and remove path connections 2022-09-20 16:25:52 +10:00
Dion Moult de2ada1141 Walls recalculate on type and layer thickness changes 2022-09-20 14:05:04 +10:00
Dion Moult eb48a68dd1 Copying elements do not copy connections to prevent mutually exclusive connectivity 2022-09-20 12:01:40 +10:00
Dion Moult fc6bf5e2bb You can now set an exact wall length 2022-09-20 10:48:44 +10:00
Dion Moult 3f2d86255d Merge projects now merges all entities, not just rooted ones. 2022-09-19 23:44:10 +10:00
Dion Moult d751ee9995 Fix critical typo in rendering styles creation that led to invisible styles 2022-09-19 23:10:56 +10:00
Dion Moult 83f70624a4 Attempt to get LXML working across all platforms. See #2422. 2022-09-19 22:23:09 +10:00
Dion Moult 356253c4f0 Minor fix 2022-09-19 21:40:53 +10:00
Dion Moult a459c39c97 Minor fix 2022-09-19 17:36:20 +10:00
Dion Moult e5719bd3c6 You can now change wall heights in bulk 2022-09-19 16:29:43 +10:00
Dion Moult 18387e7732 You can now switch contexts of of objects in bulk 2022-09-19 15:59:37 +10:00
Dion Moult 57e8e47061 Minor regression fix 2022-09-19 13:33:40 +10:00
Dion Moult 5c5bb03652 IfcPatch now allows passing in IFC objects instead of filepath strings 2022-09-19 13:31:33 +10:00
Dion Moult ea1d797605 Add documentation for IfcPatch 2022-09-19 12:25:35 +10:00
Dion Moult bc3bf07a60 Minor fix to 12DToRevitTINS patch 2022-09-19 10:49:03 +10:00
Dion Moult fbd4177d08 New feature to merge aligned walls and retain parametric connections 2022-09-19 09:12:42 +10:00
Thomas Krijnen 4842891fd7 #2322 Implement IfcSweptDiskSolidPolygonal.FilletRadius 2022-09-18 15:59:24 +02:00
Dion Moult f6056f9bc3 Shift-Y in the BIM tool for walls now regenerates wall connections 2022-09-18 23:26:18 +10:00
Dion Moult c7af6c4482 Fix #2420. Bug where project library assets sharing relationships with other assets would pull in other assets 2022-09-18 23:19:23 +10:00
Dion Moult c9c905fd3e Splitting walls now uses the new wall engine 2022-09-18 22:00:27 +10:00
Dion Moult 7f76488869 New recalculate wall operator to manually trigger an update of all wall connections 2022-09-18 15:57:07 +10:00
Dion Moult cd930d809a New wall engine slowly being wired up to wall join hotkeys 2022-09-18 15:56:23 +10:00
Dion Moult f49d7ec2cb Implement unjoin in new wall engine 2022-09-18 15:54:24 +10:00
Dion Moult 322abdff53 Minor fix 2022-09-17 22:19:49 +10:00
Dion Moult 984978bc00 Add axis support to new wall engine 2022-09-17 21:52:03 +10:00
Dion Moult ce8da8bd85 An axis context is now added by default for all new projects 2022-09-17 21:41:09 +10:00
Dion Moult 6d0fdb32c6 Fix #2416. 2022-09-17 14:51:50 +10:00
Dion Moult 0cb3c0a907 Update docs about new Apple M1 support 2022-09-16 22:29:04 +10:00
Dion Moult d9ea62a670 Distribute Apple M1 to PyPi 2022-09-16 22:21:58 +10:00
Dion Moult 2228104a8e Apple M1 builds. This stuff never works the first time. 2022-09-16 21:48:38 +10:00
Dion Moult e473574086 Setup BlenderBIM Apple M1 builds. Fingers crossed! 2022-09-16 20:30:33 +10:00
Dion Moult f93604d6b7 Implement connection aware wall L-joints 2022-09-16 18:30:57 +10:00
Dion Moult d106b0e017 Fix #2413. Bug where multiple tasks may accidentally override animation keyframes. 2022-09-16 11:53:25 +10:00
Dion Moult 90e35810d9 Prototype connection-aware wall extension and T-joints 2022-09-15 22:44:06 +10:00
Bruno Postle 4fb910fc44 wrapper is a bash script not a sh script 2022-09-15 07:42:00 +01:00
Dion Moult 280ce56431 Creating walls are more stable and now directly goes to the IFC representation instead of reverse engineering Blender meshes 2022-09-15 10:46:52 +10:00
Dion Moult 81e46b533b New API to directly create wall-like representations without Blender geometry 2022-09-15 10:44:43 +10:00
Dion Moult 171db4b241 Add support for connecting/disconnecting paths in API 2022-09-15 10:32:01 +10:00
Martin15135215 8fe9ddd599 Small Change
https://github.com/IfcOpenShell/IfcOpenShell/issues/2409#issuecomment-1246767831
2022-09-15 00:13:01 +10:00
Dirk Olbrich b299694ca0 doc: library flags for macos build 2022-09-14 15:19:53 +02:00
Dirk Olbrich ca76b77130 update doc for building on macOS 2022-09-14 15:19:53 +02:00
Thomas Krijnen e94a5c97df Merge remote-tracking branch 'remotes/origin/pr/1911' into v0.7.0 2022-09-14 14:57:27 +02:00
GisSpace df845653f3 fix IFC to xml - IFCGROUP in IFC cannot be found in xml #1154 2022-09-14 14:54:05 +02:00
Thomas Krijnen a3e499656c Don't log vertex counts in helper when they didn't change 2022-09-14 14:13:24 +02:00
Thomas Krijnen 79bb40092f Apparently --disable-static no longer works? 2022-09-14 10:57:08 +02:00
Thomas Krijnen 2c7a4ca880 Filter unsupported py versions on recent osx 2022-09-14 10:56:52 +02:00
Thomas Krijnen 6a4375cf54 Update build-all.py versions 2022-09-14 10:56:16 +02:00
Thomas Krijnen c74b79213b Remove asterisks before branch name 2022-09-14 10:55:32 +02:00
Ryan Schultz 806d147a85 A preference for locking grids upon import 2022-09-14 18:33:26 +10:00
Dion Moult 227b1391bd Clarify behaviour of funny characters and pattern matching on numbers 2022-09-14 10:15:19 +10:00
Bruno Postle 68e2bb1fa7 oops, correct version of wrapper that quotes special characters 2022-09-13 23:21:49 +10:00
Dion Moult fccc233f42 Merge pull request #2406 from brunopostle/desktop-files2
Configure blenderbim file associations and desktop icon
2022-09-13 23:06:04 +10:00
Bruno Postle 86785ab181 Configure blenderbim file associations and desktop icon 2022-09-13 13:57:13 +01:00
Dion Moult 3255053ac4 Implement all restrictions for IfcTester 2022-09-13 22:09:40 +10:00
Dion Moult 422cc4802c Implement optionality in IfcTester, and generate integration testcases 2022-09-13 19:50:51 +10:00
Dion Moult fc0b0ca188 Minor fix to ids.xsd 2022-09-13 19:44:36 +10:00
Dion Moult 3120b0c6e2 Add support for IfcTester and predefined properties 2022-09-12 23:03:50 +10:00
Dion Moult 91fac6d4bd Add IfcTester support and test cases for bounded, table, and reference props 2022-09-12 22:35:05 +10:00
Dion Moult c7c59d9496 IfcTester now supports list and enumerated properties 2022-09-12 21:34:06 +10:00
Dion Moult 73ac5d314b Complex properties and quantities are not supported in IDS 2022-09-12 20:10:25 +10:00
Dion Moult 3b2cbac9d1 Generate stable GlobalIds for IDS docs to prevent commit noise 2022-09-12 20:10:25 +10:00
Thomas Krijnen a63d6475b2 Keep moving schema agnostic code out of kernel 2022-09-12 10:34:11 +02:00
Thomas Krijnen 4cd0932d7d Fix to boolean utils, version detection, and behaviour on old versions 2022-09-12 10:34:11 +02:00
Dion Moult cb447fe902 IfcTester Add support for qto checks 2022-09-12 18:26:11 +10:00
Dion Moult 5ee4e2704d User can now explicitly choose the quantity type via the edit qto API 2022-09-12 18:05:13 +10:00
Dion Moult 5b5540494d Qtos now support being added to types and contexts (despite being rare) 2022-09-12 12:55:15 +10:00
Dion Moult 85e123fadb IfcTester now supports occurs attributes on facets 2022-09-12 11:51:03 +10:00
Dion Moult f745222d37 IfcTester now supports mandatory measures for properties 2022-09-12 11:21:47 +10:00
Sigma Dimensions 88893a8c2c BBIM - Fix cost item products not loading for 1st cost item created 2022-09-11 22:14:57 +01:00
Dion Moult eb8bf60e93 Fix #2398. Add support for IFC4X3 entities. 2022-09-11 22:17:41 +10:00
Dion Moult 319aada33b Fix #2388. 2022-09-11 22:14:11 +10:00
Dion Moult 77c9bf5bb6 Implement newly proposed IDS PartOf facet 2022-09-11 17:20:50 +10:00
Dion Moult e5e81c818e IfcTester now handles floating point precision checks 2022-09-10 23:31:21 +10:00
Dion Moult 7aad0fc119 Implement type casting for property checks in IDS, bump to IDS v0.9 2022-09-10 22:42:22 +10:00
Sigma Dimensions f7403e8eee BBIM - Feature to guess all quantities at once 2022-09-09 13:25:04 +01:00
Dion Moult e2df4f335d API now supports adding representations using the Sverchok geometry structure 2022-09-09 22:20:06 +10:00
Sigma Dimensions cbe944d030 BBIM - temp fix to refresh cost items products' quantities UI, when qto is edited 2022-09-09 11:43:28 +01:00
bosonprojets 87f0475b99 Quality - run black on api/sequence directory 2022-09-09 02:25:20 +01:00
bosonprojets 4689cc858a BBIM Feature to import resources data from CSV 2022-09-09 02:23:53 +01:00
bosonprojets 6d6b893db2 Core tests for Resource module 2022-09-09 02:18:08 +01:00
bosonprojets e2d4a3ae24 Refactor Resource module 2022-09-09 02:17:28 +01:00
Dion Moult e4c1f7518d Revert "Hiding all objects in the "Types" Collection "
This reverts commit 2c0ea75960.
2022-09-09 10:59:10 +10:00
Dion Moult 48f8c0e085 Merge pull request #2395 from IfcOpenShell/Hiding-all-objects-in-the-"Types"-Collection
Hiding all objects in the "Types" Collection
2022-09-09 10:56:26 +10:00
Ryan Schultz 2c0ea75960 Hiding all objects in the "Types" Collection
When all the objects in the "Types" collection are hidden, it makes it quicker to turn on, and isolate one type for viewing and/or toggling other types on to compare to.
2022-09-08 13:12:47 -05:00
Thomas Krijnen e643972669 #2390 Submodule 2022-09-08 13:54:17 +02:00
Dion Moult d7c4936255 New recipe to fix dense TINs from 12D loading properly in Revit 2022-09-08 16:36:12 +10:00
Sigma Dimensions 96d19952e9 Quality check - run black on api/resource 2022-09-08 03:15:41 +01:00
Sigma Dimensions 57caf6a31f IfcOpenShell API: Deep purge resources upon removal 2022-09-08 03:12:44 +01:00
Dion Moult 189fc6e1e4 Minor fix 2022-09-07 20:39:55 +10:00
Dion Moult 88117941c5 Fix #2387. Bundling for Blender makefile now works on Mac. Thanks dirkolbrich! 2022-09-07 20:26:44 +10:00
Dion Moult 0a6c8a4343 Write docs on creating a single object 2022-09-07 20:14:08 +10:00
Christian Martínez de la Rosa 5bfad88a75 [IfcConvert] Added IfcRelConnectsElements to xml export (#2197)
* [IfcConvert] Export tasks in the xml conversion

The exportion in the XML format exports in a new tag, named "tasks", all the tasks from the file with a hierarchy. Each task contains the IfcTaskTime object if it has it.

* [ifcConvert] Add related objects in tasks

* [IfcConvert] Added IsSuccessorFrom and IsPredecessorTo in xml task export

* Get IfcTasks from IfcWorkSchedule

* [IfcConver] Changed OperatesOn hierarchy level in xml task export

* Added task to property / quanty relationship

* [IfcConvert] Added calendars to xml output file

* [IfcConvert] Added Output objects in the XML file

* [IfcConvert] Fix crash when IfcWorkCalendar does not contain IfcWorkTime

* [IfcConvert] Added IfcWorkPlan to xml export

* [IfcConvert] Added IfcRelConnectsElements to xml export

* [IfcConvert] Added IfcRelConnectsElements to xml export

* [IfcConvert] Improved related and relating xml export
2022-09-05 14:58:22 +02:00
Dion Moult 72544419da External resource relationships are now also purged when removing classifications 2022-09-05 16:51:42 +10:00
Dion Moult d1baaa21e7 Update IfcFM to use actors, not contacts, support federated datasets, and be more true to spec 2022-09-05 12:41:06 +10:00
Dion Moult 5d9fc44a69 Fix #2374. Axis for walls should be in the 2D plan context. 2022-09-05 11:35:04 +10:00
Dion Moult 917344ee1d Fix bug where new instances may have a wrong Z coordinate 2022-09-05 11:13:31 +10:00
Dion Moult 68ef6458d7 Fix #2379. Regressions where context adding never had any tests and failed. 2022-09-05 11:13:31 +10:00
Sigma Dimensions 36576c8d40 IFC resources utility: batch import Resource Data from CSV 2022-09-05 01:51:53 +01:00
Sigma Dimensions b44a1cb2e2 Fix 4d import context 2022-09-05 01:46:19 +01:00
Thomas Krijnen 5c7cf4469f Compute meaningful hash and eq on simple type instances 2022-09-04 14:23:35 +02:00
Dion Moult 147216cb7b Revert "Fix bug where IFC data types were hashed as the same value in RecycleNonRootedElements IfcPatch recipe"
This reverts commit 3be8e4f1ce.
2022-09-04 22:22:54 +10:00
Dion Moult 62b1478af1 Merge pull request #2377 from Martin15135215/v0.7.0
From append methode to use numpy and ifcpatch bug
2022-09-04 20:13:51 +10:00
Dion Moult 0f0055b0f1 Merge branch 'v0.7.0' into v0.7.0 2022-09-04 20:13:38 +10:00
Dion Moult 3be8e4f1ce Fix bug where IFC data types were hashed as the same value in RecycleNonRootedElements IfcPatch recipe 2022-09-04 20:12:54 +10:00
Martin15135215 6aa4ca89fe Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0 2022-09-04 09:22:58 +00:00
Dion Moult 5fb574cf5c Fix #1589. Saving beveled curves as rswept disk solids is now supported. 2022-09-04 17:46:34 +10:00
Dion Moult 85a502a038 Support multiple swept disk solids with equal radii as a native Blender object 2022-09-04 17:45:26 +10:00
Dion Moult a7c56e742b Fix UI bug where adding nested groups closed the UI 2022-09-04 11:17:55 +10:00
Martin15135215 f11f648630 From append methode to do with numpy 2022-09-04 10:25:02 +10:00
Martin15135215 74d1bd7e3e Added "or element.is_a('IfcPropertySingleValue')" 2022-09-03 19:19:02 +02:00
Martin15135215 248ac71215 From append methode to use numpy 2022-09-03 19:13:55 +02:00
Dion Moult af4cb22756 Fix bug where appended assets sometimes duplicated geometry contexts 2022-09-03 22:16:35 +10:00
Dion Moult 56393ef358 Fix #170. Add note about not mixing OpenCascade installations. 2022-09-03 20:16:33 +10:00
Dion Moult 5d4512033d Fix #2286. Classifications can now be added for cost items. 2022-09-03 17:11:14 +10:00
Dion Moult b9cb1ab012 Black 2022-09-03 15:34:52 +10:00
Dion Moult e34b737428 Fix #2304. You can now manage classifications for materials. Bulk classification now supported. 2022-09-03 15:33:52 +10:00
Dion Moult 557d3429eb Support bulk deletion of psets on selected objects 2022-09-03 14:32:39 +10:00
Dion Moult f9019a4007 Test non-rooted classifications in IDS 2022-09-03 11:54:12 +10:00
Dion Moult 0668eadc36 IDS doc generator now also generates a sample IDS file 2022-09-03 10:48:46 +10:00
Dion Moult fdfaf54698 Support resource level classifications in IfcOpenShell API 2022-09-02 18:23:49 +10:00
Dion Moult ffa3aae132 Fix UI bug where changing classification levels sometimes didn't display 2022-09-02 15:21:27 +10:00
Dion Moult 0293c133ab Fix #2364. Bug where classifications couldn't be added in IFC2X3. 2022-09-02 14:03:05 +10:00
Dion Moult f9db685753 Fix #2343. Pset template applicability is refreshed when you save your templates. 2022-09-01 18:25:00 +10:00
Dion Moult a800d1af7e Fix pset template UI bug where changing to another prop didn't cancel editing older props. Also add support for blank or falsey enums (e.g. a real of 0.0). 2022-09-01 18:06:33 +10:00
Dion Moult f2bd362f57 Fix bug where adding a pset template didn't auto refresh the UI to the newly added template 2022-09-01 18:06:33 +10:00
krande 87021ebbe7 fix: edit ci triggers for ifcopenshell.
Changes are made to the github actions workflow files to reduce unnecessary triggering of ifcopenshell compilation, testing and conda package building
2022-09-01 09:41:30 +02:00
Dion Moult 36f2ed6da6 Allow disabling undo on save
The transaction system does not scale on large scale or complex remove_deep deletions. So if you want to delete a mesh with many thousands of vertices, it becomes very, very slow. Unfortunately we cannot also clear Blender's undo history, so this advanced feature should be used carefully.

Tested:

 - deleting 250 average objects: 264s before, 246s after (6.8% faster)
 - deleting 1 object with 6,000 vertices: 5 minutes and counting (basically impractical) before, 11s after
2022-09-01 11:17:06 +10:00
Dion Moult a61c084f27 Minor fix 2022-09-01 10:17:45 +10:00
Dion Moult 6849bb8c17 OpenCascade is not practically optional for now 2022-09-01 09:07:40 +10:00
Dion Moult a26f60861e Add pip/docker install instructions 2022-09-01 09:05:06 +10:00
Thomas Krijnen 106b8fdd07 Update Dockerfile 2022-08-31 21:01:09 +02:00
Thomas Krijnen 87c6b06cfc Update CMakeLists.txt 2022-08-31 20:48:36 +02:00
Thomas Krijnen af4917efba Sync dockerfile libs with ci libs 2022-08-31 20:28:26 +02:00
Dion Moult 806b2e2714 Add PyPi badge and switch to Shields badge for Anaconda
See https://github.com/ContinuumIO/anaconda-issues/issues/10231
2022-08-31 18:15:33 +10:00
Dion Moult 4ae8e4c74b Rename PyPi package to ifcopenshell-python due to naming conflict 2022-08-31 17:18:18 +10:00
Dion Moult 8101ffe06a Try using existing PYPI_TOKEN as the ifcopenshell package name is blocked 2022-08-31 16:54:27 +10:00
Dion Moult f07b6e14b4 Second time lucky? 2022-08-31 16:06:07 +10:00
Dion Moult f2c31cc17d Add optimistic Github action to upload wheels to PyPi 2022-08-31 16:02:39 +10:00
Dion Moult 39a626edeb Super simple wheel creator for PyPi 2022-08-31 15:41:01 +10:00
Sigma Dimensions 02379c5110 BBIM - New Sequence Toolkit Panel & new feature to highlight tasks for current selected object 2022-08-31 02:26:01 +01:00
Sigma Dimensions 1e661c42fb BBIM Feature - expand and contract all tasks in a work schedule 2022-08-31 02:21:28 +01:00
Thomas Krijnen eb7e9bacbf Update README.md 2022-08-30 21:58:11 +02:00
Thomas Krijnen c38e608a7c Update README.md 2022-08-30 21:49:51 +02:00
Dion Moult 544ed696da Add install instructions for ArchLinux for IfcOpenShell/IfcConvert 2022-08-30 23:00:18 +10:00
Dion Moult 322e47c5b5 Add badges to readme 2022-08-30 22:57:54 +10:00
Dion Moult 3ee9e6f45c Remove mention of outdated 3DS max from install instructions 2022-08-30 22:27:09 +10:00
Thomas Krijnen 7df94a906d #2305 fix cone boolean op 2022-08-30 13:31:26 +02:00
Dion Moult af1a40c913 Fix bug where if an object placement was reused elsewhere a circular reference was created 2022-08-30 20:08:50 +10:00
Dion Moult 451512ddff #1812 Add batching support for remove_deep2 for faster removals 2022-08-30 20:08:03 +10:00
Dion Moult fbd33145e1 Fix #2360. Fix documentation on IfcConvert flags that also apply to STP. 2022-08-30 18:28:30 +10:00
Dion Moult 8b0b1699f6 Fix #2370. Asset preview code did not clean up after itself properly after generating objects. 2022-08-30 16:21:25 +10:00
Dion Moult 2e13ed77d8 Fix #2371. Bug where you couldn't edit psets of a hidden object. 2022-08-30 11:38:04 +10:00
Dion Moult 804c0f4a77 Partially write tests for sequence refactor 2022-08-30 11:08:26 +10:00
Sigma Dimensions 37dec500fd massive refactor of the sequence module 2022-08-29 21:55:37 +01:00
krande ac3eb6fbda Merge remote-tracking branch 'origin/v0.7.0' into v0.7.0 2022-08-29 14:03:26 +02:00
krande 6a0bc0cd91 feature: align conda daily builds of ifcopenshell feature-wise with IfcOpenBot builds 2022-08-29 14:02:51 +02:00
Sigma Dimensions 699c8bf6b6 fix path windows test-bim 2022-08-29 12:51:18 +01:00
Dion Moult 7806f989f3 Minor fix 2022-08-29 21:47:12 +10:00
Dion Moult a0ca387ae7 Minor fix 2022-08-29 21:34:29 +10:00
Dion Moult 2a716ec242 Minor fix 2022-08-29 17:28:44 +10:00
Dion Moult 93ab51fb5e Fix #1958 - document other useful Blender add-ons 2022-08-29 16:25:17 +10:00
Dion Moult 7c59fc0b83 Document installation and usage of IfcConvert 2022-08-29 13:04:37 +10:00
Dion Moult 819466a819 Port over compilation instructions to Sphinx 2022-08-29 11:40:38 +10:00
Dion Moult 33a8ba9834 Add install instructions for Conda / Chocolatey 2022-08-29 09:14:26 +10:00
Ryan Schultz 6ce86ae368 Closing the ends of a swept_disk_solid by default 2022-08-29 08:49:46 +10:00
luz paz 68bf7a08e0 BlenderBIM: typo fixes 2022-08-28 11:39:15 +10:00
luz paz 771ba4c337 Revert 3rd party code changes 2022-08-27 15:34:16 +02:00
luz paz 33199b4bb6 Fix various typos 2022-08-27 15:34:16 +02:00
krande 74d730f368 [fix] - conda-build - set explicit env python path in conda build script for unix 2022-08-26 14:19:41 +02:00
Dion Moult eb2a71bf48 Fix #2159. Disable JUnit in Behave in Blender due to Etree conflicts. 2022-08-26 13:40:34 +10:00
Dion Moult 4b4e2e1622 Improve instructions on how to run tests and set up a dev environment 2022-08-26 13:40:34 +10:00
Thomas Krijnen b5f4c8218d #2358 fix header entity parsing in case of too few fields 2022-08-24 11:57:06 +02:00
Christian Kimmig e3a22601d5 Set Position of extrusions with height = 0 2022-08-24 11:10:45 +02:00
Dion Moult f67856a2ae Minor fix 2022-08-24 10:13:30 +10:00
Dion Moult 6b71b32e63 Improve documentation for running tests 2022-08-24 09:54:15 +10:00
Dion Moult 60d99fe2fa Update dev install process docs and bump IfcOpenShell dep 2022-08-23 17:15:11 +10:00
Dion Moult 75e1786682 Purge IDS features from BIMTester as superseded by IfcTester 2022-08-23 16:37:15 +10:00
Dion Moult 9b3f964000 Fix #2060. Large lists of types in a library can now be filtered. 2022-08-23 13:26:38 +10:00
Dion Moult de6b578d9c Fix #1375. Spatial zones as non-hierarchical entities are now supported. 2022-08-22 17:31:44 +10:00
Dion Moult f50e790b11 Minor fix 2022-08-22 17:02:05 +10:00
Dion Moult 9821c3af67 Fix #2317 add graphical support for viewing and editing referenced structures 2022-08-22 13:52:12 +10:00
Dion Moult 0844d6f52e Fix #2354 - add support for selector queries for enumerated properties 2022-08-19 20:10:54 +10:00
Dion Moult 520e7653e4 #1375 #2317 add API support for referencing in spatial structures 2022-08-19 18:43:39 +10:00
Dion Moult 9512075506 Minor fix 2022-08-19 12:41:08 +10:00
Dion Moult bdd61713ea WIP refactor sequence module 2022-08-19 11:20:18 +10:00
Dion Moult 97cf1da4ff #2339 Read directly from raw data instead of using strings for IfcDiff output 2022-08-18 17:57:11 +10:00
Dion Moult 836a162d57 #2342 Support different IFC schema versions for obj2ifc 2022-08-18 17:15:39 +10:00
Dion Moult 09a6f56f47 #2319 Add documentation for IfcDiff 2022-08-18 16:12:10 +10:00
Ryan Schultz 35ce8c2848 Updated links to point to current branch 2022-08-17 16:12:35 -05:00
Dion Moult ae2fd67260 #2342 New Meshlab based OBJ2IFC converter 2022-08-17 14:33:48 +10:00
Dion Moult 9def041f27 You can now create multiple new IDSes side by side and close them 2022-08-16 17:14:54 +10:00
Dion Moult e9d86253ec Minor fixes to IfcTester 2022-08-16 11:52:01 +10:00
Dion Moult f95466ee0c IfcTester now integrated into BlenderBIM Add-on UI 2022-08-16 11:52:01 +10:00
Dion Moult e85f21ce7a Minor fix 2022-08-16 11:52:01 +10:00
Sigma Dimensions d3795c9836 Documentation for ifcopenshell.util.element 2022-08-15 19:40:51 +01:00
krande ab647b13d9 fix ci initializer to prevent accidentally triggering and add tagged release for conda-forge release 2022-08-15 16:59:32 +02:00
Kristoffer Andersen dcad4146b6 PR daily builds of ifcopenshell and upload to conda (#2132)
* WIP PR for daily builds of ifcopenshell

* fix indentation in yaml

* fix branch name

* test daily builds using minimum occt ==v7.5.3

* chore: WIP daily builds - add tests

* add test script bat/sh. Continue work in PR

* fix: add more packages to test requires

* fix: remove space in minimum version requirement of numpy

* use manually installed MacOSX SDK on github runner. Minor rewrite of dynamic/static zlib conditional for conda_build.

* fix MacOSX SDK address and made it platform conditional

* make tests pass

* try to skip ids tests for common icopenshell builds (caused failure on windows tests probably due to special character in filepath)

* add conditional upload and testing of all available platform versions on github actions

* fix wrong path to source files

* fix errors in tests

* remove test files and skip tests

* only do import of ifcopenshell as means of testing and rely on the successful completion of ci.yml as a triggering mechanism instead

* make conditional switch between original implementation of zlib suffix and new depending on conda packaging or not

* point to occt v0.7.6

* make conditional import of BRepAdaptor_HCompCurve

* try removing libxml2 include path

* minor changes

* Test nix builds

* Test nix builds using only a single python version (in order to limit cpu/memory usage)

* remove run check

* remove testing of nix builds

* Revert "Test nix builds using only a single python version (in order to limit cpu/memory usage)"

This reverts commit 22a552a9ac.

* Revert "ci-py-only test modification"

This reverts part of commit af1e832f

* update occt version to 7.6.2 in daily builds

* Revert "Test nix builds"

This reverts commit af1e832f81.

* Complete setups for local and cloud environments. And start by adding IfcPerson as a test case for the REST api communication

* Revert changing checkout version of MVD
2022-08-15 16:40:42 +02:00
Thomas Krijnen adb2883310 #2344 Disable /MP by default 2022-08-15 13:53:23 +02:00
Dion Moult 7867c71b6e Fix IFCTester handling of falsey properties 2022-08-11 16:38:44 +10:00
Gorgious a5b1a96dfd Revert hack to automatically close enum search popups
It was insta-closing for some users
2022-08-10 21:58:00 +02:00
Carlos Villagrasa 4e5503002a Construction Type Browser, polishing of 2nd iteration (#2336)
* Polish a bit the type browser UI

* Remove SELECT from type browser

* model and geometry module tests fixed

* Polishing of type authoring layout redesign

* Further polishing of type authoring layout
2022-08-10 21:23:13 +10:00
fbpyr fa8fc161da Choco action (#2335)
* check cron based trigger isolated

* check cron based trigger isolated

* fix args

* add scheduled choco build action

* revert to original

* fix action name

* fix action name

* remove apt install as required packages exist already in the container

* remove apt install as required packages exist already in the container
2022-08-10 21:22:26 +10:00
Dion Moult e4016313c4 IfcTester now supports HTML reports 2022-08-09 17:04:12 +10:00
Dion Moult 81fdfbacd0 OffsetObjectPlacements patch now allows 3D rotations, such as to fix wrong Z axis coming from software like Solidworks 2022-08-08 14:34:06 +10:00
Dion Moult 7c2c946b2f Fix bug in diff visualisation for non-rooted entities. Refactor diff UI to data class. 2022-08-05 10:59:07 +10:00
Dion Moult 5b34e08c37 #2321 Remove IDS from IfcOpenShell to prevent confusion as it is now refactored heavily and lives in ifctester 2022-08-05 09:29:01 +10:00
Vukas Pajic 0b16c07e3a return result when calling diff() 2022-08-04 16:45:10 +02:00
Vukas Pajic 0210131a56 enhance ui for ifc-diff 2022-08-04 16:44:22 +02:00
Vukas Pajic a44d7e997a typo 2022-08-04 10:25:30 +02:00
Vukas Pajic 1df8908689 fix commit mistake 2022-08-04 10:24:13 +02:00
Vukas Pajic f9daba9fba filter diff with IFCSelector 2022-08-04 10:04:06 +02:00
Vukas Pajic fd6f2f3fc4 Diff can be restricted by elements that match Selector query 2022-08-04 09:54:11 +02:00
Dion Moult a4d0378405 More IfcDiff optimisations (461s->247s), bump DeepDiff version, less false positives with more accurate number comparisons 2022-08-04 17:49:18 +10:00
Dion Moult 1629205c3f Some optimisations to IfcDiff for a 50% speed up (461s down to 300s on sample data) 2022-08-04 11:47:34 +10:00
befr d058f17c15 add logic to build blenderbim-nightly chocolatey package 2022-08-04 11:05:12 +10:00
Carlos Villagrasa bbb3b62b8a Construction Type Browser, 2nd iteration (#2334)
* Polish a bit the type browser UI

* Remove SELECT from type browser

* model and geometry module tests fixed

* Polishing of type authoring layout redesign
2022-08-04 11:04:40 +10:00
Dion Moult f9ef00fd45 WIP experimental layout for authoring tool with Carlos and Vukas 2022-08-03 19:03:47 +10:00
Dion Moult 709c530434 Fix bug where MergeDuplicateTypesByTag recipe would've left multiple IfcRelDefinesByType relationships which is no good 2022-08-03 17:33:23 +10:00
Dion Moult 0ee93294dd Fix bug where element types wouldn't load if a filter was applied 2022-08-03 17:11:56 +10:00
Dion Moult 73124dd597 Fix failing tests 2022-08-02 19:34:12 +10:00
Dion Moult 759e134e3b Check and correct for object mode in operator, not in poll, as operator may be called headlessly (e.g. in export function).
Note that after the operator has been refactored to use the core / tool design pattern, the poll code may return if necessary.
2022-08-02 19:34:12 +10:00
Dion Moult 1f11354188 Add support for adding non-library based classification references for IFC2X3 2022-08-02 19:34:12 +10:00
Vukas Pajic eaffb9d8c0 Remove outdated test in "search" 2022-08-02 10:53:02 +02:00
Vukas Pajic f297b1709d add search to pytest.ini 2022-08-02 10:52:13 +02:00
Carlos Villagrasa 1f637660d1 model and geometry module tests fixed 2022-08-02 18:13:31 +10:00
Carlos Villagrasa c349523ea9 Remove SELECT from type browser 2022-08-02 18:13:31 +10:00
Carlos Villagrasa 5cc11a170b Polish a bit the type browser UI 2022-08-02 18:13:31 +10:00
Dion Moult 21094c59d9 API now supports adding classifications by identifier instead of from a library 2022-08-02 10:10:02 +10:00
Vukas Pajic 8ca5dd747e Fix issues with updating groups 2022-08-01 18:32:19 +02:00
Dion Moult ffe9ac6614 Run black 2022-08-01 19:45:36 +10:00
Dion Moult d2083b9a3d Refactor: move UI code for construction types panel into UI file 2022-08-01 19:42:57 +10:00
Dion Moult 70d1ad59c2 Merge pull request #2315 from cvillagrasa/v0.7.0
Addition of Construction Type Browser
2022-08-01 18:42:09 +10:00
Carlos Villagrasa 53687e6b7c Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0 2022-08-01 10:41:33 +02:00
Vukas Pajic 67993071a6 UIlist no longer closes after adding group to group 2022-08-01 10:32:05 +02:00
Vukas Pajic 29c98a5996 Support for nested IfcGroups 2022-08-01 10:16:27 +02:00
Dion Moult 5b5a4649c5 Fix #2310. Manually patch applicable class errors in official IFC pset templates. 2022-08-01 13:05:57 +10:00
Dion Moult d654110fc1 Support removing representations from IFC models that do not correctly follow the convention that mapped representations come from types 2022-08-01 11:23:35 +10:00
Carlos Villagrasa 735f308abd Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0 2022-07-28 23:50:10 +02:00
Dion Moult f86f988d6e Fix #2318. Hotfix only. Needs proper code review. 2022-07-28 12:17:23 +10:00
Sigma Dimensions a1a684cd2b refactor BBIM Module Workplans (data loading) (Part 2of2) 2022-07-28 01:32:08 +01:00
Carlos Villagrasa 6180f5be33 Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0 2022-07-27 08:37:39 +02:00
Carlos Villagrasa cd58b994dd Renaming AddConstrTypeInstance 2022-07-27 08:24:50 +02:00
Carlos Villagrasa 40854eb3d3 Renaming constr_type back to relating_type 2022-07-27 08:18:34 +02:00
Carlos Villagrasa 79453a6ae0 Renaming constr_class back to ifc_class 2022-07-27 08:11:52 +02:00
Bruno Postle eec4c23017 Transfer psets and style representations when appending a material 2022-07-27 09:53:22 +10:00
Sigma Dimensions beeba10b22 refactor BBIM sequence module - workplans (add, edit, delete) 2022-07-26 19:55:47 +01:00
Carlos Villagrasa 7a6005d0ea Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0 2022-07-26 20:43:38 +02:00
Carlos Villagrasa 58150a296d Revert "Merge remote-tracking branch 'origin/master' into v0.7.0"
This reverts commit 14bca5d199, reversing
changes made to 7321138d57.
2022-07-26 20:20:29 +02:00
Dion Moult 7de454337d Minor fix 2022-07-26 10:33:40 +10:00
Carlos Villagrasa 3e25ef9222 Merge branch 'IfcOpenShell-v0.7.0' into v0.7.0 2022-07-26 01:39:49 +02:00
Carlos Villagrasa 9278e43d72 Merge branch 'v0.7.0' of https://github.com/IfcOpenShell/IfcOpenShell into IfcOpenShell-v0.7.0 2022-07-26 01:39:09 +02:00
Dion Moult fafdc76494 Minor fix 2022-07-26 09:30:04 +10:00
Carlos Villagrasa 99c47069cd Tests polished 2022-07-26 01:22:22 +02:00
Carlos Villagrasa cee48d7424 Some more renaming/refactoring 2022-07-26 01:21:51 +02:00
Ryan Schultz dc7062e182 fix broken links in Documentation 2022-07-25 14:55:37 -05:00
Vukas Pajic 5f6c584a58 fix error where prop name wasn't shown 2022-07-25 12:25:55 +02:00
Vukas Pajic 5f7630e527 added test for creating new ifcgroups 2022-07-25 11:13:15 +02:00
Gorgious 733e80737a Fix error messages when using the search operator on blank file (F3) 2022-07-23 15:03:26 +02:00
carlos 64752e52c9 Tuning feature tests 2022-07-23 00:39:17 +02:00
carlos 6c260ef5c9 Margins function for UILayouts 2022-07-23 00:38:26 +02:00
carlos e0e0572ff5 Minor renaming and bug fixing 2022-07-23 00:37:31 +02:00
Carlos Villagrasa a3942ee5e2 Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0 2022-07-22 18:42:25 +02:00
Vukas Pajic 40d5a94e2d Added new test for Selector UI 2022-07-22 09:37:46 +02:00
Vukas Pajic f9fe1607ff load_query now closes automatically 2022-07-22 08:20:06 +02:00
Gorgious56 2a019e8090 Enum search popup now automatically closes on item selection
+ move close_operator_panel higher in hierarchy to access it
2022-07-21 17:50:43 +02:00
Gorgious56 901a582c6c Add tooltip to append library element operator 2022-07-21 16:57:31 +02:00
Vukas Pajic 29a4f78689 Integrate ifcgroups with selector 2022-07-21 16:10:10 +02:00
Vukas Pajic f1e0375b6a api: assign_group now expects product to be a list
* minor change to assign_group

* api: assign_group now expects product: list
2022-07-21 10:14:58 +02:00
Vukas Pajic 52e354d1b8 delete search.feature (not ready yet) 2022-07-21 09:31:54 +02:00
Vukas Pajic eef961a39e Merge branch 'v0.7.0' into v0.7.0 2022-07-21 08:37:44 +02:00
Vukas Pajic fcab5d445f test features 2022-07-21 08:20:25 +02:00
Dion Moult b534895ae3 Slightly more dodgy but more robust test dependency installation script 2022-07-21 15:44:11 +10:00
Vukas Pajic 5b76e83965 Selector: added new tests and improved UX
Selector: added tests to check non-standard characters and missing values
2022-07-21 07:37:48 +02:00
Dion Moult a21b73225a Fix #2296. Bug where material and profile psets couldn't be removed. 2022-07-21 15:12:00 +10:00
Dion Moult a11a4032f6 You can now audit and view basic results using the IfcTester webapp 2022-07-21 13:13:38 +10:00
carlos a09f864651 Fetch latest commits 2022-07-20 19:17:52 +02:00
Vukas Pajic 609fd35410 quality of life improvements :) 2022-07-20 14:54:03 +02:00
Vukas Pajic df0c136b6c users can now add selection to ifcgroup 2022-07-20 13:48:26 +02:00
Vukas Pajic ce6d4ea59a match elements when attribute is none and negation used 2022-07-20 09:33:58 +02:00
Vukas Pajic 29594b50b5 test when value does not exist 2022-07-20 09:06:04 +02:00
Vukas Pajic 2a2da56a0e added test to check non standard characters 2022-07-20 08:32:36 +02:00
Gorgious dd041478b3 Add utility method to retrieve dynamic enum items without needing a reference to the callback
This is useful because trying to directly access a dynamic enum with an empty items list throws an error in the console
+ Remove prop search from template_type in pset ui which has only 2 elements
2022-07-19 22:17:39 +02:00
Vukas Pajic 650e5b0cd3 Added UI for IFC Selector Tool
UI for IFC Selector tool
2022-07-19 14:54:39 +02:00
Vukas Pajic afa0db3c7e all tests in test_selector.py now passing 2022-07-19 14:51:48 +02:00
Gorgious56 b9f1f3438c Fix typo 2022-07-19 14:30:32 +02:00
Vukas Pajic 31f21dcb29 abstract away ui code from operator 2022-07-19 14:16:59 +02:00
Vukas Pajic 64a22fe5c6 further refinements 2022-07-19 13:55:20 +02:00
Dion Moult 3f85140d60 Continuing adding tests for the refactored property support 2022-07-19 20:38:53 +10:00
Gorgious56 1b1588359f Ifc class suggestion (#2290)
* You can now use suggestions in ifc class enum search popup

* Move data to data submodule

* Fix error when suggestion mapping contains only one value

* Add predefined types to the search popup

* Use defaultdict to be able to add suggestions later

* Support predefined types and format name to display the ifc class
and fix bug where the prop is not defined in the suggestion dictionary

* Move data retrieval to data module

* Simplify prop search items retrieval

* Support prop search for more enums
2022-07-19 10:19:58 +02:00
Vukas Pajic 4c562c1af0 turn ui into popup operator 2022-07-19 10:05:20 +02:00
Vukas Pajic a0fdb9f777 update according to review comments 2022-07-19 09:21:13 +02:00
Vukas Pajic 2c29f89fe9 Selector - update regex for pset filter 2022-07-19 09:19:32 +02:00
Dion Moult f4aac566d5 Handle cyclic task dependencies in schedule cascading and CPM analysis 2022-07-19 12:39:54 +10:00
Dion Moult 57521bd106 Whoops 2022-07-19 09:47:32 +10:00
Dion Moult 1413cf4f9d Fix bug when adding material sets to a non parametric object 2022-07-19 09:46:28 +10:00
Vukas Pajic 3fa74469f4 minor changes 2022-07-18 15:39:19 +02:00
Vukas Pajic e3d9dd747d draft
endofday 1207

240pm 14/07

draft version

cleanup
2022-07-18 15:11:27 +02:00
Vukas Pajic d71ebb8d25 fix typo 2022-07-18 14:44:13 +02:00
Vukas Pajic 0097d98669 fix #2289 2022-07-18 14:42:39 +02:00
Thomas Krijnen dd635340aa Retry schema macro rewriting 2022-07-18 13:51:45 +02:00
Thomas Krijnen 1b06cb8014 Retry schema macro rewriting 2022-07-18 13:04:21 +02:00
Thomas Krijnen 163e10b608 More schema macro rewriting 2022-07-18 11:57:02 +02:00
Thomas Krijnen dc67287d4b Correct extern function declaration names 2022-07-18 10:46:01 +02:00
Thomas Krijnen 73f959c45a Correct extern function declaration names 2022-07-18 10:08:46 +02:00
Thomas Krijnen 65e66352b7 CMake typo error 2022-07-18 09:31:12 +02:00
Thomas Krijnen 205ff0914c Respect tokens in macro string concatenation 2022-07-18 09:24:20 +02:00
Dion Moult 2e76a800f5 You can now download modified IDSes via the webapp 2022-07-18 14:06:29 +10:00
Dion Moult 1890bf4cb9 You can now load multiple Blends simultaneously when linking IFCs 2022-07-18 10:58:25 +10:00
Dion Moult 3fd472549e Fix bug where material categories don't exist in IFC2X3 2022-07-18 10:29:32 +10:00
Thomas Krijnen 28d15a6c9f Add IFC4x3 (https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/annex-a-express.html) 2022-07-17 21:45:07 +02:00
Thomas Krijnen 6026e037fa Update schema references 2022-07-17 21:13:46 +02:00
Vukas Pajic 0ca90817d8 Fix for Issue #2288 2022-07-17 10:08:24 +02:00
Dion Moult 3436f3e0bb Visualising diffs now show results from the actively loaded IFC 2022-07-16 11:53:08 +10:00
Dion Moult 71f0170966 IDS webapp with some basic read/write functionality 2022-07-15 15:58:39 +10:00
Dion Moult 97cd07226d Support JSON output for IDS results 2022-07-15 15:14:04 +10:00
Dion Moult 309ce20068 Added error sentences for IDS property facet results 2022-07-15 14:38:57 +10:00
Dion Moult 282c140c14 More aggressive refactoring for IfcTester to break up the monolithic codebase 2022-07-15 14:09:55 +10:00
Bruno Perdigão bdf46f9431 Update installation.rst
Minor correction
2022-07-15 12:42:19 +10:00
Dion Moult e79068854a Refactor IDS into IfcTester with less magic and less code
* String building code all deleted and replaced with more maintainable code
 * Obscure features like metaclasses deleted
 * Regular constructors used instead of factory create and parse methods
 * Use Python class naming convention
 * Use polymorphism more (e.g. asdict, init) for facet classes
 * No more boolean any/and as users may want to test everything and not fail early
 * Error state is stored instead of streamed to logger so it can be tested and more flexible formatting
2022-07-15 09:57:40 +10:00
carlos 7be93b5386 Add testing to construction type previews 2022-07-15 01:09:08 +02:00
carlos 1b4da58a15 Restore IFCFileSelector UI rendering 2022-07-14 13:59:47 +02:00
Carlos Villagrasa 356ce48af5 Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0 2022-07-14 13:42:02 +02:00
carlos 3f24d54e8f Distinguish bt active and selected constr type + renaming ifc_class -> constr_type 2022-07-14 13:40:57 +02:00
Vukas Pajic 1586c85314 added option to import all library assets 2022-07-11 11:07:40 +02:00
carlos f9f9746a9a Element Type Browser UI refactoring and improved wording 2022-07-11 07:53:14 +02:00
carlos 493faf22ff Move col_with_margins utility to helper.py 2022-07-11 07:52:10 +02:00
Thomas Krijnen bab6313313 Work on more readable ast for express schema parser 2022-07-10 21:51:25 +02:00
carlos 618bed3889 Improved version with optional cascading 2022-07-10 07:46:30 +02:00
carlos 8c9115c0ff Merge remote-tracking branch 'origin/v0.7.0' into v0.7.0 2022-07-09 01:22:44 +02:00
Carlos Villagrasa 0a561d1a30 Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0 2022-07-09 01:19:15 +02:00
dependabot[bot] 443af3f7e7 Bump lxml from 4.6.5 to 4.9.1 in /src/ifcopenshell-python/ifcopenshell
Bumps [lxml](https://github.com/lxml/lxml) from 4.6.5 to 4.9.1.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.6.5...lxml-4.9.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-07 09:29:42 +02:00
Thomas Krijnen 2f6ac0be9f Fix test_wall_opening invocation 2022-07-07 08:46:38 +02:00
Thomas Krijnen 07c152f513 Submodule 2022-07-06 22:09:13 +02:00
Thomas Krijnen 158c62bdca Update ci-py-only.yml 2022-07-06 22:01:15 +02:00
Thomas Krijnen 1029271e91 Update ci-py-only.yml 2022-07-06 18:20:32 +02:00
Thomas Krijnen 9b59d24fc7 Update test_wall_opening.py 2022-07-06 18:20:25 +02:00
Thomas Krijnen 1d0e8c59d2 Capital OR 2022-07-06 16:19:57 +02:00
Thomas Krijnen b479d82251 CI workflow with BUILD_CONVERT=Off 2022-07-06 16:09:52 +02:00
Thomas Krijnen 6b6ac38f96 Link serializers also with only BUILD_IFCPYTHON 2022-07-06 16:06:16 +02:00
Vukas Pajic 57fe895ae0 ensure modules are installed to blender python
thankyou to @Gorgious56 for help on this one ;)  https://blender.stackexchange.com/a/238995/130742
2022-07-06 13:56:24 +02:00
Gorgious56 4195d41e4c Enums from attributes can now be searched by name 2022-07-06 12:09:37 +02:00
Dion Moult 7eac5397cd Run black 2022-07-06 18:29:22 +10:00
Dion Moult 084823164e Fix #2264 refactor property data structures to be separate from attributes 2022-07-06 18:25:29 +10:00
Gorgious56 87f110bae5 Add search operator to material enum field 2022-07-05 15:24:10 +02:00
Thomas Krijnen 952719f19b #2260 correct init order 2022-07-05 10:27:44 +02:00
Thomas Krijnen 2ab4e44248 #2256 try explicit cast for clang 2022-07-05 09:46:19 +02:00
Vukas Pajic 6b9e670bf3 missing comma 2022-07-05 07:41:51 +02:00
Vukas Pajic 9e87f0f027 fix for issue #2261 2022-07-05 07:34:23 +02:00
Gorgious56 c7272065e6 Improve prop search v2 (#2262)
* Minor fixes

* Fix #2257 : enums with a different id and name now correctly show the name in the search field

* Add prop search to template_type
2022-07-04 23:09:03 +02:00
carlos 14bca5d199 Merge remote-tracking branch 'origin/master' into v0.7.0 2022-07-04 18:52:11 +02:00
Carlos Villagrasa dbeb7d392c Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0 2022-07-04 18:50:53 +02:00
Vukas Pajic 22cb7b3b67 Merge pull request #2258 from vulevukusej/create-pset_templates-in-bbim
support enums in new pset_templates
2022-07-04 14:20:14 +02:00
Vukas Pajic 018be92063 support enums in new pset_templates 2022-07-04 13:48:51 +02:00
Gorgious56 37f91461d2 Add prop search to fields with a lot of enumerated values (#2253)
* Add prop_search layout function

* Display props operator

* display search for pset

* Display search for root

* Update operator and UI layout function

* Add search buttons to brick, geometry, patch, project, pset, pset_template, root and unit modules enums
2022-07-03 22:37:41 +02:00
Vukas Pajic c73fedaaaf fix error that stopped user from adding properties 2022-07-01 13:41:20 +02:00
Gorgious56 4ad4bdef3b Initial Commit 2022-06-30 09:44:00 +10:00
Vukas Pajic e50259f8fa fix error I accidentally introduced. 2022-06-29 18:07:32 +02:00
carlos 7321138d57 Adding a preview popup for IFC Type additions 2022-06-29 17:43:28 +02:00
carlos 319f63c861 Preventing error when IfcSpatialElement has been deleted and an IFC Type instance is added 2022-06-29 17:39:40 +02:00
carlos 662cd6232c Preventing edge case (vertical slabs & horizontal walls) 2022-06-29 17:37:42 +02:00
Vukas Pajic 3461a1e96f Submodule_mvd 2022-06-29 17:23:27 +10:00
Vukas Pajic 919c007509 Submodule 2022-06-29 17:23:27 +10:00
Vukas Pajic 5b77001568 updated according to comments 2022-06-29 17:23:27 +10:00
Vukas Pajic 1f4022a41b fix error from util/element.py 2022-06-29 17:23:27 +10:00
Vukas Pajic c8c9262090 initial commit
-Added support for adding and editing enums
-changed behaviour of template psets that have enums.
2022-06-29 17:23:27 +10:00
Thomas Krijnen 104cabfb67 Minor things for drawing profiling and a warning 2022-06-29 11:35:43 +10:00
Kristoffer Andersen 4978a67e19 Add more subdirs for blenderbim build trigger
Add more subdirectories to the github actions workflow to trigger builds whenever a change occurs in relevant directories.
2022-06-29 11:33:44 +10:00
dependabot[bot] e44e03f02e Bump numpy in /src/ifcopenshell-python/ifcopenshell
Bumps [numpy](https://github.com/numpy/numpy) from 1.21.0 to 1.22.0.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/HOWTO_RELEASE.rst)
- [Commits](https://github.com/numpy/numpy/compare/v1.21.0...v1.22.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-22 09:21:40 +02:00
Thomas Krijnen 5a636a925b #2244 file.add() Don't fail on project without unit info 2022-06-21 14:15:59 +02:00
Dion Moult d23f14f8aa Cleaner IFC doc generation showing all contextual entities not only those related to the target instance 2022-06-21 22:05:26 +10:00
Dion Moult 1706ca5293 Fix bug where adding heavy classification references led to duplicate classification systems 2022-06-21 19:28:53 +10:00
Dion Moult ff3e2f14e1 Inherited and overridden classification references per system is now supported 2022-06-21 19:28:29 +10:00
Dion Moult c4433489b7 IDS now supports type casting 2022-06-21 17:01:12 +10:00
Dion Moult 4761c58571 IDS now supports failing inverse checks 2022-06-21 17:00:46 +10:00
Dion Moult 1de6a7e792 Improve geolocation docs with rotation diagram and scale explanation 2022-06-19 17:53:37 +10:00
Thomas Krijnen 7d6ba89496 Fixes after restructuring 2022-06-15 15:21:46 +02:00
Thomas Krijnen c4dc3934fa Add missing file 2022-06-15 14:46:37 +02:00
Thomas Krijnen 25f1d21a85 Fixes for some of the header renames 2022-06-15 14:23:58 +02:00
Thomas Krijnen 2bd6d9343c Fixes for some of the header renames 2022-06-15 13:56:47 +02:00
Thomas Krijnen bc2d76e685 Include-what-you-use 2022-06-15 13:41:13 +02:00
Thomas Krijnen 124f786007 Update Kernel.cpp 2022-06-14 17:18:44 +02:00
Thomas Krijnen b49cfe9744 msvc /mp 2022-06-14 15:25:14 +02:00
Thomas Krijnen 6f5f189443 Missing files 2022-06-14 15:24:38 +02:00
Thomas Krijnen d6f8448c83 IfcGeom refactoring pt. 1 2022-06-14 15:02:54 +02:00
Thomas Krijnen 0f85890c72 #2237 workaround for openings on extrusions of compound profiles 2022-06-11 15:48:04 +02:00
Thomas Krijnen bf3eb68b67 template template template template template 2022-06-10 11:20:01 +02:00
Dion Moult ce3a6bba0f Fix bug where MS Project files with optional weekdays failed to import 2022-06-10 19:07:58 +10:00
Thomas Krijnen f1e3d9c573 typename typename typename typename typename typename 2022-06-10 11:07:40 +02:00
Thomas Krijnen 0a30d1a438 #2213 Read precision from rep context in create_shape() 2022-06-10 11:00:45 +02:00
Dion Moult 8311983d5e You can now toggle visibility and wireframe modes of linked models and fix bug where links with the same name were not unique 2022-06-09 21:48:13 +10:00
Dion Moult 97bf4e3d0d Fix bug where object class selection didn't work with linked models 2022-06-09 21:46:24 +10:00
Dion Moult 15661536dc Extract elements recipe now handles spatial elements as an upgrade to the ExtractSpaces recipe 2022-06-09 19:01:32 +10:00
Dion Moult f451c51451 Fix bug where some appended assets created invalid graphs due to poor purging 2022-06-09 18:59:20 +10:00
Dion Moult b5e27e4ced Fix bug where transaction history was polluted when starting a new file in an existing Blender session 2022-06-09 15:30:18 +10:00
Dion Moult 95881c6258 Fix embarrassing geolocation bug for showing / setting grid north 2022-06-09 10:22:02 +10:00
Thomas Krijnen cb8cfd7905 Update CMakeLists.txt 2022-06-08 13:59:29 +02:00
Dmitry Sedov 96fe1018ed implemented the minimal build version 2022-06-08 13:46:18 +02:00
Thomas Krijnen 73c006a977 Use OCCT github mirror for reliability 2022-06-08 13:37:48 +02:00
Thomas Krijnen 133fa5547b Use OCCT github mirror for reliability 2022-06-08 13:37:06 +02:00
Dion Moult c95d263ca5 #2224 Write documentation for dealing with large models 2022-06-08 19:10:20 +10:00
Dion Moult 3507920c09 #2224 Expose unload project in UI to aid in unlinking IFCs for coordination only 2022-06-08 19:09:55 +10:00
Dion Moult cdabc507e1 #2224 New headless import script 2022-06-08 14:09:02 +10:00
Dion Moult bcbee0659b #2224 Coordination mode now allows interspersed (much faster) object merging during model loading to crunch down large models, with a new by type merge mode 2022-06-08 12:10:01 +10:00
Dion Moult 053e4e1185 #2224 Enabling coordination mode now excludes types and openings 2022-06-08 12:06:59 +10:00
Dion Moult d76679a45f #2224 Cache seems unstable for some large models so allow option to disable 2022-06-08 11:56:22 +10:00
Dion Moult a1ed78e755 #2224 You can now specify element offset / limit ranges when loading models 2022-06-08 11:53:58 +10:00
Dion Moult d0f38bc919 #2224 You can now filter models by type when loading 2022-06-08 11:32:00 +10:00
Dion Moult 4681d3198f #841 #2224 Users can now enable native mesh processing with support for FBSM geometry 2022-06-08 10:19:55 +10:00
Dion Moult 96e4641ebf #2224 Optimise import by 10-40% by removing unnecessary slow traversal of representations 2022-06-08 09:14:36 +10:00
Dion Moult 2ab808a216 #2224 You can now bulk check/uncheck filter categories on import 2022-06-08 09:09:35 +10:00
Dion Moult 1ec0e1ce19 Fix bug with displaying inherited psets by accident. 2022-06-08 09:07:22 +10:00
bimworld bc734c263e Comment deconstructor IfcFile 2022-06-07 12:14:30 +02:00
Thomas Krijnen a96bffa341 Update schema.py 2022-06-06 10:31:33 +02:00
Thomas Krijnen fb3e9ad1b7 Quality IteratorSettings name in wrapper gen 2022-06-06 10:13:48 +02:00
Thomas Krijnen 4c854fbb62 Try to do less allocation during parse, favour upper case entity names in spf 2022-06-06 09:59:32 +02:00
Thomas Krijnen 0e8d7bdc97 Log result of unify() in boolean_operation() 2022-06-06 09:58:57 +02:00
Thomas Krijnen a92d37cd64 Quick fix py create_shape() setting propagation 2022-06-06 09:58:42 +02:00
Dion Moult 00e46ce39e More meaningful percentage completion in console when loading models 2022-06-06 09:59:43 +10:00
Esteban Dugueperoux 54b82fd149 Update README travis badge URL 2022-06-03 09:24:22 +02:00
Esteban Dugueperoux e508fb44f2 Update CMake release requirement 2022-06-02 13:33:06 +02:00
Dion Moult 89e2ad9262 Write documentation for georeferencing 2022-06-02 19:01:09 +10:00
Dion Moult 419901649e You can now configure the distance limit for false origin offsets 2022-06-01 14:25:30 +10:00
Dion Moult 18cb767a95 More IDS test structuring and add support for checking logical unknown 2022-05-31 22:27:37 +10:00
Thomas Krijnen a11b51c971 Revert "Temporarily disable release tag guard"
This reverts commit c6b86e45f3.
2022-05-31 11:30:25 +02:00
Thomas Krijnen d7f913d464 #2218 Decreate (hardcode) angular tolerance on boolean op extrusion detection 2022-05-31 11:24:27 +02:00
Dion Moult 0d982090e3 More descriptive test names and slightly more concise IDS doc layout 2022-05-31 19:13:39 +10:00
Thomas Krijnen be6617d9f9 Update release.yml 2022-05-31 10:31:56 +02:00
Thomas Krijnen fc0efa2339 Tweak release.yml configure opts 2022-05-31 10:21:45 +02:00
Thomas Krijnen c6b86e45f3 Temporarily disable release tag guard 2022-05-31 09:55:40 +02:00
Thomas Krijnen 53b6247bb8 Unify release.yml and ci.yml deps 2022-05-31 09:54:10 +02:00
Thomas Krijnen eab06bda6c Submodule 2022-05-31 09:43:53 +02:00
Thomas Krijnen 524fa58f05 Remove erroneous .gitmodule entry 2022-05-31 09:39:31 +02:00
Dion Moult 4c4846bc8f Support userdefined process types in util module 2022-05-31 16:23:13 +10:00
Dion Moult 0dfe1d995f Update IDS to support process types and require uppercase entities 2022-05-31 16:22:44 +10:00
Dion Moult a4434413d2 Fix bug where rooted tasks with no subactivities would not export correctly to P6 2022-05-31 15:36:13 +10:00
Dion Moult 5ca19ce2f7 WIP update to support current IDS v0.6 2022-05-31 15:35:47 +10:00
Dion Moult 3e1c2c4daf Code review and tests for georeferencing module refactor 2022-05-29 23:44:18 +10:00
Dion Moult 2d4e316df2 Fix bug where unit editing failed for named units 2022-05-29 17:25:28 +10:00
maxfb87 4b426e1fc0 Refactor Georeference Module 2022-05-29 15:54:06 +10:00
Dion Moult d2ad4b2b68 Allow rooted tasks when exporting schedules to P6XML 2022-05-29 11:15:37 +10:00
Dion Moult 67522e03d1 Fix import error bugs when importing P6XML to enterprise environments 2022-05-27 17:17:31 +10:00
AToradaIBIM 24f307130c BCFv3 "get_comments" error #2212
Fix for Issue #2212 

The method get_comments wasn't working as expected because on line 649, it was trying to access the element "Topics" instead of "Topic", based on the XSD "Topics" doesn't exist.
2022-05-25 11:19:53 +02:00
Dion Moult 0a3e5f65fa Fix multiple specifications in IDS 2022-05-25 17:49:12 +10:00
Dion Moult e302fea33b Resolve merge conflict. Still WIP IDS test cases. 2022-05-22 22:10:28 +10:00
Dion Moult c352181f8c Fix bug where underlays didn't work 2022-05-22 21:32:57 +10:00
Thomas Krijnen 3a1416536a Oops 2022-05-17 22:31:37 +02:00
Thomas Krijnen 4eec863cad Example for turning IDS test cases into spec. Sorry for butchering your code @Moult 2022-05-17 22:21:11 +02:00
Thomas Krijnen 96bcd31d99 #2202 Check for nested compound of solid in boolean result handling 2022-05-16 14:24:57 +02:00
Thomas Krijnen b09ff828de #2186 rerun codegen 2022-05-16 13:07:40 +02:00
Thomas Krijnen 3cb68ca388 #2186 optional attribute setter fix 2022-05-16 13:07:40 +02:00
Dion Moult c95d3693ea OpenSourceBIM contributors badge now in README 2022-05-16 11:35:39 +10:00
Dion Moult 67d2ab2577 Write documentation on materials, locations, and measurements 2022-05-16 08:15:31 +10:00
Dion Moult 970b24b63b #1153 New annotation objects have their placement Z in the drawing direction to allow for 2D geometry. 2022-05-15 11:45:47 +10:00
Dion Moult fdaae048c2 Implement IDS partOf facet 2022-05-12 14:33:44 +10:00
Dion Moult 4da5a38237 Implement IDS material null checks, inheritance checks, and set item checks 2022-05-12 12:06:23 +10:00
Dion Moult 0cf2d7e05e Implement IDS uri/use/instructions for material facet 2022-05-11 23:30:10 +10:00
Dion Moult 3122cf21db Implement IDS property facet measure conversions 2022-05-11 19:59:10 +10:00
Dion Moult 4bf25d5d9c Minor fix 2022-05-11 18:46:27 +10:00
Dion Moult dc10c7a360 Implement null check for IDS properties, and support restrictions 2022-05-11 17:10:22 +10:00
Dion Moult 94e082fc7d Support IDS uri/use/instructions for property facets 2022-05-11 15:30:39 +10:00
Dion Moult 9671df8491 You can now choose between lightweight and full classifications 2022-05-11 15:19:31 +10:00
Dion Moult 8aa38917eb New classification utility module 2022-05-11 15:19:17 +10:00
Dion Moult 0774100606 IDS classification facets not handle inheritance, full refs, and restrictions 2022-05-11 14:49:47 +10:00
Dion Moult 95b48b02e4 Add support for IDS classification uri, use, and instructions 2022-05-11 13:27:19 +10:00
Dion Moult c5de1d8727 Add IDS support for restrictions in attribute facets and handle inheritance 2022-05-11 13:06:15 +10:00
Dion Moult 7e09ab0ced Add support for IDS attribute use/instructions 2022-05-11 10:12:36 +10:00
Dion Moult eb3e137a6e #2196 Get psets now handleds inheritance by default 2022-05-11 09:18:23 +10:00
Dion Moult f528ad9744 Minor fix 2022-05-10 23:49:14 +10:00
Dion Moult 650f9eb7f5 Attribute filtering in IDS now checks null-like values too. 2022-05-10 22:42:21 +10:00
Dion Moult a78d6f42e5 Allow IDS to audit any entity, not just IfcObjects 2022-05-10 22:13:28 +10:00
Dion Moult 4326a74c64 Fix IDS to not allow subclasses and allow restrictions in the class name 2022-05-10 21:40:35 +10:00
Dion Moult 08fc62d4d9 Add support for userdefined predefined types in IDS entity facets 2022-05-10 21:20:10 +10:00
Dion Moult ae298de8dc Implement identifier, desc, and instructions for specification node in IDS 2022-05-10 19:26:12 +10:00
Dion Moult a24c4b269a Fix mandatory title and optional date and check invalid dates in IDS 2022-05-10 17:36:24 +10:00
Dion Moult 083d9d3e3e Implement attribute facet for IDS 2022-05-10 16:23:07 +10:00
Dion Moult 12e7ed356c Fix IDS namespace bug that prevented XML encoding, simplify XML writing, and new to_string() method to decrease dependency on the filesystem 2022-05-10 16:22:38 +10:00
Dion Moult 3f15dd71b1 Run black on ids module 2022-05-10 16:18:08 +10:00
Adrien SCHVALBERG 42448cae4e Add missing exports for ifcopenshell-python when we build shared libs. 2022-05-09 14:13:47 +02:00
Dion Moult 7e28d193ec #2185 Document installation and hello world examples for IfcOpenShell-python 2022-05-09 19:27:33 +10:00
Thomas Krijnen 8f2b32532c validate.py text result formatting improvements 2022-05-09 11:00:04 +02:00
Dion Moult f0a00cbc94 #2185 Set up auto generated API reference documentation via sphinx-autoapi 2022-05-09 15:35:52 +10:00
Dion Moult fd73b2cf0a #2185 Migrate web files to website repo 2022-05-08 19:51:17 +10:00
Adrien SCHVALBERG e5b3455533 Fix memory leak for HeaderEntity instances:
the getArgumentCount() call in IfcEntityInstanceData destructor isn't virtually dispatched so it returns 0 instead of correct number of arguments => memory leak
added IfcEntityInstanceData::clearArguments() which clears all arguments and resets the attributes_ field to nullptr

Remove unnecessary copy in IfcEntityInstanceData::setArgument() when argument is created at the call site (no more leak of original argument)

Fix memory leak in IfcFile: delete entities from both byid and entity_file_map
2022-05-08 10:21:46 +02:00
Adrien SCHVALBERG e677355438 Add IfcParse::clear_schemas() and individual Ifc___::clear_schema() to clear any resource allocated by registered schemas
Add missing virtual destructors in IfcSchema.h
2022-05-08 10:21:46 +02:00
Dion Moult 93fd3f97e3 #2111 New feature to select by style and see style usage 2022-05-08 18:03:18 +10:00
Dion Moult 910d189f35 #2188 support displaying inherited materials 2022-05-08 16:27:35 +10:00
Dion Moult c8d6d12442 The material manager now groups materials into categories 2022-05-08 15:48:09 +10:00
Dion Moult 1f3ba3d09d New makefile targets to specifically run tests of single modules 2022-05-08 15:45:47 +10:00
Dion Moult f67941221e Enable syntax highlighting in test suite for easier debugging 2022-05-08 15:24:06 +10:00
Dion Moult 8252ef82ad #2185 Write documentation about viewing construction types 2022-05-07 21:50:38 +10:00
Dion Moult 3b825e35ae #1153 You can now provide custom stylesheets, markers, symbols, and hatches 2022-05-07 20:28:24 +10:00
Dion Moult 4a4912e817 Editing URIReference properties now provides a file browse button 2022-05-07 20:26:25 +10:00
Dion Moult 5a38604ec2 #1153 Boilerplate styles and definitions now can be used with any drawing layer combination 2022-05-07 17:57:05 +10:00
Dion Moult b15e0d0e2b #2185 Commit logos and homepage mockup by Paulina Godoy and Meetlat 2022-05-07 11:48:20 +10:00
Dion Moult 5a5f52a5c2 #2185 Standardise graphic style for annotation images in documentation 2022-05-07 11:42:12 +10:00
Dion Moult 816a690519 Commit WIP website design by Paulina Godoy :) 2022-05-07 10:20:11 +10:00
Dion Moult 60303c6560 Minor fix 2022-05-06 20:51:36 +10:00
Dion Moult cc88e6f112 #2185 Write documentation about viewing attributes, pets, and qtos 2022-05-06 20:44:26 +10:00
Dion Moult 5f1b5d0c63 You can now manage actor assignments to products 2022-05-06 17:47:24 +10:00
Dion Moult b54caa8967 Person / organisation roles and acting actors are now shown 2022-05-06 16:21:12 +10:00
Dion Moult 73e048a9a8 #1153 You can now manage assigned products for all annotations 2022-05-06 15:35:39 +10:00
Dion Moult 316a2b7f53 #2185 Write documentation for viewing element classes 2022-05-06 12:46:29 +10:00
Dion Moult 42edcda8bd Deleting an element now also auto deletes its associated distribution ports 2022-05-06 10:38:38 +10:00
Dion Moult 92432f453d You can now remove ports 2022-05-06 10:35:12 +10:00
Dion Moult 4b6ebe7b1c Fix bug where deleted objects were not cleared after saving 2022-05-06 09:20:37 +10:00
Emad Aghajani 3f6d25ed43 Fixes #2175: Fixes a crash when blender is in background mode or is used as bpy Python module 2022-05-05 19:23:36 +10:00
Dion Moult d4b2cc7f2c Bump IfcOpenShell build 2022-05-05 18:00:58 +10:00
Dion Moult 53c050c6ed Port locations now sync relative to their parent even when not actively displayed in the viewport 2022-05-05 18:00:42 +10:00
Dion Moult 3213af2f48 #2185 Write documentation on the object outliner panel 2022-05-05 16:29:40 +10:00
Dion Moult 85122ba184 #2185 Document how to navigate through a model 2022-05-05 14:43:34 +10:00
Dion Moult b8791d43aa Minor documentation update for loading an IFC model 2022-05-05 12:19:40 +10:00
Dion Moult 6a82f27d48 Standalone Blender import script now available for debugging 2022-05-05 09:59:04 +10:00
Dion Moult 4e2400952a Fix #2147 #2143 Fix hanging when reloading a cache of a file with 2D and 3D data 2022-05-05 09:58:10 +10:00
Dion Moult de1dae8b6e Grids are now locked by default when loading IFCs 2022-05-05 09:28:36 +10:00
Dion Moult dd127db969 Minor documentation improvements 2022-05-04 16:04:18 +10:00
Dion Moult 194bc53bdb Minor refactor 2022-05-04 15:54:15 +10:00
Dion Moult 8c3e29c351 Split BlenderBIM and other docs to not overwhelm users. Use Furo theme for Sphinx. 2022-05-04 13:06:39 +10:00
Gorgious56 6e55a04550 Refactor Standalone section shader (#2172)
* Refactor Standalone section shader

* Remove leftover code
2022-05-03 18:21:51 +02:00
Dion Moult 9333839267 Copying an element now also copies its distribution ports 2022-05-03 18:05:27 +10:00
Dion Moult 280236290e Fix #2182. File export IFC menu item now defaults to save as to improve usability for new users 2022-05-03 17:36:55 +10:00
Dion Moult c5773b5665 Port locations are now auto synchronised when hiding ports 2022-05-03 16:50:55 +10:00
Dion Moult 96babc4506 Creating a flow segment now also creates 2 ports at either end 2022-05-03 15:48:41 +10:00
Dion Moult f8acde0801 You can now add ports to types, and ports are created when placing occurrences of that type 2022-05-03 14:32:18 +10:00
Dion Moult 32c101cf74 #1153 Implement angular dimension annotations 2022-05-02 09:20:40 +10:00
Bruno Perdigão 2a1aa535ba Added VSCode extension to Live Development section 2022-05-02 08:06:27 +10:00
Dion Moult 02903c4929 #1153 New arc from 3 points command for 2D CAD 2022-05-01 17:25:03 +10:00
Dion Moult d80e26e4ab Design new icon for CAD workspace tool 2022-05-01 15:30:37 +10:00
Dion Moult a88b83306e #1153 New arc from 2 points command for 2D CAD (based on view direction) 2022-05-01 14:43:03 +10:00
jakob-beetz 89023df4e5 Update readme.md 2022-01-07 10:37:40 +01:00
admin f794b647e1 merge v0.6.0 2021-12-06 09:59:18 +08:00
admin 2e81b2bd17 merage v0.6
ADD IFCGroup parse #1154
2021-12-02 10:19:47 +08:00
admin 8527d0fcbd merage new 0.6 2021-12-01 10:32:20 +08:00
admin bc32481185 merge 2021-07-05 10:30:06 +08:00
admin 58e9f1baf9 merage 2021-05-10 14:06:18 +08:00
admin db5432d902 merge master v0.6.0 2021-05-06 09:54:33 +08:00
admin 397b6ae5fe merage master v6.0.0 2021-04-12 10:26:10 +08:00
admin 72fdca9e82 merage 2021-04-01 16:41:28 +08:00
admin 29b8e67bc8 merge all 2021-03-25 10:04:25 +08:00
admin 33ec80e54d merge update 2021-01-28 13:56:47 +08:00
admin 371e4137f1 update mager 2021-01-21 13:43:19 +08:00
admin 555a2ce79e merge master v6.0 2021-01-11 11:39:36 +08:00
admin 699742880e fix IFC to xml - IFCGROUP in IFC cannot be found in xml #1154 2021-01-11 10:59:03 +08:00
GisSpace d99348f163 Merge pull request #3 from IfcOpenShell/v0.6.0
merge master
2020-12-31 15:44:04 +08:00
GisSpace d7034c6a40 Merge pull request #2 from IfcOpenShell/v0.6.0
merge
2020-12-21 11:13:59 +08:00
GisSpace ac632d0698 Merge pull request #1 from IfcOpenShell/v0.6.0
同步源库
2020-12-18 16:23:04 +08:00
admin f3696788cb #1097
fix:IFC file conversion obj file, some cases will be rotated 90 °
2020-12-10 18:44:23 +08:00
802 changed files with 144850 additions and 24918 deletions
+100
View File
@@ -0,0 +1,100 @@
name: Publish-blenderbim-chocolatey package
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "55 23 * * *" # 5min before utc midnight
env:
major: 0
minor: 0
name: blenderbim
choco_version: 1.1.0
CHOCO_TOKEN: ${{ secrets.CHOCO_TOKEN }}
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
pyver: [py310]
config:
- {
name: "Windows Build",
short_name: win,
}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.7.7' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- run: echo ${{ env.DATE }}
- name: Check in published releases if we should do a choco release
id: do_choco
run: |
echo "::set-output name=choco_release::$(python3 /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/check_repo_infos.py --do_choco_release?)"
- name: Compile
if: ${{steps.do_choco.outputs.choco_release}} == 'do_choco_release'
run: |
target_os=${{ matrix.config.short_name }} &&
pyver=$(python3 /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/check_repo_infos.py --pyver?) &&
cp -r src/blenderbim src/blenderbim_$target_os_$pyver &&
cd src/blenderbim_$target_os_$pyver &&
make dist PLATFORM=$target_os PYVERSION=$pyver
- name: Fill chocolatey scripts on win with latest blender python
if: ${{steps.do_choco.outputs.choco_release}} == 'do_choco_release'
run: |
today_non_iso="$(date +'%y%m%d' | tr -d '\n')" &&
target_os=${{ matrix.config.short_name }} &&
latest_blender_python_version_maj_min=$(python3 /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/check_repo_infos.py --latest_blender_python_version_maj_min?) &&
echo "latest_blender_python_version_maj_min?: $latest_blender_python_version_maj_min" &&
pyver=$(python3 /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/check_repo_infos.py --pyver?) &&
export latest_blender_version_maj_min=$(python3 /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/check_repo_infos.py --latest_blender_release_maj_min?) &&
export latest_blender_version_maj_min_pat=$(python3 /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/check_repo_infos.py --latest_blender_release_maj_min_pat?) &&
echo latest_blender_version_maj_min_pat?: $latest_blender_version_maj_min_pat &&
export blenderbim_build_version="${{ env.major }}.${{ env.minor }}.$today_non_iso" &&
export url_blenderbim_py310_win_zip="https://github.com/IfcOpenShell/IfcOpenShell/releases/download/blenderbim-$today_non_iso/blenderbim-$today_non_iso-$pyver-$target_os.zip" &&
export sha256sum_blenderbim_py310_win_zip=$( sha256sum src/blenderbim_$target_os_$pyver/dist/blenderbim-$today_non_iso-$pyver-$target_os.zip --tag | cut -d ' ' -f 4 | tr -d '\n') &&
echo sha256sum_blenderbim_py310_win_zip: $sha256sum_blenderbim_py310_win_zip &&
python3 choco/blenderbim/fill_dynamic_parameters.py &&
echo __build choco with mono &&
wget "https://github.com/chocolatey/choco/archive/refs/tags/$choco_version.tar.gz" --quiet &&
tar -xzf "$choco_version.tar.gz" &&
cd choco-$choco_version &&
chmod +x build.sh zip.sh &&
./build.sh &&
cp -r build_output/chocolatey /opt/chocolatey &&
cd /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/ &&
echo __choco pack &&
mono /opt/chocolatey/choco.exe --version &&
mono /opt/chocolatey/choco.exe pack --allow-unofficial &&
echo __choco set apiKey &&
mono /opt/chocolatey/choco.exe setapikey --key="$CHOCO_TOKEN" --source="https://push.chocolatey.org/" --allow-unofficial # &&
echo __choco push &&
mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose
- name: Inform user about not packaging
if: ${{steps.do_choco.outputs.choco_release}} != 'do_choco_release'
run: |
echo "no releases found today ${{ env.DATE }}, therefore choco packaging is skipped."
+15 -1
View File
@@ -3,8 +3,18 @@ name: Publish-blenderbim-multiplatform
on:
push:
paths:
- 'src/blenderbim/**'
- '.github/workflows/ci-blenderbim-matrix.yml'
- 'src/blenderbim/**'
- 'src/ifcopenshell-python/ifcopenshell/**'
- 'src/bcf/src/bcf/**'
- 'src/ifcclash/ifcclash/**'
- 'src/ifccobie/**'
- 'src/ifcdiff/**'
- 'src/ifccsv/**'
- 'src/ifcpatch/ifcpatch/**'
- 'src/ifc4d/ifc4d/**'
- 'src/ifc5d/ifc5d/**'
- 'src/ifccityjson/**'
branches:
- v0.7.0
@@ -43,6 +53,10 @@ jobs:
name: "MacOS Build",
short_name: macos
}
- {
name: "MacOS ARM Build",
short_name: macosm1
}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
@@ -0,0 +1,64 @@
name: ci-ifcopenshell-conda-daily
on:
workflow_run:
workflows: ["ci"]
types:
- completed
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
test:
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}
needs: activate
runs-on: ${{ matrix.platform.distver }}
defaults:
run:
shell: bash -l {0}
strategy:
fail-fast: false
matrix:
pyver: [
{ name: py39, distver: '3.9' },
{ name: py310, distver: '3.10'}
]
platform: [
{ name: Windows, distver: windows-2022, upload: 'false' },
{ name: Windows, distver: windows-2019, upload: 'true' },
{ name: Linux, distver: ubuntu-20.04, upload: 'false' },
{ name: Linux, distver: ubuntu-18.04, upload: 'true' },
{ name: macOS, distver: macos-11, upload: 'true' },
{ name: macOS, distver: macos-10.15, upload: 'false' }
]
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Download MacOSX SDK
if: ${{ matrix.platform.name == 'macOS' }}
run: |
curl -o MacOSX10.13.sdk.tar.xz -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz && \
tar xf MacOSX10.13.sdk.tar.xz && \
sudo mv -v MacOSX10.13.sdk /opt/ && \
ls /opt/
- uses: seanmiddleditch/gha-setup-ninja@master
- uses: conda-incubator/setup-miniconda@v2 # https://github.com/conda-incubator/setup-miniconda
with:
activate-environment: conda-build
python-version: ${{ matrix.pyver.distver }}
environment-file: conda/environment.yml
- name: build, test and upload ifcopenshell
if: ${{ matrix.platform.upload == 'true' }}
run: |
conda-build . --python ${{ matrix.pyver.distver }} -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user ifcopenshell --no-remove-work-dir
- name: build & test ifcopenshell
if: ${{ matrix.platform.upload == 'false' }}
run: |
conda-build . --python ${{ matrix.pyver.distver }} -c conda-forge --no-remove-work-dir
@@ -0,0 +1,73 @@
name: ci-ifcopenshell-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "51 23 1 * *" # 9min before utc midnight
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
pyver: [py36, py37, py38, py39, py310]
config:
- {
name: "Windows Build",
short_name: win,
}
- {
name: "Linux Build",
short_name: linux
}
- {
name: "MacOS Build",
short_name: macos
}
- {
name: "MacOS ARM Build",
short_name: macosm1
}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.7.7' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- run: echo ${{ env.DATE }}
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%y%m%d')"
- name: Compile
run: |
pip install build
cp -r src/ifcopenshell-python src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
cd src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }}
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }}/dist
+95
View File
@@ -0,0 +1,95 @@
name: ci_py_only
on:
push:
paths:
- 'src/**'
- 'test/**'
- 'conda/**'
- 'cmake/**'
- '.github/workflows/ci_py_only.yml'
pull_request:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell' &&
!contains(github.event.head_commit.message, 'skip ci')
steps:
- run: echo ok go
build:
runs-on: ubuntu-20.04
needs: activate
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Install C++ dependencies
run: |
sudo apt update
sudo apt-get install --no-install-recommends \
git cmake gcc g++ \
libboost-date-time-dev \
libboost-filesystem-dev \
libboost-iostreams-dev \
libboost-program-options-dev \
libboost-regex-dev \
libboost-system-dev \
libboost-thread-dev \
python3-all-dev python3-pip \
swig libpcre3-dev libxml2-dev \
libtbb-dev nlohmann-json3-dev \
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
libhdf5-dev libcgal-dev
- name: ccache
uses: hendrikmuhs/ccache-action@v1
- name: Build ifcopenshell
run: |
mkdir build && cd build
cmake \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/usr \
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
-DOCC_INCLUDE_DIR=/usr/include/opencascade \
-DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
-DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \
-DCOLLADA_SUPPORT=Off \
"-DSCHEMA_VERSIONS=2x3;4" \
-DBUILD_CONVERT=Off \
-DGLTF_SUPPORT=On \
-DJSON_INCLUDE_DIR=/usr/include \
-DCGAL_INCLUDE_DIR=/usr/include \
-DGMP_INCLUDE_DIR=/usr/include \
-DMPFR_INCLUDE_DIR=/usr/include \
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DHDF5_INCLUDE_DIR=/usr/include/hdf5/serial \
../cmake
sudo make -j $(nproc)
sudo make install
- name: Install Python dependencies
run: |
sudo /usr/bin/python -m pip install -U pip
sudo /usr/bin/python -m pip install xmlschema numpy lxml
sudo /usr/bin/python -m pip install src/bcf
sudo /usr/bin/python -m pip install pytest
sudo /usr/bin/python -m pip install isodate
sudo /usr/bin/python -m pip install lark
sudo /usr/bin/python -m pip install networkx
- name: Test
run: |
cd test
sudo /usr/bin/python tests.py
cd ../src/ifcopenshell-python
mv ifcopenshell ifcopenshell-local # Force testing on installed module
make test
+14 -3
View File
@@ -3,8 +3,19 @@ name: ci
on:
push:
paths:
- 'src/**'
- 'test/**'
- 'src/ifcblender/**'
- 'src/ifcconvert/**'
- 'src/ifcgeom/**'
- 'src/ifcgeom_schema_agnostic/**'
- 'src/ifcgeomserver/**'
- 'src/ifcjni/**'
- 'src/ifcmax/**'
- 'src/ifcopenshell-python/**'
- '!src/ifcopenshell-python/docs/**'
- 'src/ifcparse/**'
- 'src/ifcwrap/**'
- 'src/qtviewer/**'
- 'src/serializers/**'
- 'conda/**'
- 'cmake/**'
- '.github/workflows/ci.yml'
@@ -62,7 +73,7 @@ jobs:
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
-DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \
-DCOLLADA_SUPPORT=Off \
"-DSCHEMA_VERSIONS=2x3;4" \
"-DSCHEMA_VERSIONS=2x3;4;4x3" \
-DGLTF_SUPPORT=On \
-DJSON_INCLUDE_DIR=/usr/include \
-DCGAL_INCLUDE_DIR=/usr/include \
+24 -6
View File
@@ -19,13 +19,23 @@ jobs:
uses: actions/checkout@v2
with:
submodules: recursive
-
name: Update ubuntu
- name: Install C++ dependencies
run: |
sudo apt update
sudo apt-get install git cmake gcc g++ libboost1.67-all-dev python3-all-dev swig libpcre3-dev libxml2-dev
sudo apt-get install libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
libhdf5-dev libcgal-dev
sudo apt-get install --no-install-recommends \
git cmake gcc g++ \
libboost-date-time-dev \
libboost-filesystem-dev \
libboost-iostreams-dev \
libboost-program-options-dev \
libboost-regex-dev \
libboost-system-dev \
libboost-thread-dev \
python3-all-dev python3-pip \
swig libpcre3-dev libxml2-dev \
libtbb-dev nlohmann-json3-dev \
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
libhdf5-dev libcgal-dev opencollada-dev
- name: Build
env:
@@ -44,10 +54,18 @@ jobs:
-DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
-DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \
-DCOLLADA_SUPPORT=Off \
-DLIBXML2_INCLUDE_DIR=/usr/include/libxml2 \
-DLIBXML2_LIBRARIES=/usr/lib/x86_64-linux-gnu/libxml2.so \
-DGLTF_SUPPORT=On \
-DJSON_INCLUDE_DIR=/usr/include \
-DCGAL_INCLUDE_DIR=/usr/include \
-DGMP_INCLUDE_DIR=/usr/include \
-DMPFR_INCLUDE_DIR=/usr/include \
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DHDF5_INCLUDE_DIR=/usr/include/hdf5/serial \
-DOPENCOLLADA_INCLUDE_DIR=/usr/include/opencollada \
-DOPENCOLLADA_LIBRARY_DIR=/usr/lib/opencollada/ \
../cmake
make -j $(nproc)
make install
+6
View File
@@ -67,3 +67,9 @@ htmlcov
# Blender
*.blend1
# Sphinx
_build/
# IDS Docs
src/ifcopenshell-python/test/build
+1 -4
View File
@@ -8,9 +8,6 @@
[submodule "src/svgfill"]
path = src/svgfill
url = https://github.com/IfcOpenShell/svgfill
[submodule "src/ifcopenshell-python/ifcopenshell/Sample-BIM-Files"]
path = src/ifcopenshell-python/ifcopenshell/Sample-BIM-Files
url = https://github.com/atomczak/Sample-BIM-Files
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
path = src/ifcopenshell-python/test/Sample-BIM-Files
url = https://github.com/atomczak/Sample-BIM-Files
url = https://github.com/IfcOpenShell/ids-test-files
+5 -3
View File
@@ -23,9 +23,11 @@ RUN echo "deb http://archive.ubuntu.com/ubuntu focal-proposed main restricted" |
echo "deb http://archive.ubuntu.com/ubuntu focal-proposed multiverse" | tee -a /etc/apt/sources.list; \
apt-get -qq update; \
apt-get -y install tzdata dos2unix rsync; \
apt-get -y install python3 libxml2 liboce-foundation11 liboce-modeling11 liboce-ocaf11 liboce-visualization11 \
liboce-ocaf-lite11 libpython3.8 libboost-system1.67.0 libboost-program-options1.67.0 \
libboost-regex1.67.0 libboost-thread1.67.0 libboost-date-time1.67.0; \
apt-get -y install python3 libxml2 libpython3.8 \
libboost-all-dev \
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev \
libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
libhdf5-serial-dev ; \
rm -rf /var/lib/apt/lists/* ;
COPY . /home/IfcOpenShell/
+64 -44
View File
@@ -9,12 +9,20 @@ For more information, see
* [http://ifcopenshell.org](http://ifcopenshell.org)
* [http://academy.ifcopenshell.org](http://academy.ifcopenshell.org)
[![Build Status](https://travis-ci.org/IfcOpenShell/IfcOpenShell.svg?branch=v0.6.0)](https://travis-ci.org/IfcOpenShell/IfcOpenShell)
| Service | Status |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Anaconda Daily Build | [![Anaconda-Server Badge](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell)](https://anaconda.org/ifcopenshell/ifcopenshell) |
| Anaconda v0.7.0 Stable | [![Anaconda-Server Badge](https://img.shields.io/conda/vn/conda-forge/ifcopenshell)](https://anaconda.org/conda-forge/ifcopenshell) |
| PyPi Daily Build | [![PyPi Badge](https://img.shields.io/pypi/v/ifcopenshell-python)](https://pypi.org/project/ifcopenshell-python/) |
| ArchLinux AUR Package | [![AUR Badge](https://img.shields.io/aur/version/ifcopenshell-git)](https://aur.archlinux.org/packages/ifcopenshell-git) |
| BlenderBIM Add-on Chocolatey (under moderation) | [![Chocolatey Badge](https://img.shields.io/chocolatey/v/blenderbim-nightly)](https://community.chocolatey.org/packages/blenderbim-nightly/) |
| Sponsor development on OpenCollective | [![Financial Contributors](https://opencollective.com/opensourcebim/tiers/badge.svg)](https://opencollective.com/opensourcebim/) |
| Docker hub | [![Docker Pulls](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell)](https://hub.docker.com/r/aecgeeks/ifcopenshell) |
Prerequisites
-------------
* Git
* CMake (2.6 or newer)
* CMake (3.1.3 or newer)
* Windows: [Visual Studio] 2008 to 2019 (2022 not yet supported by dependency CMake) with C++ toolset (or [Visual C++ Build Tools]) or [MSYS2] + MinGW
* *nix: GCC 4.7 or newer, or Clang (any version)
@@ -23,7 +31,7 @@ Dependencies
* [Boost](http://www.boost.org/)
* [Open Cascade](https://dev.opencascade.org/) - *optional*, but required for building IfcGeom
([official](https://dev.opencascade.org/release), "OCCT", or [community edition](https://github.com/tpaviot/oce), "OCE")
For converting IFC representation items into BRep solids and tesselated meshes
For converting IFC representation items into BRep solids and tessellated meshes
* [OpenCOLLADA](https://github.com/khronosGroup/OpenCOLLADA/) - *optional*
For IfcConvert to be able to write tessellated Collada (.dae) files
* [SWIG](http://www.swig.org/) and [Python](https://www.python.org/) - *optional*
@@ -87,7 +95,7 @@ The following instructions are for Ubuntu, modify as required for other operatin
can be experimented with and studied for pointers for other operating systems, but note that this script is not currently
meant to be used for a typical IfcOpenShell workspace setup.
Note 1: It is recommeded to use OCCT for IfcOpenShell. You could use OCE as well, but sometimes it may lag behind OCCT and
Note 1: It is recommended to use OCCT for IfcOpenShell. You could use OCE as well, but sometimes it may lag behind OCCT and
therefore not compile with the latest IfcOpenShell.
Note 2: where `make -j` is written, add a number roughly equal to the amount of CPU cores + 1.
@@ -161,28 +169,40 @@ If all worked out correctly you can now use IfcOpenShell. See the examples below
**1)** Install all dependencies using [Homebrew](https://brew.sh/)
```{shell}
$ brew install boost swig cmake ftgl cgal gmp libaec opencascade
$ brew install boost cmake python3 cgal ftgl gmp libaec opencascade swig hdf5 zlib
# homebrew automatically links most libraries, except some keg-only ones
$ brew link zlib --force
```
**2)** Clone the git repo and its submodules
```{shell}
$ git clone --recurse-submodules https://github.com/IfcOpenShell/IfcOpenShell.git
```
**3)** Build IfcOpenShell with flags for Homebrew dependencies (```/usr/local/```)
**3)** Build IfcOpenShell with flags for Homebrew dependencies: (``/usr/local/``) for Intel machines with x84_64 architecture,
(``/opt/homebrew/``) for Apple Silicon processors with arm64 architecture.
```{shell}
$ cd /path/to/IfcOpenShell
$ mkdir build && cd build
$ cmake ../cmake -DOCC_LIBRARY_DIR=/usr/local/lib/ \
-DOCC_INCLUDE_DIR=/usr/local/include/opencascade/ \
-DCOLLADA_SUPPORT=0 \
-DCGAL_INCLUDE_DIR=/usr/local/include/ \
-DGMP_LIBRARY_DIR=/usr/local/lib/ \
-DMPFR_LIBRARY_DIR=/usr/local/lib/
$ make -j -lboost_options
# set library flags
$ export LDFLAGS="$LDFLAGS -Wl,-flat_namespace,-undefined,suppress"
$ cmake ../cmake
-DPYTHON_EXECUTABLE=/opt/homebrew/bin/python3.10 \
-DPYTHON_LIBRARY=/opt/homebrew/opt/python@3.10/Frameworks/Python.framework/Versions/3.10/lib/libpython3.10.dylib \
-DPYTHON_INCLUDE_DIR=/opt/homebrew/opt/python@3.10/Frameworks/Python.framework/Versions/3.10/include/python3.10/ \
-DOCC_LIBRARY_DIR=/opt/homebrew/lib/ \
-DOCC_INCLUDE_DIR=/opt/homebrew/include/opencascade/ \
-DCGAL_INCLUDE_DIR=/opt/homebrew/include/ \
-DGMP_LIBRARY_DIR=/opt/homebrew/lib/ \
-DMPFR_LIBRARY_DIR=/opt/homebrew/lib/ \
-DHDF5_LIBRARY_DIR=/opt/homebrew/lib/ \
-DHDF5_INCLUDE_DIR=/opt/homebrew/include/ \
-DCOLLADA_SUPPORT=0
# `sysctl -n hw.ncpu` returns the number of cpu cores on macOS
$ make -j$(sysctl -n hw.ncpu)
```
Note: Make sure to compile using XCode, rather than a ```brew``` installed C/C++ compiler.
Note: Make sure to compile using the XCode provided Apple Clang compiler, installed with `xcode-select --install
`, rather than a ```brew``` installed C/C++ compiler.
Installing IfcOpenShell with Conda
----------------------------------
@@ -271,35 +291,35 @@ Also available are a series of utilities that are based on or related to IfcOpen
Those marked with an asterisk are part of IfcOpenShell.
Name | License
--- | ---
bcf | LGPL-3.0-or-later
blenderbim | GPL-3.0-or-later
bsdd | LGPL-3.0-or-later
ifc2ca | LGPL-3.0-or-later
ifc4d | LGPL-3.0-or-later
ifc5d | LGPL-3.0-or-later
ifcbimtester | LGPL-3.0-or-later
ifcblender | LGPL-3.0-or-later\*
ifccityjson | LGPL-3.0-or-later
ifcclash | LGPL-3.0-or-later
ifccobie | LGPL-3.0-or-later
ifcconvert | LGPL-3.0-or-later\*
ifccsv | LGPL-3.0-or-later
ifcdiff | LGPL-3.0-or-later
ifcfm | LGPL-3.0-or-later
ifcgeom | LGPL-3.0-or-later\*
ifcgeom\_schema\_agnostic | LGPL-3.0-or-later\*
ifcgeomserver | LGPL-3.0-or-later\*
ifcjni | LGPL-3.0-or-later\*
ifcmax | LGPL-3.0-or-later\*
ifcopenshell-python | LGPL-3.0-or-later\*
ifcparse | LGPL-3.0-or-later\*
ifcpatch | LGPL-3.0-or-later
ifcsverchok | GPL-3.0-or-later
ifcwrap | LGPL-3.0-or-later\*
qtviewer | LGPL-3.0-or-later\*
serializers | LGPL-3.0-or-later\*
| Name | License |
| ------------------------- | ------------------- |
| bcf | LGPL-3.0-or-later |
| blenderbim | GPL-3.0-or-later |
| bsdd | LGPL-3.0-or-later |
| ifc2ca | LGPL-3.0-or-later |
| ifc4d | LGPL-3.0-or-later |
| ifc5d | LGPL-3.0-or-later |
| ifcbimtester | LGPL-3.0-or-later |
| ifcblender | LGPL-3.0-or-later\* |
| ifccityjson | LGPL-3.0-or-later |
| ifcclash | LGPL-3.0-or-later |
| ifccobie | LGPL-3.0-or-later |
| ifcconvert | LGPL-3.0-or-later\* |
| ifccsv | LGPL-3.0-or-later |
| ifcdiff | LGPL-3.0-or-later |
| ifcfm | LGPL-3.0-or-later |
| ifcgeom | LGPL-3.0-or-later\* |
| ifcgeom\_schema\_agnostic | LGPL-3.0-or-later\* |
| ifcgeomserver | LGPL-3.0-or-later\* |
| ifcjni | LGPL-3.0-or-later\* |
| ifcmax | LGPL-3.0-or-later\* |
| ifcopenshell-python | LGPL-3.0-or-later\* |
| ifcparse | LGPL-3.0-or-later\* |
| ifcpatch | LGPL-3.0-or-later |
| ifcsverchok | GPL-3.0-or-later |
| ifcwrap | LGPL-3.0-or-later\* |
| qtviewer | LGPL-3.0-or-later\* |
| serializers | LGPL-3.0-or-later\* |
[LGPL]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/COPYING.LESSER "LGPL-3.0-or-later"
[IFC]: https://technical.buildingsmart.org/standards/ifc/ "IFC"
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Do not remove this test for UTF-8: if “Ω” doesnt appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<id>blenderbim-nightly</id>
<version>blenderbim_build_version</version>
<packageSourceUrl>https://github.com/IfcOpenShell/IfcOpenShell</packageSourceUrl>
<owners>fbpyr</owners>
<!-- == SOFTWARE SPECIFIC SECTION == -->
<title>BlenderBIM install nightly</title>
<authors>Dion Moult and many more, see: https://github.com/IfcOpenShell/IfcOpenShell/graphs/contributors</authors>
<projectUrl>https://github.com/IfcOpenShell/IfcOpenShell</projectUrl>
<iconUrl>https://rawcdn.githack.com/fbpyr/IfcOpenShell/9662598ad4a7e4b6cd8980e430d07263835d0eda/choco/blenderbim/blenderbim.png</iconUrl>
<!-- <copyright>Year Software Vendor</copyright> -->
<licenseUrl>https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/COPYING</licenseUrl>
<requireLicenseAcceptance>true</requireLicenseAcceptance>
<projectSourceUrl>https://github.com/IfcOpenShell/IfcOpenShell</projectSourceUrl>
<docsUrl>https://blenderbim.org/docs/</docsUrl>
<!--<mailingListUrl></mailingListUrl>-->
<bugTrackerUrl>https://github.com/IfcOpenShell/IfcOpenShell/issues</bugTrackerUrl>
<tags>blender bim blenderbim python opensource foss</tags>
<summary>opensource bim</summary>
<description>opensource bim addon for blender</description>
<!-- <releaseNotes>__REPLACE_OR_REMOVE__MarkDown_Okay</releaseNotes> -->
<dependencies>
<dependency id="blender" version="latest_blender_version_maj_min_pat" />
<dependency id="7zip" version="22.1"/>
</dependencies>
</metadata>
<files>
<file src="tools\**" target="tools" />
</files>
</package>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

+79
View File
@@ -0,0 +1,79 @@
import datetime
import re
import sys
from urllib import request
def request_repo_info(url: str):
req = request.Request(url)
resp = request.urlopen(req)
if not resp.status == 200:
print(f"[ERROR] could not contact server: {url}")
quit(1)
return resp
if sys.argv[1] == "--do_choco_release?":
now = datetime.datetime.now()
blenderbim_date = now.strftime("%y%m%d")
url = "https://github.com/IfcOpenShell/IfcOpenShell/releases/latest"
resp = request_repo_info(url)
if blenderbim_date in resp.url:
print("do_choco_release", end="")
elif sys.argv[1] == "--latest_blender_release_maj_min_pat?":
re_blender_version_min_maj_pat = r"Latest Version.+<span>Blender (\d+\.\d+\.\d+)</span>"
url = "https://community.chocolatey.org/packages/blender"
resp = request_repo_info(url)
html_txt = str(resp.read())
found = re.findall(re_blender_version_min_maj_pat, html_txt)
if found:
print(found[0], end="")
elif sys.argv[1] == "--latest_blender_release_maj_min?":
re_blender_version_min_maj = r"Latest Version.+<span>Blender (\d+\.\d+)\..+</span>"
url = "https://community.chocolatey.org/packages/blender"
resp = request_repo_info(url)
html_txt = str(resp.read())
found = re.findall(re_blender_version_min_maj, html_txt)
if found:
print(found[0], end="")
elif sys.argv[1] == "--latest_blender_python_version_maj_min?":
# get latest blender version first
re_blender_version_min_maj_pat = r"Latest Version.+<span>Blender (\d+\.\d+\.\d+)</span>"
url = "https://community.chocolatey.org/packages/blender"
resp = request_repo_info(url)
html_txt = str(resp.read())
found = re.findall(re_blender_version_min_maj_pat, html_txt)
if found:
latest_blender_version_tag = f"v{found[0]}"
re_blender_python_version_maj_min = r"SET\(PYTHON_VERSION (\d+.\d+) "
url = f"https://raw.githubusercontent.com/blender/blender/{latest_blender_version_tag}/build_files/cmake/Modules/FindPythonLibsUnix.cmake"
resp = request_repo_info(url)
html_txt = str(resp.read())
found = re.findall(re_blender_python_version_maj_min, html_txt)
if found:
print(found[0], end="")
elif sys.argv[1] == "--pyver?":
# get latest blender version first
re_blender_version_min_maj_pat = r"Latest Version.+<span>Blender (\d+\.\d+\.\d+)</span>"
url = "https://community.chocolatey.org/packages/blender"
resp = request_repo_info(url)
html_txt = str(resp.read())
found = re.findall(re_blender_version_min_maj_pat, html_txt)
if found:
latest_blender_version_tag = f"v{found[0]}"
re_blender_python_version_maj_min = r"SET\(PYTHON_VERSION (\d+.\d+) "
url = f"https://raw.githubusercontent.com/blender/blender/{latest_blender_version_tag}/build_files/cmake/Modules/FindPythonLibsUnix.cmake"
resp = request_repo_info(url)
html_txt = str(resp.read())
found = re.findall(re_blender_python_version_maj_min, html_txt)
if found:
print(f"py{found[0].replace('.', '')}", end="")
@@ -0,0 +1,49 @@
import os
from pathlib import Path
print("[INFO] inserting dynamic chocolatey package parameters")
HERE_DIR = Path(__file__).parent.absolute()
# print(f"[INFO] HERE_DIR: {HERE_DIR}")
topics = {
"spec": {
"path": HERE_DIR / "blenderbim.nuspec",
"env_vars": {
"latest_blender_version_maj_min_pat",
"blenderbim_build_version",
},
},
"install": {
"path": HERE_DIR / "tools" / "chocolateyinstall.ps1",
"env_vars": {
"url_blenderbim_py310_win_zip",
"sha256sum_blenderbim_py310_win_zip",
"latest_blender_version_maj_min",
},
},
"uninstall": {
"path": HERE_DIR / "tools" / "chocolateyuninstall.ps1",
"env_vars": {
"latest_blender_version_maj_min",
},
},
}
for topic, info in topics.items():
print(f"[INFO] {topic}:")
for env_var_name in info["env_vars"]:
print(f"[INFO] {env_var_name} exists?: {os.environ[env_var_name]}")
with open(info["path"], encoding="utf-8") as txt:
content = txt.read()
for env_var_name in info["env_vars"]:
# print(f"[INFO] replace: {env_var_name}")
content = content.replace(env_var_name, os.environ[env_var_name])
with open(info["path"], "w", encoding="utf-8") as txt:
txt.write(content)
print(f"[INFO] written: {info['path']}")
print("[INFO] inserting dynamic chocolatey package parameters successful")
@@ -0,0 +1,11 @@
$processName = "blender"
$blenderIsRunning = Get-Process -Name $processName -ErrorAction SilentlyContinue
if($blenderIsRunning -eq $null) {
echo "Blender is not running ready for next action."
}
else {
Write-Warning "$processName is still running - installer cannot safely proceed to next action."
Write-Warning "Please retry after closing all running blender applications."
exit 1
}
@@ -0,0 +1,29 @@
$ErrorActionPreference = 'Stop'
$url64 = 'url_blenderbim_py310_win_zip'
$checksum64 = 'sha256sum_blenderbim_py310_win_zip'
$checksumType64 = 'sha256'
$appDataUserDir = [System.Environment]::GetEnvironmentVariable('appdata')
$unzipTargetDir = "$appDataUserDir\Blender Foundation\Blender\latest_blender_version_maj_min\scripts\addons"
$chocoBaseDir = [System.Environment]::GetEnvironmentVariable('ChocolateyInstall')
$addonEnable = "$chocoBaseDir\lib\blenderbim\tools\enable_blenderbim_addon.py"
$programFilesDir = [System.Environment]::GetEnvironmentVariable('ProgramFiles')
$blenderExePath = "$env:ProgramFiles\Blender Foundation\Blender latest_blender_version_maj_min\blender.exe"
$processName = "blender"
$blenderIsRunning = Get-Process -Name $processName -ErrorAction SilentlyContinue
if($blenderIsRunning -eq $null) {
echo "attempting to install blenderbim."
Install-ChocolateyZipPackage -PackageName $env:ChocolateyPackageName -Url64 $url64 -Checksum64 $checksum64 -checksumType $checksumType64 -UnzipLocation $unzipTargetDir
Start-Process -FilePath $blenderExePath -ArgumentList "-b", "-y", "--python", "$addonEnable"
}
else {
Write-Warning "$processName is still running - blenderbim cannot be safely installed."
Write-Warning "Please retry after closing all running blender applications."
exit 1
}
@@ -0,0 +1,22 @@
$appDataUserDir = [System.Environment]::GetEnvironmentVariable('appdata')
$unzippedDir = "$appDataUserDir\Blender Foundation\Blender\latest_blender_version_maj_min\scripts\addons\blenderbim"
$chocoBaseDir = [System.Environment]::GetEnvironmentVariable('ChocolateyInstall')
$addonDisable = "$chocoBaseDir\lib\blenderbim\tools\disable_blenderbim_addon.py"
$programFilesDir = [System.Environment]::GetEnvironmentVariable('ProgramFiles')
$blenderExePath = "$env:ProgramFiles\Blender Foundation\Blender latest_blender_version_maj_min\blender.exe"
$processName = "blender"
$blenderIsRunning = Get-Process -Name $processName -ErrorAction SilentlyContinue
if($blenderIsRunning -eq $null) {
echo "attempting to uninstall blenderbim."
Start-Process -FilePath $blenderExePath -ArgumentList "-b", "-y", "--python", "$addonDisable"
Remove-Item -LiteralPath $unzippedDir -Force -Recurse
}
else {
Write-Warning "$processName is still running - blenderbim cannot be safely uninstalled."
Write-Warning "Please retry after closing all running blender applications."
exit 1
}
@@ -0,0 +1,8 @@
import bpy
bpy.ops.preferences.addon_disable(module='blenderbim')
bpy.ops.wm.save_userpref()
print("disabled blenderbim addon successfully.")
@@ -0,0 +1,8 @@
import bpy
bpy.ops.preferences.addon_enable(module='blenderbim')
bpy.ops.wm.save_userpref()
print("enabled blenderbim addon successfully.")
+85 -35
View File
@@ -38,7 +38,7 @@ MESSAGE(STATUS "Autodesk 3ds Max SDK found at ${max_sdk}")
set(HAS_MAX TRUE)
endif()
endforeach()
OPTION(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
OPTION(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON)
OPTION(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
OPTION(ENABLE_BUILD_OPTIMIZATIONS "Enable certain compiler and linker optimizations on RelWithDebInfo and Release builds." OFF)
@@ -53,6 +53,7 @@ OPTION(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
OPTION(USE_VLD "Use Visual Leak Detector for debugging memory leaks, MSVC-only." OFF)
OPTION(USE_MMAP "Adds a command line options to parse IFC files from memory mapped files using Boost.Iostreams" OFF)
OPTION(BUILD_SHARED_LIBS "Build IfcParse and IfcGeom as shared libs (SO/DLL)." OFF)
OPTION(MSVC_PARALLEL_BUILD "Multi-threaded compilation in Microsoft Visual Studio (/MP)" OFF)
if (${HAS_MAX})
OPTION(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
endif()
@@ -66,6 +67,10 @@ IF(NOT CMAKE_BUILD_TYPE)
SET(CMAKE_BUILD_TYPE "Release")
ENDIF()
if(MSVC AND MSVC_PARALLEL_BUILD)
add_definitions("/MP")
endif()
# QtViewer requires Qt5
OPTION(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF)
include(GNUInstallDirs)
@@ -121,6 +126,7 @@ ENDMACRO()
UNIFY_ENVVARS_AND_CACHE(OCC_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OCC_LIBRARY_DIR)
if (NOT MINIMAL_BUILD)
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
@@ -130,17 +136,20 @@ UNIFY_ENVVARS_AND_CACHE(PYTHON_EXECUTABLE)
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
endif()
UNIFY_ENVVARS_AND_CACHE(BOOST_ROOT)
UNIFY_ENVVARS_AND_CACHE(BOOST_LIBRARYDIR)
if (NOT MINIMAL_BUILD)
UNIFY_ENVVARS_AND_CACHE(CGAL_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(CGAL_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_LIBRARY_DIR)
endif()
if (GLTF_SUPPORT AND BUILD_CONVERT)
if (NOT MINIMAL_BUILD AND GLTF_SUPPORT AND BUILD_CONVERT)
UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR)
FIND_FILE(json_hpp "json.hpp" ${JSON_INCLUDE_DIR}/nlohmann)
IF(json_hpp)
@@ -201,10 +210,12 @@ if(USE_MMAP)
add_definitions(-DUSE_MMAP)
endif()
if (NOT MINIMAL_BUILD)
# libxml2 is required for IFCXML (optional) and SVGFILL (mandatory)
find_package(LibXml2 REQUIRED)
find_package(LibXml2 REQUIRED)
endif()
if (IFCXML_SUPPORT)
if (NOT MINIMAL_BUILD AND IFCXML_SUPPORT)
add_definitions(-DWITH_IFCXML)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_IFCXML)
endif()
@@ -346,7 +357,7 @@ endif()
endif(BUILD_IFCGEOM)
IF(COLLADA_SUPPORT)
IF(NOT MINIMAL_BUILD AND COLLADA_SUPPORT)
# Find OpenCOLLADA
IF("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
MESSAGE(STATUS "No OpenCOLLADA include directory specified")
@@ -426,7 +437,7 @@ IF(COLLADA_SUPPORT)
ENDIF()
ENDIF()
if(HDF5_SUPPORT)
if(NOT MINIMAL_BUILD AND HDF5_SUPPORT)
IF("${HDF5_INCLUDE_DIR}" STREQUAL "")
MESSAGE(STATUS "No HDF5 include directory specified")
ElSE()
@@ -441,30 +452,60 @@ if(HDF5_SUPPORT)
if (HDF5_LIBRARY_DIR)
# result of the HDF5 ctest package
if (WIN32)
set(zlib_post lib)
set(lib_ext lib)
else()
set(lib_ext a)
# Find zlib using cmake find_library. How should this be implemented?
# FIND_LIBRARY(NAMES z libz libz_debug PATHS ... NO_DEFAULT_PATH)
if ("$ENV{CONDA_BUILD}" STREQUAL "")
# result of the HDF5 ctest package
if (WIN32)
set(zlib_post lib)
set(lib_ext lib)
else()
set(lib_ext a)
endif()
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(debug_postfix "_debug")
endif()
SET(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
)
ELSE()
MESSAGE(STATUS "Packaging hdf5 and zlib for conda distribution")
if (WIN32)
# Windows
set(zlib_post zlib)
set(lib_ext lib)
elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
# macOS
set(zlib_post libz)
set(lib_ext dylib)
else()
# linux
set(zlib_post libz)
set(lib_ext so)
endif()
SET(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
)
endif()
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(debug_postfix "_debug")
endif()
SET(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
)
endif()
if (NOT HDF5_LIBRARIES)
# debian default
SET(HDF5_LIBRARIES
/usr/lib/x86_64-linux-gnu/hdf5/serial/libhdf5_cpp.so
/usr/lib/x86_64-linux-gnu/hdf5/serial/libhdf5.so
@@ -579,13 +620,16 @@ function(files_for_ifc_version IFC_VERSION RESULT_NAME)
endfunction()
if(NOT SCHEMA_VERSIONS)
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4")
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4" "4x3")
endif()
foreach(s ${SCHEMA_VERSIONS})
add_definitions(-DHAS_SCHEMA_${s})
endforeach()
string(REPLACE ";" ")(" schema_version_seq "(${SCHEMA_VERSIONS})")
ADD_DEFINITIONS(-DSCHEMA_SEQ=${schema_version_seq})
if(COMPILE_SCHEMA)
# @todo, this appears to be untested at the moment
@@ -655,7 +699,7 @@ if (BUILD_IFCGEOM)
endforeach()
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
endif()
if (BUILD_CONVERT)
if (BUILD_CONVERT OR BUILD_IFCPYTHON)
foreach(s ${SCHEMA_VERSIONS})
set(SERIALIZER_SCHEMA_LIBRARIES ${SERIALIZER_SCHEMA_LIBRARIES} Serializers_ifc${s})
endforeach()
@@ -728,7 +772,7 @@ TARGET_LINK_LIBRARIES(IfcGeom IfcParse ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREA
endif(BUILD_IFCGEOM)
if (BUILD_CONVERT)
if (BUILD_CONVERT OR BUILD_IFCPYTHON)
# Serializers
file(GLOB SERIALIZERS_H_FILES ../src/serializers/*.h)
@@ -749,6 +793,10 @@ set_target_properties(Serializers PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS $
TARGET_LINK_LIBRARIES(Serializers ${SERIALIZER_SCHEMA_LIBRARIES} ${OPENCOLLADA_LIBRARIES})
endif(BUILD_CONVERT or BUILD_IFCPYTHON)
if (BUILD_CONVERT)
# IfcConvert
file(GLOB IFCCONVERT_CPP_FILES ../src/ifcconvert/*.cpp)
file(GLOB IFCCONVERT_H_FILES ../src/ifcconvert/*.h)
@@ -774,7 +822,7 @@ INSTALL(TARGETS IfcConvert
endif(BUILD_CONVERT)
# IfcGeomServer
if(BUILD_GEOMSERVER)
if(NOT MINIMAL_BUILD AND BUILD_GEOMSERVER)
file(GLOB CPP_FILES ../src/ifcgeomserver/*.cpp)
file(GLOB H_FILES ../src/ifcgeomserver/*.h)
@@ -805,7 +853,8 @@ if (GIT_FOUND)
)
string(REPLACE "\n" ";" git_branch_list "${git_branches}")
foreach(git_branch_candidate IN ITEMS ${git_branch_list})
string(STRIP "${git_branch_candidate}" git_branch_candidate_2)
string(REPLACE "*" "" git_branch_candidate_temp "${git_branch_candidate}")
string(STRIP "${git_branch_candidate_temp}" git_branch_candidate_2)
if (NOT git_branch_candidate_2 MATCHES "^HEAD$")
set(git_branch ${git_branch_candidate_2})
endif()
@@ -828,7 +877,7 @@ IF(BUILD_DOCUMENTATION)
ADD_SUBDIRECTORY(../docs docs)
ENDIF()
IF(BUILD_IFCPYTHON)
IF(NOT MINIMAL_BUILD AND BUILD_IFCPYTHON)
ADD_SUBDIRECTORY(../src/ifcwrap ifcwrap)
ENDIF()
@@ -836,11 +885,12 @@ IF(BUILD_EXAMPLES)
ADD_SUBDIRECTORY(../src/examples examples)
ENDIF()
IF(BUILD_IFCMAX)
IF(NOT MINIMAL_BUILD AND BUILD_IFCMAX)
ADD_SUBDIRECTORY(../src/ifcmax ifcmax)
ENDIF()
if (NOT MINIMAL_BUILD)
ADD_SUBDIRECTORY(../src/svgfill svgfill)
endif()
# CMake installation targets
INSTALL(FILES ${IFCPARSE_H_FILES}
@@ -912,12 +962,12 @@ set(CPACK_SOURCE_GENERATOR "TGZ")
FOREACH(COMPONENT IN ITEMS ${BOOST_COMPONENTS})
string(REPLACE "_" "-" COMP ${COMPONENT})
set(BOOST_DEPS "${BOOST_DEPS}, libboost-${COMP}1.67.0")
set(BOOST_DEPS "${BOOST_DEPS}, libboost-${COMP}-dev")
endforeach(COMPONENT)
set(CPACK_DEBIAN_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${CPACK_PACKAGE_CONTACT}")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, liboce-foundation11, liboce-modeling11, liboce-ocaf11, liboce-visualization11, liboce-ocaf-lite11, libpython3.8 ${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.8 ${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")
+8
View File
@@ -0,0 +1,8 @@
compile:
conda activate conda-build && conda-build -c conda-forge . --keep-old-work --python 3.10.5
debug:
conda activate conda-build && conda-debug -c conda-forge . --python 3.10.4
env:
conda env update --f environment.yml --prune
+38 -23
View File
@@ -1,29 +1,44 @@
mkdir build && cd build
REM Remove dot from PY_VER for use in library name
REM From https://github.com/tpaviot/pythonocc-core/blob/master/ci/conda/bld.bat
set MY_PY_VER=%PY_VER:.=%
set LIBXML2="%LIBRARY_PREFIX%/lib/libxml2.lib"
cmake -G "NMake Makefiles" ^
-DCMAKE_INSTALL_PREFIX="%LIBRARY_PREFIX%" ^
-DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_PREFIX_PATH="%LIBRARY_PREFIX%" ^
-DCMAKE_SYSTEM_PREFIX_PATH="%LIBRARY_PREFIX%" ^
-DPYTHON_EXECUTABLE="%PYTHON%" ^
-DPYTHON_INCLUDE_DIR="%PREFIX%"/include ^
-DPYTHON_LIBRARY="%PREFIX%"/libs/python%MY_PY_VER%.lib ^
-DBOOST_LIBRARYDIR="%LIBRARY_PREFIX%\lib" ^
-DBOOST_INCLUDEDIR="%LIBRARY_PREFIX%\include" ^
-DOCC_INCLUDE_DIR="%LIBRARY_PREFIX%\include\oce" ^
-DOCC_LIBRARY_DIR="%LIBRARY_PREFIX%\lib" ^
-DCOLLADA_SUPPORT=Off ^
-DBUILD_EXAMPLES=Off ^
-DBUILD_GEOMSERVER=Off ^
-DBUILD_CONVERT=Off ^
../cmake
if errorlevel 1 exit 1
cmake --build . --target INSTALL --config Release
cmake -G "Ninja" ^
-D CMAKE_BUILD_TYPE:STRING=Release ^
-D CMAKE_INSTALL_PREFIX:FILEPATH="%LIBRARY_PREFIX%" ^
-D CMAKE_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^
-D CMAKE_SYSTEM_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^
-D OCC_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include\opencascade" ^
-D OCC_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
-D CGAL_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
-D GMP_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
-D GMP_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
-D MPFR_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
-D COLLADA_SUPPORT=OFF ^
-D HDF5_SUPPORT=ON ^
-D HDF5_INCLUDE_DIR="%LIBRARY_PREFIX%\include" ^
-D HDF5_LIBRARY_DIR="%LIBRARY_PREFIX%\lib" ^
-D JSON_INCLUDE_DIR="%LIBRARY_PREFIX%\include" ^
-D PYTHON_INCLUDE_DIR=%PREFIX%\include ^
-D PYTHON_EXECUTABLE:FILEPATH=%PREFIX%\python.exe ^
-D PYTHON_LIBRARY:FILEPATH="%PREFIX%"\libs/python%MY_PY_VER%.lib ^
-D BUILD_IFCPYTHON=ON ^
-D BUILD_IFCGEOM=ON ^
-D COLLADA_SUPPORT:BOOL=OFF ^
-D BUILD_EXAMPLES:BOOL=OFF ^
-D BUILD_GEOMSERVER:BOOL=OFF ^
-D GLTF_SUPPORT:BOOL=ON ^
-D BUILD_CONVERT:BOOL=ON ^
-D BUILD_IFCMAX:BOOL=OFF ^
-D IFCXML_SUPPORT:BOOL=ON ^
-D Boost_LIBRARYDIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
-D Boost_INCLUDEDIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
-D Boost_USE_STATIC_LIBS:BOOL=OFF ^
%SRC_DIR%/cmake
if errorlevel 1 exit 1
:: Build and install
cmake --build . -- install
if errorlevel 1 exit 1
+34 -24
View File
@@ -1,34 +1,44 @@
# From https://github.com/tpaviot/pythonocc-core/blob/master/ci/conda/build.sh
if [ "$PY3K" == "1" ]; then
MY_PY_VER="${PY_VER}m"
else
MY_PY_VER="${PY_VER}"
#!/bin/bash
declare -a CMAKE_PLATFORM_FLAGS
if [[ ${HOST} =~ .*linux.* ]]; then
CMAKE_PLATFORM_FLAGS+=(-DCMAKE_TOOLCHAIN_FILE=$RECIPE_DIR/cross-linux.cmake)
fi
if [ `uname` == Darwin ]; then
PY_LIB="libpython${MY_PY_VER}.dylib"
export CFLAGS="$CFLAGS -Wl,-flat_namespace,-undefined,suppress"
export CXXFLAGS="$CXXFLAGS -Wl,-flat_namespace,-undefined,suppress"
export LDFLAGS="$LDFLAGS -Wl,-flat_namespace,-undefined,suppress"
else
PY_LIB="libpython${MY_PY_VER}.so"
export CFLAGS="$CFLAGS -Wl,-flat_namespace,-undefined,suppress"
export CXXFLAGS="$CXXFLAGS -Wl,-flat_namespace,-undefined,suppress"
export LDFLAGS="$LDFLAGS -Wl,-flat_namespace,-undefined,suppress"
fi
mkdir build && cd build
cmake \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
cmake -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
${CMAKE_PLATFORM_FLAGS[@]} \
-DCMAKE_PREFIX_PATH=$PREFIX \
-DCMAKE_SYSTEM_PREFIX_PATH=$PREFIX \
-DOCC_INCLUDE_DIR=$PREFIX/include/oce \
-DOCC_LIBRARY_DIR=$PREFIX/lib \
-DPYTHON_EXECUTABLE:FILEPATH=$PYTHON \
-DPYTHON_INCLUDE_DIR:PATH=$PREFIX/include/python$MY_PY_VER \
-DPYTHON_LIBRARY:FILEPATH=$PREFIX/lib/${PY_LIB} \
-DCOLLADA_SUPPORT=Off \
../cmake
-DGMP_LIBRARY_DIR=$PREFIX/lib \
-DMPFR_LIBRARY_DIR=$PREFIX/lib \
-DOCC_INCLUDE_DIR=$PREFIX/include/opencascade \
-DOCC_LIBRARY_DIR=$PREFIX/lib \
-DHDF5_SUPPORT:BOOL=ON \
-DHDF5_INCLUDE_DIR=$PREFIX/include \
-DHDF5_LIBRARY_DIR=$PREFIX/lib \
-DJSON_INCLUDE_DIR=$PREFIX/include \
-DCGAL_INCLUDE_DIR=$PREFIX/include \
-DCOLLADA_SUPPORT=0 \
-DBUILD_EXAMPLES:BOOL=OFF \
-DIFCXML_SUPPORT:BOOL=ON \
-DGLTF_SUPPORT:BOOL=ON \
-DBUILD_CONVERT:BOOL=ON \
-DBUILD_IFCPYTHON:BOOL=ON \
-DBUILD_IFCGEOM:BOOL=ON \
-DBUILD_GEOMSERVER:BOOL=OFF \
-DBOOST_USE_STATIC_LIBS:BOOL=OFF \
./cmake
make -j$CPU_COUNT _ifcopenshell_wrapper
cd ifcwrap
make install/local
ninja
ninja install
+2
View File
@@ -0,0 +1,2 @@
CONDA_BUILD_SYSROOT:
- /opt/MacOSX10.13.sdk # [osx]
+16
View File
@@ -0,0 +1,16 @@
# this one is important
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_PLATFORM Linux)
# specify the cross compiler
set(CMAKE_C_COMPILER $ENV{CC})
set(CMAKE_CXX_COMPILER $ENV{CXX})
# where is the target environment
set(CMAKE_FIND_ROOT_PATH $ENV{PREFIX} $ENV{BUILD_PREFIX}/$ENV{HOST}/sysroot)
# search for programs in the build host directories
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# for libraries and headers in the target directories
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+14
View File
@@ -0,0 +1,14 @@
name: conda-build
channels:
- conda-forge
dependencies:
- conda-build
- conda-verify
- anaconda-client
- ninja
- doxygen
- ripgrep
- pip
- pip:
- --extra-index-url https://lief.s3-website.fr-par.scw.cloud/latest
- lief==0.13.0.dev0
+50 -30
View File
@@ -1,42 +1,62 @@
{% set name = "ifcopenshell" %}
{% set version = "0.7.0" %}
{% set occt_version = "7.6.2" %}
{% set cgal_cpp_version = "5.3" %}
{% set hdf5_version = "1.12.1" %}
package:
name: ifcopenshell
version: "0.6.0a1"
name: {{ name }}
version: {{ version }}
source:
git_rev: "v0.6.0a1"
git_url: https://github.com/IfcOpenShell/IfcOpenShell
path: ..
build:
number: 0
features:
- vc9 # [win and py27]
- vc10 # [win and py34]
- vc14 # [win and py35]
- vc14 # [win and py36]
binary_relocation: false [osx]
requirements:
build:
- gcc # [osx]
- make
- python
- oce ==0.18.3
- {{ compiler('c') }}
- {{ compiler('cxx') }}
- ninja >=1.10.2
- cmake
- swig >=3.0.9
- libboost
- icu
- xmlschema
- lxml
- numpy
run:
- libgcc # [osx]
- swig >=4.0.2
host:
- python
- oce ==0.18.3
- libboost
- icu
- xmlschema
- lxml
- numpy
- boost-cpp <=1.74.0
- occt
- libxml2
- cgal-cpp
- hdf5
- mpfr
- gmp # [unix]
- mpir # [win]
- nlohmann_json
run:
- python
- boost-cpp <=1.74.0
- occt
- libxml2
- cgal-cpp
- hdf5
- mpfr
- gmp # [unix]
- mpir # [win]
- nlohmann_json
test:
imports:
- ifcopenshell
about:
home: http://ifcopenshell.org
license: LGPL
license: LGPL-3.0-or-later
license_file: COPYING
summary: 'IfcOpenShell is a library to support the IFC file format'
description: |
IfcOpenShell is an open source (LGPL) software library for
working with the Industry Foundation Classes (IFC) file format.
doc_url: http://ifcopenshell.org/
dev_url: https://github.com/IfcOpenShell/IfcOpenShell
+12 -8
View File
@@ -23,7 +23,7 @@
# This script builds IfcOpenShell and its dependencies #
# #
# Prerequisites for this script to function correctly: #
# * cmake * git * bzip2 * tar * c(++) compilers * yacc * autoconf #
# * cmake * git * bzip2 * tar * c(++) compilers * autoconf #
# #
# if building with USE_OCCT additionally: #
# * freetype * glx.h #
@@ -72,7 +72,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.12", "3.8.12", "3.9.7", "3.10.0"]
PYTHON_VERSIONS = ["3.6.14", "3.7.13", "3.8.13", "3.9.11", "3.10.3"]
JSON_VERSION = "v3.6.1"
OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.5.3"
@@ -83,8 +83,8 @@ SWIG_VERSION = "4.0.2"
OPENCOLLADA_VERSION = "v1.6.68"
HDF5_VERSION = "1.12.1"
GMP_VERSION = "6.1.2"
MPFR_VERSION = "3.1.5" # latest is 4.1.0
GMP_VERSION = "6.2.1"
MPFR_VERSION = "3.1.6" # latest is 4.1.0
CGAL_VERSION = "5.3"
# binaries
@@ -97,7 +97,6 @@ cc = "cc"
cplusplus = "c++"
autoconf = "autoconf"
automake = "automake"
yacc = "yacc"
make = "make"
date = "date"
curl = "curl"
@@ -220,7 +219,7 @@ print("Building:", *sorted(targets, key=lambda t: len(list(v(t)))))
# Check that required tools are in PATH
for cmd in [git, bunzip2, tar, cc, cplusplus, autoconf, automake, yacc, make, "patch", "cmake"]:
for cmd in [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake"]:
if which(cmd) is None:
raise ValueError(f"Required tool '{cmd}' not installed or not added to PATH")
@@ -263,6 +262,11 @@ def run(cmds, cwd=None):
return stdout.strip()
if platform.system() == "Darwin":
if run(["sw_vers", "-productVersion"]) >= "11.":
# Apparently not supported
PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) >= (3, 7)]
BOOST_VERSION_UNDERSCORE = BOOST_VERSION.replace(".", "_")
OCE_LOCATION = f"https://github.com/tpaviot/oce/archive/OCE-{OCE_VERSION}.tar.gz"
@@ -508,7 +512,7 @@ if USE_OCCT and "occ" in targets:
"-DBUILD_MODULE_Draw=0",
"-DBUILD_RELEASE_DISABLE_EXCEPTIONS=Off"
],
download_url = "https://git.dev.opencascade.org/repos/occt.git",
download_url = "https://github.com/Open-Cascade-SAS/OCCT",
download_name = "occt",
download_tool=download_tool_git,
patch=None if OCCT_VERSION >= "7.4" else "./patches/occt/enable-exception-handling.patch",
@@ -577,7 +581,7 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION:
# with the system python because of some threading initialization
PYTHON_CONFIGURE_ARGS = []
if platform.system() == "Darwin":
PYTHON_CONFIGURE_ARGS = ["--disable-static", "--enable-shared"]
PYTHON_CONFIGURE_ARGS = ["--enable-shared"]
for PYTHON_VERSION in PYTHON_VERSIONS:
try:
+1 -1
View File
@@ -646,7 +646,7 @@ class BcfXml:
def get_comments(self, guid):
comments = {}
data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd")
if "Comments" not in data["Topics"]:
if "Comments" not in data["Topic"]:
return comments
for item in data["Topic"]["Comments"].get("Comment", []):
+109 -17
View File
@@ -16,6 +16,24 @@
# 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/>.
PYTHON:=python
PATCH:=patch
SED:=sed -i
ifeq ($(OS),Windows_NT)
HOSTOS:=win
else
UNAME_S:=$(shell uname -s)
ifeq ($(UNAME_S),Linux)
HOSTOS:=linux
endif
ifeq ($(UNAME_S),Darwin)
HOSTOS:=macos
PYTHON:=python3
PATCH:=patch -d
SED:=sed -i '' -e
endif
endif
VERSION:=`date '+%y%m%d'`
PYVERSION:=py37
@@ -38,18 +56,21 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py37h95e2c48_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py37h0379df6_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/30/c0/d0526314971fc661b083ab135747dc68446a3022686da8c16d25fcf6ef07/lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/a2/73/07f507b539527579c026ed5f411c35e287685ce794c4dfb7edf6675cdc24/Shapely-1.8.4-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
endif
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp-fcl-1.7.5-py39hbcdfc36_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py39h5aed9d1_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py39h5472131_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/19/d9/a69c6aff5673554df48120565a14a50eaa41d29ae03b02faa0b023666318/lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/08/12/5af915b2bc0470f4a652ef9978557a0fa8b705b122ccaec6626029e1674c/Shapely-1.8.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/linux-64/hpp-fcl-1.8.0-py310hdaf7e41_1.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/linux-64/eigenpy-2.6.11-py310hf3e5c9c_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py310h7c3ba0c_5.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/25/1e/19b46d8e8881fe0df2e20945d51919eeb1817836d62a90efa8506530e45c/lxml-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/60/20/c6321e3da92a4ad9495727c9aa8c423e47295572ccd35ece82d1ac013e28/Shapely-1.8.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/linux-64/assimp-5.0.1-hedfc422_6.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/linux-64/octomap-1.9.7-h4bd325d_0.tar.bz2
@@ -62,42 +83,76 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-f
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py37h0695097_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py37hd79e0ac_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/1e/3e/f0abc15d5dac50939bccc589aae336d5ead4c72e7ad1039a2e0f3630ea92/lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/47/a9/7c644e71638bff2bbaa87f05ee62fdf6b4552a55e74bbf731d6210b6d436/Shapely-1.8.4-cp37-cp37m-macosx_10_9_x86_64.whl
endif
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py39h1e32b98_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py39h5405915_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py39ha641261_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/b8/74/a71f7ad72e8db54ce899efab84507b801660750cbbfa6a39e6717557d36a/lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/f5/f9/bf6cc34b02b3c7d3777c5e47fa57d2f69387613bbd8decdf3577a9d83a83/Shapely-1.8.4-cp39-cp39-macosx_10_9_x86_64.whl
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/osx-64/hpp-fcl-1.8.0-py310h651ac30_1.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/osx-64/eigenpy-2.6.11-py310hc03097c_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py310h509978a_5.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/a1/44/17b7dac7a18807d30e2fe10c3328c152808f5464565e230bfd0e77f178c6/lxml-4.8.0-cp310-cp310-macosx_10_15_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/f5/a8/02347c450b5507b9a7a2c15ad6a029613149468fc55ac2100ab215666159/Shapely-1.8.4-cp310-cp310-macosx_10_9_x86_64.whl
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-64/assimp-5.0.1-h1224e73_6.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/osx-64/octomap-1.9.7-h940c156_0.tar.bz2
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)
ifeq ($(PYVERSION), py37)
# Warning: py37 not supported on Apple M1
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py37h2d7f23a_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py37h0695097_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py37hd79e0ac_3.tar.bz2
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-64/lxml-4.9.1-py37h994c40b_0.tar.bz2
SHAPELY_URL:=https://files.pythonhosted.org/packages/47/a9/7c644e71638bff2bbaa87f05ee62fdf6b4552a55e74bbf731d6210b6d436/Shapely-1.8.4-cp37-cp37m-macosx_10_9_x86_64.whl
endif
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-arm64/hpp-fcl-1.7.5-py39ha69f3c1_2.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.7/download/osx-arm64/eigenpy-2.6.7-py39ha69f3c1_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py39h2550fe3_0.tar.bz2
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py39h9eb174b_0.tar.bz2
SHAPELY_URL:=https://files.pythonhosted.org/packages/8e/60/8186995a96c5a27c9b79c015bbdd89c6d9a6df9988f86f3f1027ecd4a56a/Shapely-1.8.4-cp39-cp39-macosx_11_0_arm64.whl
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/osx-arm64/hpp-fcl-1.8.0-py310h5699539_2.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.9/download/osx-arm64/eigenpy-2.6.9-py310habfc766_1.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py310h4566fd1_0.tar.bz2
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py310h02f21da_0.tar.bz2
SHAPELY_URL:=https://files.pythonhosted.org/packages/ba/d0/75cd36c81a1aa26dd6d37cf7ae1c79b0bc2d379e0f32c359bcb00b14e5c3/Shapely-1.8.4-cp310-cp310-macosx_11_0_arm64.whl
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-arm64/assimp-5.0.1-h0f81e16_7.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/osx-arm64/octomap-1.9.7-hc021e02_0.tar.bz2
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)
ifeq ($(PYVERSION), py37)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/win-64/hpp-fcl-1.7.5-py37h839d6b1_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eigenpy-2.6.5-py37h2c32e34_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py37h3b38789_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/9e/5e/171ee9d40a600f565fe691ec5bf7596247ec62cfb2edc00c91afe8ea837b/lxml-4.6.3-cp37-cp37m-win_amd64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/ca/4d/85b912d7947261e6b3218b01946bcd7c2efb2bbafaf517c6fe1dc3bb8399/Shapely-1.8.4-cp37-cp37m-win_amd64.whl
endif
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/win-64/hpp-fcl-1.7.5-py39h2e7c763_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eigenpy-2.6.5-py39h3ce40e6_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py39hefe7e4c_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/72/d4/426ecb8849c47c3e370c87aa0ac05d85768df917ffea27fcd6686a5e6495/lxml-4.6.3-cp39-cp39-win_amd64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/72/c3/0d52812db25ca71203f6203891277e6ee4f0244b21a36c062123ece15f36/Shapely-1.8.4-cp39-cp39-win_amd64.whl
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/win-64/hpp-fcl-1.8.0-py310hc5a3c62_1.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/win-64/eigenpy-2.6.11-py310hbd43d28_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py310hc781a3c_5.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/f6/71/65c80a4caa1617a4c6e8fe1500cebb179db96232e2f623bfe6a1f4294e39/lxml-4.8.0-cp310-cp310-win_amd64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/24/b5/46d13799804b5fe9ede2eca8b19023410e29b78baca0bff727671a1df8c1/Shapely-1.8.4-cp310-cp310-win_amd64.whl
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/win-64/assimp-5.0.1-hc2aa0de_6.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/win-64/octomap-1.9.7-h5362a0b_0.tar.bz2
@@ -115,7 +170,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-2985bba-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-4fb910f-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -138,11 +193,13 @@ endif
# Provides IFCClash functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcclash/ifcclash dist/blenderbim/libs/site/packages/
# Provides BIMTester functionality
cd dist/working && python -m venv env
cd dist/working && $(PYTHON) -m venv env
cd dist/working && . env/bin/activate && pip install pybabel
cd dist/working && . env/bin/activate && pip install babel
cd dist/working && . env/bin/activate && ./env/bin/pybabel compile -d ./IfcOpenShell-0.7.0/src/ifcbimtester/bimtester/locale/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcbimtester/bimtester dist/blenderbim/libs/site/packages/
# Provides IFCTester functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifctester/ifctester dist/blenderbim/libs/site/packages/
# Provides IFCCOBie functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifccobie/* dist/blenderbim/libs/site/packages/
# Provides IFCDiff functionality
@@ -161,9 +218,9 @@ endif
# Provides Mustache templating in construction documentation
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/d6/fd/eb8c212053addd941cc90baac307c00ac246ac3fce7166b86434c6eae963/pystache-0.5.4.tar.gz
cd dist/working && wget https://files.pythonhosted.org/packages/3f/e7/8750ba6c6101d6aa5ceeb20c013adf2c6f3554a12c71d75654b468404bfa/pystache-0.6.0.tar.gz
cd dist/working && tar -xzvf pystache*
cd dist/working/pystache-0.5.4/ && python setup.py build && cp -r build/lib/pystache ../../blenderbim/libs/site/packages/
cd dist/working/pystache-0.6.0/ && $(PYTHON) setup.py build && cp -r build/lib/pystache ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Provides SVG export in construction documentation
@@ -210,9 +267,9 @@ endif
# Required by IFCDiff
mkdir dist/working
cd dist/working && wget https://github.com/Moult/deepdiff/archive/master.zip
cd dist/working && unzip master.zip
cp -r dist/working/deepdiff-master/deepdiff dist/blenderbim/libs/site/packages/
cd dist/working && wget https://files.pythonhosted.org/packages/0f/ca/caead2949fbb824c7142e3774fa841aa853bb4d4331b440da8c8514dfc6f/deepdiff-5.8.1.tar.gz
cd dist/working && tar -xzvf deepdiff*
cp -r dist/working/deepdiff-5.8.1/deepdiff dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by deepdiff
@@ -238,16 +295,16 @@ endif
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/bb/41/ad9ce53bb978b68af8ae415293cafc89b165b8ad55a593725299dca76729/xmlschema-1.1.1.tar.gz
cd dist/working && wget https://files.pythonhosted.org/packages/5b/ef/f97c3e1a7efa00e989a793fe15297214fc95ad7d9e3810586bd08ce9f0f3/xmlschema-2.0.2.tar.gz
cd dist/working && tar -xzvf xmlschema*
cp -r dist/working/xmlschema-1.1.1/xmlschema dist/blenderbim/libs/site/packages/
cp -r dist/working/xmlschema-2.0.2/xmlschema dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/12/f9/f9960222d5274944b01391749e55e4dcdf28d8f0c108b64ac931ceff6fdb/elementpath-1.4.3.tar.gz
cd dist/working && wget https://files.pythonhosted.org/packages/11/bc/5afb61dd5d863e5cf77cd952445c50c17e65953405986f19e97e4389692a/elementpath-3.0.2.tar.gz
cd dist/working && tar -xzvf elementpath*
cp -r dist/working/elementpath-1.4.3/elementpath dist/blenderbim/libs/site/packages/
cp -r dist/working/elementpath-3.0.2/elementpath dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
@@ -271,6 +328,19 @@ endif
cp -r dist/working/PyP6Xer-1.13.0/xerparser dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by QTOCalculator
mkdir dist/working
cd dist/working && wget $(SHAPELY_URL)
cd dist/working && cp *.whl shapely.zip && unzip shapely.zip
cp -r dist/working/shapely dist/blenderbim/libs/site/packages/
ifeq ($(PLATFORM), win)
cp -r dist/working/Shapely.libs dist/blenderbim/libs/site/packages/
endif
ifeq ($(PLATFORM), linux)
cp -r dist/working/Shapely.libs dist/blenderbim/libs/site/packages/
endif
rm -rf dist/working
# Required by xerparser and IFC4D
# TODO: remove this dependency. It's only used to show a progress bar.
mkdir dist/working
@@ -281,7 +351,7 @@ endif
# Required by IFCCityJSON
mkdir dist/working
cd dist/working && python -m venv env
cd dist/working && $(PYTHON) -m venv env
cd dist/working && mkdir site-packages
cd dist/working && . env/bin/activate && pip install cjio --target=./site-packages
cd dist/working/site-packages/ && rm -r *dist-info*
@@ -389,16 +459,30 @@ endif
# See bug #1294
cd dist/working/ && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.7.0/src/ifcbimtester/patch/model_core.patch
cd dist/working/ && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.7.0/src/ifcbimtester/patch/runner_util.patch
cd dist/working/ && patch ../blenderbim/libs/site/packages/behave/model_core.py < model_core.patch
cd dist/working/ && patch ../blenderbim/libs/site/packages/behave/runner_util.py < runner_util.patch
cd dist/working/ && $(PATCH) ../blenderbim/libs/site/packages/behave/model_core.py < model_core.patch
cd dist/working/ && $(PATCH) ../blenderbim/libs/site/packages/behave/runner_util.py < runner_util.patch
# See bug #2159
cd dist/working/ && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.7.0/src/ifcbimtester/patch/junit.patch
cd dist/working/ && $(PATCH) ../blenderbim/libs/site/packages/behave/reporter/junit.py < junit.patch
rm -rf dist/working
# Required by ids - uh, not any more, but I'm trying it out in the drawing module
ifeq ($(PLATFORM), macosm1)
# No wheels available for MacOS M1, but turns out this Anaconda build works
mkdir dist/working
cd dist/working && wget $(LXML_URL)
cd dist/working && tar -xf lxml*
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/lxml dist/blenderbim/libs/site/packages/
rm -rf dist/working
else
# Wheels are preferred for other OSes. A first attempt at using Anaconda
# builds for everything shows it breaks on windows. See #2422.
mkdir dist/working
cd dist/working && wget $(LXML_URL)
cd dist/working && cp *.whl lxml.zip && unzip lxml.zip
cp -r dist/working/lxml dist/blenderbim/libs/site/packages/
rm -rf dist/working
endif
# Required by behave
mkdir dist/working
@@ -470,14 +554,14 @@ endif
cp -r dist/working/IFC2JSON_python-master/file_converters/ifcjson dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides OpenLCA fucntionality
# Provides OpenLCA functionality
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/85/33/f91b96e9e8608ff65a003b692e8a9cdd60f2178f60617e5b1d21334c009c/olca-ipc-0.0.10.tar.gz
cd dist/working && tar -xzvf olca-ipc*
cd dist/working/olca-ipc-0.0.10/ && cp -r olca ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Provides Brickschema fucntionality
# Provides Brickschema functionality
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/ca/37/8309da9a72407d2f5eb5489c197ac3cfe96ee3ccba1902d2a0ecc7843f92/brickschema-0.5.1.tar.gz
cd dist/working && tar -xzvf brickschema*
@@ -518,7 +602,7 @@ endif
cd dist/working/ && cp -r pkg_resources ../blenderbim/libs/site/packages/
rm -rf dist/working
cd dist/blenderbim && sed -i "s/999999/$(VERSION)/" __init__.py
cd dist/blenderbim && $(SED) "s/999999/$(VERSION)/" __init__.py
cd dist && zip -r blenderbim-$(VERSION)-$(PYVERSION)-$(PLATFORM).zip ./*
rm -rf dist/blenderbim
@@ -534,11 +618,19 @@ test-core:
.PHONY: test-bim
test-bim:
ifndef MODULE
pytest test/bim
else
pytest test/bim -m "$(MODULE)" ./ --maxfail=1
endif
.PHONY: test-tool
test-tool:
ifndef MODULE
pytest test/tool
else
pytest test/tool/test_$(MODULE).py
endif
.PHONY: qa
qa:
+5 -1
View File
@@ -62,6 +62,7 @@ modules = {
"clash": None,
"lca": None,
"csv": None,
"tester": None,
"bimtester": None,
"diff": None,
"patch": None,
@@ -92,7 +93,9 @@ classes = [
operator.SelectIfcFile,
operator.SelectSchemaDir,
operator.SelectURIAttribute,
operator.EditBlenderCollection,
prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
prop.ObjProperty,
prop.Attribute,
prop.ModuleVisibility,
@@ -131,7 +134,8 @@ for mod in modules.values():
def on_register(scene):
handler.setDefaultProperties(scene)
bpy.app.handlers.depsgraph_update_post.remove(on_register)
if not bpy.app.background:
bpy.app.handlers.depsgraph_update_post.remove(on_register)
def register():
@@ -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));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -16,5 +16,9 @@ DATA;
#9=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
#14=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
#15=IFCSIMPLEPROPERTYTEMPLATE('2sHDBuW7P4TROy$hL2w7ct',$,'Patterns','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
@@ -31,10 +31,11 @@ text { fill: black; stroke: none; }
.PredefinedType-SECTIONLEVEL { marker-start: url(#section-level-marker); stroke-dasharray: 12.5, 3, 3, 3; }
.PredefinedType-PLANLEVEL { marker-end: url(#plan-level-marker); }
.PredefinedType-DIMENSION { marker-start: url(#dimension-marker-start); marker-end: url(#dimension-marker-end); }
.PredefinedType-ANGLE { marker-start: url(#angle-marker-start); marker-end: url(#angle-marker-end); }
.PredefinedType-RADIUS { marker-end: url(#radius-marker-end); }
.PredefinedType-DIAMETER { marker-start: url(#diameter-marker-start); marker-end: url(#diameter-marker-end); }
.PredefinedType-HIDDENLINE { stroke-dasharray: 3, 2; }
.PredefinedType-STAIR { marker-start: url(#stair-marker-start); marker-end: url(#stair-marker-end); }
.PredefinedType-STAIRARROW { marker-start: url(#stair-marker-start); marker-end: url(#stair-marker-end); }
.PredefinedType-BOUNDARY { fill: none; stroke: red; stroke-width: 1; stroke-dasharray: 12, 4, 3, 4, 3, 4; }
.PredefinedType-BATTING { marker-start: url(#batting); stroke: none; }
.PredefinedType-SEALANT { fill: url(#crosshatch1); stroke-width: 0.25; }
@@ -21,6 +21,18 @@
<path d="M 0 3.5 L 0 10.5 L 10 7" class="annotation" style="fill:black;" />
</g>
</marker>
<marker id="angle-marker-start" markerHeight="14" markerWidth="11" orient="auto" refX="1" refY="7">
<g>
<path d="M 11 3.5 L 11 10.5 L 1 7" class="annotation" style="fill:black;" />
<path d="M 1 0 L 1 14" class="annotation" style="stroke-width:1;" />
</g>
</marker>
<marker id="angle-marker-end" markerHeight="14" markerWidth="11" orient="auto" refX="10" refY="7">
<g>
<path d="M 0 3.5 L 0 10.5 L 10 7" class="annotation" style="fill:black;" />
<path d="M 10 0 L 10 14" class="annotation" style="stroke-width:1;" />
</g>
</marker>
<marker id="diameter-marker-start" markerHeight="14" markerWidth="11" orient="auto" refX="1" refY="7">
<g>
<path d="M 11 3.5 L 11 10.5 L 1 7" class="annotation" style="fill:black;" />

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

+2 -12
View File
@@ -97,6 +97,7 @@ class IfcExporter:
except:
continue
ifcopenshell.api.run("root.remove_product", self.file, **{"product": product})
IfcStore.deleted_ids.clear()
return results
def sync_all_objects(self):
@@ -148,7 +149,7 @@ class IfcExporter:
return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s])
def sync_object_placement(self, obj):
if not self.has_object_moved(obj):
if not tool.Ifc.is_moved(obj):
return
blender_matrix = np.array(obj.matrix_world)
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
@@ -162,17 +163,6 @@ class IfcExporter:
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
return element
def has_object_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 sync_grid_axis_object_placement(self, obj, element):
grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
grid_obj = tool.Ifc.get_object(grid)
+73 -5
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
# from datetime import date
import bpy
import json
import math
@@ -38,11 +39,19 @@ def draw_attribute(attribute, layout, copy_operator=None):
if not value_name:
layout.label(text=attribute.name)
return
layout.prop(
attribute,
value_name,
text=attribute.name,
)
if value_name == "enum_value":
prop_with_search(layout, attribute, "enum_value", text=attribute.name)
else:
layout.prop(
attribute,
value_name,
text=attribute.name,
)
if "ScheduleDuration" in attribute.name:
layout.prop(bpy.context.scene.BIMDuration, "duration_days", text="D")
layout.prop(bpy.context.scene.BIMDuration, "duration_hours", text="H")
layout.prop(bpy.context.scene.BIMDuration, "duration_minutes", text="M")
if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
if copy_operator:
@@ -75,6 +84,7 @@ def import_attribute(attribute, props, data, callback=None):
new.is_optional = attribute.optional()
new.data_type = data_type if isinstance(data_type, str) else ""
is_handled_by_callback = callback(attribute.name(), new, data) if callback else None
if is_handled_by_callback:
pass # Our job is done
elif is_handled_by_callback is False:
@@ -105,6 +115,64 @@ def export_attributes(props, callback=None):
return attributes
def prop_with_search(layout, data, prop_name, **kwargs):
# kwargs are layout.prop arguments (text, icon, etc.)
row = layout.row(align=True)
# Magick courtesy of https://blender.stackexchange.com/a/203443/86891
row.context_pointer_set(name="data", data=data)
row.prop(data, prop_name, **kwargs)
op = row.operator("bim.enum_property_search", text="", icon="VIEWZOOM")
op.prop_name = prop_name
def get_enum_items(data, prop_name, context):
# Retrieve items from a dynamic EnumProperty, which is otherwise not supported
# Or throws an error in the console when the items callback returns an empty list
# See https://blender.stackexchange.com/q/215781/86891
prop = data.__annotations__[prop_name]
items = prop.keywords.get("items")
if items is None:
return
if not isinstance(items, (list, tuple)):
# items are retrieved through a callback, not a static list :
items = items(data, context)
return items
def get_obj_ifc_definition_id(context, obj, obj_type):
if obj_type == "Object":
return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "Material":
return bpy.data.materials.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "Task":
return context.scene.BIMTaskTreeProperties.tasks[
context.scene.BIMWorkScheduleProperties.active_task_index
].ifc_definition_id
elif obj_type == "Cost":
return context.scene.BIMCostProperties.cost_items[
context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id
elif obj_type == "Resource":
return context.scene.BIMResourceTreeProperties.resources[
context.scene.BIMResourceProperties.active_resource_index
].ifc_definition_id
elif obj_type == "Profile":
return context.scene.BIMProfileProperties.profiles[
context.scene.BIMProfileProperties.active_profile_index
].ifc_definition_id
elif obj_type == "WorkSchedule":
return context.scene.BIMWorkScheduleProperties.active_work_schedule_id
# hack to close popup
# https://blender.stackexchange.com/a/202576/130742
def close_operator_panel(event):
x, y = event.mouse_x, event.mouse_y
bpy.context.window.cursor_warp(10, 10)
move_back = lambda: bpy.context.window.cursor_warp(x, y)
bpy.app.timers.register(move_back, first_interval=0.01)
class IfcHeaderExtractor:
def __init__(self, filepath: str):
self.filepath = filepath
+33 -3
View File
@@ -39,6 +39,8 @@ class IfcStore:
edited_objs = set()
pset_template_path = ""
pset_template_file = None
classification_path = ""
classification_file = None
library_path = ""
library_file = None
element_listeners = set()
@@ -48,7 +50,7 @@ class IfcStore:
last_transaction = ""
history = []
future = []
schema_identifiers = ["IFC4", "IFC2X3"]
schema_identifiers = ["IFC4", "IFC2X3", "IFC4X3"]
@staticmethod
def purge():
@@ -65,7 +67,10 @@ class IfcStore:
IfcStore.pset_template_file = None
IfcStore.library_path = ""
IfcStore.library_file = None
IfcStore.schema_identifiers = ["IFC4", "IFC2X3"]
IfcStore.last_transaction = ""
IfcStore.history = []
IfcStore.future = []
IfcStore.schema_identifiers = ["IFC4", "IFC2X3", "IFC4X3"]
@staticmethod
def get_file():
@@ -229,7 +234,6 @@ class IfcStore:
data["guid"] = element.GlobalId
IfcStore.commit_link_element(data)
@staticmethod
def relink_all_objects():
if not IfcStore.get_file():
@@ -319,6 +323,21 @@ class IfcStore:
# TODO Listeners are not re-registered. Does this cause nasty problems to debug later on?
# 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):
if "id" not in data or "obj" not in data:
return
obj = bpy.data.objects.get(data["obj"])
IfcStore.id_map[data["id"]] = obj
if data["guid"]:
IfcStore.guid_map[data["guid"]] = obj
@staticmethod
def commit_unlink_element(data):
del IfcStore.id_map[data["id"]]
if data["guid"]:
del IfcStore.guid_map[data["guid"]]
@staticmethod
def unlink_element(element=None, obj=None):
if element is None:
@@ -354,6 +373,17 @@ class IfcStore:
elif obj:
obj.BIMObjectProperties.ifc_definition_id = 0
if IfcStore.history:
data = {}
if element:
data["id"] = element.id()
data["guid"] = getattr(element, "GlobalId", None)
if obj:
data["obj"] = obj.name
IfcStore.history[-1]["operations"].append(
{"rollback": IfcStore.rollback_unlink_element, "commit": IfcStore.commit_unlink_element, "data": data}
)
@staticmethod
def execute_ifc_operator(operator, context):
is_top_level_operator = not bool(IfcStore.current_transaction)
+242 -117
View File
@@ -121,7 +121,14 @@ class MaterialCreator:
def parse_representation_item(self, item):
if not item.StyledByItem:
return
style_ids = [e.id() for e in self.ifc_importer.file.traverse(item.StyledByItem[0]) if e.is_a("IfcSurfaceStyle")]
style_ids = []
styles = list(item.StyledByItem[0].Styles)
while styles:
style = styles.pop()
if style.is_a("IfcSurfaceStyle"):
style_ids.append(style.id())
elif style.is_a("IfcPresentationStyleAssignment"):
styles.extend(style.Styles)
if not style_ids:
return
for style_id in style_ids:
@@ -168,12 +175,12 @@ class IfcImporter:
self.settings_native.set(self.settings_native.INCLUDE_CURVES, True)
self.settings_2d = ifcopenshell.geom.settings()
self.settings_2d.set(self.settings_2d.INCLUDE_CURVES, True)
self.settings_2d.set(self.settings.STRICT_TOLERANCE, True)
self.project = None
self.collections = {}
self.elements = set()
self.type_collection = None
self.type_products = {}
self.openings = {}
self.meshes = {}
self.mesh_shapes = {}
self.time = 0
@@ -181,6 +188,7 @@ class IfcImporter:
self.added_data = {}
self.native_elements = set()
self.native_data = {}
self.progress = 0
self.material_creator = MaterialCreator(ifc_import_settings, self)
@@ -198,7 +206,6 @@ class IfcImporter:
def execute(self):
bpy.context.window_manager.progress_begin(0, 100)
self.progress = 0
self.profile_code("Starting import process")
self.load_file()
self.profile_code("Loading file")
@@ -216,8 +223,6 @@ class IfcImporter:
self.profile_code("Process context filter")
self.create_collections()
self.profile_code("Create collections")
self.create_openings_collection()
self.profile_code("Create opening collection")
self.create_materials()
self.profile_code("Create materials")
self.create_styles()
@@ -236,24 +241,19 @@ class IfcImporter:
self.profile_code("Create spatial elements")
self.create_structural_items()
self.profile_code("Create structural items")
self.create_type_products()
self.profile_code("Create type products")
if not self.ifc_import_settings.is_coordinating:
self.create_element_types()
self.profile_code("Create element types")
self.place_objects_in_collections()
self.profile_code("Place objects in collections")
if self.ifc_import_settings.should_merge_by_class:
self.merge_by_class()
self.profile_code("Merging by class")
elif self.ifc_import_settings.should_merge_by_material:
self.merge_by_material()
self.profile_code("Merging by material")
if self.ifc_import_settings.should_merge_materials_by_colour or len(self.material_creator.materials) > 300:
self.merge_materials_by_colour()
self.profile_code("Merging by colour")
self.add_project_to_scene()
self.profile_code("Add project to scene")
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
self.clean_mesh()
self.profile_code("Mesh cleaning")
if self.ifc_import_settings.should_merge_materials_by_colour or len(self.material_creator.materials) > 300:
self.merge_materials_by_colour()
self.profile_code("Merging by colour")
self.set_default_context()
self.profile_code("Setting default context")
self.update_progress(100)
@@ -269,7 +269,9 @@ class IfcImporter:
def is_point_far_away(self, point, is_meters=True):
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
limit = 1000 if is_meters else (1000 / self.unit_scale)
# Users can configure this if they have to handle larger sites but beware of surveying precision
limit = self.ifc_import_settings.distance_limit
limit = limit if is_meters else (limit / self.unit_scale)
coords = point
if hasattr(point, "Coordinates"):
coords = point.Coordinates
@@ -304,14 +306,30 @@ class IfcImporter:
self.settings_2d.set_context_ids(self.plan_contexts)
def process_element_filter(self):
offset = self.ifc_import_settings.element_offset
offset_limit = offset + self.ifc_import_settings.element_limit
if self.ifc_import_settings.has_filter:
self.elements = set(self.ifc_import_settings.elements)
self.elements = self.ifc_import_settings.elements
if isinstance(self.elements, set):
self.elements = list(self.elements)
# TODO: enable filtering for annotations
self.annotations = set(self.file.by_type("IfcAnnotation"))
else:
self.elements = set(self.file.by_type("IfcElement"))
self.elements = self.file.by_type("IfcElement")
self.annotations = set(self.file.by_type("IfcAnnotation"))
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement")]
if self.ifc_import_settings.is_coordinating:
self.elements = [e for e in self.elements if e.Representation]
self.elements = set(self.elements[offset:offset_limit])
if self.ifc_import_settings.has_filter or offset or offset_limit < len(self.elements):
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
else:
self.element_types = set(self.file.by_type("IfcElementType"))
if self.ifc_import_settings.has_filter and self.ifc_import_settings.should_filter_spatial_elements:
self.spatial_elements = self.get_spatial_elements_filtered_by_elements(self.elements)
else:
@@ -354,21 +372,38 @@ class IfcImporter:
"type": "IfcSweptDiskSolid",
}
return True
if not self.ifc_import_settings.should_use_native_meshes:
return False # Performance improvements only occur on edge cases currently
# FacetedBreps (without voids) are meshes. See #841.
# Commented out as seems currently too slow.
# if self.is_native_faceted_brep(representations):
# self.native_data[element.GlobalId] = {
# "representations": representations,
# "representation": self.get_body_representation(element.Representation.Representations),
# "type": "IfcFacetedBrep",
# }
# return True
if self.is_native_faceted_brep(representations):
self.native_data[element.GlobalId] = {
"representations": representations,
"representation": self.get_body_representation(element.Representation.Representations),
"type": "IfcFacetedBrep",
}
return True
if self.is_native_face_based_surface_model(representations):
self.native_data[element.GlobalId] = {
"representations": representations,
"representation": self.get_body_representation(element.Representation.Representations),
"type": "IfcFaceBasedSurfaceModel",
}
return True
def is_native_swept_disk_solid(self, representations):
for representation in representations:
items = representation["raw"].Items or [] # Be forgiving of invalid IFCs because Revit :(
if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"):
return True
elif (
items[0].is_a("IfcSweptDiskSolid")
and len({i.is_a() for i in items}) == 1
and len({i.Radius for i in items}) == 1
):
return True
return False
def is_native_faceted_brep(self, representations):
@@ -378,6 +413,13 @@ class IfcImporter:
return False
return True
def is_native_face_based_surface_model(self, representations):
for representation in representations:
for i in representation["raw"].Items:
if i.is_a() != "IfcFaceBasedSurfaceModel":
return False
return True
def get_products_from_shape_representation(self, element):
products = [pr.ShapeOfProduct[0] for pr in element.OfProductRepresentation]
for rep_map in element.RepresentationMap:
@@ -391,7 +433,7 @@ class IfcImporter:
props = bpy.context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
return
if self.ifc_import_settings.should_offset_model:
if self.ifc_import_settings.false_origin:
return self.set_manual_blender_offset()
if self.file.schema == "IFC2X3":
project = self.file.by_type("IfcProject")[0]
@@ -407,9 +449,9 @@ class IfcImporter:
def set_manual_blender_offset(self):
props = bpy.context.scene.BIMGeoreferenceProperties
props.blender_eastings = str(self.ifc_import_settings.model_offset_coordinates[0])
props.blender_northings = str(self.ifc_import_settings.model_offset_coordinates[1])
props.blender_orthogonal_height = str(self.ifc_import_settings.model_offset_coordinates[2])
props.blender_eastings = str(self.ifc_import_settings.false_origin[0])
props.blender_northings = str(self.ifc_import_settings.false_origin[1])
props.blender_orthogonal_height = str(self.ifc_import_settings.false_origin[2])
props.has_blender_offset = True
def guess_georeferencing(self, element):
@@ -518,6 +560,9 @@ class IfcImporter:
if grid.Representation:
shape = ifcopenshell.geom.create_shape(self.settings_2d, grid)
grid_obj = self.create_product(grid, shape)
if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import:
grid_obj.lock_location = (True, True, True)
grid_obj.lock_rotation = (True, True, True)
collection = bpy.data.collections.new(self.get_name(grid))
u_axes = bpy.data.collections.new("UAxes")
collection.children.link(u_axes)
@@ -535,23 +580,20 @@ class IfcImporter:
shape = ifcopenshell.geom.create_shape(self.settings_2d, axis.AxisCurve)
mesh = self.create_mesh(axis, shape)
obj = bpy.data.objects.new(f"IfcGridAxis/{axis.AxisTag}", mesh)
if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import:
obj.lock_location = (True, True, True)
obj.lock_rotation = (True, True, True)
self.link_element(axis, obj)
self.set_matrix_world(obj, grid_obj.matrix_world)
grid_collection.objects.link(obj)
def create_type_products(self):
# TODO allow filtering of spatial elements too
if self.ifc_import_settings.has_filter:
type_products = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
else:
type_products = self.file.by_type("IfcTypeProduct")
for type_product in type_products:
if not type_product:
def create_element_types(self):
for element_type in self.element_types:
if not element_type:
continue
self.create_type_product(type_product)
self.create_element_type(element_type)
def create_type_product(self, element):
def create_element_type(self, element):
self.ifc_import_settings.logger.info("Creating object %s", element)
representation_map = self.get_type_product_body_representation_map(element)
mesh = None
@@ -585,13 +627,20 @@ class IfcImporter:
return representation_map
def create_native_elements(self):
total = 0
progress = 0
checkpoint = time.time()
total = len(self.native_elements)
for element in self.native_elements:
total += 1
if total % 250 == 0:
print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint))
progress += 1
if progress % 250 == 0:
percent = round(progress / total * 100)
print(
"{} / {} ({}%) elements processed in {:.2f}s ...".format(
progress, total, percent, time.time() - checkpoint
)
)
checkpoint = time.time()
self.incrementally_merge_objects()
native_data = self.native_data[element.GlobalId]
representation = native_data["representation"]
if not representation:
@@ -604,6 +653,8 @@ class IfcImporter:
mesh = self.create_native_swept_disk_solid(element, mesh_name)
elif native_data["type"] == "IfcFacetedBrep":
mesh = self.create_native_faceted_brep(element, mesh_name)
elif native_data["type"] == "IfcFaceBasedSurfaceModel":
mesh = self.create_native_faceted_brep(element, mesh_name)
mesh.BIMMeshProperties.ifc_definition_id = representation.id()
mesh.name = mesh_name
self.meshes[mesh_name] = mesh
@@ -643,26 +694,32 @@ class IfcImporter:
)
else:
iterator = ifcopenshell.geom.iterator(self.settings, self.file, include=products)
cache = IfcStore.get_cache()
if cache:
iterator.set_cache(cache)
if self.ifc_import_settings.should_cache:
cache = IfcStore.get_cache()
if cache:
iterator.set_cache(cache)
valid_file = iterator.initialize()
if not valid_file:
return results
checkpoint = time.time()
total = 0
progress = 0
total = len(products)
start_progress = self.progress
progress_range = 85 - start_progress
while True:
total += 1
if total % 250 == 0:
progress += 1
if progress % 250 == 0:
percent_created = round(progress / total * 100)
percent_preprocessed = iterator.progress()
percent_average = (percent_created + percent_preprocessed) / 2
print(
"{} ({}%) elements processed in {:.2f}s ...".format(
total, iterator.progress(), time.time() - checkpoint
"{} / {} ({}% created, {}% preprocessed) elements processed in {:.2f}s ...".format(
progress, total, percent_created, percent_preprocessed, time.time() - checkpoint
)
)
checkpoint = time.time()
self.update_progress((iterator.progress() / 100 * progress_range) + start_progress)
self.update_progress((percent_average / 100 * progress_range) + start_progress)
self.incrementally_merge_objects()
shape = iterator.get()
if shape:
product = self.file.by_id(shape.guid)
@@ -681,6 +738,19 @@ class IfcImporter:
print("Done creating geometry")
return results
def incrementally_merge_objects(self):
if not self.ifc_import_settings.is_coordinating:
return
if self.ifc_import_settings.merge_mode == "IFC_CLASS":
self.merge_by_class()
self.profile_code("Merging by class")
elif self.ifc_import_settings.merge_mode == "IFC_TYPE":
self.merge_by_type()
self.profile_code("Merging by type")
elif self.ifc_import_settings.merge_mode == "MATERIAL":
self.merge_by_material()
self.profile_code("Merging by material")
def create_structural_items(self):
# Create structural collections
self.structural_member_collection = bpy.data.collections.new("Members")
@@ -791,9 +861,10 @@ class IfcImporter:
)
else:
iterator = ifcopenshell.geom.iterator(self.settings_2d, self.file, include=products)
cache = IfcStore.get_cache()
if cache:
iterator.set_cache(cache)
if self.ifc_import_settings.should_cache:
cache = IfcStore.get_cache()
if cache:
iterator.set_cache(cache)
valid_file = iterator.initialize()
if not valid_file:
return results
@@ -807,7 +878,7 @@ class IfcImporter:
shape = iterator.get()
if shape:
product = self.file.by_id(shape.guid)
self.create_product(self.file.by_id(shape.guid), shape)
self.create_product(product, shape)
results.add(product)
if not iterator.next():
break
@@ -825,7 +896,7 @@ class IfcImporter:
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
mesh = self.create_camera(element, shape)
self.link_mesh(shape, mesh)
elif element.is_a("IfcAnnotation") and self.is_curve_annotation(element):
elif element.is_a("IfcAnnotation") and self.is_curve_annotation(element) and shape:
mesh = self.create_curve(element, shape)
self.link_mesh(shape, mesh)
elif shape:
@@ -855,17 +926,18 @@ class IfcImporter:
elif hasattr(element, "ObjectPlacement"):
self.set_matrix_world(obj, self.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element)))
self.add_opening_relation(element, obj)
if element.is_a("IfcOpeningElement"):
obj.display_type = "WIRE"
return obj
def get_representation_item_material_name(self, item):
if not item.StyledByItem:
return
style_ids = [e.id() for e in self.file.traverse(item.StyledByItem[0]) if e.is_a("IfcSurfaceStyle")]
return style_ids[0] if style_ids else None
styles = list(item.StyledByItem[0].Styles)
while styles:
style = styles.pop()
if style.is_a("IfcSurfaceStyle"):
return style.id()
elif style.is_a("IfcPresentationStyleAssignment"):
styles.extend(style.Styles)
def create_native_faceted_brep(self, element, mesh_name):
# TODO: georeferencing?
@@ -914,22 +986,32 @@ class IfcImporter:
self.convert_representation(item.MappingSource.MappedRepresentation)
elif item.is_a() == "IfcFacetedBrep":
self.convert_representation_item_faceted_brep(item)
elif item.is_a() == "IfcFaceBasedSurfaceModel":
self.convert_representation_item_face_based_surface_model(item)
def convert_representation_item_face_based_surface_model(self, item):
mesh = item.get_info_2(recursive=True)
for face_set in mesh["FbsmFaces"]:
self.convert_representation_item_face_set(item, face_set)
def convert_representation_item_faceted_brep(self, item):
mesh = item.get_info_2(recursive=True)
return self.convert_representation_item_face_set(item, mesh["Outer"])
def convert_representation_item_face_set(self, item, mesh):
# On a few occasions, we flatten a list. This seems to be the most efficient way to do it.
# https://stackoverflow.com/questions/20112776/how-do-i-flatten-a-list-of-lists-nested-lists
mesh = item.get_info_2(recursive=True)
# For huge face sets it might be better to do a "flatmap" instead of sum()
# bounds = sum((f["Bounds"] for f in mesh["Outer"]["CfsFaces"] if len(f["Bounds"]) == 1), ())
bounds = tuple(chain.from_iterable(f["Bounds"] for f in mesh["Outer"]["CfsFaces"] if len(f["Bounds"]) == 1))
bounds = tuple(chain.from_iterable(f["Bounds"] for f in mesh["CfsFaces"] if len(f["Bounds"]) == 1))
# Here are some untested alternatives, are they faster?
# bounds = tuple((f["Bounds"] for f in mesh["Outer"]["CfsFaces"] if len(f["Bounds"]) == 1))[0]
# bounds = chain.from_iterable(f["Bounds"] for f in mesh["Outer"]["CfsFaces"] if len(f["Bounds"]) == 1)
polygons = [[(p["id"], p["Coordinates"]) for p in b["Bound"]["Polygon"]] for b in bounds]
for face in mesh["Outer"]["CfsFaces"]:
for face in mesh["CfsFaces"]:
# Blender cannot handle faces with holes.
if len(face["Bounds"]) > 1:
inner_bounds = []
@@ -1027,6 +1109,7 @@ class IfcImporter:
polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v2)
curve.bevel_depth = self.unit_scale * item.Radius
curve.use_fill_caps = True
return curve
def create_native_annotation(self, element, mesh_name):
@@ -1059,33 +1142,74 @@ class IfcImporter:
def merge_by_class(self):
merge_set = {}
for obj in self.added_data.values():
if not isinstance(obj, bpy.types.Object):
id_set = {}
for ifc_definition_id, obj in self.added_data.items():
if not isinstance(obj, bpy.types.Object) or not obj.data:
continue
if "/" not in obj.name or "IfcRelAggregates" in obj.users_collection[0].name:
element = self.file.by_id(ifc_definition_id)
if not element.is_a("IfcElement"):
continue
merge_set.setdefault(obj.name.split("/")[0], []).append(obj)
self.merge_objects(merge_set)
merge_set.setdefault(element.is_a(), []).append(obj)
id_set.setdefault(element.is_a(), []).append(ifc_definition_id)
self.merge_objects(merge_set, id_set)
def merge_by_type(self):
merge_set = {}
id_set = {}
for ifc_definition_id, obj in self.added_data.items():
if not isinstance(obj, bpy.types.Object) or not obj.data:
continue
element = self.file.by_id(ifc_definition_id)
if not element.is_a("IfcElement"):
continue
element_type = ifcopenshell.util.element.get_type(element)
if not element_type:
continue
merge_key = str(element_type.id()) + "-" + element_type.Name or "Unnamed"
merge_set.setdefault(merge_key, []).append(obj)
id_set.setdefault(merge_key, []).append(ifc_definition_id)
self.merge_objects(merge_set, id_set)
def merge_by_material(self):
merge_set = {}
for obj in self.added_data.values():
if not isinstance(obj, bpy.types.Object):
id_set = {}
for ifc_definition_id, obj in self.added_data.items():
if not isinstance(obj, bpy.types.Object) or not obj.data:
continue
if "/" not in obj.name or "IfcRelAggregates" in obj.users_collection[0].name:
element = self.file.by_id(ifc_definition_id)
if not element.is_a("IfcElement"):
continue
if not obj.material_slots:
merge_set.setdefault("no-material", []).append(obj)
else:
merge_set.setdefault(obj.material_slots[0].name, []).append(obj)
self.merge_objects(merge_set)
merge_key = obj.material_slots[0].name if obj.material_slots else "no-material"
merge_set.setdefault(merge_key, []).append(obj)
id_set.setdefault(merge_key, []).append(ifc_definition_id)
self.merge_objects(merge_set, id_set)
def merge_objects(self, merge_set):
for ifc_class, objs in merge_set.items():
context_override = {}
context_override["object"] = context_override["active_object"] = objs[0]
context_override["selected_objects"] = context_override["selected_editable_objects"] = objs
bpy.ops.object.join(context_override)
def merge_objects(self, merge_set, id_set):
total_objs = sum([len(o) for o in merge_set.values()])
cumulative_total = 0
merge_set = {k: v for k, v in sorted(merge_set.items(), key=lambda i: len(i[1]), reverse=True)}
for group_name, objs in merge_set.items():
total_group_objs = len(objs)
if total_group_objs < 10:
continue
merge_potential = total_objs - cumulative_total
if merge_potential < 250:
print("Merge target achieved")
return
cumulative_total += total_group_objs
print(f"Merging {total_group_objs} objects, {merge_potential} potentially remaining -", group_name)
try:
target = objs[0]
target.data = target.data.copy()
context_override = {}
context_override["object"] = context_override["active_object"] = target
context_override["selected_objects"] = context_override["selected_editable_objects"] = objs
bpy.ops.object.join(context_override)
target.data.name += "-merge"
for ifc_definition_id in id_set[group_name][1:]:
del self.added_data[ifc_definition_id]
except:
print("Merge failed")
def merge_materials_by_colour(self):
cleaned_materials = {}
@@ -1117,7 +1241,6 @@ class IfcImporter:
# Occurs when reloading a project
pass
project_collection = bpy.context.view_layer.layer_collection.children[self.project["blender"].name]
project_collection.children[self.opening_collection.name].hide_viewport = True
project_collection.children[self.type_collection.name].hide_viewport = True
def clean_mesh(self):
@@ -1140,11 +1263,6 @@ class IfcImporter:
bpy.ops.object.editmode_toggle(context_override)
IfcStore.edited_objs.clear()
def add_opening_relation(self, element, obj):
if not element.is_a("IfcOpeningElement"):
return
self.openings[element.GlobalId] = obj
def load_file(self):
self.ifc_import_settings.logger.info("loading file %s", self.ifc_import_settings.input_file)
if not bpy.context.scene.BIMProperties.ifc_file:
@@ -1275,10 +1393,6 @@ class IfcImporter:
if parent:
self.collections[parent.GlobalId].children.link(aggregate["collection"])
def create_openings_collection(self):
self.opening_collection = bpy.data.collections.new("IfcOpeningElements")
self.project["blender"].children.link(self.opening_collection)
def create_materials(self):
for material in self.file.by_type("IfcMaterial"):
self.create_material(material)
@@ -1295,11 +1409,18 @@ class IfcImporter:
for material_definition_representation in self.file.by_type("IfcMaterialDefinitionRepresentation"):
material = material_definition_representation.RepresentedMaterial
blender_material = self.material_creator.materials[material.id()]
for representation in material_definition_representation.Representations:
for style in [e for e in self.file.traverse(representation) if e.is_a("IfcSurfaceStyle")]:
blender_material = self.material_creator.materials[material.id()]
self.create_style(style, blender_material)
parsed_styles.add(style.id())
styles = []
for styled_item in representation.Items:
styles.extend(styled_item.Styles)
while styles:
style = styles.pop()
if style.is_a("IfcSurfaceStyle"):
self.create_style(style, blender_material)
parsed_styles.add(style.id())
elif style.is_a("IfcPresentationStyleAssignment"):
styles.extend(style.Styles)
for style in self.file.by_type("IfcSurfaceStyle"):
if style.id() in parsed_styles:
@@ -1516,8 +1637,6 @@ class IfcImporter:
return self.collections[element.GlobalId].objects.link(obj)
elif element.is_a("IfcTypeObject"):
return self.type_collection.objects.link(obj)
elif element.is_a("IfcOpeningElement"):
return self.opening_collection.objects.link(obj)
elif element.is_a("IfcStructuralMember"):
return self.structural_member_collection.objects.link(obj)
elif element.is_a("IfcStructuralConnection"):
@@ -1828,7 +1947,7 @@ class IfcImporter:
def set_default_context(self):
for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"):
if subcontext.ContextIdentifier == "Body":
bpy.context.scene.BIMProperties.contexts = str(subcontext.id())
bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
break
def link_element(self, element, obj):
@@ -1857,14 +1976,18 @@ class IfcImportSettings:
self.input_file = None
self.diff_file = None
self.should_use_cpu_multiprocessing = True
self.should_merge_by_class = False
self.should_merge_by_material = False
self.merge_mode = None
self.should_merge_materials_by_colour = False
self.should_use_native_meshes = False
self.should_clean_mesh = True
self.should_cache = True
self.is_coordinating = True
self.deflection_tolerance = 0.001
self.angular_tolerance = 0.5
self.should_offset_model = False
self.model_offset_coordinates = (0, 0, 0)
self.distance_limit = 1000
self.false_origin = None
self.element_offset = 0
self.element_limit = 30000
self.has_filter = None
self.should_filter_spatial_elements = True
self.elements = set()
@@ -1882,16 +2005,18 @@ class IfcImportSettings:
settings.diff_file = scene_diff.diff_json_file
settings.collection_mode = props.collection_mode
settings.should_use_cpu_multiprocessing = props.should_use_cpu_multiprocessing
settings.should_merge_by_class = props.should_merge_by_class
settings.should_merge_by_material = props.should_merge_by_material
settings.merge_mode = props.merge_mode
settings.should_merge_materials_by_colour = props.should_merge_materials_by_colour
settings.should_use_native_meshes = props.should_use_native_meshes
settings.should_clean_mesh = props.should_clean_mesh
settings.should_cache = props.should_cache
settings.is_coordinating = props.is_coordinating
settings.deflection_tolerance = props.deflection_tolerance
settings.angular_tolerance = props.angular_tolerance
settings.should_offset_model = props.should_offset_model
settings.model_offset_coordinates = (
[float(o) for o in props.model_offset_coordinates.split(",")]
if props.model_offset_coordinates
else (0, 0, 0)
)
settings.distance_limit = props.distance_limit
settings.false_origin = [float(o) for o in props.false_origin.split(",")] if props.false_origin else None
if settings.false_origin == [0, 0, 0]:
settings.false_origin = None
settings.element_offset = props.element_offset
settings.element_limit = props.element_limit
return settings
@@ -768,6 +768,8 @@ class ActivateBcfViewpoint(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
props = context.scene.BCFProperties
blender_topic = props.active_topic
if blender_topic is None:
return False
topic = bcfxml.topics[blender_topic.name]
return topic.viewpoints
@@ -880,7 +882,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
# Operators with context overrides are used because they are
# significantly faster than looping through all objects
exception_global_ids = [v.ifc_guid for v in viewpoint.components.visibility.exceptions]
exception_global_ids = {v.ifc_guid for v in viewpoint.components.visibility.exceptions}
if viewpoint.components.visibility.default_visibility:
old = context.area.type
@@ -95,7 +95,7 @@ def getBcfViewpoints(self, context, force_update=False):
props = context.scene.BCFProperties
bcfxml = bcfstore.BcfStore.get_bcfxml()
topic = props.active_topic
viewpoints = bcfxml.get_viewpoints(topic.name)
viewpoints = bcfxml.get_viewpoints(topic.name) if topic else {}
bcfviewpoints_enum.extend([(v, f"Viewpoint {i+1}", "") for i, v in enumerate(viewpoints.keys())])
return bcfviewpoints_enum
@@ -62,7 +62,7 @@ def refreshScenarios(self, context):
class BimTesterProperties(PropertyGroup):
feature: StringProperty(default="", name="Feature / IDS", update=refreshScenarios)
feature: StringProperty(default="", name="Feature", update=refreshScenarios)
steps: StringProperty(default="", name="Custom Steps")
ifc_file: StringProperty(default="", name="IFC File")
audit_ifc_class: EnumProperty(items=get_ifc_classes, name="Audit Class")
@@ -49,7 +49,7 @@ class Loader:
if not boundary.ConnectionGeometry:
return None
surface = boundary.ConnectionGeometry.SurfaceOnRelatingElement
# workaround for unvalid geometry provided by Revit. See https://github.com/IfcOpenShell/IfcOpenShell/issues/635#issuecomment-770366838
# workaround for invalid geometry provided by Revit. See https://github.com/IfcOpenShell/IfcOpenShell/issues/635#issuecomment-770366838
if surface.is_a("IfcCurveBoundedPlane") and not getattr(surface, "InnerBoundaries", None):
surface.InnerBoundaries = ()
shape = ifcopenshell.geom.create_shape(self.settings, surface)
@@ -18,6 +18,7 @@
import blenderbim.tool as tool
from bpy.types import Panel, UIList
from blenderbim.bim.helper import prop_with_search
from blenderbim.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData
@@ -48,7 +49,8 @@ class BIM_PT_brickschema(Panel):
row.operator("bim.close_brick_project", text="", icon="CANCEL")
row = self.layout.row(align=True)
row.prop(self.props, "namespace", text="")
prop_with_search(row, self.props, "namespace", text="")
prop_with_search(row, self.props, "brick_equipment_class", text="")
row.prop(self.props, "brick_equipment_class", text="")
row.operator("bim.add_brick", text="", icon="ADD")
@@ -101,7 +103,7 @@ class BIM_PT_ifc_brickschema_references(Panel):
return
row = self.layout.row(align=True)
row.prop(self.props, "libraries")
prop_with_search(row, self.props, "libraries")
row.operator("bim.convert_brick_project", text="", icon="ADD")
row = self.layout.row(align=True)
@@ -17,12 +17,20 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from . import operator, workspace
from . import operator, prop, workspace
classes = (
operator.CadTrimExtend,
operator.CadMitre,
operator.AlignViewToProfile,
operator.AddIfcArcIndexFillet,
operator.AddIfcCircle,
operator.AddRectangle,
operator.CadArcFrom2Points,
operator.CadArcFrom3Points,
operator.CadFillet,
operator.CadMitre,
operator.CadOffset,
operator.CadTrimExtend,
prop.BIMCadProperties,
workspace.CadHotkey,
)
@@ -30,8 +38,10 @@ classes = (
def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.CadTool, after={"builtin.transform"}, separator=True, group=True)
bpy.types.Scene.BIMCadProperties = bpy.props.PointerProperty(type=prop.BIMCadProperties)
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.CadTool)
del bpy.types.Scene.BIMCadProperties
@@ -20,7 +20,10 @@ import bpy
import math
import bmesh
import mathutils
import bpy_extras
import blenderbim.tool as tool
from mathutils import Vector, Matrix
from math import pi, radians, sin, cos, sqrt
messages = {
"SHARED_VERTEX": "Shared Vertex, no intersection possible",
@@ -39,7 +42,6 @@ class CadTrimExtend(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def cancel_message(self, msg):
print(msg)
self.report({"WARNING"}, msg)
return {"CANCELLED"}
@@ -68,10 +70,10 @@ class CadTrimExtend(bpy.types.Operator):
else:
return self.cancel_message("select two edges!")
bmesh.ops.remove_doubles(bm, verts=list(set(edges[0].verts) | set(edges[1].verts)), dist=1e-5)
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(me, loop_triangles=True)
return {"FINISHED"}
@@ -85,7 +87,6 @@ class CadMitre(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def cancel_message(self, msg):
print(msg)
self.report({"WARNING"}, msg)
return {"CANCELLED"}
@@ -114,10 +115,10 @@ class CadMitre(bpy.types.Operator):
else:
return self.cancel_message("select two edges!")
bmesh.ops.remove_doubles(bm, verts=list(set(edges[0].verts) | set(edges[1].verts)), dist=1e-5)
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(me, loop_triangles=True)
return {"FINISHED"}
@@ -139,8 +140,9 @@ class CadFillet(bpy.types.Operator):
layout.prop(self, prop)
def execute(self, context):
if bpy.context.mode != "EDIT_MESH":
return {"CANCELLED"}
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
obj = bpy.context.active_object
cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location
mesh = obj.data
@@ -202,6 +204,610 @@ class CadFillet(bpy.types.Operator):
chamfer_edge = [e for e in bm.edges if shared_vert in e.verts and v2 in e.verts][0]
bm.edges.remove(chamfer_edge)
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(mesh)
return {"FINISHED"}
class CadArcFrom2Points(bpy.types.Operator):
bl_idname = "bim.cad_arc_from_2_points"
bl_label = "CAD Arc from 2 Points"
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False)
def draw(self, context):
layout = self.layout
for prop in self.__class__.__annotations__.keys():
layout.prop(self, prop)
def execute(self, context):
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
if bpy.context.mode != "EDIT_MESH":
return {"CANCELLED"}
obj = bpy.context.active_object
region = bpy.context.region
region_3d = bpy.context.area.spaces.active.region_3d
cursor = bpy.context.scene.cursor.location
center = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, cursor)
if not center:
return {"CANCELLED"}
mesh = obj.data
mw = obj.matrix_world
bm = bmesh.from_edit_mesh(mesh)
selected_verts = [v for v in bm.verts if v.select]
if len(selected_verts) != 2:
return {"CANCELLED"}
v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[0].co)
v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co)
l1 = v1 - center
l2 = v2 - center
angle = l1.angle_signed(l2)
if self.should_flip:
if angle > 0:
angle = -((math.pi * 2) - angle)
else:
angle = (math.pi * 2) + angle
v = selected_verts[0]
bm.verts.remove(selected_verts[1])
axis = region_3d.view_rotation @ Vector((0, 0, 1))
bmesh.ops.spin(
bm,
geom=[v],
axis=mw.inverted().to_quaternion() @ axis,
cent=mw.inverted() @ cursor,
steps=self.resolution * 4,
angle=-angle,
)
bmesh.update_edit_mesh(mesh)
mesh.update()
return {"FINISHED"}
class CadArcFrom3Points(bpy.types.Operator):
bl_idname = "bim.cad_arc_from_3_points"
bl_label = "CAD Arc from 3 Points"
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
only_recalculate_center: bpy.props.BoolProperty(name="Only Recalculate Center", default=False)
@classmethod
def poll(cls, context):
obj = context.active_object
return obj and obj.type == "MESH" and context.mode == "EDIT_MESH"
def draw(self, context):
layout = self.layout
for prop in self.__class__.__annotations__.keys():
layout.prop(self, prop)
def execute(self, context):
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
if bpy.context.mode != "EDIT_MESH":
return {"CANCELLED"}
obj = bpy.context.edit_object
mesh = obj.data
bm = bmesh.from_edit_mesh(mesh)
arc = [v for v in bm.verts if v.select]
if len(arc) != 3:
return {"CANCELLED"}
elif len([e for e in bm.edges if e.select]) != 2:
return {"CANCELLED"}
sorted_arc = [None, None, None]
for v1 in arc:
connections = 0
for link_edge in v1.link_edges:
v2 = link_edge.other_vert(v1)
if v2 in arc:
connections += 1
if connections == 2: # Midpoint
sorted_arc[1] = v1
else:
sorted_arc[2 if sorted_arc[2] is None else 0] = v1
points = [tuple(v.co) for v in sorted_arc]
verts, _ = tool.Cad.create_arc_segments(points, num_verts=(self.resolution * 4) + 1, make_edges=False)
bm.verts.index_update()
bm.edges.index_update()
index_offset = len(bm.verts)
new_verts = [bm.verts.new(v) for v in verts]
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.verts.remove(sorted_arc[1])
bmesh.ops.remove_doubles(bm, verts=new_verts + [sorted_arc[0], sorted_arc[2]], dist=1e-5)
bmesh.update_edit_mesh(mesh)
return {"FINISHED"}
class CadOffset(bpy.types.Operator):
bl_idname = "bim.cad_offset"
bl_label = "CAD Offset"
bl_options = {"REGISTER", "UNDO"}
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
@classmethod
def poll(self, context):
return context.mode == "EDIT_MESH"
def draw(self, context):
layout = self.layout
for prop in self.__class__.__annotations__.keys():
layout.prop(self, prop)
def cancel_message(self, msg):
self.report({"WARNING"}, msg)
return {"CANCELLED"}
def execute(self, context):
# This is not the same as Mesh Tools or Sverchok. Mesh Tools is based on
# a 3D "rail" which allows fancy 3D edge offsets, but will get different
# results to traditional CAD, which expects offsets to occur on a
# defined 2D workplane. This CAD offset does a pure 2D operation, so it
# always looks correct from the desired "2D workplane". This also
# reduces the settings down to one parameter instead of the huge config
# dialog that Mesh Tools has. Sverchok is 2D based, but has a weird side
# effect of converting an unclosed loop into a closed loop which is also
# not what users expect.
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
obj = context.active_object
mw = obj.matrix_world
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
edges = [e for e in bm.edges if e.select and not e.hide]
if len(edges) < 1:
return self.cancel_message("NO_EDGES")
verts = set()
[verts.update(e.verts) for e in edges]
# Use the viewport angle to determine the offset direction
for area in bpy.context.screen.areas:
if area.type == "VIEW_3D":
# Don't ask me, I don't know.
# view_matrix = area.spaces.active.region_3d.view_matrix
x = area.spaces.active.region_3d.view_rotation @ Vector((1, 0, 0))
y = area.spaces.active.region_3d.view_rotation @ Vector((0, 1, 0))
z = area.spaces.active.region_3d.view_rotation @ Vector((0, 0, 1))
wp = Matrix([x, y, z, Vector((0, 0, 0))]).to_4x4().transposed()
break
rotation = Matrix.Rotation(pi / 2, 2, "Z")
rotation_i = Matrix.Rotation(-pi / 2, 2, "Z")
# Create loops from edges
loop_edges = set(edges)
loops = []
while loop_edges:
edge = loop_edges.pop()
loop = [edge]
has_found_connected_edge = True
while has_found_connected_edge:
has_found_connected_edge = False
for edge in loop_edges.copy():
edge_verts = set(edge.verts)
if edge_verts & set(loop[0].verts):
loop.insert(0, edge)
loop_edges.remove(edge)
has_found_connected_edge = True
elif edge_verts & set(loop[-1].verts):
loop.append(edge)
loop_edges.remove(edge)
has_found_connected_edge = True
loops.append(loop)
for loop in loops:
all_verts = {v.index for e in loop for v in e.verts}
possible_v1s = []
is_closed = True
for edge in loop:
# If we have an open loop, start at either end instead
v = edge.verts[0]
if len(v.link_edges) == 1:
possible_v1s.append(v)
is_closed = False
else:
for link_edge in v.link_edges:
if link_edge.other_vert(v).index not in all_verts:
possible_v1s.append(v)
is_closed = False
break
v = edge.verts[1]
if len(v.link_edges) == 1:
possible_v1s.append(v)
is_closed = False
else:
for link_edge in v.link_edges:
if link_edge.other_vert(v).index not in all_verts:
possible_v1s.append(v)
is_closed = False
break
# Always start at the same vertex, so that when the operator is
# refreshed with new parameters the axes don't randomly get flipped.
if is_closed:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
start = bm.verts[min(all_verts)]
else:
start = possible_v1s[0] if possible_v1s[0].index < possible_v1s[1].index else possible_v1s[1]
v1 = start
new_verts = []
processed_verts = set()
# [v0] --> v1 --> v2
while v1:
if v1.index in processed_verts:
break
link_verts = []
if len(v1.link_edges) == 1:
v = v1.link_edges[0].other_vert(v1)
link_verts.append(v) if v.index in all_verts else None
else:
v = v1.link_edges[0].other_vert(v1)
link_verts.append(v) if v.index in all_verts else None
v = v1.link_edges[1].other_vert(v1)
link_verts.append(v) if v.index in all_verts else None
if len(link_verts) == 1:
v2 = link_verts[0]
v0 = None if v2.index not in processed_verts else v2
v2 = None if v2.index in processed_verts else v2
else:
v2 = link_verts[0]
v0 = link_verts[1]
if v2.index in processed_verts:
if is_closed and v0.index in processed_verts:
v3 = start
v0 = v0 if v2 == v3 else v2
v2 = v3
else:
v0, v2 = v2, v0
normals = []
v1co = (wp.inverted() @ mw @ v1.co).to_2d()
if v2:
v2co = (wp.inverted() @ mw @ v2.co).to_2d()
direction = (v2co - v1co).normalized()
local_direction = (rotation @ direction).normalized()
normals.append(local_direction)
if v0:
v0co = (wp.inverted() @ mw @ v0.co).to_2d()
direction = (v0co - v1co).normalized()
local_direction = (rotation_i @ direction).normalized()
normals.append(local_direction)
if len(normals) == 2:
# https://stackoverflow.com/a/54042831/9627415
new_normal = (normals[0].lerp(normals[1], 0.5)).normalized()
offset_length = self.distance / sqrt((1 + normals[0].dot(normals[1])) / 2)
offset = mw.inverted().to_quaternion() @ (wp.to_quaternion() @ (new_normal * offset_length).to_3d())
new_vert = v1.co + offset
new_verts.append(bm.verts.new(new_vert))
else:
normal = (normals[0] * self.distance).to_3d()
offset = mw.inverted().to_quaternion() @ (wp.to_quaternion() @ normal)
new_vert = v1.co + offset
new_verts.append(bm.verts.new(new_vert))
processed_verts.add(v1.index)
if v2 in processed_verts:
break
v1 = v2
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
if is_closed:
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
return {"FINISHED"}
class AddIfcCircle(bpy.types.Operator):
bl_idname = "bim.add_ifccircle"
bl_label = "Add IfcCircle"
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
@classmethod
def poll(cls, context):
obj = context.active_object
return bool(obj) and obj.type == "MESH"
def execute(self, context):
if self.has_selected_existing_circle(context):
self.change_radius(context)
else:
self.create_circle(context)
return {"FINISHED"}
def has_selected_existing_circle(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
verts = [v for v in bm.verts if v.select and not v.hide]
if len(verts) != 2:
return False
groups = []
for i, group in enumerate(obj.vertex_groups):
if "IFCCIRCLE" in group.name:
groups.append(i)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
for group in groups:
if group in verts[0][deform_layer] and group in verts[1][deform_layer]:
return True
def change_radius(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
verts = [v for v in bm.verts if v.select and not v.hide]
center = verts[0].co.lerp(verts[1].co, 0.5)
verts[0].co = center + ((verts[0].co - center).normalized() * self.radius)
verts[1].co = center + ((verts[1].co - center).normalized() * self.radius)
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
def create_circle(self, context):
obj = context.active_object
bpy.ops.object.mode_set(mode="OBJECT")
# The last group may be the result of a prior run of this operator.
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
if len(obj.vertex_groups):
last_group = obj.vertex_groups[-1]
verts_in_group = [v for v in obj.data.vertices if last_group.index in [vg.group for vg in v.groups]]
if "IFCCIRCLE" in last_group.name and len(verts_in_group) != 2:
obj.vertex_groups.remove(last_group)
group = obj.vertex_groups.new(name="IFCCIRCLE")
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
center = obj.matrix_world.inverted() @ context.scene.cursor.location
v1 = bm.verts.new((center[0], center[1] + self.radius, 0.0))
v2 = bm.verts.new((center[0], center[1] - self.radius, 0.0))
bm.verts.index_update()
bm.edges.new((v1, v2))
for vert in [v1, v2]:
vert[deform_layer][group.index] = 1
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
class AddIfcArcIndexFillet(bpy.types.Operator):
bl_idname = "bim.add_ifcarcindex_fillet"
bl_label = "Add Arc Index Fillet"
bl_options = {"REGISTER", "UNDO"}
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
@classmethod
def poll(cls, context):
obj = context.active_object
return obj and obj.type == "MESH" and context.mode == "EDIT_MESH"
def draw(self, context):
layout = self.layout
for prop in self.__class__.__annotations__.keys():
layout.prop(self, prop)
def execute(self, context):
if self.has_selected_existing_arc(context):
self.change_radius(context)
else:
self.create_arc(context)
return {"FINISHED"}
def has_selected_existing_arc(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
verts = [v for v in bm.verts if v.select and not v.hide]
if len(verts) != 3:
return False
groups = []
for i, group in enumerate(obj.vertex_groups):
if "IFCARCINDEX" in group.name:
groups.append(i)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
for group in groups:
try:
if group in verts[0][deform_layer] and group in verts[1][deform_layer]:
return True
except:
pass # Potentially fail if the vert has been removed in the previous operation
def change_radius(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
edges = [e for e in bm.edges if e.select and not e.hide]
mid = list(set(edges[0].verts) & set(edges[1].verts))[0]
v1 = edges[0].other_vert(mid)
v2 = edges[1].other_vert(mid)
center = tool.Cad.get_center_of_arc([v1.co, mid.co, v2.co])
if len(v1.link_edges) != 2 or len(v2.link_edges) != 2:
return
v3 = v1.link_edges[1].other_vert(v1) if mid in v1.link_edges[0].verts else v1.link_edges[0].other_vert(v1)
v4 = v2.link_edges[1].other_vert(v2) if mid in v2.link_edges[0].verts else v2.link_edges[0].other_vert(v2)
dir1 = (v3.co - v1.co).normalized()
dir2 = (v4.co - v2.co).normalized()
intersect = mathutils.geometry.intersect_line_line(v3.co, v1.co, v4.co, v2.co)[0]
edge_angle = dir1.angle(dir2)
slide_distance = self.radius / math.tan(edge_angle / 2)
v1.co = intersect + (dir1 * slide_distance)
v2.co = intersect + (dir2 * slide_distance)
normal = mathutils.geometry.normal([v1.co, intersect, v2.co])
center = mathutils.geometry.intersect_line_line(
v1.co, v1.co + normal.cross(dir1), v2.co, v2.co + normal.cross(dir2)
)[0]
mid.co = center + ((v1.co.lerp(v2.co, 0.5) - center).normalized() * self.radius)
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
def create_arc(self, context):
obj = bpy.context.active_object
bpy.ops.object.mode_set(mode="OBJECT")
# The last group may be the result of a prior run of this operator.
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
if len(obj.vertex_groups):
last_group = obj.vertex_groups[-1]
verts_in_group = [v for v in obj.data.vertices if last_group.index in [vg.group for vg in v.groups]]
if "IFCARCINDEX" in last_group.name and len(verts_in_group) != 3:
obj.vertex_groups.remove(last_group)
group = obj.vertex_groups.new(name="IFCARCINDEX")
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
selected_edges = [e for e in bm.edges if e.select]
if len(selected_edges) != 2:
return {"CANCELLED"}
# Assume the user has selected two edges sharing a vert, but merge verts
# just in case the vertex is coincident but not shared.
all_verts = list(set(selected_edges[0].verts) | set(selected_edges[1].verts))
bmesh.ops.remove_doubles(bm, verts=all_verts, dist=1e-4)
# Calculate the distance to slide each edge to make space for the fillet arc
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
v1 = selected_edges[0].other_vert(shared_vert)
v2 = selected_edges[1].other_vert(shared_vert)
dir1 = (v1.co - shared_vert.co).normalized()
dir2 = (v2.co - shared_vert.co).normalized()
edge_angle = dir1.angle(dir2)
slide_distance = self.radius / math.tan(edge_angle / 2)
angle = math.pi - edge_angle
fillet_v1co = shared_vert.co + (dir1 * slide_distance)
fillet_v2co = shared_vert.co + (dir2 * slide_distance)
normal = mathutils.geometry.normal([v1.co, shared_vert.co, v2.co])
center = mathutils.geometry.intersect_line_line(
fillet_v1co, fillet_v1co + normal.cross(dir1), fillet_v2co, fillet_v2co + normal.cross(dir2)
)[0]
fillet_v1 = bm.verts.new(fillet_v1co)
fillet_v2 = bm.verts.new(fillet_v2co)
midpointco = center + ((fillet_v1co.lerp(fillet_v2.co, 0.5) - center).normalized() * self.radius)
midpoint = bm.verts.new(midpointco)
bm.edges.new((v1, fillet_v1))
bm.edges.new((v2, fillet_v2))
bm.edges.new((fillet_v1, midpoint))
bm.edges.new((fillet_v2, midpoint))
bmesh.ops.delete(bm, geom=[shared_vert], context="VERTS")
for vert in [fillet_v1, midpoint, fillet_v2]:
vert[deform_layer][group.index] = 1
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
class AlignViewToProfile(bpy.types.Operator):
bl_idname = "bim.align_view_to_profile"
bl_label = "Align View To Profile"
# __ ___
# .'. -- . '.
# /U) __ (O|
# /.' ()() '.\._
# .',/;,_.--._.;;) . '--..__
# / ,///|.__.|.\\\ \ '. '.''---..___
# /'._ '' || || '' _'\ : \ ' . '.
# / || || '., ) ) : \
# :'-.__ _ || || _ __.' _\_ .' ' ' ,)
# ( ' |' ( __= ___..-._ ( (.\\
# ('\ .___ ___. /'.___= \.\.\
# \\\-..____________..-'///
@classmethod
def poll(cls, context):
obj = context.active_object
return bool(obj) and obj.type == "MESH"
def execute(self, context):
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.view3d.view_axis(type="TOP", align_active=True)
region_3d = bpy.context.area.spaces.active.region_3d
region_3d.view_perspective = "ORTHO"
bpy.ops.object.mode_set(mode="EDIT")
return {"FINISHED"}
class AddRectangle(bpy.types.Operator):
bl_idname = "bim.add_rectangle"
bl_label = "Add Rectangle"
x: bpy.props.FloatProperty(name="X", default=0.1)
y: bpy.props.FloatProperty(name="Y", default=0.1)
@classmethod
def poll(cls, context):
obj = context.active_object
return bool(obj) and obj.type == "MESH"
def execute(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
cursor = obj.matrix_world.inverted() @ context.scene.cursor.location
new_verts = [
bm.verts.new((cursor[0], cursor[1], 0.0)),
bm.verts.new((cursor[0] + self.x, cursor[1], 0.0)),
bm.verts.new((cursor[0] + self.x, cursor[1] + self.y, 0.0)),
bm.verts.new((cursor[0], cursor[1] + self.y, 0.0)),
]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
return {"FINISHED"}
@@ -0,0 +1,30 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 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 blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
from bpy.types import PropertyGroup
class BIMCadProperties(PropertyGroup):
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
x: bpy.props.FloatProperty(name="X", default=0.2)
y: bpy.props.FloatProperty(name="Y", default=0.1)
@@ -20,7 +20,6 @@ import os
import bpy
import blenderbim.bim.module.type.prop as type_prop
from bpy.types import WorkSpaceTool
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.data import AuthoringData
@@ -35,29 +34,95 @@ class CadTool(WorkSpaceTool):
bl_widget = None
# https://docs.blender.org/api/current/bpy.types.KeyMapItems.html
bl_keymap = (
("bim.cad_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
("bim.cad_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
("bim.cad_hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
("bim.cad_hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
("bim.cad_hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
("bim.cad_hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}),
("bim.cad_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
("bim.cad_fillet", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
# Enable Mesh Tools add-on to get this amazing tool
("mesh.offset_edges", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
("bim.cad_hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
)
def draw_settings(context, layout, tool):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Extend", icon="EVENT_E")
obj = context.active_object
if obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile:
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q")
row.operator("bim.edit_extrusion_profile", text="Save Profile")
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Mitre", icon="EVENT_T")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.cad_hotkey", text="Extend").hotkey = "S_E"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Fillet", icon="EVENT_F")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_T")
row.operator("bim.cad_hotkey", text="Mitre").hotkey = "S_T"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Offset", icon="EVENT_O")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_F")
row.operator("bim.cad_hotkey", text="Fillet").hotkey = "S_F"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
row.operator("bim.cad_hotkey", text="Offset").hotkey = "S_O"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_R")
row.operator("bim.add_rectangle", text="Rectangle")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_C")
row.operator("bim.add_ifccircle", text="Circle")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_V")
row.operator("bim.set_arc_index", text="3-Point Arc")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_X")
row.operator("bim.reset_vertex", text="Reset Vertex")
else:
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.cad_hotkey", text="Extend").hotkey = "S_E"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_T")
row.operator("bim.cad_hotkey", text="Mitre").hotkey = "S_T"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_F")
row.operator("bim.cad_hotkey", text="Fillet").hotkey = "S_F"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
row.operator("bim.cad_hotkey", text="Offset").hotkey = "S_O"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="2-Point Arc", icon="EVENT_C")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_V")
row.operator("bim.cad_hotkey", text="3-Point Arc").hotkey = "S_V"
class CadHotkey(bpy.types.Operator):
@@ -67,17 +132,73 @@ class CadHotkey(bpy.types.Operator):
hotkey: bpy.props.StringProperty()
def execute(self, context):
self.props = context.scene.BIMModelProperties
self.has_ifc_class = True
try:
self.has_ifc_class = bool(self.props.ifc_class)
except:
pass
self.props = context.scene.BIMCadProperties
getattr(self, f"hotkey_{self.hotkey}")()
return {"FINISHED"}
def draw(self, context):
props = context.scene.BIMCadProperties
if self.hotkey == "S_C":
if self.is_profile():
row = self.layout.row()
row.prop(props, "radius")
elif self.hotkey == "S_F":
if not self.is_profile():
row = self.layout.row()
row.prop(props, "resolution")
row = self.layout.row()
row.prop(props, "radius")
elif self.hotkey == "S_O":
row = self.layout.row()
row.prop(props, "distance")
elif self.hotkey == "S_R":
row = self.layout.row()
row.prop(props, "x")
row = self.layout.row()
row.prop(props, "y")
elif self.hotkey == "S_V":
if not self.is_profile():
row = self.layout.row()
row.prop(props, "resolution")
def hotkey_S_C(self):
if self.is_profile():
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
else:
bpy.ops.bim.cad_arc_from_2_points()
def hotkey_S_E(self):
bpy.ops.bim.cad_trim_extend()
def hotkey_S_F(self):
if self.is_profile():
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
else:
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
def hotkey_S_O(self):
bpy.ops.bim.cad_offset(distance=self.props.distance)
def hotkey_S_Q(self):
bpy.ops.bim.edit_extrusion_profile()
def hotkey_S_R(self):
if self.is_profile():
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
def hotkey_S_T(self):
bpy.ops.bim.cad_mitre()
def hotkey_S_V(self):
if self.is_profile():
bpy.ops.bim.set_arc_index()
else:
bpy.ops.bim.cad_arc_from_3_points(resolution=self.resolution)
def hotkey_S_X(self):
if self.is_profile():
bpy.ops.bim.reset_vertex()
def is_profile(self):
obj = bpy.context.active_object
return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile
@@ -20,23 +20,26 @@ import bpy
from . import ui, prop, operator
classes = (
operator.LoadClassificationLibrary,
operator.AddClassification,
operator.RemoveClassification,
operator.EnableEditingClassification,
operator.DisableEditingClassification,
operator.EditClassification,
operator.RemoveClassificationReference,
operator.EnableEditingClassificationReference,
operator.DisableEditingClassificationReference,
operator.EditClassificationReference,
operator.AddClassificationReference,
operator.ChangeClassificationLevel,
operator.DisableEditingClassification,
operator.DisableEditingClassificationReference,
operator.DisableEditingClassificationReferences,
operator.EditClassification,
operator.EditClassificationReference,
operator.EnableEditingClassification,
operator.EnableEditingClassificationReference,
operator.LoadClassificationLibrary,
operator.RemoveClassification,
operator.RemoveClassificationReference,
prop.ClassificationReference,
prop.BIMClassificationProperties,
prop.BIMClassificationReferenceProperties,
ui.BIM_PT_classifications,
ui.BIM_PT_classification_references,
ui.BIM_PT_material_classifications,
ui.BIM_PT_cost_classifications,
ui.BIM_UL_classifications,
)
@@ -0,0 +1,143 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell
import ifcopenshell.util.date
import ifcopenshell.util.classification
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
def refresh():
ClassificationsData.is_loaded = False
ClassificationReferencesData.is_loaded = False
MaterialClassificationsData.is_loaded = False
CostClassificationsData.is_loaded = False
class ClassificationsData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data["has_classification_file"] = cls.has_classification_file()
cls.data["classifications"] = cls.classifications()
cls.data["available_classifications"] = cls.available_classifications()
@classmethod
def has_classification_file(cls):
return bool(IfcStore.classification_file)
@classmethod
def classifications(cls):
results = []
for element in tool.Ifc.get().by_type("IfcClassification"):
data = element.get_info()
if tool.Ifc.get().schema == "IFC2X3" and element.EditionDate:
data["EditionDate"] = ifcopenshell.util.date.ifc2datetime(data["EditionDate"])
results.append(data)
return results
@classmethod
def available_classifications(cls):
if not IfcStore.classification_file:
return []
return [(str(e.id()), e.Name, "") for e in IfcStore.classification_file.by_type("IfcClassification")]
class ReferencesData:
@classmethod
def is_available_classification_added(cls):
if not IfcStore.classification_file or not IfcStore.classification_file.by_type("IfcClassification"):
return False
props = bpy.context.scene.BIMClassificationProperties
name = IfcStore.classification_file.by_id(int(props.available_classifications)).Name
return name in [e.Name for e in tool.Ifc.get().by_type("IfcClassification")]
class ClassificationReferencesData(ReferencesData):
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data["references"] = cls.references()
cls.data["is_available_classification_added"] = cls.is_available_classification_added()
@classmethod
def references(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
del data["ReferencedSource"]
results.append(data)
return results
class MaterialClassificationsData(ReferencesData):
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data["references"] = cls.references()
cls.data["is_available_classification_added"] = cls.is_available_classification_added()
@classmethod
def references(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object.active_material)
if element:
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
del data["ReferencedSource"]
results.append(data)
return results
class CostClassificationsData(ReferencesData):
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data["references"] = cls.references()
cls.data["is_available_classification_added"] = cls.is_available_classification_added()
@classmethod
def references(cls):
results = []
element = tool.Ifc.get().by_id(
bpy.context.scene.BIMCostProperties.cost_items[
bpy.context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id
)
if element:
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
del data["ReferencedSource"]
results.append(data)
return results
@@ -1,5 +1,5 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
# Copyright (C) 2020-2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
@@ -18,46 +18,40 @@
import bpy
import json
import ifcopenshell
import ifcopenshell.api
import blenderbim.tool as tool
import blenderbim.bim.helper
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.classification.data import Data
from blenderbim.bim.module.classification.prop import getClassifications, getReferences
class LoadClassificationLibrary(bpy.types.Operator):
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.load_classification_library"
bl_label = "Load Classification Library"
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
Data.load_library(self.filepath)
getClassifications(self, context)
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def _execute(self, context):
IfcStore.classification_file = ifcopenshell.open(self.filepath)
class AddClassification(bpy.types.Operator):
class AddClassification(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_classification"
bl_label = "Add Classification"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
props = context.scene.BIMClassificationProperties
ifcopenshell.api.run(
"classification.add_classification",
IfcStore.get_file(),
**{"classification": Data.library_file.by_id(int(props.available_classifications))},
tool.Ifc.get(),
classification=IfcStore.classification_file.by_id(int(props.available_classifications)),
)
Data.load(IfcStore.get_file())
return {"FINISHED"}
class EnableEditingClassification(bpy.types.Operator):
@@ -67,19 +61,21 @@ class EnableEditingClassification(bpy.types.Operator):
classification: bpy.props.IntProperty()
def execute(self, context):
def callback(name, prop, data):
if name == "ReferenceTokens":
new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add()
new.name = name
new.data_type = "string"
new.is_null = data[name] is None
new.is_optional = True
new.string_value = "" if new.is_null else json.dumps(data[name])
return True
props = context.scene.BIMClassificationProperties
props.classification_attributes.clear()
classification_data = Data.classifications[self.classification]
for attribute in IfcStore.get_schema().declaration_by_name("IfcClassification").all_attributes():
new = props.classification_attributes.add()
new.name = attribute.name()
new.is_null = classification_data[attribute.name()] is None
new.is_optional = attribute.optional()
new.data_type = "string"
if attribute.name() == "ReferenceTokens":
new.string_value = "" if new.is_null else json.dumps(classification_data[attribute.name()])
else:
new.string_value = "" if new.is_null else classification_data[attribute.name()]
blenderbim.bim.helper.import_attributes2(
tool.Ifc.get().by_id(self.classification), props.classification_attributes, callback
)
props.active_classification_id = self.classification
return {"FINISHED"}
@@ -94,34 +90,26 @@ class DisableEditingClassification(bpy.types.Operator):
return {"FINISHED"}
class RemoveClassification(bpy.types.Operator):
class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_classification"
bl_label = "Remove Classification"
bl_options = {"REGISTER", "UNDO"}
classification: bpy.props.IntProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.file = IfcStore.get_file()
ifcopenshell.api.run(
"classification.remove_classification",
self.file,
**{"classification": self.file.by_id(self.classification)},
tool.Ifc.get(),
classification=self.file.by_id(self.classification),
)
Data.load(IfcStore.get_file())
return {"FINISHED"}
class EditClassification(bpy.types.Operator):
class EditClassification(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_classification"
bl_label = "Edit Classification"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
props = context.scene.BIMClassificationProperties
attributes = {}
@@ -138,9 +126,7 @@ class EditClassification(bpy.types.Operator):
self.file,
**{"classification": self.file.by_id(props.active_classification_id), "attributes": attributes},
)
Data.load(IfcStore.get_file())
bpy.ops.bim.disable_editing_classification()
return {"FINISHED"}
class EnableEditingClassificationReference(bpy.types.Operator):
@@ -154,16 +140,7 @@ class EnableEditingClassificationReference(bpy.types.Operator):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMClassificationReferenceProperties
props.reference_attributes.clear()
reference_data = Data.references[self.reference]
for attribute in IfcStore.get_schema().declaration_by_name("IfcClassificationReference").all_attributes():
if attribute.name() == "ReferencedSource":
continue
new = props.reference_attributes.add()
new.name = attribute.name()
new.is_null = reference_data[attribute.name()] is None
new.is_optional = attribute.optional()
new.data_type = "string"
new.string_value = "" if new.is_null else reference_data[attribute.name()]
blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
props.active_reference_id = self.reference
return {"FINISHED"}
@@ -180,41 +157,47 @@ class DisableEditingClassificationReference(bpy.types.Operator):
return {"FINISHED"}
class RemoveClassificationReference(bpy.types.Operator):
class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_classification_reference"
bl_label = "Remove Classification Reference"
bl_options = {"REGISTER", "UNDO"}
reference: bpy.props.IntProperty()
obj: bpy.props.StringProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
obj_type: bpy.props.StringProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.file = IfcStore.get_file()
ifcopenshell.api.run(
"classification.remove_reference",
self.file,
**{
"reference": self.file.by_id(self.reference),
"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
},
)
Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
Data.load(IfcStore.get_file())
return {"FINISHED"}
if self.obj_type == "Object":
if context.selected_objects:
objects = [o.name for o in context.selected_objects]
else:
objects = [context.active_object.name]
else:
objects = [self.obj]
identification = tool.Ifc.get().by_id(self.reference)[0]
for obj in objects:
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
element = tool.Ifc.get().by_id(ifc_definition_id)
references = ifcopenshell.util.classification.get_references(element, should_inherit=False)
for reference in references:
if reference[0] == identification:
ifcopenshell.api.run(
"classification.remove_reference",
tool.Ifc.get(),
reference=reference,
product=element,
)
break
class EditClassificationReference(bpy.types.Operator):
class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_classification_reference"
bl_label = "Edit Classification Reference"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMClassificationReferenceProperties
@@ -228,48 +211,47 @@ class EditClassificationReference(bpy.types.Operator):
ifcopenshell.api.run(
"classification.edit_reference",
self.file,
**{"reference": self.file.by_id(props.active_reference_id), "attributes": attributes},
reference=self.file.by_id(props.active_reference_id),
attributes=attributes,
)
Data.load(IfcStore.get_file())
bpy.ops.bim.disable_editing_classification_reference()
return {"FINISHED"}
class AddClassificationReference(bpy.types.Operator):
class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_classification_reference"
bl_label = "Add Classification Reference"
bl_options = {"REGISTER", "UNDO"}
reference: bpy.props.IntProperty()
obj: bpy.props.StringProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
obj_type: bpy.props.StringProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.file = IfcStore.get_file()
classification = None
if self.obj_type == "Object":
if context.selected_objects:
objects = [o.name for o in context.selected_objects]
else:
objects = [context.active_object.name]
else:
objects = [self.obj]
props = context.scene.BIMClassificationProperties
classification_name = Data.library_classifications[int(props.available_classifications)]
for classification_id, classification in Data.classifications.items():
if classification["Name"] == classification_name:
classification = self.file.by_id(classification_id)
classification = None
classification_name = IfcStore.classification_file.by_id(int(props.available_classifications)).Name
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == classification_name:
classification = element
break
ifcopenshell.api.run(
"classification.add_reference",
self.file,
**{
"reference": Data.library_file.by_id(self.reference),
"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
"classification": classification,
},
)
Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
Data.load(IfcStore.get_file())
return {"FINISHED"}
for obj in objects:
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
if not ifc_definition_id:
continue
ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
reference=IfcStore.classification_file.by_id(self.reference),
product=tool.Ifc.get().by_id(ifc_definition_id),
classification=classification,
)
class ChangeClassificationLevel(bpy.types.Operator):
@@ -279,5 +261,28 @@ class ChangeClassificationLevel(bpy.types.Operator):
parent_id: bpy.props.IntProperty()
def execute(self, context):
getReferences(self, context, parent_id=self.parent_id)
props = context.scene.BIMClassificationProperties
props.available_library_references.clear()
for reference in IfcStore.classification_file.by_id(self.parent_id).HasReferences:
new = props.available_library_references.add()
new.identification = reference.Identification or ""
new.name = reference.Name or ""
new.ifc_definition_id = reference.id()
new.has_references = bool(reference.HasReferences)
new.referenced_source
if reference.ReferencedSource.is_a("IfcClassificationReference"):
props.active_library_referenced_source = reference.ReferencedSource.ReferencedSource.id()
else:
props.active_library_referenced_source = 0
return {"FINISHED"}
class DisableEditingClassificationReferences(bpy.types.Operator):
bl_idname = "bim.disable_editing_classification_references"
bl_label = "Disable Editing Classification References"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMClassificationProperties
props.available_library_references.clear()
return {"FINISHED"}
@@ -17,8 +17,9 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.prop import StrProperty, Attribute
from ifcopenshell.api.classification.data import Data
from blenderbim.bim.module.classification.data import ClassificationsData
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -31,42 +32,11 @@ from bpy.props import (
CollectionProperty,
)
classification_enum = []
def purge():
global classification_enum
classification_enum = []
def getClassifications(self, context):
global classification_enum
if len(classification_enum) < 1:
classification_enum.clear()
classification_enum.extend([(str(i), n, "") for i, n in Data.library_classifications.items()])
if classification_enum:
getReferences(self, context, parent_id=int(classification_enum[0][0]))
return classification_enum
def updateClassification(self, context):
getReferences(self, context, parent_id=int(self.available_classifications))
def getReferences(self, context, parent_id=None):
props = context.scene.BIMClassificationProperties
props.available_library_references.clear()
for reference in Data.library_file.by_id(parent_id).HasReferences:
new = props.available_library_references.add()
new.identification = reference.Identification or ""
new.name = reference.Name or ""
new.ifc_definition_id = reference.id()
new.has_references = bool(reference.HasReferences)
new.referenced_source
if reference.ReferencedSource.is_a("IfcClassificationReference"):
props.active_library_referenced_source = reference.ReferencedSource.ReferencedSource.id()
else:
props.active_library_referenced_source = 0
def get_available_classifications(self, context):
if not ClassificationsData.is_loaded:
ClassificationsData.load()
return ClassificationsData.data["available_classifications"]
class ClassificationReference(PropertyGroup):
@@ -78,9 +48,7 @@ class ClassificationReference(PropertyGroup):
class BIMClassificationProperties(PropertyGroup):
available_classifications: EnumProperty(
items=getClassifications, name="Available Classifications", update=updateClassification
)
available_classifications: EnumProperty(items=get_available_classifications, name="Available Classifications")
classification_attributes: CollectionProperty(name="Classification Attributes", type=Attribute)
active_classification_id: IntProperty(name="Active Classification Id")
available_library_references: CollectionProperty(name="Available Library References", type=ClassificationReference)
@@ -16,10 +16,17 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import blenderbim.bim.helper
import blenderbim.tool as tool
import blenderbim.bim.module.classification.prop as classification_prop
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.classification.data import Data
from blenderbim.bim.module.classification.data import (
ClassificationsData,
ClassificationReferencesData,
MaterialClassificationsData,
CostClassificationsData,
)
class BIM_PT_classifications(Panel):
@@ -33,15 +40,15 @@ class BIM_PT_classifications(Panel):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
if not Data.is_loaded:
Data.load(IfcStore.get_file())
if not ClassificationsData.is_loaded:
ClassificationsData.load()
self.props = context.scene.BIMClassificationProperties
if Data.library_file:
if ClassificationsData.data["has_classification_file"]:
row = self.layout.row(align=True)
row.prop(self.props, "available_classifications", text="")
row.operator("bim.load_classification_library", text="", icon="IMPORT")
@@ -51,36 +58,100 @@ class BIM_PT_classifications(Panel):
row.label(text="No Active Classification Library")
row.operator("bim.load_classification_library", text="", icon="IMPORT")
for classification_id, classification in Data.classifications.items():
if self.props.active_classification_id == classification_id:
self.draw_editable_ui(classification)
for classification in ClassificationsData.data["classifications"]:
if self.props.active_classification_id == classification["id"]:
self.draw_editable_ui()
else:
self.draw_ui(classification_id, classification)
self.draw_ui(classification)
def draw_editable_ui(self, classification):
def draw_editable_ui(self):
row = self.layout.row(align=True)
row.prop(self.props.classification_attributes.get("Name"), "string_value", text="", icon="ASSET_MANAGER")
row.operator("bim.edit_classification", text="", icon="CHECKMARK")
row.operator("bim.edit_classification", text="Save changes", icon="CHECKMARK")
row.operator("bim.disable_editing_classification", text="", icon="CANCEL")
blenderbim.bim.helper.draw_attributes(self.props.classification_attributes, self.layout)
for attribute in self.props.classification_attributes:
if attribute.name == "Name":
continue
row = self.layout.row(align=True)
row.prop(attribute, "string_value", text=attribute.name)
if attribute.is_optional:
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
def draw_ui(self, classification_id, classification):
def draw_ui(self, classification):
row = self.layout.row(align=True)
row.label(text=classification["Name"], icon="ASSET_MANAGER")
if not self.props.active_classification_id:
op = row.operator("bim.enable_editing_classification", text="", icon="GREASEPENCIL")
op.classification = classification_id
row.operator("bim.remove_classification", text="", icon="X").classification = classification_id
op.classification = classification["id"]
row.operator("bim.remove_classification", text="", icon="X").classification = classification["id"]
class BIM_PT_classification_references(Panel):
class ReferenceUI:
def draw_ui(self, context):
obj = context.active_object
self.oprops = obj.BIMObjectProperties
self.sprops = context.scene.BIMClassificationProperties
self.props = obj.BIMClassificationReferenceProperties
self.file = IfcStore.get_file()
self.draw_add_ui(context)
if not self.data.data["references"]:
row = self.layout.row(align=True)
row.label(text="No References")
for reference in self.data.data["references"]:
if self.props.active_reference_id == reference["id"]:
self.draw_editable_ui()
else:
self.draw_reference_ui(reference)
def draw_add_ui(self, context):
if not self.data.data["is_available_classification_added"]:
return
row = self.layout.row(align=True)
row.prop(self.sprops, "available_classifications", text="")
if not self.sprops.available_library_references:
op = row.operator("bim.change_classification_level", text="", icon="IMPORT")
op.parent_id = int(self.sprops.available_classifications)
return
if self.sprops.active_library_referenced_source:
op = row.operator("bim.change_classification_level", text="", icon="FRAME_PREV")
op.parent_id = self.sprops.active_library_referenced_source
if self.sprops.active_library_reference_index < len(self.sprops.available_library_references):
op = row.operator("bim.add_classification_reference", text="", icon="ADD")
op.obj = self.obj
op.obj_type = self.obj_type
op.reference = self.sprops.available_library_references[
self.sprops.active_library_reference_index
].ifc_definition_id
row.operator("bim.disable_editing_classification_references", text="", icon="CANCEL")
self.layout.template_list(
"BIM_UL_classifications",
"",
self.sprops,
"available_library_references",
self.sprops,
"active_library_reference_index",
)
def draw_editable_ui(self):
row = self.layout.row(align=True)
row.operator("bim.edit_classification_reference", text="Save changes", icon="CHECKMARK")
row.operator("bim.disable_editing_classification_reference", text="", icon="CANCEL")
blenderbim.bim.helper.draw_attributes(self.props.reference_attributes, self.layout)
def draw_reference_ui(self, reference):
row = self.layout.row(align=True)
if self.file.schema == "IFC2X3":
name = reference["ItemReference"] or "No Identification"
else:
name = reference["Identification"] or "No Identification"
row.label(text=name, icon="ASSET_MANAGER")
row.label(text=reference["Name"] or "")
if not self.props.active_reference_id:
op = row.operator("bim.enable_editing_classification_reference", text="", icon="GREASEPENCIL")
op.reference = reference["id"]
op = row.operator("bim.remove_classification_reference", text="", icon="X")
op.reference = reference["id"]
op.obj = self.obj
op.obj_type = self.obj_type
class BIM_PT_classification_references(Panel, ReferenceUI):
bl_label = "IFC Classification References"
bl_idname = "BIM_PT_classification_references"
bl_options = {"DEFAULT_CLOSED"}
@@ -93,85 +164,65 @@ class BIM_PT_classification_references(Panel):
def poll(cls, context):
if not context.active_object:
return False
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
return bool(tool.Ifc.get_entity(context.active_object))
def draw(self, context):
obj = context.active_object
self.oprops = obj.BIMObjectProperties
self.sprops = context.scene.BIMClassificationProperties
self.props = obj.BIMClassificationReferenceProperties
self.file = IfcStore.get_file()
if not Data.is_loaded:
Data.load(IfcStore.get_file())
if self.oprops.ifc_definition_id not in Data.products:
Data.load(IfcStore.get_file(), self.oprops.ifc_definition_id)
if not ClassificationReferencesData.is_loaded:
ClassificationReferencesData.load()
self.data = ClassificationReferencesData
self.obj = context.active_object.name
self.obj_type = "Object"
self.draw_ui(context)
self.draw_add_ui(context)
reference_ids = Data.products[self.oprops.ifc_definition_id]
if not reference_ids:
row = self.layout.row(align=True)
row.label(text="No References")
class BIM_PT_material_classifications(Panel, ReferenceUI):
bl_label = "IFC Material Classifications"
bl_idname = "BIM_PT_material_classifications"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
for reference_id in reference_ids:
reference = Data.references[reference_id]
if self.props.active_reference_id == reference_id:
self.draw_editable_ui(reference)
else:
self.draw_ui(reference_id, reference)
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
return False
try:
return bool(context.active_object.active_material.BIMObjectProperties.ifc_definition_id)
except:
return False
def draw_add_ui(self, context):
if not classification_prop.getClassifications(self.sprops, context):
return
def draw(self, context):
if not MaterialClassificationsData.is_loaded:
MaterialClassificationsData.load()
self.data = MaterialClassificationsData
self.obj = context.active_object.active_material.name
self.obj_type = "Material"
self.draw_ui(context)
name = Data.library_classifications[int(self.sprops.available_classifications)]
if name in [c["Name"] for c in Data.classifications.values()]:
row = self.layout.row(align=True)
row.prop(self.sprops, "available_classifications", text="")
if self.sprops.active_library_referenced_source:
op = row.operator("bim.change_classification_level", text="", icon="FRAME_PREV")
op.parent_id = self.sprops.active_library_referenced_source
op = row.operator("bim.add_classification_reference", text="", icon="ADD")
op.reference = self.sprops.available_library_references[
self.sprops.active_library_reference_index
].ifc_definition_id
self.layout.template_list(
"BIM_UL_classifications",
"",
self.sprops,
"available_library_references",
self.sprops,
"active_library_reference_index",
)
def draw_editable_ui(self, reference):
row = self.layout.row(align=True)
row.prop(self.props.reference_attributes.get("Name"), "string_value", text="", icon="ASSET_MANAGER")
row.operator("bim.edit_classification_reference", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_classification_reference", text="", icon="CANCEL")
class BIM_PT_cost_classifications(Panel, ReferenceUI):
bl_label = "IFC Cost Classifications"
bl_idname = "BIM_PT_cost_classifications"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_cost_schedules"
for attribute in self.props.reference_attributes:
if attribute.name == "Name":
continue
row = self.layout.row(align=True)
row.prop(attribute, "string_value", text=attribute.name)
if attribute.is_optional:
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
return False
return bool(context.scene.BIMCostProperties.cost_items)
def draw_ui(self, reference_id, reference):
row = self.layout.row(align=True)
if self.file.schema == "IFC2X3":
name = reference["ItemReference"] or "No Identification"
else:
name = reference["Identification"] or "No Identification"
row.label(text=name, icon="ASSET_MANAGER")
row.label(text=reference["Name"] or "")
if not self.props.active_reference_id:
op = row.operator("bim.enable_editing_classification_reference", text="", icon="GREASEPENCIL")
op.reference = reference_id
row.operator("bim.remove_classification_reference", text="", icon="X").reference = reference_id
def draw(self, context):
if not CostClassificationsData.is_loaded:
CostClassificationsData.load()
self.data = CostClassificationsData
self.obj = ""
self.obj_type = "Cost"
self.draw_ui(context)
class BIM_UL_classifications(UIList):
@@ -64,8 +64,8 @@ class BIM_PT_context(bpy.types.Panel):
row.operator("bim.remove_context", icon="X", text="").context = ifc_context["id"]
row = box.row(align=True)
row.prop(props, "subcontexts", text="")
row.prop(props, "target_views", text="")
blenderbim.bim.helper.prop_with_search(row, props, "subcontexts", text="")
blenderbim.bim.helper.prop_with_search(row, props, "target_views", text="")
op = row.operator("bim.add_context", icon="ADD", text="")
op.context_type = ifc_context["context_type"]
op.context_identifier = props.subcontexts
@@ -27,7 +27,7 @@ from blenderbim.bim.ifc import IfcStore
from bpy_extras.io_utils import ImportHelper
from ifcopenshell.api.cost.data import Data
from ifcopenshell.api.unit.data import Data as UnitData
from ifcopenshell.api.resource.data import Data as ResourceData
from blenderbim.bim.module.resource.data import ResourceData
class AddCostSchedule(bpy.types.Operator):
@@ -604,7 +604,7 @@ class AddCostValue(bpy.types.Operator):
value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=parent)
ifcopenshell.api.run("cost.edit_cost_value", self.file, cost_value=value, attributes=attributes)
if parent.is_a("IfcConstructionResource"):
ResourceData.load(self.file)
ResourceData.load()
else:
Data.load(self.file)
return {"FINISHED"}
@@ -630,7 +630,7 @@ class RemoveCostItemValue(bpy.types.Operator):
cost_value=self.file.by_id(self.cost_value),
)
if parent.is_a("IfcConstructionResource"):
ResourceData.load(self.file)
ResourceData.load()
else:
Data.load(self.file)
return {"FINISHED"}
@@ -923,24 +923,26 @@ class LoadCostItemQuantities(bpy.types.Operator):
def execute(self, context):
self.props = context.scene.BIMCostProperties
self.file = IfcStore.get_file()
Data.load(self.file)
self.props.cost_item_products.clear()
self.props.cost_item_processes.clear()
self.props.cost_item_resources.clear()
ifc_definition_id = self.props.cost_items[self.props.active_cost_item_index].ifc_definition_id
for control_id, quantity_ids in Data.cost_items[ifc_definition_id]["Controls"].items():
related_object = self.file.by_id(control_id)
if related_object.is_a("IfcProduct"):
new = self.props.cost_item_products.add()
elif related_object.is_a("IfcProcess"):
new = self.props.cost_item_processes.add()
elif related_object.is_a("IfcResource"):
new = self.props.cost_item_resources.add()
new.ifc_definition_id = control_id
new.name = related_object.Name or "Unnamed"
total_quantity = 0
for quantity_id in quantity_ids:
total_quantity += self.file.by_id(quantity_id)[3]
new.total_quantity = total_quantity
if self.props.cost_items:
ifc_definition_id = self.props.cost_items[self.props.active_cost_item_index].ifc_definition_id
for control_id, quantity_ids in Data.cost_items[ifc_definition_id]["Controls"].items() or {}:
related_object = self.file.by_id(control_id)
if related_object.is_a("IfcProduct"):
new = self.props.cost_item_products.add()
elif related_object.is_a("IfcProcess"):
new = self.props.cost_item_processes.add()
elif related_object.is_a("IfcResource"):
new = self.props.cost_item_resources.add()
new.ifc_definition_id = control_id
new.name = related_object.Name or "Unnamed"
total_quantity = 0
for quantity_id in quantity_ids:
total_quantity += self.file.by_id(quantity_id)[3]
new.total_quantity = total_quantity
return {"FINISHED"}
@@ -19,6 +19,7 @@
import bpy
import ifcopenshell.api
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.classification.data import CostClassificationsData
from ifcopenshell.api.cost.data import Data
from ifcopenshell.api.pset.data import Data as PsetData
from blenderbim.bim.prop import StrProperty, Attribute
@@ -164,6 +165,7 @@ def update_active_cost_item_index(self, context):
bpy.ops.bim.load_cost_item_types()
else:
bpy.ops.bim.load_cost_item_quantities()
CostClassificationsData.load()
def update_cost_item_identification(self, context):
@@ -343,6 +343,11 @@ class BIM_PT_cost_item_quantities(Panel):
return False
def draw(self, context):
if not Data.is_loaded:
Data.load(IfcStore.get_file())
if not UnitData.is_loaded:
UnitData.load(IfcStore.get_file())
self.props = context.scene.BIMCostProperties
cost_item = self.props.cost_items[self.props.active_cost_item_index]
@@ -112,6 +112,7 @@ class CreateAllShapes(bpy.types.Operator):
def execute(self, context):
self.file = IfcStore.get_file()
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcSpace")
total = len(elements)
settings = ifcopenshell.geom.settings()
failures = []
@@ -25,6 +25,7 @@ classes = (
operator.SelectDiffOldFile,
operator.SelectDiffNewFile,
operator.ExecuteIfcDiff,
prop.Relationships,
prop.DiffProperties,
ui.BIM_PT_diff,
)
@@ -0,0 +1,86 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import blenderbim.tool as tool
def refresh():
DiffData.is_loaded = False
class DiffData:
data = {}
is_loaded = False
diff_json_file = None
diff = None
@classmethod
def load(cls):
cls.data["diff_json"] = cls.diff_json()
cls.data["total_added"] = cls.total_added()
cls.data["total_deleted"] = cls.total_deleted()
cls.data["total_changed"] = cls.total_changed()
cls.data["changes"] = cls.changes()
cls.is_loaded = True
@classmethod
def diff_json(cls):
props = bpy.context.scene.DiffProperties
if not props.diff_json_file:
cls.diff = None
return
if props.diff_json_file != cls.diff_json_file:
cls.diff_json_file = props.diff_json_file
with open(props.diff_json_file, "r") as file:
cls.diff = json.load(file)
return cls.diff
@classmethod
def total_added(cls):
diff = cls.diff_json()
if diff:
return len(diff["added"])
@classmethod
def total_deleted(cls):
diff = cls.diff_json()
if diff:
return len(diff["deleted"])
@classmethod
def total_changed(cls):
diff = cls.diff_json()
if diff:
return len(diff["changed"].keys())
@classmethod
def changes(cls):
diff = cls.diff_json()
if not diff:
return {}
element = tool.Ifc.get_entity(bpy.context.active_object)
if not element or not hasattr(element, "GlobalId"):
return {}
changes = {k.upper().replace("_", " "): str(v) for k, v in diff["changed"].get(element.GlobalId, {}).items()}
if element.GlobalId in diff["added"]:
changes["Added"] = True
elif element.GlobalId in diff["deleted"]:
changes["Deleted"] = True
return changes
@@ -17,10 +17,11 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import ifccsv
import ifcopenshell
import json
from blenderbim.bim.ifc import IfcStore
import blenderbim.bim.handler
import blenderbim.tool as tool
class SelectDiffJsonFile(bpy.types.Operator):
@@ -28,6 +29,7 @@ class SelectDiffJsonFile(bpy.types.Operator):
bl_label = "Select Diff JSON File"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def execute(self, context):
context.scene.DiffProperties.diff_json_file = self.filepath
@@ -44,21 +46,23 @@ class VisualiseDiff(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
# ifc_file = IfcStore.get_file() # In case we get from Store
ifc_file = ifcopenshell.open(context.scene.DiffProperties.diff_new_file) # for Now refer to the new file
ifc_file = tool.Ifc.get()
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
diff = json.load(file)
for obj in context.visible_objects:
obj.color = (1.0, 1.0, 1.0, 0.2)
global_id = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId
obj.color = (1.0, 1.0, 1.0, 1.0)
element = tool.Ifc.get_entity(obj)
if not element:
continue
global_id = getattr(element, "GlobalId", None)
if not global_id:
continue
if global_id in diff["deleted"]:
obj.color = (1.0, 0.0, 0.0, 0.2)
obj.color = (1.0, 0.0, 0.0, 1.0)
elif global_id in diff["added"]:
obj.color = (0.0, 1.0, 0.0, 0.2)
obj.color = (0.0, 1.0, 0.0, 1.0)
elif global_id in diff["changed"]:
obj.color = (0.0, 0.0, 1.0, 0.2)
obj.color = (0.0, 0.0, 1.0, 1.0)
area = next(area for area in context.screen.areas if area.type == "VIEW_3D")
area.spaces[0].shading.color_type = "OBJECT"
return {"FINISHED"}
@@ -69,9 +73,10 @@ class SelectDiffOldFile(bpy.types.Operator):
bl_label = "Select Diff Old File"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
def execute(self, context):
context.scene.DiffProperties.diff_old_file = self.filepath
context.scene.DiffProperties.old_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
@@ -84,9 +89,10 @@ class SelectDiffNewFile(bpy.types.Operator):
bl_label = "Select Diff New File"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
def execute(self, context):
context.scene.DiffProperties.diff_new_file = self.filepath
context.scene.DiffProperties.new_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
@@ -110,12 +116,14 @@ class ExecuteIfcDiff(bpy.types.Operator):
import ifcdiff
ifc_diff = ifcdiff.IfcDiff(
context.scene.DiffProperties.diff_old_file,
context.scene.DiffProperties.diff_new_file,
context.scene.DiffProperties.old_file,
context.scene.DiffProperties.new_file,
self.filepath,
context.scene.DiffProperties.diff_relationships.split(),
[r.relationship for r in context.scene.DiffProperties.diff_relationships],
context.scene.DiffProperties.diff_filter_elements,
)
ifc_diff.diff()
ifc_diff.export()
context.scene.DiffProperties.diff_json_file = self.filepath
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
@@ -18,6 +18,7 @@
import bpy
from blenderbim.bim.prop import StrProperty
from blenderbim.bim.module.diff.data import DiffData
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -31,8 +32,20 @@ from bpy.props import (
)
def update_diff_json_file(self, context):
DiffData.data["diff_json"] = DiffData.diff_json()
class Relationships(PropertyGroup):
relationship: EnumProperty(
name="Relationship",
items=[(r, r.capitalize(), r) for r in ["type", "property", "container", "aggregate", "classification"]],
)
class DiffProperties(PropertyGroup):
diff_json_file: StringProperty(default="", name="Diff JSON File")
diff_old_file: StringProperty(default="", name="Diff Old IFC File")
diff_new_file: StringProperty(default="", name="Diff New IFC File")
diff_relationships: StringProperty(default="", name="Diff Relationships")
diff_json_file: StringProperty(default="", name="JSON Output", update=update_diff_json_file)
old_file: StringProperty(default="", name="Old IFC File")
new_file: StringProperty(default="", name="New IFC File")
diff_relationships: CollectionProperty(type=Relationships, name="Relationships")
diff_filter_elements: StringProperty(default="", name="Filter")
@@ -18,6 +18,9 @@
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.diff.data import DiffData
import blenderbim.tool as tool
import json
class BIM_PT_diff(Panel):
@@ -30,30 +33,70 @@ class BIM_PT_diff(Panel):
bl_parent_id = "BIM_PT_quality_control"
def draw(self, context):
if not DiffData.is_loaded:
DiffData.load()
layout = self.layout
layout.use_property_split = True
scene = context.scene
bim_properties = scene.DiffProperties
props = scene.DiffProperties
layout.label(text="IFC Diff Setup:")
row = layout.row(align=True)
row.prop(bim_properties, "diff_old_file")
row.prop(props, "old_file")
row.operator("bim.select_diff_old_file", icon="FILE_FOLDER", text="")
row = layout.row(align=True)
row.prop(bim_properties, "diff_new_file")
row.prop(props, "new_file")
row.operator("bim.select_diff_new_file", icon="FILE_FOLDER", text="")
row = layout.row(align=True)
row.prop(bim_properties, "diff_relationships")
row.prop(props, "diff_relationships")
row.context_pointer_set("bim_prop_group", props)
add = row.operator("bim.edit_blender_collection", icon="ADD", text="")
add.option = "add"
add.collection = "diff_relationships"
for index, r in enumerate(props.diff_relationships):
row = layout.row(align=True)
row.context_pointer_set("bim_prop_group", props)
row.prop(r, "relationship", text=" ")
remove = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
remove.option = "remove"
remove.collection = "diff_relationships"
remove.index = index
row = layout.row(align=True)
row.prop(props, "diff_filter_elements")
row.operator("bim.ifc_selector", icon="FILTER", text="")
row = layout.row()
row.operator("bim.execute_ifc_diff")
# TODO: show if there ifc diff operation is sucessful
row = layout.row(align=True)
row.prop(bim_properties, "diff_json_file")
row.prop(props, "diff_json_file")
row.operator("bim.select_diff_json_file", icon="FILE_FOLDER", text="")
row.operator("bim.visualise_diff", icon="HIDE_OFF", text="")
if DiffData.data["diff_json"]:
row = layout.row()
row.alignment = "CENTER"
row.label(text=f"{DiffData.data['total_added']} added")
row.label(text=f"{DiffData.data['total_deleted']} deleted")
row.label(text=f"{DiffData.data['total_changed']} changed")
if DiffData.data["changes"]:
box = layout.box()
row = box.row()
row.label(text="Active Object Changes:")
for key, value in DiffData.data["changes"].items():
row = box.row()
if key == "Added":
icon = "ADD"
elif key == "Deleted":
icon = "X"
else:
icon = "GREASEPENCIL"
row.label(text=key, icon=icon)
@@ -34,20 +34,19 @@ classes = (
operator.BuildSchedule,
operator.CleanWireframes,
operator.ContractSheet,
operator.ConvertEdgeToArc,
operator.CopyGrid,
operator.CreateDrawing,
operator.CreateSheets,
operator.DisableEditingAssignedProduct,
operator.DisableEditingDrawings,
operator.DisableEditingSchedules,
operator.DisableEditingSheets,
operator.DisableEditingText,
operator.DisableEditingTextProduct,
operator.EditAssignedProduct,
operator.EditText,
operator.EditTextProduct,
operator.EditVectorStyle,
operator.EnableEditingAssignedProduct,
operator.EnableEditingText,
operator.EnableEditingTextProduct,
operator.ExpandSheet,
operator.LoadDrawings,
operator.LoadSchedules,
@@ -72,12 +71,14 @@ classes = (
prop.DocProperties,
prop.BIMCameraProperties,
prop.BIMTextProperties,
prop.BIMAssignedProductProperties,
ui.BIM_PT_camera,
ui.BIM_PT_drawing_underlay,
ui.BIM_PT_annotation_utilities,
ui.BIM_PT_sheets,
ui.BIM_PT_drawings,
ui.BIM_PT_schedules,
ui.BIM_PT_product_assignments,
ui.BIM_PT_text,
ui.BIM_UL_drawinglist,
ui.BIM_UL_sheets,
@@ -92,6 +93,7 @@ classes = (
def register():
bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties)
bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties)
bpy.types.Object.BIMAssignedProductProperties = bpy.props.PointerProperty(type=prop.BIMAssignedProductProperties)
bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
bpy.app.handlers.load_post.append(handler.toggleDecorationsOnLoad)
@@ -101,6 +103,8 @@ def register():
def unregister():
del bpy.types.Scene.DocProperties
del bpy.types.Camera.BIMCameraProperties
del bpy.types.Object.BIMAssignedProductProperties
del bpy.types.Object.BIMTextProperties
del bpy.types.TextCurve.BIMTextProperties
bpy.app.handlers.load_post.remove(handler.toggleDecorationsOnLoad)
bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler)
@@ -151,10 +151,17 @@ class Annotator:
return obj
@staticmethod
def get_annotation_obj(object_type, data_type):
collection = bpy.context.scene.camera.users_collection[0]
def get_annotation_obj(drawing, object_type, data_type):
camera = tool.Ifc.get_object(drawing)
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
matrix_world = camera.matrix_world.copy()
matrix_world[0][3] = co1.x
matrix_world[1][3] = co1.y
matrix_world[2][3] = co1.z
collection = camera.users_collection[0]
if object_type == "TEXT":
obj = bpy.data.objects.new(object_type, None)
obj.matrix_world = matrix_world
collection.objects.link(obj)
return obj
elif object_type in ("TEXT_LEADER", "SECTION_LEVEL"):
@@ -162,12 +169,14 @@ class Annotator:
data.dimensions = "3D"
data.resolution_u = 2
obj = bpy.data.objects.new(object_type, data)
obj.matrix_world = matrix_world
collection.objects.link(obj)
return obj
for obj in collection.objects:
element = tool.Ifc.get_entity(obj)
if element and element.ObjectType == object_type:
return obj
if object_type != "ANGLE":
for obj in collection.objects:
element = tool.Ifc.get_entity(obj)
if element and element.ObjectType == object_type:
return obj
if data_type == "mesh":
data = bpy.data.meshes.new(object_type)
elif data_type == "curve":
@@ -175,12 +184,14 @@ class Annotator:
data.dimensions = "3D"
data.resolution_u = 2
obj = bpy.data.objects.new(object_type, data)
obj.matrix_world = matrix_world
collection.objects.link(obj)
return obj
@staticmethod
def get_placeholder_coords():
camera = bpy.context.scene.camera
def get_placeholder_coords(camera=None):
if not camera:
camera = bpy.context.scene.camera
z_offset = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y:
y = (
@@ -22,19 +22,40 @@ import blenderbim.tool as tool
def refresh():
ProductAssignmentsData.is_loaded = False
TextData.is_loaded = False
SheetsData.is_loaded = False
SchedulesData.is_loaded = False
DrawingsData.is_loaded = False
class ProductAssignmentsData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"relating_product": cls.relating_product()}
cls.is_loaded = True
@classmethod
def relating_product(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if not element or not element.is_a("IfcAnnotation"):
return
for rel in element.HasAssignments:
if rel.is_a("IfcRelAssignsToProduct"):
name = rel.RelatingProduct.Name or "Unnamed"
return f"{rel.RelatingProduct.is_a()}/{name}"
class TextData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"attributes": cls.attributes(), "relating_product": cls.relating_product()}
cls.data = {"attributes": cls.attributes()}
cls.is_loaded = True
@classmethod
@@ -49,15 +70,6 @@ class TextData:
{"name": "BoxAlignment", "value": text_literal.BoxAlignment},
]
@classmethod
def relating_product(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["TEXT", "TEXT_LEADER"]:
return
for rel in element.HasAssignments:
if rel.is_a("IfcRelAssignsToProduct"):
return rel.RelatingProduct.Name or "Unnamed"
class SheetsData:
data = {}
@@ -317,7 +317,7 @@ class OffsetHandle:
base = Vector((0, 0, 0))
axis = Vector((0, 0, 1))
# projecttion of the arrow to a plane, perpendicular to view ray
# projection of the arrow to a plane, perpendicular to view ray
axis_proj = axis - ray_norm * axis.dot(ray_norm)
# intersection of the axis with the plane through view origin perpendicular to the arrow projection
@@ -88,17 +88,17 @@ class BoundingBox:
def intersect(self, bb):
retVal = False
if self.vertex != None and bb.vertex != None:
# check all the faces of this object for a seperation axis
# check all the faces of this object for a separation axis
for i, f in enumerate(self.face):
d = f.normal
if self.whichSide(bb.vertex, d, f.vertex[0]) > 0:
return False # all the vertexes are on the +ve side of the face
return False # all the vertices are on the +ve side of the face
# now do it again for the other objects faces
for i, f in enumerate(bb.face):
d = f.normal
if self.whichSide(self.vertex, d, f.vertex[0]) > 0:
return False # all the vertexes are on the +ve side of the face
return False # all the vertices are on the +ve side of the face
# do edge checks
for e1 in self.edge:
@@ -139,7 +139,7 @@ def format_distance(value, isArea=False, hide_units=True):
value *= scaleFactor
# Imperial Formating
# Imperial Formatting
if unit_system == "IMPERIAL":
precision = bpy.context.scene.BIMProperties.imperial_precision
if precision == "NONE":
@@ -152,7 +152,7 @@ def format_distance(value, isArea=False, hide_units=True):
base = int(precision)
decInches = value * toInches
# Seperate ft and inches
# Separate ft and inches
# Unless Inches are the specified Length Unit
if unit_length != "INCHES":
feet = math.floor(decInches / inPerFoot)
@@ -160,7 +160,7 @@ def format_distance(value, isArea=False, hide_units=True):
else:
feet = 0
# Seperate Fractional Inches
# Separate Fractional Inches
inches = math.floor(decInches)
if inches != 0:
frac = round(base * (decInches - inches))
@@ -202,7 +202,7 @@ def format_distance(value, isArea=False, hide_units=True):
else:
tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft."
# METRIC FORMATING
# METRIC FORMATTING
elif unit_system == "METRIC":
precision = bpy.context.scene.BIMProperties.metric_precision
if precision != 0:
@@ -40,8 +40,8 @@ import blenderbim.bim.module.drawing.scheduler as scheduler
import blenderbim.bim.module.drawing.helper as helper
import blenderbim.bim.export_ifc
from lxml import etree
from math import pi
from mathutils import Vector, Matrix, Euler, geometry
from mathutils import Vector
from timeit import default_timer as timer
from blenderbim.bim.module.drawing.prop import RasterStyleProperty
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.group.data import Data as GroupData
@@ -50,6 +50,21 @@ from ifcopenshell.api.pset.data import Data as PsetData
cwd = os.path.dirname(os.path.realpath(__file__))
class profile:
"""
A python context manager timing utility
"""
def __init__(self, task):
self.task = task
def __enter__(self):
self.start = timer()
def __exit__(self, *args):
print(self.task, timer() - self.start)
def open_with_user_command(user_command, path):
if user_command:
commands = eval(user_command)
@@ -116,53 +131,75 @@ class CreateDrawing(bpy.types.Operator):
self.camera = context.scene.camera
self.camera_element = tool.Ifc.get_entity(self.camera)
self.file = IfcStore.get_file()
self.time = None
start = time.time()
self.profile_code("Start drawing generation process")
self.props = context.scene.DocProperties
self.cprops = self.camera.data.BIMCameraProperties
self.drawing_name = self.file.by_id(self.camera.BIMObjectProperties.ifc_definition_id).Name
self.get_scale()
if self.cprops.update_representation(self.camera):
bpy.ops.bim.update_representation(obj=self.camera.name, ifc_representation_class="")
underlay_svg = self.generate_underlay(context)
self.profile_code("Generate underlay")
linework_svg = self.generate_linework(context)
self.profile_code("Generate linework")
annotation_svg = self.generate_annotation(context)
self.profile_code("Generate annotation")
svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg)
self.profile_code("Combine SVG layers")
with profile("Drawing generation process"):
with profile("Initialize drawing generation process"):
self.props = context.scene.DocProperties
self.cprops = self.camera.data.BIMCameraProperties
self.drawing_name = self.file.by_id(self.camera.BIMObjectProperties.ifc_definition_id).Name
self.get_scale()
if self.cprops.update_representation(self.camera):
bpy.ops.bim.update_representation(obj=self.camera.name, ifc_representation_class="")
self.svg_writer = svgwriter.SvgWriter()
self.svg_writer.human_scale = self.human_scale
self.svg_writer.scale = self.scale
self.svg_writer.data_dir = context.scene.BIMProperties.data_dir
self.svg_writer.camera = self.camera
self.svg_writer.camera_width, self.svg_writer.camera_height = self.get_camera_dimensions()
self.svg_writer.camera_projection = tuple(self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)))
with profile("Generate underlay"):
underlay_svg = self.generate_underlay(context)
with profile("Generate linework"):
linework_svg = self.generate_linework(context)
with profile("Generate annotation"):
annotation_svg = self.generate_annotation(context)
with profile("Combine SVG layers"):
svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg)
open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, svg_path)
print("Total Time: {:.2f}".format(time.time() - start))
return {"FINISHED"}
def profile_code(self, message):
if not self.time:
self.time = time.time()
print("{} :: {:.2f}".format(message, time.time() - self.time))
self.time = time.time()
def get_camera_dimensions(self):
render = bpy.context.scene.render
if self.is_landscape(render):
width = self.camera.data.ortho_scale
height = width / render.resolution_x * render.resolution_y
else:
height = self.camera.data.ortho_scale
width = height / render.resolution_y * render.resolution_x
return width, height
def combine_svgs(self, context, underlay, linework, annotation):
# Hacky :)
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.drawing_name + ".svg")
with open(svg_path, "w") as outfile:
has_boilerplate = False
pset = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]
self.svg_writer.create_blank_svg(svg_path).define_boilerplate(
pset.get("Stylesheet"), pset.get("Markers"), pset.get("Symbols"), pset.get("Patterns")
)
boilerplate = self.svg_writer.svg.tostring()
outfile.write(boilerplate.replace("</svg>", ""))
if underlay:
with open(underlay) as infile:
for line in infile:
if "<svg " in line:
line = line.replace('">', '" xmlns:ifc="http://www.ifcopenshell.org/ns">')
if "</svg>" in line:
for i, line in enumerate(infile):
if i < 2:
continue
elif "</svg>" in line:
continue
outfile.write(line)
shutil.copyfile(underlay[0:-4] + ".png", svg_path[0:-4] + "-underlay.png")
has_boilerplate = True
if linework:
with open(linework) as infile:
should_skip = False
for i, line in enumerate(infile):
if has_boilerplate and i == 0:
if i == 0:
continue
if "</svg>" in line:
continue
@@ -175,11 +212,10 @@ class CreateDrawing(bpy.types.Operator):
elif should_skip:
continue
outfile.write(line)
has_boilerplate = True
if annotation:
with open(annotation) as infile:
for i, line in enumerate(infile):
if has_boilerplate and i in [0, 1]:
if i < 2:
continue
if "</svg>" in line:
continue
@@ -224,25 +260,7 @@ class CreateDrawing(bpy.types.Operator):
for name, value in previous_visibility.items():
bpy.data.objects[name].hide_set(value)
svg_writer = svgwriter.SvgWriter()
render = context.scene.render
if self.is_landscape(render):
width = self.camera.data.ortho_scale
height = width / render.resolution_x * render.resolution_y
else:
height = self.camera.data.ortho_scale
width = height / render.resolution_y * render.resolution_x
svg_writer.human_scale = self.human_scale
svg_writer.scale = self.scale
svg_writer.output = svg_path
svg_writer.data_dir = context.scene.BIMProperties.data_dir
svg_writer.vector_style = drawing_style.vector_style
svg_writer.camera = self.camera
svg_writer.camera_width = width
svg_writer.camera_height = height
svg_writer.camera_projection = tuple(self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)))
svg_writer.background_image = context.scene.render.filepath
svg_writer.write("underlay")
self.svg_writer.create_blank_svg(svg_path).draw_underlay(context.scene.render.filepath).save()
return svg_path
def generate_linework(self, context):
@@ -252,13 +270,14 @@ class CreateDrawing(bpy.types.Operator):
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
return svg_path
# All very hackish whilst prototyping
exporter = blenderbim.bim.export_ifc.IfcExporter(None)
exporter.file = tool.Ifc.get()
invalidated_guids = exporter.sync_deletions()
invalidated_elements = exporter.sync_all_objects()
invalidated_elements += exporter.sync_edited_objects()
[invalidated_guids.append(e.GlobalId) for e in invalidated_elements if hasattr(e, "GlobalId")]
with profile("sync"):
# All very hackish whilst prototyping
exporter = blenderbim.bim.export_ifc.IfcExporter(None)
exporter.file = tool.Ifc.get()
invalidated_guids = exporter.sync_deletions()
invalidated_elements = exporter.sync_all_objects()
invalidated_elements += exporter.sync_edited_objects()
[invalidated_guids.append(e.GlobalId) for e in invalidated_elements if hasattr(e, "GlobalId")]
# If we have already calculated it in the SVG in the past, don't recalculate
edited_guids = set()
@@ -309,40 +328,46 @@ class CreateDrawing(bpy.types.Operator):
cache = IfcStore.get_cache()
[cache.remove(guid) for guid in invalidated_guids]
if target_view_contexts and elements:
geom_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
geom_settings.set_context_ids(target_view_contexts)
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)
elements -= processed
with profile("Processing target view context"):
if target_view_contexts and elements:
geom_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
geom_settings.set_context_ids(target_view_contexts)
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)
elements -= processed
if body_contexts and elements:
with profile("Processing body context"):
if body_contexts and elements:
geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True)
geom_settings.set_context_ids(body_contexts)
it = ifcopenshell.geom.iterator(geom_settings, ifc, multiprocessing.cpu_count(), include=elements)
it.set_cache(cache)
for elem in self.yield_from_iterator(it):
self.serialiser.write(elem)
with profile("Camera element"):
# @todo tfk: is this needed?
geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True)
geom_settings.set_context_ids(body_contexts)
it = ifcopenshell.geom.iterator(geom_settings, ifc, multiprocessing.cpu_count(), include=elements)
it.set_cache(cache)
it = ifcopenshell.geom.iterator(geom_settings, ifc, include=[self.camera_element])
for elem in self.yield_from_iterator(it):
self.serialiser.write(elem)
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):
self.serialiser.write(elem)
self.serialiser.finalize()
with profile("Finalizing"):
self.serialiser.finalize()
results = self.svg_buffer.get_value()
root = etree.fromstring(results)
self.enrich_linework_with_metadata(root)
self.move_projection_to_bottom(root)
with open(svg_path, "wb") as svg:
svg.write(etree.tostring(root))
with profile("Post processing"):
root = etree.fromstring(results)
self.enrich_linework_with_metadata(root)
self.move_projection_to_bottom(root)
with open(svg_path, "wb") as svg:
svg.write(etree.tostring(root))
return svg_path
@@ -416,35 +441,13 @@ class CreateDrawing(bpy.types.Operator):
if os.path.isfile(svg_path) and self.props.should_use_annotation_cache:
return svg_path
camera = self.camera
svg_writer = svgwriter.SvgWriter()
drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index]
render = context.scene.render
if self.is_landscape(render):
width = camera.data.ortho_scale
height = width / render.resolution_x * render.resolution_y
else:
height = camera.data.ortho_scale
width = height / render.resolution_y * render.resolution_x
svg_writer.human_scale = self.human_scale
svg_writer.scale = self.scale
svg_writer.output = svg_path
svg_writer.data_dir = context.scene.BIMProperties.data_dir
svg_writer.vector_style = drawing_style.vector_style
svg_writer.camera = camera
svg_writer.camera_width = width
svg_writer.camera_height = height
svg_writer.camera_projection = tuple(camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)))
elements = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(self.camera_element))
svg_writer.annotations = sorted(elements, key=lambda a: tool.Drawing.get_annotation_z_index(a))
svg_writer.metadata = tool.Drawing.get_drawing_metadata(self.camera_element)
annotations = sorted(elements, key=lambda a: tool.Drawing.get_annotation_z_index(a))
svg_writer.write("annotation")
return svg_writer.output
self.svg_writer.metadata = tool.Drawing.get_drawing_metadata(self.camera_element)
self.svg_writer.create_blank_svg(svg_path).draw_annotations(annotations).save()
return svg_path
def get_scale(self):
if self.camera.data.BIMCameraProperties.diagram_scale == "CUSTOM":
@@ -506,8 +509,11 @@ class AddAnnotation(bpy.types.Operator, Operator):
def _execute(self, context):
drawing = tool.Ifc.get_entity(context.scene.camera)
if not drawing:
self.report({"WARNING"}, "Not a BIM camera")
return
core.add_annotation(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing, object_type=self.object_type)
r = core.add_annotation(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing, object_type=self.object_type)
if isinstance(r, str):
self.report({"WARNING"}, r)
class AddSheet(bpy.types.Operator, Operator):
@@ -1189,7 +1195,7 @@ class AddSectionsAnnotations(bpy.types.Operator):
]
def sideview(cam):
# leftmost and righmost points of camera view area, local coords
# leftmost and rightmost points of camera view area, local coords
xmin, xmax, _, _, _, _ = helper.ortho_view_frame(cam.data, margin=0)
proj = camera.matrix_world.inverted() @ cam.matrix_world
p_l = proj @ Vector((xmin, 0, 0))
@@ -1247,34 +1253,34 @@ class DisableEditingText(bpy.types.Operator, Operator):
core.disable_editing_text(tool.Drawing, obj=context.active_object)
class EditTextProduct(bpy.types.Operator, Operator):
bl_idname = "bim.edit_text_product"
class EditAssignedProduct(bpy.types.Operator, Operator):
bl_idname = "bim.edit_assigned_product"
bl_label = "Edit Text Product"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
product = None
if context.active_object.BIMTextProperties.relating_product:
product = tool.Ifc.get_entity(context.active_object.BIMTextProperties.relating_product)
core.edit_text_product(tool.Ifc, tool.Drawing, obj=context.active_object, product=product)
if context.active_object.BIMAssignedProductProperties.relating_product:
product = tool.Ifc.get_entity(context.active_object.BIMAssignedProductProperties.relating_product)
core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=context.active_object, product=product)
class EnableEditingTextProduct(bpy.types.Operator, Operator):
bl_idname = "bim.enable_editing_text_product"
class EnableEditingAssignedProduct(bpy.types.Operator, Operator):
bl_idname = "bim.enable_editing_assigned_product"
bl_label = "Enable Editing Text Product"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.enable_editing_text_product(tool.Drawing, obj=context.active_object)
core.enable_editing_assigned_product(tool.Drawing, obj=context.active_object)
class DisableEditingTextProduct(bpy.types.Operator, Operator):
bl_idname = "bim.disable_editing_text_product"
class DisableEditingAssignedProduct(bpy.types.Operator, Operator):
bl_idname = "bim.disable_editing_assigned_product"
bl_label = "Disable Editing Text Product"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.disable_editing_text_product(tool.Drawing, obj=context.active_object)
core.disable_editing_assigned_product(tool.Drawing, obj=context.active_object)
class LoadSheets(bpy.types.Operator, Operator):
@@ -1357,44 +1363,3 @@ class ContractSheet(bpy.types.Operator):
sheet.is_expanded = False
core.load_sheets(tool.Drawing)
return {"FINISHED"}
class ConvertEdgeToArc(bpy.types.Operator):
bl_idname = "bim.convert_edge_to_arc"
bl_label = "Convert Edge to Arc"
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False)
def draw(self, context):
layout = self.layout
for prop in self.__class__.__annotations__.keys():
layout.prop(self, prop)
def execute(self, context):
if bpy.context.mode != "EDIT_MESH":
return {"CANCELLED"}
obj = bpy.context.active_object
cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location
mesh = obj.data
bm = bmesh.from_edit_mesh(mesh)
selected_verts = [v for v in bm.verts if v.select]
if len(selected_verts) != 2:
return {"CANCELLED"}
l1 = selected_verts[0].co - cursor
l2 = selected_verts[1].co - cursor
angle = l1.xy.angle_signed(l2.xy)
if self.should_flip:
if angle > 0:
angle = -((pi * 2) - angle)
else:
angle = (pi * 2) + angle
v = selected_verts[0]
bm.verts.remove(selected_verts[1])
bmesh.ops.spin(bm, geom=[v], axis=(0, 0, 1), cent=cursor, steps=self.resolution * 4, angle=-angle)
bmesh.update_edit_mesh(mesh)
mesh.update()
return {"FINISHED"}
@@ -377,7 +377,6 @@ class BIMCameraProperties(PropertyGroup):
class BIMTextProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
attributes: CollectionProperty(name="Attributes", type=Attribute)
value: StringProperty(name="Value", default="TEXT")
font_size: EnumProperty(
@@ -392,4 +391,8 @@ class BIMTextProperties(PropertyGroup):
update=refreshFontSize,
name="Font Size",
)
class BIMAssignedProductProperties(PropertyGroup):
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object)
@@ -118,7 +118,7 @@ class SheetBuilder:
sheet_tree = ET.parse(sheet_path)
sheet_root = sheet_tree.getroot()
for g in sheet_root.findall('{http://www.w3.org/2000/svg}g'):
for g in sheet_root.findall("{http://www.w3.org/2000/svg}g"):
if g.attrib.get("data-id") == str(reference.id()):
sheet_root.remove(g)
break
@@ -20,7 +20,9 @@ import os
import re
import bpy
import math
import bmesh
import pystache
import mathutils
import xml.etree.ElementTree as ET
import svgwrite
import ifcopenshell
@@ -34,14 +36,6 @@ from mathutils import geometry
from blenderbim.bim.ifc import IfcStore
def load_occ():
# Don't import until we really need to, as a temporary step before we can purge OCC
try:
from OCC.Core import BRep, BRepTools, TopExp, TopAbs
except ImportError:
from OCC import BRep, BRepTools, TopExp, TopAbs
class External(svgwrite.container.Group):
def __init__(self, xml, **extra):
self.xml = xml
@@ -61,87 +55,78 @@ class External(svgwrite.container.Group):
class SvgWriter:
def __init__(self):
self.output = "out.svg"
self.data_dir = None
self.vector_style = None
self.human_scale = "NTS"
self.annotations = []
self.metadata = []
self.background_image = None
self.scale = 1 / 100 # 1:100
self.camera_width = None
self.camera_height = None
def write(self, layer):
def create_blank_svg(self, output_path):
self.calculate_scale()
self.svg = svgwrite.Drawing(
self.output,
debug=False,
output_path,
size=("{}mm".format(self.width), "{}mm".format(self.height)),
viewBox=("0 0 {} {}".format(self.width, self.height)),
id="root",
data_scale=self.human_scale,
debug=False, # Disable validation so that we can insert the IFC namespace
)
self.svg.attribs["xmlns:ifc"] = "http://www.ifcopenshell.org/ns"
return self
if layer == "underlay":
self.draw_background_image()
elif layer == "annotation":
self.add_stylesheet()
self.add_markers()
self.add_symbols()
self.add_patterns()
# self.draw_background_elements()
# self.draw_cut_polygons()
self.draw_annotations()
def save(self):
self.svg.save(pretty=True)
def calculate_scale(self):
self.scale *= 1000 # IFC is in meters, SVG is in mm
self.raw_width = self.camera_width
self.raw_height = self.camera_height
self.width = self.raw_width * self.scale
self.height = self.raw_height * self.scale
def add_stylesheet(self):
with open(os.path.join(self.data_dir, "styles", f"{self.vector_style}.css"), "r") as stylesheet:
self.svg.defs.add(self.svg.style(stylesheet.read()))
def add_markers(self):
tree = ET.parse(os.path.join(self.data_dir, "templates", "markers.svg"))
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def add_symbols(self):
tree = ET.parse(os.path.join(self.data_dir, "templates", "symbols.svg"))
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def add_patterns(self):
tree = ET.parse(os.path.join(self.data_dir, "templates", "patterns.svg"))
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def draw_background_image(self):
def draw_underlay(self, image):
self.svg.add(
self.svg.image(
os.path.join("..", "diagrams", os.path.basename(self.background_image)),
**{"width": self.width, "height": self.height},
os.path.join("..", "diagrams", os.path.basename(image)),
width=self.width,
height=self.height,
)
)
return self
def draw_background_elements(self):
return # TODO purge?
for element in self.ifc_cutter.background_elements:
if element["type"] == "polygon":
self.draw_polygon(element, "background")
elif element["type"] == "polyline":
self.draw_polyline(element, "background")
elif element["type"] == "line":
self.draw_line(element, "background")
def define_boilerplate(self, stylesheet=None, markers=None, symbols=None, patterns=None):
self.add_stylesheet(stylesheet)
self.add_markers(markers)
self.add_symbols(symbols)
self.add_patterns(patterns)
return self
def draw_annotations(self):
for element in self.annotations:
def calculate_scale(self):
self.svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
self.raw_width = self.camera_width
self.raw_height = self.camera_height
self.width = self.raw_width * self.svg_scale
self.height = self.raw_height * self.svg_scale
def add_stylesheet(self, uri):
default_stylesheet = os.path.join(self.data_dir, "styles", f"default.css")
with open(tool.Ifc.resolve_uri(uri) or default_stylesheet, "r") as stylesheet:
self.svg.defs.add(self.svg.style(stylesheet.read()))
def add_markers(self, uri):
tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "markers.svg"))
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def add_symbols(self, uri):
tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "symbols.svg"))
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def add_patterns(self, uri):
tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "patterns.svg"))
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def draw_annotations(self, annotations):
for element in annotations:
obj = tool.Ifc.get_object(element)
if not obj or element.ObjectType == "DRAWING":
continue
@@ -153,6 +138,8 @@ class SvgWriter:
self.draw_stair_annotation(obj)
elif element.ObjectType == "DIMENSION":
self.draw_dimension_annotations(obj)
elif element.ObjectType == "ANGLE":
self.draw_angle_annotations(obj)
elif element.ObjectType == "RADIUS":
self.draw_radius_annotations(obj)
elif element.ObjectType == "DIAMETER":
@@ -176,6 +163,7 @@ class SvgWriter:
# Experimental integration with the MeasureIt-ARCH Add-on
self.draw_measureit_arch_dimension_annotations()
return self
def draw_section_level_annotation(self, obj):
x_offset = self.raw_width / 2
@@ -190,7 +178,7 @@ class SvgWriter:
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = " ".join(
[
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
@@ -198,8 +186,8 @@ class SvgWriter:
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
text_position = Vector(
(
(x_offset + projected_points[0].x) * self.scale,
((y_offset - projected_points[0].y) * self.scale) - 3.5,
(x_offset + projected_points[0].x) * self.svg_scale,
((y_offset - projected_points[0].y) * self.svg_scale) - 3.5,
)
)
# TODO: allow metric to be configurable
@@ -229,14 +217,14 @@ class SvgWriter:
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = " ".join(
[
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
d = "M{}".format(d[1:])
start = Vector(((x_offset + projected_points[0].x), (y_offset - projected_points[0].y)))
next_point = Vector(((x_offset + projected_points[1].x), (y_offset - projected_points[1].y)))
text_position = (start * self.scale) - ((next_point - start).normalized() * 5)
text_position = (start * self.svg_scale) - ((next_point - start).normalized() * 5)
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
self.svg.add(
self.svg.text(
@@ -266,14 +254,16 @@ class SvgWriter:
end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
vector = end - start
line = self.svg.add(
self.svg.line(start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes))
self.svg.line(
start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes)
)
)
line["stroke-dasharray"] = "12.5, 3, 3, 3"
axis_tag = tool.Ifc.get_entity(obj).Name
self.svg.add(
self.svg.text(
axis_tag,
insert=tuple(start * self.scale),
insert=tuple(start * self.svg_scale),
**{
"font-size": annotation.Annotator.get_svg_text_size(5.0),
"font-family": "OpenGost Type B TT",
@@ -286,7 +276,7 @@ class SvgWriter:
self.svg.add(
self.svg.text(
axis_tag,
insert=tuple(end * self.scale),
insert=tuple(end * self.svg_scale),
**{
"font-size": annotation.Annotator.get_svg_text_size(5.0),
"font-family": "OpenGost Type B TT",
@@ -316,7 +306,7 @@ class SvgWriter:
projected_points.append(projected_points[0])
d = " ".join(
[
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
@@ -353,7 +343,7 @@ class SvgWriter:
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = " ".join(
[
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
@@ -375,7 +365,9 @@ class SvgWriter:
end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
vector = end - start
line = self.svg.add(
self.svg.line(start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes))
self.svg.line(
start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes)
)
)
def draw_leader_annotation(self, obj):
@@ -404,17 +396,17 @@ class SvgWriter:
symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y)))
transform = "rotate({}, {}, {})".format(
angle,
(symbol_position * self.scale)[0],
(symbol_position * self.scale)[1],
(symbol_position * self.svg_scale)[0],
(symbol_position * self.svg_scale)[1],
)
self.svg.add(
self.svg.use("#section-arrow", insert=tuple(symbol_position * self.scale), transform=transform)
self.svg.use("#section-arrow", insert=tuple(symbol_position * self.svg_scale), transform=transform)
)
self.svg.add(self.svg.use("#section-tag", insert=tuple(symbol_position * self.scale)))
self.svg.add(self.svg.use("#section-tag", insert=tuple(symbol_position * self.svg_scale)))
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
text_position = list(symbol_position * self.scale)
text_position = list(symbol_position * self.svg_scale)
text_style = {
"font-size": annotation.Annotator.get_svg_text_size(2.5),
"font-family": "OpenGost Type B TT",
@@ -437,15 +429,17 @@ class SvgWriter:
transform = "rotate({}, {}, {})".format(
angle,
(symbol_position * self.scale)[0],
(symbol_position * self.scale)[1],
(symbol_position * self.svg_scale)[0],
(symbol_position * self.svg_scale)[1],
)
self.svg.add(self.svg.use("#elevation-arrow", insert=tuple(symbol_position * self.scale), transform=transform))
self.svg.add(self.svg.use("#elevation-tag", insert=tuple(symbol_position * self.scale)))
self.svg.add(
self.svg.use("#elevation-arrow", insert=tuple(symbol_position * self.svg_scale), transform=transform)
)
self.svg.add(self.svg.use("#elevation-tag", insert=tuple(symbol_position * self.svg_scale)))
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
text_position = list(symbol_position * self.scale)
text_position = list(symbol_position * self.svg_scale)
text_style = {
"font-size": annotation.Annotator.get_svg_text_size(2.5),
"font-family": "OpenGost Type B TT",
@@ -493,13 +487,13 @@ class SvgWriter:
transform = "rotate({}, {}, {})".format(
angle,
(text_position * self.scale)[0],
(text_position * self.scale)[1],
(text_position * self.svg_scale)[0],
(text_position * self.svg_scale)[1],
)
symbol = tool.Drawing.get_annotation_symbol(element)
if symbol:
self.svg.add(self.svg.use(f"#{symbol}", insert=tuple(text_position * self.scale)))
self.svg.add(self.svg.use(f"#{symbol}", insert=tuple(text_position * self.svg_scale)))
if text_literal.BoxAlignment == "top-left":
alignment_baseline = "hanging"
@@ -531,17 +525,18 @@ class SvgWriter:
literal = text_literal.Literal
product = tool.Drawing.get_text_product(element)
product = tool.Drawing.get_assigned_product(element)
selector = ifcopenshell.util.selector.Selector
variables = {}
for variable in re.findall("{{.*?}}", literal):
literal = literal.replace(variable, selector.get_element_value(product, variable[2:-2]) or "")
if product != None:
for variable in re.findall("{{.*?}}", literal):
literal = literal.replace(variable, str(selector.get_element_value(product, variable[2:-2]) or ""))
for line_number, text_line in enumerate(literal.replace("\\n", "\n").split("\n")):
self.svg.add(
self.svg.text(
text_line,
insert=tuple((text_position * self.scale) + Vector((0, 3.5 * line_number))),
insert=tuple((text_position * self.svg_scale) + Vector((0, 3.5 * line_number))),
class_=" ".join(self.get_attribute_classes(text_obj)),
**{
"font-size": annotation.Annotator.get_svg_text_size(text_obj.BIMTextProperties.font_size),
@@ -570,7 +565,7 @@ class SvgWriter:
]
d = " ".join(
[
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
@@ -587,7 +582,7 @@ class SvgWriter:
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = " ".join(
[
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
@@ -595,8 +590,8 @@ class SvgWriter:
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
text_position = Vector(
(
(x_offset + projected_points[0].x) * self.scale,
((y_offset - projected_points[0].y) * self.scale) - 2.5,
(x_offset + projected_points[0].x) * self.svg_scale,
((y_offset - projected_points[0].y) * self.svg_scale) - 2.5,
)
)
# TODO: allow metric to be configurable
@@ -623,6 +618,119 @@ class SvgWriter:
)
)
def draw_angle_annotations(self, obj):
# This implementation uses an SVG arc, which means that it can only draw
# arcs that are orthogonal to the view (e.g. not arcs in 3D).
# Gosh this is bad code :(
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
classes = self.get_attribute_classes(obj)
matrix_world = obj.matrix_world
points = [v.co for v in obj.data.vertices][:3]
center = tool.Cad.get_center_of_arc(points, obj)
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.verts.ensure_lookup_table()
arc_end_verts = [v for v in bm.verts if len(v.link_edges) == 1]
arc_end_pts = [matrix_world @ v.co for v in arc_end_verts]
# Probably need this when rewriting to use an SVG polyline instead of an arc
# arc_path = [arc_end_verts[0]]
# while True:
# last_point = arc_end_verts[0]
# found_another_point = False
# for edge in last_point.link_edges:
# v = edge.other_vert(last_point)
# if v not in arc_path:
# found_another_point = True
# arc_path.append(v)
# if not found_another_point:
# break
distance_between_end_verts = (arc_end_verts[0].co - arc_end_verts[1].co).length
arc_mid_vert = arc_end_verts[0].link_edges[0].other_vert(arc_end_verts[0])
is_reflex = 0 if (arc_mid_vert.co - arc_end_verts[1].co).length < distance_between_end_verts else 1
bm.free()
# Calculate the angle
# This is the true normal in 3D, whereas the camera projection we use is the drawing direction.
# This assumes (because we use SVG arcs) that the radius is always orthogonal to our view.
# When rewriting to use polylines, we should use this normal instead.
# normal = mathutils.geometry.normal([arc_end_pts[0], arc_end_pts[1], center])
normal = Vector(self.camera_projection)
dir1 = (arc_end_pts[0] - center).normalized()
dir2 = (arc_end_pts[1] - center).normalized()
# Let's get the matrix that represents the coordinate system of the arc.
# This matrix allows us to get 2D vectors for calculating the signed arc angle.
z = normal
x = (arc_end_pts[0] - center).normalized()
y = z.cross(x)
arc_matrix = mathutils.Matrix([x, y, z]).transposed().to_4x4()
dir1 = ((arc_matrix.inverted() @ arc_end_pts[0]) - (arc_matrix.inverted() @ center)).normalized()
dir2 = ((arc_matrix.inverted() @ arc_end_pts[1]) - (arc_matrix.inverted() @ center)).normalized()
angle = -dir1.xy.angle_signed(dir2.xy)
# if is_reflex:
# angle = angle % (math.pi * 2)
# Center of gravity of all vertices, used to help position the text
cog = Vector((0, 0, 0))
for vert in obj.data.vertices:
cog += vert.co
cog = matrix_world @ (cog / len(obj.data.vertices))
radius = ((matrix_world @ obj.data.vertices[0].co) - center).length
arc_midpoint = center + ((cog - center).normalized() * radius)
text_position = self.project_point_onto_camera(arc_midpoint)
text_position = Vector(
((x_offset + text_position.x) * self.svg_scale, (y_offset - text_position.y) * self.svg_scale)
)
center_projected = self.project_point_onto_camera(center)
center_position = Vector(
((x_offset + center_projected.x) * self.svg_scale, (y_offset - center_projected.y) * self.svg_scale)
)
text_offset = (text_position - center_position).xy.normalized() * 5
text_position += text_offset
text_style = {
"font-size": annotation.Annotator.get_svg_text_size(2.5),
"font-family": "OpenGost Type B TT",
"text-anchor": "middle",
"alignment-baseline": "middle",
"dominant-baseline": "middle",
}
angle_text = abs(round(math.degrees(angle), 3))
if is_reflex:
angle_text = 360 - angle_text
self.svg.add(self.svg.text(f"{angle_text}deg", insert=tuple(text_position), **text_style))
# Draw SVG arc, see for details: http://xahlee.info/js/svg_circle_arc.html
arc_proj_end_pts = [self.project_point_onto_camera(v) for v in arc_end_pts]
p1 = Vector(
((x_offset + arc_proj_end_pts[0].x) * self.svg_scale, (y_offset - arc_proj_end_pts[0].y) * self.svg_scale)
)
p2 = Vector(
((x_offset + arc_proj_end_pts[1].x) * self.svg_scale, (y_offset - arc_proj_end_pts[1].y) * self.svg_scale)
)
r = radius * self.svg_scale
# reflex = 1 if angle > math.pi else 0
reflex = is_reflex
if reflex:
sense = 0 if angle > 0 else 1
else:
sense = 1 if angle > 0 else 0
d = f"M {p1.x} {p1.y} A {r} {r} 0 {reflex} {sense} {p2.x} {p2.y}"
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
def draw_radius_annotations(self, obj):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -634,7 +742,7 @@ class SvgWriter:
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = " ".join(
[
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
@@ -642,15 +750,21 @@ class SvgWriter:
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
p0 = Vector(
((x_offset + projected_points[0].x) * self.scale, (y_offset - projected_points[0].y) * self.scale)
(
(x_offset + projected_points[0].x) * self.svg_scale,
(y_offset - projected_points[0].y) * self.svg_scale,
)
)
p1 = Vector(
((x_offset + projected_points[1].x) * self.scale, (y_offset - projected_points[1].y) * self.scale)
(
(x_offset + projected_points[1].x) * self.svg_scale,
(y_offset - projected_points[1].y) * self.svg_scale,
)
)
text_offset = (p0 - p1).xy.normalized() * 5
text_position = projected_points[0]
text_position = Vector(
((x_offset + text_position.x) * self.scale, (y_offset - text_position.y) * self.scale)
((x_offset + text_position.x) * self.svg_scale, (y_offset - text_position.y) * self.svg_scale)
)
text_position += text_offset
@@ -722,15 +836,17 @@ class SvgWriter:
perpendicular = Vector((vector.y, -vector.x)).normalized()
dimension = (v1_global - v0_global).length
dimension = helper.format_distance(dimension)
sheet_dimension = ((end * self.scale) - (start * self.scale)).length
sheet_dimension = ((end * self.svg_scale) - (start * self.svg_scale)).length
if sheet_dimension < 5: # annotation can't fit
# offset text to right of marker
text_position = (end * self.scale) + perpendicular + (3 * vector.normalized())
text_position = (end * self.svg_scale) + perpendicular + (3 * vector.normalized())
else:
text_position = (mid * self.scale) + perpendicular
text_position = (mid * self.svg_scale) + perpendicular
rotation = math.degrees(vector.angle_signed(Vector((1, 0))))
line = self.svg.add(
self.svg.line(start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes))
self.svg.line(
start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes)
)
)
if text_override is not None:
text = text_override
@@ -762,35 +878,8 @@ class SvgWriter:
def get_spline_points(self, spline):
return spline.bezier_points if spline.bezier_points else spline.points
def draw_cut_polygons(self):
return # deprecate?
for polygon in self.ifc_cutter.cut_polygons:
self.draw_polygon(polygon, "cut")
def draw_polyline(self, element, position):
load_occ()
classes = self.get_classes(element["raw"], position)
exp = BRepTools.BRepTools_WireExplorer(element["geometry"])
points = []
while exp.More():
point = BRep.BRep_Tool.Pnt(exp.CurrentVertex())
points.append((point.X() * self.scale, -point.Y() * self.scale))
exp.Next()
self.svg.add(self.svg.polyline(points=points, class_=" ".join(classes)))
def draw_line(self, element, position):
load_occ()
classes = self.get_classes(element["raw"], position)
exp = TopExp.TopExp_Explorer(element["geometry"], TopAbs.TopAbs_VERTEX)
points = []
while exp.More():
point = BRep.BRep_Tool.Pnt(topods.Vertex(exp.Current()))
points.append((point.X() * self.scale, -point.Y() * self.scale))
exp.Next()
self.svg.add(self.svg.line(start=points[0], end=points[1], class_=" ".join(classes)))
def draw_polygon(self, polygon, position):
points = [(p[0] * self.scale, p[1] * self.scale) for p in polygon["points"]]
points = [(p[0] * self.svg_scale, p[1] * self.svg_scale) for p in polygon["points"]]
if "classes" in polygon["metadata"]:
classes = " ".join(polygon["metadata"]["classes"])
else:
@@ -20,7 +20,7 @@ import bpy
import blenderbim.bim.helper
import blenderbim.tool as tool
from bpy.types import Panel
from blenderbim.bim.module.drawing.data import TextData, SheetsData, SchedulesData, DrawingsData
from blenderbim.bim.module.drawing.data import ProductAssignmentsData, TextData, SheetsData, SchedulesData, DrawingsData
class BIM_PT_camera(Panel):
@@ -170,13 +170,16 @@ class BIM_PT_drawings(Panel):
if self.props.active_drawing_index < len(self.props.drawings):
active_drawing = self.props.drawings[self.props.active_drawing_index]
row = self.layout.row(align=True)
row.alignment = "RIGHT"
col = row.column()
col.alignment = 'LEFT'
col.operator("bim.remove_drawing", icon="X", text="").drawing = active_drawing.ifc_definition_id
col = row.column()
col.alignment = 'RIGHT'
op = row.operator("bim.open_view", icon="URL", text="")
op.view = active_drawing.name
op = row.operator("bim.activate_view", icon="OUTLINER_OB_CAMERA", text="")
op.drawing = active_drawing.ifc_definition_id
row.operator("bim.create_drawing", text="", icon="OUTPUT")
row.operator("bim.remove_drawing", icon="X", text="").drawing = active_drawing.ifc_definition_id
self.layout.template_list(
"BIM_UL_drawinglist", "", self.props, "drawings", self.props, "active_drawing_index"
)
@@ -225,10 +228,14 @@ class BIM_PT_schedules(Panel):
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.operator("bim.open_schedule", icon="URL", text="").schedule = active_schedule.ifc_definition_id
row.operator("bim.build_schedule", icon="LINENUMBERS_ON", text="").schedule = active_schedule.ifc_definition_id
row.operator(
"bim.build_schedule", icon="LINENUMBERS_ON", text=""
).schedule = active_schedule.ifc_definition_id
row.operator("bim.remove_schedule", icon="X", text="").schedule = active_schedule.ifc_definition_id
self.layout.template_list("BIM_UL_generic", "", self.props, "schedules", self.props, "active_schedule_index")
self.layout.template_list(
"BIM_UL_generic", "", self.props, "schedules", self.props, "active_schedule_index"
)
class BIM_PT_sheets(Panel):
@@ -276,6 +283,39 @@ class BIM_PT_sheets(Panel):
self.layout.template_list("BIM_UL_sheets", "", self.props, "sheets", self.props, "active_sheet_index")
class BIM_PT_product_assignments(Panel):
bl_label = "IFC Product Assignments"
bl_idname = "BIM_PT_product_assignments"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
@classmethod
def poll(cls, context):
if not tool.Ifc.get() or not context.active_object:
return
element = tool.Ifc.get_entity(context.active_object)
if not element:
return
return element.is_a("IfcAnnotation")
def draw(self, context):
if not ProductAssignmentsData.is_loaded:
ProductAssignmentsData.load()
props = context.active_object.BIMAssignedProductProperties
if props.is_editing_product:
row = self.layout.row(align=True)
row.prop(props, "relating_product", text="")
row.operator("bim.edit_assigned_product", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_assigned_product", icon="CANCEL", text="")
else:
row = self.layout.row(align=True)
row.label(text=ProductAssignmentsData.data["relating_product"] or "No Relating Product", icon="OBJECT_DATA")
row.operator("bim.enable_editing_assigned_product", icon="GREASEPENCIL", text="")
class BIM_PT_text(Panel):
bl_label = "IFC Text"
bl_idname = "BIM_PT_text"
@@ -312,16 +352,6 @@ class BIM_PT_text(Panel):
row.label(text=attribute["name"])
row.label(text=attribute["value"])
if props.is_editing_product:
row = self.layout.row(align=True)
row.prop(props, "relating_product", text="")
row.operator("bim.edit_text_product", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_text_product", icon="CANCEL", text="")
else:
row = self.layout.row(align=True)
row.label(text=TextData.data["relating_product"] or "No Relating Product", icon="OBJECT_DATA")
row.operator("bim.enable_editing_text_product", icon="GREASEPENCIL", text="")
row = self.layout.row()
row.prop(props, "font_size")
row = self.layout.row()
@@ -25,17 +25,20 @@ classes = (
operator.EditObjectPlacement,
operator.GetRepresentationIfcParameters,
operator.OverrideDelete,
operator.OverrideOutlinerDelete,
operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked,
operator.OverrideOutlinerDelete,
operator.OverridePasteBuffer,
operator.RemoveConnection,
operator.RemoveRepresentation,
operator.SelectConnection,
operator.SwitchRepresentation,
operator.UpdateParametricRepresentation,
operator.UpdateRepresentation,
prop.BIMGeometryProperties,
ui.BIM_PT_derived_placements,
ui.BIM_PT_representations,
ui.BIM_PT_connections,
ui.BIM_PT_mesh,
ui.BIM_PT_workarounds,
)
@@ -25,6 +25,7 @@ from mathutils import Vector
def refresh():
DerivedPlacementsData.is_loaded = False
RepresentationsData.is_loaded = False
ConnectionsData.is_loaded = False
class RepresentationsData:
@@ -64,6 +65,40 @@ class RepresentationsData:
return results
class ConnectionsData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"connections": cls.connections()}
cls.is_loaded = True
@classmethod
def connections(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object)
for rel in getattr(element, "ConnectedTo", []):
results.append(
{
"id": rel.id(),
"is_relating": True,
"Name": rel.RelatedElement.Name or "Unnamed",
"ConnectionType": rel.RelatingConnectionType,
}
)
for rel in getattr(element, "ConnectedFrom", []):
results.append(
{
"id": rel.id(),
"is_relating": False,
"Name": rel.RelatingElement.Name or "Unnamed",
"ConnectionType": rel.RelatedConnectionType,
}
)
return results
class DerivedPlacementsData:
data = {}
is_loaded = False
@@ -21,7 +21,7 @@ import bmesh
import mathutils
import ifcopenshell
import ifcopenshell.util.unit
from math import pi
from math import pi, pow
from mathutils import Vector, Matrix, geometry
@@ -112,6 +112,167 @@ class Helper:
return {"profile": profile, "extrusion": extrusion}
def auto_detect_arbitrary_profile_with_voids(self, obj, mesh):
groups = {"IFCARCINDEX": [], "IFCCIRCLE": []}
for i, group in enumerate(obj.vertex_groups):
if "IFCARCINDEX" in group.name:
groups["IFCARCINDEX"].append(i)
elif "IFCCIRCLE" in group.name:
groups["IFCCIRCLE"].append(i)
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
bmesh.ops.delete(bm, geom=bm.faces, context="FACES_ONLY")
# https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess
# This is how we access vertex groups via bmesh, apparently, it's not very intuitive
deform_layer = bm.verts.layers.deform.active
# Sanity check
group_verts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}}
for vert in bm.verts:
total_groups = 0
is_circle = False
for group_type, group_indices in groups.items():
for group_index in group_indices:
if group_index in vert[deform_layer]:
if group_type == "IFCCIRCLE":
is_circle = True
group_verts[group_type].setdefault(group_index, 0)
group_verts[group_type][group_index] += 1
total_groups += 0
if total_groups > 1: # A vert can only belong to one group
return (False, "AMBIGUOUS_SPECIAL_VERTEX")
elif is_circle:
pass # Circles are allowed to be unclosed
elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop
return (False, "UNCLOSED_LOOP")
for group_type, group_counts in group_verts.items():
if group_type == "IFCARCINDEX":
for group_count in group_counts.values():
if group_count != 3: # Each arc needs 3 verts
return (False, "3POINT_ARC")
elif group_type == "IFCCIRCLE":
for group_count in group_counts.values():
if group_count != 2: # Each circle needs 2 verts
return (False, "CIRCLE")
loop_edges = set(bm.edges)
# Create loops from edges
loops = []
while loop_edges:
edge = loop_edges.pop()
loop = [edge]
has_found_connected_edge = True
while has_found_connected_edge:
has_found_connected_edge = False
for edge in loop_edges.copy():
edge_verts = set(edge.verts)
if edge_verts & set(loop[0].verts):
loop.insert(0, edge)
loop_edges.remove(edge)
has_found_connected_edge = True
elif edge_verts & set(loop[-1].verts):
loop.append(edge)
loop_edges.remove(edge)
has_found_connected_edge = True
loops.append(loop)
# Determine outer loop
max_area = 0
outer_loop = None
inner_loops = []
for loop in loops:
loop_vertices = []
total_edges = len(loop)
if total_edges == 1:
loop_vertices = [loop[0].verts[0], loop[0].verts[1]]
else:
for i, edge in enumerate(loop):
if i + 1 == total_edges and edge.verts[0] in loop[i - 1].verts:
loop_vertices.append(edge.verts[0])
elif i + 1 == total_edges and edge.verts[1] in loop[i - 1].verts:
loop_vertices.append(edge.verts[1])
elif edge.verts[0] in loop[i + 1].verts:
loop_vertices.append(edge.verts[1])
elif edge.verts[1] in loop[i + 1].verts:
loop_vertices.append(edge.verts[0])
if len(loop_vertices) == 2:
# Unofficial convention right now that a loop of 2 verts always is a circle
radius = (loop_vertices[0].co - loop_vertices[1].co).length / 2
face_area = pi * pow(radius, 2)
else:
loop_bm = bmesh.new()
for vert in loop_vertices:
loop_bm.verts.new(vert.co)
face = loop_bm.faces.new(loop_bm.verts)
face_area = face.calc_area()
loop_bm.free()
loop_vertex_indices = []
active_arc_id = None
arc_stack = []
last_index = len(loop_vertices) - 1
# It is possible to loop through loop_vertices halfway through an arc.
# If that is the case, we store it in incomplete_arc
incomplete_arc = []
for i, v in enumerate(loop_vertices):
if len(arc_stack) == 3:
loop_vertex_indices.append(arc_stack)
loop_vertex_indices.append((arc_stack[-1], v.index))
arc_stack = []
active_arc_id = None
is_arc = False
for group_index in groups["IFCARCINDEX"]:
if group_index in v[deform_layer]:
if active_arc_id is not None and active_arc_id != group_index:
incomplete_arc = arc_stack
arc_stack = []
active_arc_id = group_index
is_arc = True
break
if i == last_index and not is_arc:
continue
if is_arc:
arc_stack.append(v.index)
else:
if active_arc_id is not None:
incomplete_arc = arc_stack
active_arc_id = None
arc_stack = []
loop_vertex_indices.append((v.index, loop_vertices[i + 1].index))
if active_arc_id is not None:
arc_stack.extend(incomplete_arc)
loop_vertex_indices.append(arc_stack)
loop_vertex_indices.append((loop_vertex_indices[-1][-1], loop_vertex_indices[0][0]))
if face_area > max_area:
max_area = face_area
outer_loop = loop_vertex_indices
inner_loops.append(loop_vertex_indices)
inner_loops.remove(outer_loop)
points = [v.co for v in bm.verts]
bm.to_mesh(mesh)
mesh.update()
bm.free()
return {"points": points, "profile": outer_loop, "inner_curves": inner_loops}
# An arbitrary closed profile with voids is similar to one without voids.
# We start the same way with any ngon (no tri), but instead of being the entire
# profile, it is only one of the possible faces that make up the end of our
@@ -86,6 +86,26 @@ class AddRepresentation(bpy.types.Operator, Operator):
)
class SelectConnection(bpy.types.Operator, Operator):
bl_idname = "bim.select_connection"
bl_label = "Select Connection"
bl_options = {"REGISTER", "UNDO"}
connection: bpy.props.IntProperty()
def _execute(self, context):
core.select_connection(tool.Geometry, connection=tool.Ifc.get().by_id(self.connection))
class RemoveConnection(bpy.types.Operator, Operator):
bl_idname = "bim.remove_connection"
bl_label = "Remove Connection"
bl_options = {"REGISTER", "UNDO"}
connection: bpy.props.IntProperty()
def _execute(self, context):
core.remove_connection(tool.Geometry, connection=tool.Ifc.get().by_id(self.connection))
class SwitchRepresentation(bpy.types.Operator, Operator):
bl_idname = "bim.switch_representation"
bl_label = "Switch Representation"
@@ -97,15 +117,29 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
should_switch_all_meshes: bpy.props.BoolProperty()
def _execute(self, context):
core.switch_representation(
tool.Geometry,
obj=bpy.data.objects.get(self.obj) if self.obj else context.active_object,
representation=tool.Ifc.get().by_id(self.ifc_definition_id),
should_reload=self.should_reload,
enable_dynamic_voids=self.disable_opening_subtractions,
is_global=self.should_switch_all_meshes,
should_sync_changes_first=True,
)
target = tool.Ifc.get().by_id(self.ifc_definition_id).ContextOfItems
is_subcontext = target.is_a("IfcGeometricRepresentationSubContext")
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if is_subcontext:
representation = ifcopenshell.util.representation.get_representation(
element, target.ContextType, target.ContextIdentifier, target.TargetView
)
else:
representation = ifcopenshell.util.representation.get_representation(element, target.ContextType)
if not representation:
continue
core.switch_representation(
tool.Geometry,
obj=obj,
representation=representation,
should_reload=self.should_reload,
enable_dynamic_voids=self.disable_opening_subtractions,
is_global=self.should_switch_all_meshes,
should_sync_changes_first=True,
)
class RemoveRepresentation(bpy.types.Operator, Operator):
@@ -130,10 +164,6 @@ class UpdateRepresentation(bpy.types.Operator):
obj: bpy.props.StringProperty()
ifc_representation_class: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
return context.active_object.mode == "OBJECT"
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
@@ -141,6 +171,10 @@ class UpdateRepresentation(bpy.types.Operator):
if not ContextData.is_loaded:
ContextData.load(IfcStore.get_file())
if context.active_object and context.active_object.mode != "OBJECT":
# Ensure mode is object to prevent invalid mesh data causing CTD
bpy.ops.object.mode_set(mode="OBJECT", toggle=False)
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
self.file = IfcStore.get_file()
@@ -156,18 +190,23 @@ class UpdateRepresentation(bpy.types.Operator):
def update_obj_mesh_representation(self, context, obj):
product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
material = ifcopenshell.util.element.get_material(product, should_skip_usage=True)
if not product.is_a("IfcGridAxis"):
tool.Geometry.clear_cache(product)
if product.is_a("IfcGridAxis"):
# Grid geometry does not follow the "representation" paradigm and needs to be treated specially
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"axis_curve": obj, "grid_axis": product})
return
if product.is_a("IfcRelSpaceBoundary"):
elif product.is_a("IfcRelSpaceBoundary"):
# TODO refactor
settings = tool.Boundary.get_assign_connection_geometry_settings(obj)
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
return
elif material and material.is_a() in ["IfcMaterialProfileSet", "IfcMaterialLayerSet"]:
# These objects are parametrically based on an axis and should not be modified as a mesh
return
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
@@ -207,7 +246,10 @@ class UpdateRepresentation(bpy.types.Operator):
new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
if tool.Geometry.is_body_representation(new_representation):
[tool.Geometry.run_style_add_style(obj=mat) for mat in tool.Geometry.get_object_materials_without_styles(obj)]
[
tool.Geometry.run_style_add_style(obj=mat)
for mat in tool.Geometry.get_object_materials_without_styles(obj)
]
ifcopenshell.api.run(
"style.assign_representation_styles",
self.file,
@@ -237,7 +279,7 @@ class UpdateParametricRepresentation(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.active_object.mode == "OBJECT"
return context.active_object and context.active_object.mode == "OBJECT"
def execute(self, context):
self.file = IfcStore.get_file()
@@ -331,9 +373,12 @@ class OverrideDeleteTrait:
self.remove_filling(rel.RelatedBuildingElement)
if element.VoidsElements:
self.delete_opening_element(element)
elif getattr(element, "HasOpenings", None):
for rel in element.HasOpenings:
self.delete_opening_element(rel.RelatedOpeningElement)
else:
if getattr(element, "HasOpenings", None):
for rel in element.HasOpenings:
self.delete_opening_element(rel.RelatedOpeningElement)
for port in ifcopenshell.util.system.get_ports(element):
self.remove_port(port)
def delete_opening_element(self, element):
obj = IfcStore.get_element(element.VoidsElements[0].RelatingBuildingElement.id())
@@ -343,6 +388,9 @@ class OverrideDeleteTrait:
obj = IfcStore.get_element(element.id())
bpy.ops.bim.remove_filling(obj=obj.name)
def remove_port(self, port):
blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
class OverrideDelete(bpy.types.Operator, OverrideDeleteTrait):
bl_idname = "object.delete"
@@ -19,7 +19,8 @@
import bpy
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.geometry.data import RepresentationsData, DerivedPlacementsData
from blenderbim.bim.helper import prop_with_search
from blenderbim.bim.module.geometry.data import RepresentationsData, ConnectionsData, DerivedPlacementsData
class BIM_PT_representations(Panel):
@@ -50,7 +51,7 @@ class BIM_PT_representations(Panel):
layout.label(text="No representations found")
row = layout.row(align=True)
row.prop(context.scene.BIMRootProperties, "contexts", text="")
prop_with_search(row, context.scene.BIMRootProperties, "contexts", text="")
row.operator("bim.add_representation", icon="ADD", text="")
for representation in RepresentationsData.data["representations"]:
@@ -67,6 +68,43 @@ class BIM_PT_representations(Panel):
row.operator("bim.remove_representation", icon="X", text="").representation_id = representation["id"]
class BIM_PT_connections(Panel):
bl_label = "IFC Connections"
bl_idname = "BIM_PT_connections"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_parent_id = "BIM_PT_geometry_object"
@classmethod
def poll(cls, context):
if not context.active_object:
return False
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return IfcStore.get_file()
def draw(self, context):
if not ConnectionsData.is_loaded:
ConnectionsData.load()
layout = self.layout
props = context.active_object.BIMObjectProperties
if not ConnectionsData.data["connections"]:
layout.label(text="No connections found")
for connection in ConnectionsData.data["connections"]:
row = self.layout.row(align=True)
row.label(text=connection["Name"], icon="SNAP_ON" if connection["is_relating"] else "SNAP_OFF")
row.label(text=connection["ConnectionType"])
op = row.operator("bim.select_connection", icon="RESTRICT_SELECT_OFF", text="")
op.connection = connection["id"]
op = row.operator("bim.remove_connection", icon="X", text="")
op.connection = connection["id"]
class BIM_PT_mesh(Panel):
bl_label = "IFC Representation"
bl_idname = "BIM_PT_mesh"
@@ -0,0 +1,123 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell.util.geolocation
import blenderbim.tool as tool
def refresh():
GeoreferenceData.is_loaded = False
class GeoreferenceData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data["blender_derived_angle"] = cls.blender_derived_angle()
cls.data["map_conversion"] = cls.map_conversion()
cls.data["map_derived_angle"] = cls.map_derived_angle()
cls.data["projected_crs"] = cls.projected_crs()
cls.data["true_north"] = cls.true_north()
cls.data["true_derived_angle"] = cls.true_derived_angle()
cls.is_loaded = True
@classmethod
def blender_derived_angle(cls):
props = bpy.context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
return str(
round(
ifcopenshell.util.geolocation.xaxis2angle(
float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate)
),
3,
)
)
@classmethod
def map_conversion(cls):
if tool.Ifc.get_schema() == "IFC2X3":
return {}
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.HasCoordinateOperation:
map_conversion = context.HasCoordinateOperation[0].get_info()
del map_conversion["id"]
del map_conversion["type"]
del map_conversion["SourceCRS"]
del map_conversion["TargetCRS"]
return map_conversion
return {}
@classmethod
def map_derived_angle(cls):
if (
cls.data["map_conversion"]
and cls.data["map_conversion"]["XAxisAbscissa"] is not None
and cls.data["map_conversion"]["XAxisOrdinate"] is not None
):
return str(
round(
ifcopenshell.util.geolocation.xaxis2angle(
cls.data["map_conversion"]["XAxisAbscissa"], cls.data["map_conversion"]["XAxisOrdinate"]
),
3,
)
)
return ""
@classmethod
def projected_crs(cls):
if tool.Ifc.get_schema() == "IFC2X3":
return {}
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.HasCoordinateOperation:
projected_crs = context.HasCoordinateOperation[0].TargetCRS.get_info()
del projected_crs["id"]
del projected_crs["type"]
if projected_crs["MapUnit"]:
unit = projected_crs["MapUnit"]
projected_crs["MapUnit"] = (getattr(unit, "Prefix", "") or "") + unit.Name
return projected_crs
return {}
@classmethod
def true_north(cls):
ifc = tool.Ifc.get()
true_north = {}
if ifc.schema == "IFC2X3":
return
for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if not context.TrueNorth:
continue
true_north = context.TrueNorth.DirectionRatios
break
return true_north
@classmethod
def true_derived_angle(cls):
if cls.data["true_north"]:
return str(round(ifcopenshell.util.geolocation.yaxis2angle(*cls.data["true_north"][0:2]), 3))
@@ -17,345 +17,157 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import ifcopenshell
import ifcopenshell.util.unit
import ifcopenshell.util.attribute
import ifcopenshell.api
import blenderbim.bim.helper
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.georeference.data import Data
from ifcopenshell.api.unit.data import Data as UnitData
from math import radians, degrees, atan, tan, cos, sin
import blenderbim.tool as tool
import blenderbim.core.georeference as core
class EnableEditingGeoreferencing(bpy.types.Operator):
bl_idname = "bim.enable_editing_georeferencing"
bl_label = "Enable Editing Georeferencing"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.file = IfcStore.get_file()
props = context.scene.BIMGeoreferenceProperties
self.props = props
props.projected_crs.clear()
blenderbim.bim.helper.import_attributes(
"IfcProjectedCRS", props.projected_crs, Data.projected_crs, self.import_projected_crs_attributes
)
props.map_conversion.clear()
blenderbim.bim.helper.import_attributes(
"IfcMapConversion", props.map_conversion, Data.map_conversion, self.import_map_conversion_attributes
)
props.has_true_north = bool(Data.true_north)
if Data.true_north:
props.true_north_abscissa = str(Data.true_north[0])
props.true_north_ordinate = str(Data.true_north[1])
props.is_editing = True
return {"FINISHED"}
def import_projected_crs_attributes(self, name, prop, data):
if name == "MapUnit":
new = self.props.projected_crs.add()
new.name = name
new.data_type = "enum"
new.is_null = data[name] is None
new.is_optional = True
new.enum_items = json.dumps(
{u["id"]: u["Name"] for u in UnitData.units.values() if u["UnitType"] == "LENGTHUNIT"}
)
if data["MapUnit"]:
new.enum_value = str(data["MapUnit"]["id"])
return True
def import_map_conversion_attributes(self, name, prop, data):
if name not in ["SourceCRS", "TargetCRS"]:
# Enforce a string data type to prevent data loss in single-precision Blender props
prop.data_type = "string"
prop.string_value = "" if prop.is_null else str(data[name])
return True
class DisableEditingGeoreferencing(bpy.types.Operator):
bl_idname = "bim.disable_editing_georeferencing"
bl_label = "Disable Editing Georeferencing"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMGeoreferenceProperties
props.is_editing = False
return {"FINISHED"}
class EditGeoreferencing(bpy.types.Operator):
bl_idname = "bim.edit_georeferencing"
bl_label = "Edit Georeferencing"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.file = IfcStore.get_file()
props = context.scene.BIMGeoreferenceProperties
projected_crs = blenderbim.bim.helper.export_attributes(props.projected_crs, self.export_crs_attributes)
map_conversion = blenderbim.bim.helper.export_attributes(props.map_conversion, self.export_map_attributes)
true_north = None
if props.has_true_north:
try:
true_north = [float(props.true_north_abscissa), float(props.true_north_ordinate)]
except ValueError:
self.report({"ERROR"}, "True North Abscissa and Ordinate expect a number")
ifcopenshell.api.run(
"georeference.edit_georeferencing",
self.file,
**{
"map_conversion": map_conversion,
"projected_crs": projected_crs,
"true_north": true_north,
}
)
Data.load(self.file)
bpy.ops.bim.disable_editing_georeferencing()
return {"FINISHED"}
def export_map_attributes(self, attributes, prop):
if not prop.is_null and prop.data_type == "string":
# We store our floats as string to prevent single precision data loss
attributes[prop.name] = float(prop.string_value)
return True
def export_crs_attributes(self, attributes, prop):
if not prop.is_null and prop.name == "MapUnit":
attributes[prop.name] = self.file.by_id(int(prop.enum_value))
return True
class SetBlenderGridNorth(bpy.types.Operator):
bl_idname = "bim.set_blender_grid_north"
bl_label = "Set Blender Grid North"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
context.scene.sun_pos_properties.north_offset = -radians(
ifcopenshell.util.geolocation.xaxis2angle(
float(context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value),
float(context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value),
)
)
return {"FINISHED"}
class SetIfcGridNorth(bpy.types.Operator):
bl_idname = "bim.set_ifc_grid_north"
bl_label = "Set IFC Grid North"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
x_angle = -context.scene.sun_pos_properties.north_offset
context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value = str(cos(x_angle))
context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value = str(sin(x_angle))
return {"FINISHED"}
class SetBlenderTrueNorth(bpy.types.Operator):
bl_idname = "bim.set_blender_true_north"
bl_label = "Set Blender True North"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
context.scene.sun_pos_properties.north_offset = -radians(
ifcopenshell.util.geolocation.yaxis2angle(
float(context.scene.BIMGeoreferenceProperties.true_north_abscissa),
float(context.scene.BIMGeoreferenceProperties.true_north_ordinate),
)
)
return {"FINISHED"}
class SetIfcTrueNorth(bpy.types.Operator):
bl_idname = "bim.set_ifc_true_north"
bl_label = "Set IFC True North"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
y_angle = -context.scene.sun_pos_properties.north_offset + radians(90)
context.scene.BIMGeoreferenceProperties.true_north_abscissa = str(cos(y_angle))
context.scene.BIMGeoreferenceProperties.true_north_ordinate = str(sin(y_angle))
return {"FINISHED"}
class RemoveGeoreferencing(bpy.types.Operator):
bl_idname = "bim.remove_georeferencing"
bl_label = "Remove Georeferencing"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
ifcopenshell.api.run("georeference.remove_georeferencing", IfcStore.get_file())
Data.load(IfcStore.get_file())
return {"FINISHED"}
class AddGeoreferencing(bpy.types.Operator):
class AddGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_georeferencing"
bl_label = "Add Georeferencing"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
bl_description = "Add a new georeference"
def _execute(self, context):
ifcopenshell.api.run("georeference.add_georeferencing", IfcStore.get_file())
Data.load(IfcStore.get_file())
return {"FINISHED"}
core.add_georeferencing(tool.Ifc)
class ConvertLocalToGlobal(bpy.types.Operator):
bl_idname = "bim.convert_local_to_global"
bl_label = "Convert Local To Global"
class EnableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_georeferencing"
bl_label = "Enable Editing Georeferencing"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Enable editing georeferencing"
@classmethod
def poll(cls, context):
file = IfcStore.get_file()
props = context.scene.BIMGeoreferenceProperties
return file and props.coordinate_input.count(",") == 2
def execute(self, context):
if not Data.is_loaded:
Data.load(IfcStore.get_file())
props = context.scene.BIMGeoreferenceProperties
x, y, z = [float(co) for co in props.coordinate_input.split(",")]
if props.has_blender_offset:
results = ifcopenshell.util.geolocation.xyz2enh(
x,
y,
z,
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
1.0,
)
x, y, z = results
# TODO: what if the project CRS units and the project units are different?
if Data.map_conversion:
results = ifcopenshell.util.geolocation.xyz2enh(
x,
y,
z,
Data.map_conversion["Eastings"],
Data.map_conversion["Northings"],
Data.map_conversion["OrthogonalHeight"],
Data.map_conversion.get("XAxisAbscissa", 1.0),
Data.map_conversion.get("XAxisOrdinate", 0.0),
Data.map_conversion.get("Scale", 1.0),
)
else:
results = (x, y, z)
props.coordinate_output = ",".join([str(r) for r in results])
context.scene.cursor.location = results
return {"FINISHED"}
def _execute(self, context):
core.enable_editing_georeferencing(tool.Georeference)
class ConvertGlobalToLocal(bpy.types.Operator):
bl_idname = "bim.convert_global_to_local"
bl_label = "Convert Global To Local"
class RemoveGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_georeferencing"
bl_label = "Remove Georeferencing"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Remove the georeferencing"
@classmethod
def poll(cls, context):
file = IfcStore.get_file()
props = context.scene.BIMGeoreferenceProperties
return file and file.by_type("IfcUnitAssignment") and props.coordinate_input.count(",") == 2
def execute(self, context):
if not Data.is_loaded:
Data.load(IfcStore.get_file())
props = context.scene.BIMGeoreferenceProperties
x, y, z = [float(co) for co in props.coordinate_input.split(",")]
if Data.map_conversion:
results = ifcopenshell.util.geolocation.enh2xyz(
x,
y,
z,
Data.map_conversion["Eastings"],
Data.map_conversion["Northings"],
Data.map_conversion["OrthogonalHeight"],
Data.map_conversion.get("XAxisAbscissa", 1.0),
Data.map_conversion.get("XAxisOrdinate", 0.0),
Data.map_conversion.get("Scale", 1.0),
)
else:
results = (x, y, z)
if props.has_blender_offset:
results = ifcopenshell.util.geolocation.enh2xyz(
results[0],
results[1],
results[2],
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
1.0,
)
props.coordinate_output = ",".join([str(r) for r in results])
scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
context.scene.cursor.location = [o * scale for o in results]
return {"FINISHED"}
def _execute(self, context):
core.remove_georeferencing(tool.Ifc)
class GetCursorLocation(bpy.types.Operator):
class EditGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_georeferencing"
bl_label = "Edit Georeferencing"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Edit the georeferencing"
def _execute(self, context):
core.edit_georeferencing(tool.Ifc, tool.Georeference)
class DisableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_georeferencing"
bl_label = "Disable Editing Georeferencing"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Close editing panel"
def _execute(self, context):
core.disable_editing_georeferencing(tool.Georeference)
class SetIfcGridNorth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.set_ifc_grid_north"
bl_label = "Set IFC Grid North"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Set IFC grid north"
def _execute(self, context):
core.set_ifc_grid_north(tool.Georeference)
class SetBlenderGridNorth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.set_blender_grid_north"
bl_label = "Set Blender Grid North"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Set Blender grid north"
def _execute(self, context):
core.set_blender_grid_north(tool.Georeference)
class GetCursorLocation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.get_cursor_location"
bl_label = "Get Cursor Location"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Insert the current cursor coordinates"
@classmethod
def poll(cls, context):
file = IfcStore.get_file()
return file and file.by_type("IfcUnitAssignment")
return tool.Ifc.get()
def execute(self, context):
props = context.scene.BIMGeoreferenceProperties
scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
project_coordinates = [o / scale for o in context.scene.cursor.location]
props.coordinate_input = ",".join([str(o) for o in project_coordinates])
return {"FINISHED"}
def _execute(self, context):
core.get_cursor_location(tool.Georeference)
class SetCursorLocation(bpy.types.Operator):
class SetCursorLocation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.set_cursor_location"
bl_label = "Set Cursor Location"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Move curson location to the specified coordinates"
@classmethod
def poll(cls, context):
file = IfcStore.get_file()
props = context.scene.BIMGeoreferenceProperties
return file and file.by_type("IfcUnitAssignment") and props.coordinate_output.count(",") == 2
return tool.Ifc.get() and props.coordinate_output.count(",") == 2
def execute(self, context):
def _execute(self, context):
core.set_cursor_location(tool.Georeference)
class SetIfcTrueNorth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.set_ifc_true_north"
bl_label = "Set IFC True North"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Set IFC True north"
def _execute(self, context):
core.set_ifc_true_north(tool.Georeference)
class SetBlenderTrueNorth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.set_blender_true_north"
bl_label = "Set Blender True North"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Set Blender true north"
def _execute(self, context):
core.set_blender_true_north(tool.Georeference)
class ConvertLocalToGlobal(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.convert_local_to_global"
bl_label = "Convert Local To Global"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Convert local coordinate to global coordinate"
@classmethod
def poll(cls, context):
file = tool.Ifc.get()
props = context.scene.BIMGeoreferenceProperties
scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
context.scene.cursor.location = [float(co) * scale for co in props.coordinate_output.split(",")]
return {"FINISHED"}
return file and props.coordinate_input.count(",") == 2
def _execute(self, context):
core.convert_local_to_global(tool.Georeference)
class ConvertGlobalToLocal(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.convert_global_to_local"
bl_label = "Convert Global To Local"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Convert global coordinate to local coordinate"
@classmethod
def poll(cls, context):
file = tool.Ifc.get()
props = context.scene.BIMGeoreferenceProperties
return file and file.by_type("IfcUnitAssignment") and props.coordinate_input.count(",") == 2
def _execute(self, context):
core.convert_global_to_local(tool.Georeference)
@@ -16,11 +16,10 @@
# 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 ifcopenshell.util.geolocation
import blenderbim.tool as tool
from bpy.types import Panel
from ifcopenshell.api.georeference.data import Data
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes, draw_attribute
from blenderbim.bim.module.georeference.data import GeoreferenceData
class BIM_PT_gis(Panel):
@@ -32,16 +31,13 @@ class BIM_PT_gis(Panel):
bl_context = "scene"
bl_parent_id = "BIM_PT_geometry"
@classmethod
def poll(cls, context):
return IfcStore.get_file()
def draw(self, context):
self.layout.use_property_split = True
self.layout.use_property_decorate = False
props = context.scene.BIMGeoreferenceProperties
if not Data.is_loaded:
Data.load(IfcStore.get_file())
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
if props.is_editing:
return self.draw_editable_ui(context)
@@ -82,10 +78,10 @@ class BIM_PT_gis(Panel):
def draw_ui(self, context):
props = context.scene.BIMGeoreferenceProperties
if not Data.projected_crs:
if not GeoreferenceData.data["projected_crs"]:
row = self.layout.row(align=True)
row.label(text="Not Georeferenced")
if IfcStore.get_file().schema != "IFC2X3":
if tool.Ifc.get_schema != "IFC2X3":
row.operator("bim.add_georeferencing", icon="ADD", text="")
if props.has_blender_offset:
@@ -109,40 +105,27 @@ class BIM_PT_gis(Panel):
row.label(text=props.blender_x_axis_ordinate)
row = self.layout.row(align=True)
row.label(text="Derived Grid North")
row.label(
text=str(
round(
ifcopenshell.util.geolocation.xaxis2angle(
float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate)
),
3,
)
)
)
row.label(text=GeoreferenceData.data["blender_derived_angle"])
if Data.projected_crs:
if GeoreferenceData.data["projected_crs"]:
row = self.layout.row(align=True)
row.label(text="Projected CRS", icon="WORLD")
row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="")
row.operator("bim.remove_georeferencing", icon="X", text="")
for key, value in Data.projected_crs.items():
if key == "id" or key == "type" or not value:
for key, value in GeoreferenceData.data["projected_crs"].items():
if not value:
continue
if key == "MapUnit":
unit_value = value.get("Prefix", "") or ""
unit_value += value["Name"]
value = unit_value
row = self.layout.row(align=True)
row.label(text=key)
row.label(text=str(value))
if Data.map_conversion:
if GeoreferenceData.data["map_conversion"]:
row = self.layout.row(align=True)
row.label(text="Map Conversion", icon="GRID")
for key, value in Data.map_conversion.items():
if key == "id" or key == "type" or key == "SourceCRS" or key == "TargetCRS" or value is None:
for key, value in GeoreferenceData.data["map_conversion"].items():
if value is None:
continue
row = self.layout.row(align=True)
row.label(text=key)
@@ -150,26 +133,17 @@ class BIM_PT_gis(Panel):
if key == "XAxisOrdinate":
row = self.layout.row(align=True)
row.label(text="Derived Angle")
row.label(
text=str(
round(
ifcopenshell.util.geolocation.xaxis2angle(
Data.map_conversion["XAxisAbscissa"], Data.map_conversion["XAxisOrdinate"]
),
3,
)
)
)
row.label(text=GeoreferenceData.data["map_derived_angle"])
if Data.true_north:
if GeoreferenceData.data["true_north"]:
row = self.layout.row()
row.label(text="True North", icon="LIGHT_SUN")
row = self.layout.row(align=True)
row.label(text="Vector")
row.label(text=str(Data.true_north[0:2])[1:-1])
row.label(text=str(GeoreferenceData.data["true_north"][0:2])[1:-1])
row = self.layout.row(align=True)
row.label(text="Derived Angle")
row.label(text=str(round(ifcopenshell.util.geolocation.yaxis2angle(*Data.true_north[0:2]), 3)))
row.label(text=GeoreferenceData.data["true_derived_angle"])
class BIM_PT_gis_utilities(Panel):
@@ -20,17 +20,20 @@ import bpy
from . import ui, prop, operator
classes = (
operator.LoadGroups,
operator.DisableGroupEditingUI,
operator.AddGroup,
operator.EditGroup,
operator.RemoveGroup,
operator.ToggleAssigningGroup,
operator.AssignGroup,
operator.UnassignGroup,
operator.EnableEditingGroup,
operator.DisableEditingGroup,
operator.DisableGroupEditingUI,
operator.EditGroup,
operator.EnableEditingGroup,
operator.LoadGroups,
operator.RemoveGroup,
operator.SelectGroupProducts,
operator.ToggleAssigningGroup,
operator.ToggleGroup,
operator.UnassignGroup,
operator.UpdateGroup,
prop.ExpandedGroups,
prop.Group,
prop.BIMGroupProperties,
ui.BIM_PT_groups,
@@ -42,7 +45,9 @@ classes = (
def register():
bpy.types.Scene.BIMGroupProperties = bpy.props.PointerProperty(type=prop.BIMGroupProperties)
bpy.types.Scene.ExpandedGroups = bpy.props.PointerProperty(type=prop.ExpandedGroups)
def unregister():
del bpy.types.Scene.BIMGroupProperties
del bpy.types.Scene.ExpandedGroups
@@ -20,24 +20,89 @@ import bpy
import ifcopenshell.util.attribute
import ifcopenshell.api
import blenderbim.bim.helper
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.group.data import Data
from ifcopenshell.util.selector import Selector
import json
class LoadGroups(bpy.types.Operator):
bl_idname = "bim.load_groups"
bl_label = "Load Groups"
bl_options = {"REGISTER", "UNDO"}
is_refresh: bpy.props.BoolProperty(default=False)
def execute(self, context):
props = context.scene.BIMGroupProperties
props.groups.clear()
self.props = context.scene.BIMGroupProperties
self.expanded_groups = json.loads(context.scene.ExpandedGroups.json_string)
self.props.groups.clear()
self.ifc = IfcStore.get_file()
if not self.is_refresh:
context.scene.ExpandedGroups.json_string = "{}"
for ifc_definition_id, group in Data.groups.items():
new = props.groups.add()
new.ifc_definition_id = ifc_definition_id
new.name = group["Name"]
props.is_editing = True
if not group["HasAssignments"]:
new = self.props.groups.add()
new.ifc_definition_id = ifc_definition_id
new.name = group["Name"]
new.selection_query = group["Description"].split("*selector*")[1] if group["Description"] else ""
new.tree_depth = 0
if group["IsGroupedBy"]:
# assumes 1:1 cardinality, will need to be updated to reflect IFC4 changes
# where the cardinality is 0:? - vulevukusej
sub_groups = [g for g in group["IsGroupedBy"][0].RelatedObjects if g.is_a("IfcGroup")]
new.has_children = True if len(sub_groups) != 0 else False
if self.is_refresh:
self.recursively_load_sub_groups(ifc_definition_id, new)
self.props.is_editing = True
bpy.ops.bim.disable_editing_group()
Data.load(IfcStore.get_file())
return {"FINISHED"}
def recursively_load_sub_groups(self, ifc_definition_id, parent):
sub_groups = [
k for k, v in Data.products.items() if self.ifc.by_id(k).is_a("IfcGroup") and ifc_definition_id in v
]
if str(ifc_definition_id) not in self.expanded_groups or sub_groups == []:
return
else:
parent.is_expanded = True
parent.has_children = True
for group in sub_groups:
new = self.props.groups.add()
new.ifc_definition_id = group
new.name = Data.groups[group]["Name"]
new.selection_query = (
Data.groups[group]["Description"].split("*selector*")[1]
if Data.groups[group]["Description"]
else ""
)
new.has_children = True if len(Data.groups[group]["IsGroupedBy"]) != 0 else False
new.tree_depth = parent.tree_depth + 1
self.recursively_load_sub_groups(new.ifc_definition_id, new)
class ToggleGroup(bpy.types.Operator):
bl_idname = "bim.toggle_group"
bl_label = "Toggle Group"
bl_options = {"REGISTER", "UNDO"}
ifc_definition_id: bpy.props.IntProperty()
index: bpy.props.IntProperty()
option: bpy.props.StringProperty(name="Expand or Collapse")
def execute(self, context):
context.scene.BIMGroupProperties.groups[self.index].is_expanded = True if self.option == "Expand" else False
json_string = json.loads(context.scene.ExpandedGroups.json_string)
if self.option == "Expand":
json_string.setdefault(str(self.ifc_definition_id), None)
else:
json_string.pop(str(self.ifc_definition_id), None)
context.scene.ExpandedGroups.json_string = json.dumps(json_string)
bpy.ops.bim.load_groups(is_refresh=True)
return {"FINISHED"}
@@ -54,17 +119,21 @@ class DisableGroupEditingUI(bpy.types.Operator):
class AddGroup(bpy.types.Operator):
bl_idname = "bim.add_group"
bl_label = "Add Group"
bl_label = "Add New Group"
bl_options = {"REGISTER", "UNDO"}
group: bpy.props.IntProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
result = ifcopenshell.api.run("group.add_group", IfcStore.get_file())
Data.load(IfcStore.get_file())
bpy.ops.bim.load_groups()
bpy.ops.bim.enable_editing_group(group=result.id())
result = ifcopenshell.api.run("group.add_group", tool.Ifc.get())
if self.group:
ifcopenshell.api.run(
"group.assign_group", tool.Ifc.get(), products=[result], group=tool.Ifc.get().by_id(self.group)
)
Data.load(tool.Ifc.get())
bpy.ops.bim.load_groups(is_refresh=True)
return {"FINISHED"}
@@ -72,6 +141,7 @@ class EditGroup(bpy.types.Operator):
bl_idname = "bim.edit_group"
bl_label = "Edit Group"
bl_options = {"REGISTER", "UNDO"}
copy_from_selector: bpy.props.BoolProperty(name="Copy from Selector", default=False)
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
@@ -84,12 +154,16 @@ class EditGroup(bpy.types.Operator):
attributes[attribute.name] = None
else:
attributes[attribute.name] = attribute.string_value
if self.copy_from_selector:
attributes["Description"] = context.scene.IFCSelector.selection_query
self.file = IfcStore.get_file()
ifcopenshell.api.run(
"group.edit_group", self.file, **{"group": self.file.by_id(props.active_group_id), "attributes": attributes}
)
Data.load(IfcStore.get_file())
bpy.ops.bim.load_groups()
bpy.ops.bim.load_groups(is_refresh=True)
return {"FINISHED"}
@@ -107,7 +181,7 @@ class RemoveGroup(bpy.types.Operator):
self.file = IfcStore.get_file()
ifcopenshell.api.run("group.remove_group", self.file, **{"group": self.file.by_id(self.group)})
Data.load(IfcStore.get_file())
bpy.ops.bim.load_groups()
bpy.ops.bim.load_groups(is_refresh=True)
return {"FINISHED"}
@@ -120,9 +194,7 @@ class EnableEditingGroup(bpy.types.Operator):
def execute(self, context):
props = context.scene.BIMGroupProperties
props.group_attributes.clear()
blenderbim.bim.helper.import_attributes("IfcGroup", props.group_attributes, Data.groups[self.group])
props.active_group_id = self.group
return {"FINISHED"}
@@ -166,10 +238,8 @@ class AssignGroup(bpy.types.Operator):
ifcopenshell.api.run(
"group.assign_group",
self.file,
**{
"product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id),
"group": self.file.by_id(self.group),
}
products=[self.file.by_id(product.BIMObjectProperties.ifc_definition_id)],
group=self.file.by_id(self.group),
)
Data.load(self.file)
return {"FINISHED"}
@@ -219,3 +289,32 @@ class SelectGroupProducts(bpy.types.Operator):
if self.group in product_groups:
obj.select_set(True)
return {"FINISHED"}
class UpdateGroup(bpy.types.Operator):
bl_idname = "bim.update_group"
bl_label = "Update Group"
bl_options = {"REGISTER", "UNDO"}
query: bpy.props.StringProperty()
group_id: bpy.props.IntProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.file = IfcStore.get_file()
group = self.file.by_id(self.group_id)
query = self.query
new_products = Selector.parse(self.file, query)
ifcopenshell.api.run(
"group.update_group_products",
self.file,
**{
"group": group,
"products": new_products,
}
)
Data.load(IfcStore.get_file())
bpy.ops.bim.load_groups(is_refresh=True)
return {"FINISHED"}
@@ -18,7 +18,10 @@
import bpy
from blenderbim.bim.prop import StrProperty, Attribute
from blenderbim.bim.helper import import_attributes
from ifcopenshell.api.group.data import Data
from bpy.types import PropertyGroup
import json
from bpy.props import (
PointerProperty,
StringProperty,
@@ -31,9 +34,17 @@ from bpy.props import (
)
class ExpandedGroups(StrProperty):
json_string: StringProperty(name="JSON String", default="{}")
class Group(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
selection_query: StringProperty(name="Selection Query")
is_expanded: BoolProperty(name="Is Expanded", default=False)
has_children: BoolProperty(name="Has Children", default=False)
tree_depth: IntProperty(name="Tree Depth")
class BIMGroupProperties(PropertyGroup):
@@ -40,9 +40,9 @@ class BIM_PT_groups(Panel):
self.props = context.scene.BIMGroupProperties
row = self.layout.row(align=True)
row.label(text="{} Groups Found".format(len(Data.groups)), icon="OUTLINER")
row.label(text=f"{len(Data.groups)} Groups Found", icon="OUTLINER")
if self.props.is_editing:
row.operator("bim.add_group", text="", icon="ADD")
row.operator("bim.add_group", text="", icon="ADD").group = 0
row.operator("bim.disable_group_editing_ui", text="", icon="CANCEL")
else:
row.operator("bim.load_groups", text="", icon="GREASEPENCIL")
@@ -115,10 +115,22 @@ class BIM_PT_object_groups(Panel):
class BIM_UL_groups(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
if item:
row = layout.row(align=True)
row.label(text=item.name)
for i in range(0, item.tree_depth):
row.label(text="", icon="BLANK1")
if item.has_children:
op = row.operator(
"bim.toggle_group", icon="TRIA_DOWN" if item.is_expanded else "TRIA_RIGHT", text="", emboss=False
)
op.ifc_definition_id = item.ifc_definition_id
op.index = index
op.option = "Collapse" if item.is_expanded else "Expand"
else:
row.label(text="", icon="BLANK1")
row.label(text=f"*{item.name}") if item.selection_query != "" else row.label(text=item.name)
group_id = item.ifc_definition_id
if context.scene.BIMGroupProperties.active_group_id == group_id:
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
@@ -128,21 +140,46 @@ class BIM_UL_groups(UIList):
elif context.scene.BIMGroupProperties.active_group_id:
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
op.group = group_id
op = row.operator("bim.add_group", text="", icon="ADD")
op.group = group_id
op = row.operator("bim.remove_group", text="", icon="X")
op.group = group_id
if item.selection_query != "":
op = row.operator("bim.update_group", text="", icon="FILE_REFRESH")
op.group_id = item.ifc_definition_id
op.query = item.selection_query
else:
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
op.group = group_id
op = row.operator("bim.enable_editing_group", text="", icon="GREASEPENCIL")
op.group = group_id
op = row.operator("bim.add_group", text="", icon="ADD")
op.group = group_id
op = row.operator("bim.remove_group", text="", icon="X")
op.group = group_id
if item.selection_query != "":
op = row.operator("bim.update_group", text="", icon="FILE_REFRESH")
op.group_id = item.ifc_definition_id
op.query = item.selection_query
class BIM_UL_object_groups(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
if item:
row = layout.row(align=True)
for i in range(0, item.tree_depth):
row.label(text="", icon="BLANK1")
if item.has_children:
op = row.operator(
"bim.toggle_group", icon="TRIA_DOWN" if item.is_expanded else "TRIA_RIGHT", text="", emboss=False
)
op.ifc_definition_id = item.ifc_definition_id
op.index = index
op.option = "Collapse" if item.is_expanded else "Expand"
else:
row.label(text="", icon="BLANK1")
row.label(text=item.name)
op = row.operator("bim.remove_group", text="", icon="X")
op.group = item.ifc_definition_id
op = row.operator("bim.assign_group", text="", icon="ADD")
op.group = item.ifc_definition_id
@@ -28,6 +28,7 @@ classes = (
operator.AddProfile,
operator.AssignMaterial,
operator.AssignParameterizedProfile,
operator.ContractMaterialCategory,
operator.CopyMaterial,
operator.DisableEditingAssignedMaterial,
operator.DisableEditingMaterialSetItem,
@@ -36,6 +37,7 @@ classes = (
operator.EditMaterialSetItem,
operator.EnableEditingAssignedMaterial,
operator.EnableEditingMaterialSetItem,
operator.ExpandMaterialCategory,
operator.LoadMaterials,
operator.RemoveConstituent,
operator.RemoveLayer,
@@ -78,9 +78,26 @@ class ObjectMaterialData:
def load(cls):
cls.data = {
"materials": cls.materials(),
"type_material": cls.type_material(),
}
cls.is_loaded = True
@classmethod
def materials(cls):
return sorted([(str(m.id()), m.Name or "Unnamed", "") for m in tool.Ifc.get().by_type("IfcMaterial")], key=lambda x: x[1])
return sorted(
[(str(m.id()), m.Name or "Unnamed", "") for m in tool.Ifc.get().by_type("IfcMaterial")], key=lambda x: x[1]
)
@classmethod
def type_material(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
element_type = ifcopenshell.util.element.get_type(element)
if element_type and element_type != element:
material = ifcopenshell.util.element.get_material(element_type)
if not material:
return
if material.is_a() in ("IfcMaterialLayerSetUsage", "IfcMaterialLayerSet"):
name_attr = "LayerSetName"
else:
name_attr = "Name"
return getattr(material, name_attr, "Unnamed") or "Unnamed"
@@ -778,3 +778,31 @@ class CopyMaterial(bpy.types.Operator):
if material.id() in object_material_ids:
return
obj.data.materials.append(IfcStore.get_element(material.id()))
class ExpandMaterialCategory(bpy.types.Operator):
bl_idname = "bim.expand_material_category"
bl_label = "Expand Material Category"
bl_options = {"REGISTER", "UNDO"}
category: bpy.props.StringProperty()
def execute(self, context):
props = context.scene.BIMMaterialProperties
for category in [c for c in props.materials if c.is_category and c.name == self.category]:
category.is_expanded = True
core.load_materials(tool.Material, props.material_type)
return {"FINISHED"}
class ContractMaterialCategory(bpy.types.Operator):
bl_idname = "bim.contract_material_category"
bl_label = "Contract Material Category"
bl_options = {"REGISTER", "UNDO"}
category: bpy.props.StringProperty()
def execute(self, context):
props = context.scene.BIMMaterialProperties
for category in [c for c in props.materials if c.is_category and c.name == self.category]:
category.is_expanded = False
core.load_materials(tool.Material, props.material_type)
return {"FINISHED"}
@@ -109,6 +109,8 @@ def get_material_types(self, context):
class Material(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
is_category: BoolProperty(name="Is Category", default=False)
is_expanded: BoolProperty(name="Is Expanded", default=True)
total_elements: IntProperty(name="Total Elements")
@@ -22,6 +22,7 @@ from ifcopenshell.api.material.data import Data
from ifcopenshell.api.profile.data import Data as ProfileData
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes
from blenderbim.bim.helper import prop_with_search
from blenderbim.bim.module.material.data import MaterialsData, ObjectMaterialData
@@ -50,7 +51,7 @@ class BIM_PT_materials(Panel):
row.operator("bim.disable_editing_materials", text="", icon="CANCEL")
else:
row = self.layout.row(align=True)
row.prop(self.props, "material_type", text="")
prop_with_search(row, self.props, "material_type", text="")
row.operator("bim.load_materials", text="", icon="IMPORT")
return
@@ -61,16 +62,18 @@ class BIM_PT_materials(Panel):
row.operator("bim.add_material", text="", icon="ADD")
if self.props.materials and self.props.active_material_index < len(self.props.materials):
material = self.props.materials[self.props.active_material_index]
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
op.material = material.ifc_definition_id
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
if material.ifc_definition_id:
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
op.material = material.ifc_definition_id
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
else:
row.operator("bim.add_material_set", text="", icon="ADD").set_type = self.props.material_type
if self.props.materials and self.props.active_material_index < len(self.props.materials):
material = self.props.materials[self.props.active_material_index]
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
op.material = material.ifc_definition_id
row.operator("bim.remove_material_set", text="", icon="X").material = material.ifc_definition_id
if material.ifc_definition_id:
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
op.material = material.ifc_definition_id
row.operator("bim.remove_material_set", text="", icon="X").material = material.ifc_definition_id
self.layout.template_list("BIM_UL_materials", "", self.props, "materials", self.props, "active_material_index")
@@ -139,6 +142,10 @@ class BIM_PT_object_material(Panel):
row.operator("bim.add_material", icon="ADD", text="").obj = ""
return
if ObjectMaterialData.data["type_material"]:
row = self.layout.row(align=True)
row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="FILE_PARENT")
if self.product_data:
if self.product_data["type"] == "IfcMaterialConstituentSet":
self.material_set_id = self.product_data["id"]
@@ -182,9 +189,9 @@ class BIM_PT_object_material(Panel):
return self.draw_material_ui()
row = self.layout.row(align=True)
row.prop(self.props, "material_type", text="")
prop_with_search(row, self.props, "material_type", text="")
if self.props.material_type == "IfcMaterial" or self.props.material_type == "IfcMaterialList":
row.prop(self.props, "material", text="")
prop_with_search(row, self.props, "material", text="")
row.operator("bim.assign_material", icon="ADD", text="")
def draw_material_ui(self):
@@ -214,8 +221,7 @@ class BIM_PT_object_material(Panel):
return self.draw_read_only_single_ui()
def draw_editable_single_ui(self):
row = self.layout.row(align=True)
row.prop(self.props, "material", text="")
prop_with_search(self.layout, self.props, "material", text="")
def draw_read_only_single_ui(self):
material = Data.materials[self.product_data["id"]]
@@ -236,7 +242,7 @@ class BIM_PT_object_material(Panel):
row.prop(attribute, "string_value", text=attribute.name)
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
row = self.layout.row(align=True)
row.prop(self.props, "material", text="")
prop_with_search(row, self.props, "material", text="")
op = row.operator(f"bim.add_{self.set_item_name}", icon="ADD", text="")
setattr(op, f"{self.set_item_name}_set", self.material_set_id)
@@ -395,7 +401,21 @@ class BIM_UL_materials(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
row.label(text=item.name)
row2 = row.row()
row2.alignment = "RIGHT"
row2.label(text=str(item.total_elements))
if item.is_category:
if item.is_expanded:
row.operator(
"bim.contract_material_category", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
).category = item.name
else:
row.operator(
"bim.expand_material_category", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).category = item.name
row.label(text=item.name or "Uncategorised")
else:
row.label(text="", icon="BLANK1")
row.label(text=item.name, icon="MATERIAL")
row2 = row.row()
row2.alignment = "RIGHT"
row2.label(text=str(item.total_elements))
@@ -49,3 +49,8 @@ class BIM_PT_misc_utilities(bpy.types.Panel):
row.operator("bim.draw_system_arrows")
row = layout.row()
row.operator("bim.clean_wireframes")
row = layout.row(align=True)
row.operator("bim.enable_editing_sketch_extrusion_profile", text="Start Sketching")
row.operator("bim.edit_sketch_extrusion_profile", text="", icon="FILE_REFRESH")
row.operator("bim.disable_editing_sketch_extrusion_profile", text="", icon="CANCEL")
@@ -17,26 +17,57 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from . import handler, prop, ui, grid, product, wall, slab, stair, opening, pie, workspace
from . import handler, prop, ui, grid, product, wall, slab, stair, opening, pie, workspace, profile
classes = (
product.AddEmptyType,
product.AddTypeInstance,
product.AddConstrTypeInstance,
product.DisplayConstrTypes,
product.ReinvokeOperator,
product.AlignProduct,
product.DynamicallyVoidProduct,
workspace.Hotkey,
wall.JoinWall,
wall.AlignWall,
wall.ChangeExtrusionDepth,
wall.ChangeLayerLength,
wall.FlipWall,
wall.JoinWall,
wall.MergeWall,
wall.RecalculateWall,
wall.SplitWall,
opening.AddBoolean,
opening.AddElementOpening,
opening.AddPotentialHalfSpaceSolid,
opening.AddPotentialOpening,
opening.EditOpenings,
opening.HideBooleans,
opening.HideOpenings,
opening.RemoveBooleans,
opening.ShowBooleans,
opening.ShowOpenings,
profile.ChangeCardinalPoint,
profile.ChangeProfileDepth,
profile.ExtendProfile,
profile.RecalculateProfile,
profile.Rotate90,
slab.DisableEditingExtrusionProfile,
slab.DisableEditingSketchExtrusionProfile,
slab.EditExtrusionProfile,
slab.EditSketchExtrusionProfile,
slab.EnableEditingExtrusionProfile,
slab.EnableEditingSketchExtrusionProfile,
slab.ResetVertex,
slab.SetArcIndex,
prop.ConstrTypeInfo,
prop.ConstrClassInfo,
prop.ConstrBrowserState,
prop.BIMModelProperties,
ui.BIM_PT_authoring,
ui.BIM_PT_authoring_architectural,
ui.DisplayConstrTypesUI,
ui.HelpConstrTypes,
ui.BIM_MT_model,
grid.BIM_OT_add_object,
stair.BIM_OT_add_object,
opening.BIM_OT_add_object,
pie.OpenPieClass,
pie.PieUpdateContainer,
pie.PieAddOpening,
@@ -53,8 +84,7 @@ def register():
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(opening.add_object_button)
bpy.types.VIEW3D_MT_add.append(product.add_empty_type_button)
bpy.types.VIEW3D_MT_add.append(ui.add_menu)
bpy.app.handlers.load_post.append(handler.load_post)
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
@@ -71,8 +101,7 @@ def unregister():
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(opening.add_object_button)
bpy.types.VIEW3D_MT_add.remove(product.add_empty_type_button)
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if kc:
@@ -16,8 +16,11 @@
# 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 functools
import bpy
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
def refresh():
@@ -31,10 +34,40 @@ class AuthoringData:
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {
"ifc_classes": cls.ifc_classes(),
"relating_types": cls.relating_types(),
}
if not hasattr(cls, "data"):
cls.data = {}
cls.props = bpy.context.scene.BIMModelProperties
cls.load_ifc_classes()
cls.load_relating_types()
cls.load_relating_types_browser()
cls.data["is_voidable_element"] = cls.is_voidable_element()
cls.data["has_visible_openings"] = cls.has_visible_openings()
@classmethod
def load_ifc_classes(cls):
cls.data["ifc_classes"] = cls.ifc_classes()
@classmethod
def load_relating_types(cls):
cls.data["relating_types_ids"] = cls.relating_types()
@classmethod
def load_relating_types_browser(cls):
cls.data["relating_types_ids_browser"] = cls.relating_types_browser()
@classmethod
def is_voidable_element(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
return element and element.is_a("IfcElement") and not element.is_a("IfcOpeningElement")
@classmethod
def has_visible_openings(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element and element.is_a("IfcElement") and not element.is_a("IfcOpeningElement"):
for opening in [r.RelatedOpeningElement for r in element.HasOpenings]:
if tool.Ifc.get_object(opening):
return True
return False
@classmethod
def ifc_classes(cls):
@@ -49,16 +82,147 @@ class AuthoringData:
return results
@classmethod
def relating_types(cls):
ifc_classes = cls.ifc_classes()
def constr_class_entities(cls, ifc_class=None):
ifc_classes = cls.data["ifc_classes"]
if not ifc_classes:
return []
results = []
ifc_class = bpy.context.scene.BIMModelProperties.ifc_class
if ifc_class is None:
ifc_class = cls.props.ifc_class
if not ifc_class and ifc_classes:
ifc_class = ifc_classes[0][0]
if ifc_class:
elements = [(str(e.id()), e.Name, e.Description or "") for e in tool.Ifc.get().by_type(ifc_class)]
results.extend(sorted(elements, key=lambda s: s[1]))
elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name)
results.extend(elements)
return results
return []
@classmethod
def relating_types(cls, ifc_class=None):
return [(str(e.id()), e.Name, e.Description or "") for e in cls.constr_class_entities(ifc_class=ifc_class)]
@classmethod
def relating_types_browser(cls):
return cls.relating_types(ifc_class=cls.props.ifc_class_browser)
@classmethod
def new_constr_class_info(cls, ifc_class):
if ifc_class not in cls.props.constr_classes:
cls.props.constr_classes.add().name = ifc_class
return cls.props.constr_classes[ifc_class]
@classmethod
def assetize_constr_class(cls, ifc_class=None):
selected_ifc_class = cls.props.ifc_class
selected_relating_type_id = cls.props.relating_type_id
if ifc_class is None:
ifc_class = cls.props.ifc_class_browser
if cls.constr_class_info(ifc_class) is None:
cls.new_constr_class_info(ifc_class)
constr_class_occurrences = cls.constr_class_entities(ifc_class)
constr_classes = cls.props.constr_classes
for constr_class_entity in constr_class_occurrences:
if (
ifc_class not in constr_classes
or constr_class_entity.Name not in constr_classes[ifc_class].constr_types
):
obj = tool.Ifc.get_object(constr_class_entity)
cls.assetize_object(obj, ifc_class, constr_class_entity)
cls.constr_class_info(ifc_class).fully_loaded = True
cls.props.updating = True
cls.props.ifc_class = selected_ifc_class
cls.props.relating_type_id = selected_relating_type_id
cls.props.updating = False
@classmethod
def assetize_object(cls, obj, ifc_class, ifc_class_entity, from_selection=False):
relating_type_id = ifc_class_entity.id()
to_be_deleted = False
if obj.type == "EMPTY":
kwargs = {}
if not from_selection:
kwargs.update({"ifc_class": ifc_class, "relating_type_id": relating_type_id})
new_obj = cls.new_relating_type(**kwargs)
if new_obj is not None:
to_be_deleted = True
obj = new_obj
obj.hide_set(True)
obj.asset_mark()
obj.asset_generate_preview()
blender33_or_above = bpy.app.version >= (3, 3, 0)
interval = 1e-4
def wait_for_asset_previews_generation(check_interval_seconds=interval):
if blender33_or_above and bpy.app.is_job_running("RENDER_PREVIEW"):
return check_interval_seconds
else:
if ifc_class not in cls.props.constr_classes:
cls.props.constr_classes.add().name = ifc_class
constr_class_info = cls.props.constr_classes[ifc_class]
# relating_type = cls.relating_type_name_by_id(ifc_class, relating_type_id)
if str(relating_type_id) not in constr_class_info.constr_types:
constr_class_info.constr_types.add().name = str(relating_type_id)
relating_type_info = constr_class_info.constr_types[str(relating_type_id)]
relating_type_info.object = obj
relating_type_info.icon_id = obj.preview.icon_id
if to_be_deleted:
element = tool.Ifc.get_entity(obj)
if element:
tool.Ifc.delete(element)
tool.Ifc.unlink(obj=obj)
for collection in obj.users_collection:
collection.objects.unlink(obj)
return None
first_interval = 0 if blender33_or_above else interval
bpy.app.timers.register(wait_for_asset_previews_generation, first_interval=first_interval)
@classmethod
def assetize_relating_type_from_selection(cls, browser=False):
ifc_class = cls.props.ifc_class_browser if browser else cls.props.ifc_class
relating_type_id = cls.props.relating_type_id_browser if browser else cls.props.relating_type_id
constr_class_occurrences = cls.constr_class_entities(ifc_class=ifc_class)
constr_class_occurrences = [
entity for entity in constr_class_occurrences if entity.id() == int(relating_type_id)
]
if len(constr_class_occurrences) == 0:
raise ConstrTypeEntityNotFound()
constr_class_entity = constr_class_occurrences[0]
if (obj := tool.Ifc.get_object(constr_class_entity)) is None:
raise ConstrTypeEntityNotFound()
cls.assetize_object(obj, ifc_class, constr_class_entity, from_selection=True)
@staticmethod
def constr_class_info(ifc_class):
props = bpy.context.scene.BIMModelProperties
return props.constr_classes[ifc_class] if ifc_class in props.constr_classes else None
@classmethod
def new_relating_type(cls, ifc_class=None, relating_type_id=None):
if ifc_class is None:
bpy.ops.bim.add_constr_type_instance(
ifc_class=cls.props.ifc_class, relating_type_id=int(cls.props.relating_type_id), link_to_scene=True
)
else:
cls.props.updating = True
cls.props.ifc_class = ifc_class
cls.props.relating_type_id = str(relating_type_id)
cls.props.updating = False
bpy.ops.bim.add_constr_type_instance(link_to_scene=True)
return bpy.context.selected_objects[-1]
@staticmethod
def relating_type_name_by_id(ifc_class, relating_type_id):
file = IfcStore.get_file()
try:
constr_class_entity = file.by_id(int(relating_type_id))
except (RuntimeError, ValueError):
return None
return constr_class_entity.Name if constr_class_entity.is_a() == ifc_class else None
@classmethod
def relating_type_id_by_name(cls, ifc_class, relating_type):
relating_types = [ct[0] for ct in cls.relating_types(ifc_class=ifc_class) if ct[1] == relating_type]
return None if len(relating_types) == 0 else relating_types[0]
@@ -56,14 +56,9 @@ def load_post(*args):
product.regenerate_profile_usage,
)
IfcStore.add_element_listener(opening.element_listener)
IfcStore.add_element_listener(wall.element_listener)
IfcStore.add_element_listener(slab.element_listener)
IfcStore.add_element_listener(profile.element_listener)
ifcopenshell.api.add_post_listener(
"geometry.add_representation", "BlenderBIM.DumbWall.GenerateAxis", wall.generate_axis
)
ifcopenshell.api.add_post_listener(
"geometry.add_representation", "BlenderBIM.DumbWall.CalculateQuantities", wall.calculate_quantities
)
@@ -74,9 +69,6 @@ def load_post(*args):
"type.assign_type", "BlenderBIM.DumbWall.RegenerateFromType", wall.DumbWallPlaner().regenerate_from_type
)
ifcopenshell.api.add_post_listener(
"geometry.add_representation", "BlenderBIM.DumbSlab.GenerateFootprint", slab.generate_footprint
)
ifcopenshell.api.add_post_listener(
"geometry.add_representation", "BlenderBIM.DumbSlab.CalculateQuantities", slab.calculate_quantities
)
@@ -87,11 +79,6 @@ def load_post(*args):
"type.assign_type", "BlenderBIM.DumbSlab.RegenerateFromType", slab.DumbSlabPlaner().regenerate_from_type
)
ifcopenshell.api.add_pre_listener(
"material.edit_profile",
"BlenderBIM.DumbProfile.SyncObjectFromProfile",
profile.DumbProfileRegenerator().sync_object_from_profile,
)
ifcopenshell.api.add_post_listener(
"material.edit_profile",
"BlenderBIM.DumbProfile.RegenerateFromProfile",
@@ -38,7 +38,7 @@ class MepGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
def generate(self):
def generate(self, link_to_scene=True):
self.file = tool.Ifc.get()
self.collection = bpy.context.view_layer.active_layer_collection.collection
@@ -54,26 +54,25 @@ class MepGenerator:
self.width = dimensions.get("NominalDiameterOrWidth")
self.height = dimensions.get("NominalHeight")
self.length = 1
self.rotation = 0
return self.derive_from_cursor()
return self.derive_from_cursor(link_to_scene=link_to_scene)
elif self.relating_type.is_a("IfcPipeSegmentType"):
pass
def derive_from_cursor(self):
def derive_from_cursor(self, link_to_scene):
self.location = bpy.context.scene.cursor.location
return self.create_rectangle_segment()
return self.create_rectangle_segment(link_to_scene)
def create_rectangle_segment(self):
def create_rectangle_segment(self, link_to_scene):
verts = [
Vector((0, self.width, 0)),
Vector((0, 0, 0)),
Vector((0, self.width, self.height)),
Vector((0, 0, self.height)),
Vector((self.length, self.width, 0)),
Vector((self.length, 0, 0)),
Vector((self.length, self.width, self.height)),
Vector((self.length, 0, self.height)),
Vector((-self.width / 2, self.height / 2, 0)),
Vector((-self.width / 2, -self.height / 2, 0)),
Vector((-self.width / 2, self.height / 2, self.length)),
Vector((-self.width / 2, -self.height / 2, self.length)),
Vector((self.width / 2, self.height / 2, 0)),
Vector((self.width / 2, -self.height / 2, 0)),
Vector((self.width / 2, self.height / 2, self.length)),
Vector((self.width / 2, -self.height / 2, self.length)),
]
faces = [
[1, 3, 2, 0],
@@ -90,9 +89,11 @@ class MepGenerator:
ifc_class = ifc_classes[0]
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
obj.location = self.location
obj.rotation_euler[2] = self.rotation
self.collection.objects.link(obj)
if link_to_scene:
obj.location = self.location
obj.rotation_euler[0] = math.pi / 2
obj.rotation_euler[2] = math.pi / 2
self.collection.objects.link(obj)
bpy.ops.bim.assign_class(
obj=obj.name,
@@ -100,9 +101,27 @@ class MepGenerator:
ifc_representation_class="IfcExtrudedAreaSolid/IfcRectangleProfileDef",
)
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=tool.Ifc.get_entity(obj), type=self.relating_type)
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = tool.Ifc.get_entity(obj)
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=self.relating_type)
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.Mep"})
obj.select_set(True)
start_port_matrix = Matrix()
end_port_matrix = Matrix.Translation((0, 0, self.length))
for mat in [start_port_matrix, end_port_matrix]:
port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort")
tool.Ifc.run("system.assign_port", element=element, port=port)
tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=obj.matrix_world @ mat, is_si=True)
try:
obj.select_set(True)
except RuntimeError:
def msg(self, context):
txt = "The created object could not be assigned to a collection. "
txt += "Has any IfcSpatialElement been deleted?"
self.layout.label(text=txt)
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return obj
@@ -17,6 +17,8 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import gpu
import bgl
import bmesh
import blenderbim.bim.handler
import ifcopenshell
@@ -25,54 +27,13 @@ import blenderbim.tool as tool
import blenderbim.core.geometry
from blenderbim.bim.ifc import IfcStore
from math import pi
from mathutils import Vector
from mathutils import Vector, Matrix
from bpy.types import Operator
from bpy.types import SpaceView3D
from bpy.props import FloatProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
def element_listener(element, obj):
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
def mode_callback(obj, data):
for obj in set(bpy.context.selected_objects + [bpy.context.active_object]):
if (
obj.mode != "EDIT"
or not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
if not product.is_a("IfcOpeningElement"):
return
for rel in product.VoidsElements:
building_element_obj = IfcStore.get_element(rel.RelatingBuildingElement.id())
if not building_element_obj:
continue
if [m for m in building_element_obj.modifiers if m.type == "BOOLEAN"]:
continue
representation = ifcopenshell.util.representation.get_representation(
rel.RelatingBuildingElement, "Model", "Body", "MODEL_VIEW"
)
if not representation:
continue
blenderbim.core.geometry.switch_representation(
tool.Geometry,
obj=building_element_obj,
representation=representation,
should_reload=True,
enable_dynamic_voids=True,
is_global=True,
should_sync_changes_first=False,
)
IfcStore.edited_objs.add(obj)
bm = bmesh.from_edit_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.update_edit_mesh(obj.data)
bm.free()
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
class AddElementOpening(bpy.types.Operator):
@@ -201,31 +162,448 @@ class AddElementOpening(bpy.types.Operator):
return obj
def add_object(self, context):
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=self.size)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
mesh = bpy.data.meshes.new(name="Dumb Opening")
bm.to_mesh(mesh)
bm.free()
obj = object_data_add(context, mesh, operator=self)
obj.name = "Opening"
obj.display_type = "WIRE"
class BIM_OT_add_object(Operator, AddObjectHelper):
bl_idname = "mesh.add_opening"
bl_label = "Dumb Opening"
class AddPotentialOpening(Operator, AddObjectHelper):
bl_idname = "bim.add_potential_opening"
bl_label = "Add Potential Opening"
bl_options = {"REGISTER", "UNDO"}
x: FloatProperty(name="X", default=0.5)
y: FloatProperty(name="Y", default=0.5)
z: FloatProperty(name="Z", default=0.5)
size: FloatProperty(name="Size", default=2)
def draw_settings(context, layout, tool):
row = self.layout.row()
row.prop(data=self, property="x", label="Size X")
row = self.layout.row()
row.prop(data=self, property="y")
row = self.layout.row()
row.prop(data=self, property="z")
def execute(self, context):
add_object(self, context)
props = context.scene.BIMModelProperties
new_matrix = None
if context.selected_objects and context.active_object:
new_matrix = context.active_object.matrix_world.copy()
new_matrix.col[3] = context.scene.cursor.location.to_4d().to_4d()
x = self.x / 2
y = self.y / 2
z = self.z / 2
verts = [
Vector((-x, -y, -z)),
Vector((-x, -y, z)),
Vector((-x, y, -z)),
Vector((-x, y, z)),
Vector((x, -y, -z)),
Vector((x, -y, z)),
Vector((x, y, -z)),
Vector((x, y, z)),
]
edges = []
faces = [
[0, 1, 3, 2],
[2, 3, 7, 6],
[6, 7, 5, 4],
[4, 5, 1, 0],
[2, 6, 4, 0],
[7, 3, 1, 5],
]
mesh = bpy.data.meshes.new(name="Opening")
mesh.from_pydata(verts, edges, faces)
obj = object_data_add(context, mesh, operator=self)
obj.name = "Opening"
if new_matrix:
obj.matrix_world = new_matrix
tool.Model.clear_scene_openings()
new = props.openings.add()
new.obj = obj
DecorationsHandler.install(context)
return {"FINISHED"}
def add_object_button(self, context):
self.layout.operator(BIM_OT_add_object.bl_idname, icon="PLUGIN")
class AddPotentialHalfSpaceSolid(Operator, AddObjectHelper):
bl_idname = "bim.add_potential_half_space_solid"
bl_label = "Add Potential Half Space Solid"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMModelProperties
bm = bmesh.new()
bmesh.ops.create_grid(bm, size=0.5)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
mesh = bpy.data.meshes.new(name="Dumb Opening")
bm.to_mesh(mesh)
bm.free()
obj = object_data_add(context, mesh, operator=self)
obj.name = "HalfSpaceSolid"
tool.Model.clear_scene_openings()
new = props.openings.add()
new.obj = obj
DecorationsHandler.install(context)
return {"FINISHED"}
class AddBoolean(Operator, tool.Ifc.Operator):
bl_idname = "bim.add_boolean"
bl_label = "Add Boolean"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMModelProperties
if len(context.selected_objects) != 2:
return {"FINISHED"}
obj1, obj2 = context.selected_objects
element1 = tool.Ifc.get_entity(obj1)
element2 = tool.Ifc.get_entity(obj2)
if element1 and element2:
return {"FINISHED"}
if not element1 and not element2:
return {"FINISHED"}
if element2 and not element1:
obj1, obj2 = obj2, obj1
if not obj1.data or not hasattr(obj1.data, "BIMMeshProperties"):
return {"FINISHED"}
representation = tool.Ifc.get().by_id(obj1.data.BIMMeshProperties.ifc_definition_id)
ifcopenshell.api.run(
"geometry.add_boolean",
tool.Ifc.get(),
representation=representation,
operator="DIFFERENCE",
matrix=obj1.matrix_world.inverted() @ obj2.matrix_world,
)
tool.Model.clear_scene_openings()
blenderbim.core.geometry.switch_representation(
tool.Geometry,
obj=obj1,
representation=representation,
should_reload=True,
enable_dynamic_voids=False,
is_global=True,
should_sync_changes_first=False,
)
tool.Ifc.unlink(obj=obj2)
bpy.data.objects.remove(obj2)
return {"FINISHED"}
class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
bl_idname = "bim.show_booleans"
bl_label = "Show Booleans"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
if (
not obj.data
or not hasattr(obj.data, "BIMMeshProperties")
or not obj.data.BIMMeshProperties.ifc_definition_id
):
return {"FINISHED"}
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
booleans = []
for item in representation.Items:
booleans.extend(self.get_booleans(item))
for boolean in booleans:
if boolean.is_a() == "IfcHalfSpaceSolid":
if boolean.BaseSurface.is_a("IfcPlane"):
boolean_obj = self.create_half_space_solid()
position = boolean.BaseSurface.Position
position = Matrix(ifcopenshell.util.placement.get_axis2placement(position).tolist())
boolean_obj.matrix_world = obj.matrix_world @ position
boolean_obj.data.BIMMeshProperties.ifc_boolean_id = boolean.id()
boolean_obj.data.BIMMeshProperties.obj = obj
return {"FINISHED"}
def get_booleans(self, item):
results = []
if item.is_a("IfcBooleanResult"):
results.extend(self.get_booleans(item.FirstOperand))
results.append(item.SecondOperand)
return results
def create_half_space_solid(self):
props = bpy.context.scene.BIMModelProperties
bm = bmesh.new()
bmesh.ops.create_grid(bm, size=0.5)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
mesh = bpy.data.meshes.new(name="Dumb Opening")
bm.to_mesh(mesh)
bm.free()
obj = object_data_add(bpy.context, mesh, operator=self)
obj.name = "HalfSpaceSolid"
tool.Model.clear_scene_openings()
new = props.openings.add()
new.obj = obj
DecorationsHandler.install(bpy.context)
return obj
class HideBooleans(Operator, tool.Ifc.Operator):
bl_idname = "bim.hide_booleans"
bl_label = "Hide Booleans"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMModelProperties
for opening in props.openings:
obj = opening.obj
if obj.data.BIMMeshProperties.ifc_boolean_id:
bpy.data.objects.remove(obj)
return {"FINISHED"}
class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
bl_idname = "bim.remove_booleans"
bl_label = "Remove Booleans"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
for obj in context.selected_objects:
if (
not obj.data
or not hasattr(obj.data, "BIMMeshProperties")
or not obj.data.BIMMeshProperties.ifc_boolean_id
):
continue
try:
boolean = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_boolean_id)
except:
continue
ifcopenshell.api.run("geometry.remove_boolean", tool.Ifc.get(), item=boolean)
if obj.data.BIMMeshProperties.obj:
upstream_obj = obj.data.BIMMeshProperties.obj
element = tool.Ifc.get_entity(upstream_obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
blenderbim.core.geometry.switch_representation(
tool.Geometry,
obj=upstream_obj,
representation=body,
should_reload=True,
enable_dynamic_voids=False,
is_global=True,
should_sync_changes_first=False,
)
bpy.data.objects.remove(obj)
return {"FINISHED"}
class ShowOpenings(Operator, tool.Ifc.Operator):
bl_idname = "bim.show_openings"
bl_label = "Show Openings"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMModelProperties
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
openings = tool.Model.load_openings(element, [r.RelatedOpeningElement for r in element.HasOpenings])
for opening in openings:
new = props.openings.add()
new.obj = opening
DecorationsHandler.install(bpy.context)
return {"FINISHED"}
class HideOpenings(Operator, tool.Ifc.Operator):
bl_idname = "bim.hide_openings"
bl_label = "Hide Openings"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMModelProperties
to_delete = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
for opening in openings:
opening_obj = tool.Ifc.get_object(opening)
if opening_obj:
to_delete.add(opening_obj)
for opening_obj in to_delete:
tool.Ifc.unlink(element=opening, obj=opening_obj)
bpy.data.objects.remove(opening_obj)
tool.Model.clear_scene_openings()
return {"FINISHED"}
class EditOpenings(Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_openings"
bl_label = "Edit Openings"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMModelProperties
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
for opening in openings:
opening_obj = tool.Ifc.get_object(opening)
if opening_obj:
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
tool.Ifc.unlink(element=opening, obj=opening_obj)
bpy.data.objects.remove(opening_obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
enable_dynamic_voids=False,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
class DecorationsHandler:
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 __call__(self, context):
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
for opening in context.scene.BIMModelProperties.openings:
obj = opening.obj
if not obj:
continue
white = (1, 1, 1, 1)
white_t = (1, 1, 1, 0.1)
green = (0.545, 0.863, 0, 1)
red = (1, 0.2, 0.322, 1)
red_t = (1, 0.2, 0.322, 0.1)
blue = (0.157, 0.565, 1, 1)
blue_t = (0.157, 0.565, 1, 0.1)
grey = (0.2, 0.2, 0.2, 1)
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
verts = []
selected_edges = []
unselected_edges = []
selected_vertices = []
unselected_vertices = []
if obj.mode == "EDIT":
bm = bmesh.from_edit_mesh(obj.data)
for vertex in bm.verts:
co = tuple(obj.matrix_world @ vertex.co)
verts.append(co)
if vertex.hide:
continue
if vertex.select:
selected_vertices.append(co)
else:
unselected_vertices.append(co)
for edge in bm.edges:
edge_indices = [v.index for v in edge.verts]
if edge.hide:
continue
if edge.select:
selected_edges.append(edge_indices)
else:
unselected_edges.append(edge_indices)
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=unselected_edges)
self.shader.bind()
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=selected_edges)
self.shader.uniform_float("color", green)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices})
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices})
self.shader.uniform_float("color", green)
batch.draw(self.shader)
else:
bm = bmesh.new()
bm.from_mesh(obj.data)
verts = [tuple(obj.matrix_world @ v.co) for v in bm.verts]
edges = [tuple([v.index for v in e.verts]) for e in bm.edges]
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.bind()
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
batch.draw(self.shader)
obj.data.calc_loop_triangles()
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
batch = batch_for_shader(self.shader, "TRIS", {"pos": verts}, indices=tris)
self.shader.bind()
self.shader.uniform_float("color", blue_t)
batch.draw(self.shader)
if "HalfSpaceSolid" in obj.name:
# Arrow shape
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)]
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.bind()
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
batch.draw(self.shader)
if obj.mode != "EDIT":
bm.free()
@@ -18,18 +18,24 @@
import bpy
import mathutils
from functools import reduce
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.system
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import blenderbim.tool as tool
import blenderbim.core.type
import blenderbim.core.geometry
from . import wall, slab, profile, mep
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.prop import store_cursor_position
from ifcopenshell.api.pset.data import Data as PsetData
from mathutils import Vector, Matrix
from bpy_extras.object_utils import AddObjectHelper
from . import prop
class AddEmptyType(bpy.types.Operator, AddObjectHelper):
@@ -51,13 +57,18 @@ def add_empty_type_button(self, context):
self.layout.operator(AddEmptyType.bl_idname, icon="FILE_3D")
class AddTypeInstance(bpy.types.Operator):
bl_idname = "bim.add_type_instance"
bl_label = "Add Type Instance"
class AddConstrTypeInstance(bpy.types.Operator):
bl_idname = "bim.add_constr_type_instance"
bl_label = "Add"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add the selected Type Instance to the model"
bl_description = "Add Type Instance to the model"
ifc_class: bpy.props.StringProperty()
relating_type: bpy.props.IntProperty()
relating_type_id: bpy.props.IntProperty()
from_invoke: bpy.props.BoolProperty(default=False)
link_to_scene: bpy.props.BoolProperty(default=True)
def invoke(self, context, event):
return self.execute(context)
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
@@ -65,29 +76,34 @@ class AddTypeInstance(bpy.types.Operator):
def _execute(self, context):
props = context.scene.BIMModelProperties
ifc_class = self.ifc_class or props.ifc_class
relating_type_id = self.relating_type or props.relating_type
relating_type_id = self.relating_type_id or props.relating_type_id
if not ifc_class or not relating_type_id:
return {"FINISHED"}
if self.from_invoke:
props.ifc_class = self.ifc_class
props.relating_type_id = str(self.relating_type_id)
self.file = IfcStore.get_file()
instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, self.file.schema)[0]
relating_type = self.file.by_id(int(relating_type_id))
material = ifcopenshell.util.element.get_material(relating_type)
if material and material.is_a("IfcMaterialProfileSet"):
if profile.DumbProfileGenerator(relating_type).generate():
if profile.DumbProfileGenerator(relating_type).generate(link_to_scene=self.link_to_scene):
return {"FINISHED"}
elif material and material.is_a("IfcMaterialLayerSet"):
if self.generate_layered_element(ifc_class, relating_type):
if self.generate_layered_element(ifc_class, relating_type, link_to_scene=self.link_to_scene):
return {"FINISHED"}
if relating_type.is_a("IfcFlowSegmentType"):
if mep.MepGenerator(relating_type).generate():
if relating_type.is_a("IfcFlowSegmentType") and not relating_type.RepresentationMaps:
if mep.MepGenerator(relating_type).generate(link_to_scene=self.link_to_scene):
return {"FINISHED"}
building_obj = None
if len(context.selected_objects) == 1 and context.active_object:
building_obj = context.active_object
building_element = tool.Ifc.get_entity(building_obj)
# A cube
verts = [
@@ -112,36 +128,52 @@ class AddTypeInstance(bpy.types.Operator):
mesh = bpy.data.meshes.new(name="Instance")
mesh.from_pydata(verts, edges, faces)
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(relating_type, instance_class), mesh)
obj.location = context.scene.cursor.location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
collection_obj = bpy.data.objects.get(collection.name)
if self.link_to_scene:
obj.location = context.scene.cursor.location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
collection_obj = bpy.data.objects.get(collection.name)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
blenderbim.core.type.assign_type(
tool.Ifc,
tool.Type,
element=tool.Ifc.get_entity(obj),
type=tool.Ifc.get().by_id(int(props.relating_type)),
)
element = tool.Ifc.get_entity(obj)
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
if self.link_to_scene:
# Update required as core.type.assign_type may change obj.data
bpy.context.view_layer.update()
if building_obj:
if (
building_obj
and building_element
and building_element.is_a() in ["IfcWall", "IfcWallStandardCase", "IfcCovering"]
):
if instance_class in ["IfcWindow", "IfcDoor"]:
# TODO For now we are hardcoding windows and doors as a prototype
bpy.ops.bim.add_element_opening(
voided_building_element=building_obj.name, filling_building_element=obj.name
)
if instance_class == "IfcDoor":
if instance_class == "IfcDoor" and self.link_to_scene:
obj.location[2] = building_obj.location[2] - min([v[2] for v in obj.bound_box])
else:
elif self.link_to_scene:
if collection_obj and collection_obj.BIMObjectProperties.ifc_definition_id:
obj.location[2] = collection_obj.location[2] - min([v[2] for v in obj.bound_box])
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for port in ifcopenshell.util.system.get_ports(relating_type):
mat = ifcopenshell.util.placement.get_local_placement(port.ObjectPlacement)
mat[0][3] *= unit_scale
mat[1][3] *= unit_scale
mat[2][3] *= unit_scale
mat = obj.matrix_world @ mathutils.Matrix(mat)
new_port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort")
tool.Ifc.run("system.assign_port", element=element, port=new_port)
tool.Ifc.run("geometry.edit_object_placement", product=new_port, matrix=mat, is_si=True)
bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True)
context.view_layer.objects.active = obj
return {"FINISHED"}
def generate_layered_element(self, ifc_class, relating_type):
@staticmethod
def generate_layered_element(ifc_class, relating_type, link_to_scene=True):
layer_set_direction = None
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
@@ -154,15 +186,75 @@ class AddTypeInstance(bpy.types.Operator):
layer_set_direction = "AXIS2"
if layer_set_direction == "AXIS3":
if slab.DumbSlabGenerator(relating_type).generate():
if slab.DumbSlabGenerator(relating_type).generate(link_to_scene=link_to_scene):
return True
elif layer_set_direction == "AXIS2":
if wall.DumbWallGenerator(relating_type).generate():
if wall.DumbWallGenerator(relating_type).generate(link_to_scene=link_to_scene):
return True
else:
pass # Dumb block generator? Eh? :)
class DisplayConstrTypes(bpy.types.Operator):
bl_idname = "bim.display_constr_types"
bl_label = "Browse Construction Types"
bl_options = {"REGISTER"}
bl_description = "Display all available Construction Types to add new instances"
def invoke(self, context, event):
if not AuthoringData.is_loaded:
AuthoringData.load()
props = context.scene.BIMModelProperties
ifc_class = props.ifc_class
constr_class_info = AuthoringData.constr_class_info(ifc_class)
if constr_class_info is None or not constr_class_info.fully_loaded:
AuthoringData.assetize_constr_class(ifc_class)
bpy.ops.bim.display_constr_types_ui("INVOKE_DEFAULT")
return {"FINISHED"}
class ReinvokeOperator(bpy.types.Operator):
bl_idname = "bim.reinvoke_operator"
bl_label = "Reinvoke Popup Operator"
bl_options = {"REGISTER"}
bl_description = "Reinvoke a popup operator"
operator: bpy.props.StringProperty()
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
browser_state = context.scene.BIMModelProperties.constr_browser_state
store_cursor_position(context, event, window=False)
cursor_x, cursor_y = browser_state.cursor_x, browser_state.cursor_y
window_x, window_y = browser_state.window_x, browser_state.window_y
window = context.window
operator = self.operator
self.move_cursor_away(context, window)
def move_cursor_to_window():
window.cursor_warp(window_x, window_y)
def run_operator(operator, *args, **kwargs):
reduce(lambda x, arg: getattr(x, arg), operator.split("."), bpy.ops)(*args, **kwargs)
def reinvoke():
run_operator(operator, "INVOKE_DEFAULT", reinvoked=True)
window.cursor_warp(cursor_x, cursor_y)
bpy.app.timers.register(move_cursor_to_window, first_interval=browser_state.update_delay)
bpy.app.timers.register(reinvoke, first_interval=3*browser_state.update_delay)
return {"FINISHED"}
def move_cursor_away(self, context, window): # closes current popup
browser_state = context.scene.BIMModelProperties.constr_browser_state
window.cursor_warp(browser_state.far_away_x, browser_state.far_away_y)
@staticmethod
def run_operator(operator, *args, **kwargs):
reduce(lambda x, arg: getattr(x, arg), operator.split("."), bpy.ops)(*args, **kwargs)
class AlignProduct(bpy.types.Operator):
bl_idname = "bim.align_product"
bl_label = "Align Product"
@@ -331,10 +423,13 @@ def ensure_material_assigned(usecase_path, ifc_file, settings):
if om is not None and om.BIMObjectProperties.ifc_definition_id
]
if material[0].id() in object_material_ids:
if material and material[0].id() in object_material_ids:
continue
if len(obj.data.materials) == 1:
obj.data.materials.clear()
if not material:
continue
obj.data.materials.append(IfcStore.get_element(material[0].id()))
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import copy
import math
import bmesh
import mathutils.geometry
@@ -96,92 +97,114 @@ class DumbProfileGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
def generate(self):
def generate(self, link_to_scene=True):
self.file = IfcStore.get_file()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
material = ifcopenshell.util.element.get_material(self.relating_type)
if material and material.is_a("IfcMaterialProfileSet"):
self.profile_set = material
else:
return
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Axis", "GRAPH_VIEW")
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = bpy.data.objects.get(self.collection.name)
self.length = 3
self.depth = 3
self.rotation = 0
self.location = Vector((0, 0, 0))
return self.derive_from_cursor()
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
return self.derive_from_cursor(link_to_scene=link_to_scene)
def derive_from_cursor(self):
def derive_from_cursor(self, link_to_scene):
self.location = bpy.context.scene.cursor.location
return self.create_profile()
def create_profile(self):
# A cube
verts = [
Vector((-1, -1, -1)),
Vector((-1, -1, 1)),
Vector((-1, 1, -1)),
Vector((-1, 1, 1)),
Vector((1, -1, -1)),
Vector((1, -1, 1)),
Vector((1, 1, -1)),
Vector((1, 1, 1)),
]
edges = []
faces = [
[0, 2, 3, 1],
[2, 3, 7, 6],
[4, 5, 7, 6],
[0, 1, 5, 4],
[1, 3, 7, 5],
[0, 2, 6, 4],
]
return self.create_profile(link_to_scene)
def create_profile(self, link_to_scene):
ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema)
# Standard cases are deprecated, so let's cull them
ifc_class = [c for c in ifc_classes if "StandardCase" not in c][0]
mesh = bpy.data.meshes.new(name="Dumb Profile")
mesh.from_pydata(verts, edges, faces)
mesh = bpy.data.meshes.new("Dummy")
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
obj.location = self.location
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
if link_to_scene and self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
obj.location[2] = self.collection_obj.location[2]
self.collection.objects.link(obj)
bpy.context.view_layer.update()
if link_to_scene:
self.collection.objects.link(obj)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=ifc_class, should_add_representation=False)
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=ifc_class,
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type)
material = ifcopenshell.util.element.get_material(element)
material.CardinalPoint = self.cardinal_point
if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"]:
obj.rotation_euler[0] = math.pi / 2
obj.rotation_euler[2] = math.pi / 2
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=tool.Ifc.get_entity(obj), type=self.relating_type)
profile_set_usage = ifcopenshell.util.element.get_material(element)
if self.axis_context:
representation = ifcopenshell.api.run(
"geometry.add_axis_representation",
tool.Ifc.get(),
context=self.axis_context,
axis=[(0.0, 0.0, 0.0), (0.0, 0.0, self.depth)],
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=representation
)
representation = ifcopenshell.api.run(
"geometry.add_profile_representation",
tool.Ifc.get(),
context=self.body_context,
profile=self.profile_set.CompositeProfile or self.profile_set.MaterialProfiles[0].Profile,
cardinal_point=self.cardinal_point,
depth=self.depth,
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=representation
)
blenderbim.core.geometry.switch_representation(
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
enable_dynamic_voids=False,
is_global=True,
should_sync_changes_first=False,
)
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbProfile"})
MaterialData.load(self.file)
obj.select_set(True)
if link_to_scene:
obj.select_set(True)
return obj
class DumbProfileRegenerator:
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
self.file = ifc_file
objs = []
profile = settings["profile"].Profile
if not profile:
return
for element in self.get_elements_using_profile(profile):
self.change_profile(element)
def sync_object_from_profile(self, usecase_path, ifc_file, settings):
self.file = ifc_file
profile = settings["profile"].Profile
if not profile:
return
for element in self.get_elements_using_profile(profile):
self.sync_object(element)
obj = tool.Ifc.get_object(element)
if obj:
objs.append(obj)
DumbProfileRecalculator().recalculate(objs)
def get_elements_using_profile(self, profile):
results = []
@@ -202,162 +225,630 @@ class DumbProfileRegenerator:
return results
def regenerate_from_type(self, usecase_path, ifc_file, settings):
obj = tool.Ifc.get_object(settings["related_object"])
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
if not new_material or not new_material.is_a("IfcMaterialProfileSet"):
return
self.change_profile(settings["related_object"])
def change_profile(self, element):
obj = IfcStore.get_element(element.id())
if not obj:
return
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if representation:
blenderbim.core.geometry.switch_representation(
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
enable_dynamic_voids=True,
is_global=True,
should_sync_changes_first=False,
)
def sync_object(self, element):
obj = IfcStore.get_element(element.id())
if not obj or obj not in IfcStore.edited_objs:
return
bpy.ops.bim.update_representation(obj=obj.name)
DumbProfileRecalculator().recalculate([obj])
class ExtendProfile(bpy.types.Operator):
class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.extend_profile"
bl_label = "Extend Profile"
bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.StringProperty()
def execute(self, context):
def _execute(self, context):
selected_objs = context.selected_objects
for obj in selected_objs:
bpy.ops.bim.dynamically_void_product(obj=obj.name)
if len(selected_objs) == 0:
joiner = DumbProfileJoiner()
if not self.join_type:
for obj in selected_objs:
joiner.unjoin(obj)
return {"FINISHED"}
if not context.active_object:
return {"FINISHED"}
if len(selected_objs) == 1:
DumbProfileExtender(context.active_object, target_coordinate=context.scene.cursor.location).extend()
IfcStore.edited_objs.add(context.active_object)
joiner.join_E(context.active_object, context.scene.cursor.location)
return {"FINISHED"}
if len(selected_objs) == 2:
if self.join_type == "L":
joiner.join_L([o for o in selected_objs if o != context.active_object][0], context.active_object)
elif self.join_type == "V":
joiner.join_V([o for o in selected_objs if o != context.active_object][0], context.active_object)
if len(selected_objs) < 2:
return {"FINISHED"}
for obj in selected_objs:
if obj == context.active_object:
continue
DumbProfileExtender(obj, context.active_object).extend()
IfcStore.edited_objs.add(obj)
if self.join_type == "T":
for obj in selected_objs:
if obj == context.active_object:
continue
joiner.join_T(obj, context.active_object)
return {"FINISHED"}
class DumbProfileExtender:
# A profile is a prismatic extrusion along its local Z axis.
def __init__(self, profile, target=None, target_coordinate=None):
self.profile = profile
self.target = target
self.target_coordinate = target_coordinate
class DumbProfileJoiner:
def __init__(self):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Axis", "GRAPH_VIEW")
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
# An extension of a profile is determined by casting a ray from the cardinal
# point of either extreme along the profiles local Z axis to a target
# object. The nearest result from this raycast will be the target extension
# point.
def extend(self):
extension_data = self.get_closest_extension_point()
if not extension_data:
def unjoin(self, profile1):
element1 = tool.Ifc.get_entity(profile1)
if not element1:
return
if extension_data["end"] == "bottom":
self.profile.matrix_world.translation = extension_data["contact"]
if extension_data["direction"] == "up":
self.shift_top_faces(-extension_data["distance"])
elif extension_data["direction"] == "down":
self.shift_top_faces(extension_data["distance"])
if extension_data["end"] == "top":
if extension_data["direction"] == "up":
self.shift_top_faces(extension_data["distance"])
elif extension_data["direction"] == "down":
self.shift_top_faces(-extension_data["distance"])
def shift_top_faces(self, z):
vertices = []
for f in self.get_profile_top_faces():
vertices.extend(f.vertices)
for v in set(vertices):
self.profile.data.vertices[v].co.z += z
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATSTART")
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
# An top face is defined as having at least one vertex on the max
# Z-axis, and a non-insignificant Z component of its face normal
def get_profile_top_faces(self):
faces = []
max_z = max([v[2] for v in self.profile.bound_box])
for f in self.profile.data.polygons:
if abs(f.normal.z) < 0.1:
continue
for v in f.vertices:
if self.profile.data.vertices[v].co.z == max_z:
faces.append(f)
break
return faces
axis1 = self.get_profile_axis(profile1)
axis = copy.deepcopy(axis1)
body = copy.deepcopy(axis1)
self.recreate_profile(element1, profile1, axis, body)
def get_closest_extension_point(self):
top = self.profile.matrix_world @ Vector((0, 0, self.profile.dimensions[2]))
bottom = self.profile.matrix_world.translation
up = self.profile.matrix_world.to_quaternion() @ Vector((0, 0, 1))
down = self.profile.matrix_world.to_quaternion() @ Vector((0, 0, -1))
if self.target:
return self.get_closest_extension_point_from_target_obj(top, bottom, up, down)
elif self.target_coordinate:
return self.get_closest_extension_point_from_target_coordinate(top, bottom, up, down)
def get_closest_extension_point_from_target_coordinate(self, top, bottom, up, down):
point = mathutils.geometry.intersect_line_plane(
bottom, top, self.target_coordinate, self.profile.matrix_world.to_quaternion() @ Vector((0, 0, 1))
)
if not point:
def join_E(self, profile1, target):
element1 = tool.Ifc.get_entity(profile1)
if not element1:
return
top_distance = (point - top).length
bottom_distance = (point - bottom).length
if top_distance < bottom_distance:
intersection_point, signed_distance = mathutils.geometry.intersect_point_line(point, top, bottom)
if signed_distance > 0:
return {"contact": point, "distance": top_distance, "end": "top", "direction": "down"}
else:
return {"contact": point, "distance": top_distance, "end": "top", "direction": "up"}
axis1 = self.get_profile_axis(profile1)
intersect, connection = mathutils.geometry.intersect_point_line(target, *axis1)
connection = "ATEND" if connection > 0.5 else "ATSTART"
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type=connection)
axis = copy.deepcopy(axis1)
body = copy.deepcopy(axis1)
axis[1 if connection == "ATEND" else 0] = intersect
body[1 if connection == "ATEND" else 0] = intersect
self.recreate_profile(element1, profile1, axis, body)
def set_depth(self, profile1, length):
element1 = tool.Ifc.get_entity(profile1)
if not element1:
return
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
axis1 = self.get_profile_axis(profile1)
axis = copy.deepcopy(axis1)
body = copy.deepcopy(axis1)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
si_length = unit_scale * length
end = profile1.matrix_world @ Vector((si_length, 0, 0))
axis[1] = end
body[1] = end
self.recreate_profile(element1, profile1, axis, body)
def join_T(self, profile1, profile2):
element1 = tool.Ifc.get_entity(profile1)
element2 = tool.Ifc.get_entity(profile2)
axis1 = self.get_profile_axis(profile1)
axis2 = self.get_profile_axis(profile2)
intersect = tool.Cad.intersect_edges(axis1, axis2)
if intersect:
intersect, _ = intersect
else:
intersection_point, signed_distance = mathutils.geometry.intersect_point_line(point, bottom, top)
if signed_distance > 0:
return {"contact": point, "distance": bottom_distance, "end": "bottom", "direction": "up"}
return
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1) > 0.5 else "ATSTART"
ifcopenshell.api.run(
"geometry.connect_path",
tool.Ifc.get(),
related_element=element1,
relating_element=element2,
relating_connection="ATPATH",
related_connection=connection,
description="BUTT",
)
self.recreate_profile(element1, profile1, axis1, axis1)
def join_V(self, profile1, profile2):
element1 = tool.Ifc.get_entity(profile1)
element2 = tool.Ifc.get_entity(profile2)
axis1 = self.get_profile_axis(profile1)
axis2 = self.get_profile_axis(profile2)
intersect = tool.Cad.intersect_edges(axis1, axis2)
if intersect:
intersect, _ = intersect
else:
return
profile1_end = "ATEND" if tool.Cad.edge_percent(intersect, axis1) > 0.5 else "ATSTART"
profile2_end = "ATEND" if tool.Cad.edge_percent(intersect, axis2) > 0.5 else "ATSTART"
ifcopenshell.api.run(
"geometry.connect_path",
tool.Ifc.get(),
relating_element=element1,
related_element=element2,
relating_connection=profile1_end,
related_connection=profile2_end,
description="MITRE",
)
self.recreate_profile(element1, profile1, axis1, axis1)
self.recreate_profile(element2, profile2, axis2, axis2)
def join_L(self, profile1, profile2):
element1 = tool.Ifc.get_entity(profile1)
element2 = tool.Ifc.get_entity(profile2)
axis1 = self.get_profile_axis(profile1)
axis2 = self.get_profile_axis(profile2)
intersect = tool.Cad.intersect_edges(axis1, axis2)
if intersect:
intersect, _ = intersect
else:
return
profile1_end = "ATEND" if tool.Cad.edge_percent(intersect, axis1) > 0.5 else "ATSTART"
profile2_end = "ATEND" if tool.Cad.edge_percent(intersect, axis2) > 0.5 else "ATSTART"
ifcopenshell.api.run(
"geometry.connect_path",
tool.Ifc.get(),
relating_element=element1,
related_element=element2,
relating_connection=profile1_end,
related_connection=profile2_end,
description="BUTT",
)
self.recreate_profile(element1, profile1, axis1, axis1)
self.recreate_profile(element2, profile2, axis2, axis2)
def recreate_profile(self, element, obj, axis=None, body=None):
if axis is None or body is None:
axis = body = self.get_profile_axis(obj)
self.axis = copy.deepcopy(axis)
self.body = copy.deepcopy(body)
material = ifcopenshell.util.element.get_material(element, should_skip_usage=False)
usage = None
if material.is_a("IfcMaterialProfileSetUsage"):
usage = material
material = material.ForProfileSet
self.profile = material.CompositeProfile or material.MaterialProfiles[0].Profile
self.clippings = []
for rel in element.ConnectedTo:
connection = rel.RelatingConnectionType
other = tool.Ifc.get_object(rel.RelatedElement)
if connection not in ["ATPATH", "NOTDEFINED"]:
self.join(
obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description
)
for rel in element.ConnectedFrom:
connection = rel.RelatedConnectionType
other = tool.Ifc.get_object(rel.RelatingElement)
if connection not in ["ATPATH", "NOTDEFINED"]:
self.join(
obj, other, connection, rel.RelatingConnectionType, is_relating=False, description=rel.Description
)
new_matrix = copy.deepcopy(obj.matrix_world)
new_matrix.col[3] = self.body[0].to_4d()
new_matrix.invert()
self.clippings = [new_matrix @ c for c in self.clippings]
depth = (self.body[1] - self.body[0]).length
if self.axis_context:
axis = [(new_matrix @ a) for a in self.axis]
new_axis = ifcopenshell.api.run(
"geometry.add_axis_representation", tool.Ifc.get(), context=self.axis_context, axis=axis
)
old_axis = ifcopenshell.util.representation.get_representation(element, "Model", "Axis", "GRAPH_VIEW")
if old_axis:
for inverse in tool.Ifc.get().get_inverse(old_axis):
ifcopenshell.util.element.replace_attribute(inverse, old_axis, new_axis)
blenderbim.core.geometry.remove_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=old_axis
)
else:
return {"contact": point, "distance": bottom_distance, "end": "bottom", "direction": "down"}
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_axis
)
def get_closest_extension_point_from_target_obj(self, top, bottom, up, down):
t_top = self.target.matrix_world.inverted() @ top
t_bottom = self.target.matrix_world.inverted() @ bottom
t_up = self.target.matrix_world.inverted().to_quaternion() @ up
t_down = self.target.matrix_world.inverted().to_quaternion() @ down
new_body = ifcopenshell.api.run(
"geometry.add_profile_representation",
tool.Ifc.get(),
context=self.body_context,
profile=self.profile,
depth=depth,
cardinal_point=usage.CardinalPoint if usage else None,
clippings=self.clippings,
)
results = []
results.append((self.target.ray_cast(t_top, t_up, distance=50), top, "top", "up"))
results.append((self.target.ray_cast(t_top, t_down, distance=50), top, "top", "down"))
results.append((self.target.ray_cast(t_bottom, t_up, distance=50), bottom, "bottom", "up"))
results.append((self.target.ray_cast(t_bottom, t_down, distance=50), bottom, "bottom", "down"))
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if old_body:
for inverse in tool.Ifc.get().get_inverse(old_body):
ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body)
obj.data.BIMMeshProperties.ifc_definition_id = int(new_body.id())
obj.data.name = f"{self.body_context.id()}/{new_body.id()}"
blenderbim.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_body)
else:
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_body
)
min_distance = float(inf)
final = None
obj.location[0], obj.location[1], obj.location[2] = self.body[0]
bpy.context.view_layer.update()
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
blenderbim.core.geometry.switch_representation(
tool.Geometry,
obj=obj,
representation=new_body,
should_reload=True,
enable_dynamic_voids=False,
is_global=True,
should_sync_changes_first=False,
)
tool.Geometry.record_object_materials(obj)
for result in results:
if result[0][0]:
contact = self.target.matrix_world @ result[0][1]
distance = (contact - result[1]).length
if distance < min_distance:
min_distance = distance
final = {"contact": contact, "distance": distance, "end": result[2], "direction": result[3]}
def join(self, profile1, profile2, connection1, connection2, is_relating=True, description="BUTT"):
element1 = tool.Ifc.get_entity(profile1)
element2 = tool.Ifc.get_entity(profile2)
axis1 = self.get_profile_axis(profile1)
axis2 = self.get_profile_axis(profile2)
return final
angle = tool.Cad.angle_edges(axis1, axis2, signed=False, degrees=True)
if tool.Cad.is_x(angle, (0, 180)):
return False
intersect, _ = tool.Cad.intersect_edges(axis1, axis2)
proposed_axis = [self.axis[0], intersect] if connection1 == "ATEND" else [intersect, self.axis[1]]
if tool.Cad.is_x(tool.Cad.angle_edges(self.axis, proposed_axis, degrees=True), 180):
# The user has moved the wall into an invalid position that cannot connect at the desired end
return False
self.axis[1 if connection1 == "ATEND" else 0] = intersect
# Work out body extents
if connection1 == "ATEND":
axisl = (profile2.matrix_world.inverted() @ axis1[1]) - (profile2.matrix_world.inverted() @ axis1[0])
elif connection1 == "ATSTART":
axisl = (profile2.matrix_world.inverted() @ axis1[0]) - (profile2.matrix_world.inverted() @ axis1[1])
xy_angle = math.degrees(Vector((1, 0)).angle_signed(axisl.normalized().to_2d()))
if xy_angle >= -135 and xy_angle <= -45:
closest_plane = "bottom"
furthest_plane = "top"
elif xy_angle >= 45 and xy_angle <= 135:
closest_plane = "top"
furthest_plane = "bottom"
elif xy_angle >= -45 and xy_angle <= 45:
closest_plane = "left"
furthest_plane = "right"
else:
closest_plane = "right"
furthest_plane = "left"
is_orthogonal = tool.Cad.is_x(tool.Cad.angle_edges(axis1, axis2, degrees=True), 90, tolerance=0.001)
if description == "MITRE":
# Mitre joints are an unofficial convention
# Check the closest plane from the perspective of the other element
if connection2 == "ATEND":
axisl = (profile1.matrix_world.inverted() @ axis2[1]) - (profile1.matrix_world.inverted() @ axis2[0])
elif connection2 == "ATSTART":
axisl = (profile1.matrix_world.inverted() @ axis2[0]) - (profile1.matrix_world.inverted() @ axis2[1])
xy_angle2 = math.degrees(Vector((1, 0)).angle_signed(axisl.normalized().to_2d()))
if xy_angle2 >= -135 and xy_angle2 <= -45:
closest_plane2 = "bottom"
furthest_plane2 = "top"
elif xy_angle2 >= 45 and xy_angle2 <= 135:
closest_plane2 = "top"
furthest_plane2 = "bottom"
elif xy_angle2 >= -45 and xy_angle2 <= 45:
closest_plane2 = "left"
furthest_plane2 = "right"
else:
closest_plane2 = "right"
furthest_plane2 = "left"
if connection1 == "ATEND":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
self.body[1] = intersect
else:
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
# Mitre clip
if connection1 == connection2:
plane1 = self.get_profile_plane(profile1, furthest_plane2)
plane2 = self.get_profile_plane(profile2, furthest_plane)
clip1, direction1 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
plane1 = self.get_profile_plane(profile1, closest_plane2)
plane2 = self.get_profile_plane(profile2, closest_plane)
clip2, direction2 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
else:
plane1 = self.get_profile_plane(profile1, furthest_plane2)
plane2 = self.get_profile_plane(profile2, furthest_plane)
clip1, direction1 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
plane1 = self.get_profile_plane(profile1, closest_plane2)
plane2 = self.get_profile_plane(profile2, closest_plane)
clip2, direction2 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
y_axis = direction2
x_axis = (clip2 - clip1).normalized().to_3d()
z_axis = x_axis.cross(y_axis)
self.clippings.append(self.create_matrix(clip1, x_axis, y_axis, z_axis))
elif connection1 == "ATSTART":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
self.body[0] = intersect
else:
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
if connection1 == connection2:
plane1 = self.get_profile_plane(profile1, furthest_plane2)
plane2 = self.get_profile_plane(profile2, furthest_plane)
clip1, direction1 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
plane1 = self.get_profile_plane(profile1, closest_plane2)
plane2 = self.get_profile_plane(profile2, closest_plane)
clip2, direction2 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
else:
plane1 = self.get_profile_plane(profile1, furthest_plane2)
plane2 = self.get_profile_plane(profile2, furthest_plane)
clip1, direction1 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
plane1 = self.get_profile_plane(profile1, closest_plane2)
plane2 = self.get_profile_plane(profile2, closest_plane)
clip2, direction2 = mathutils.geometry.intersect_plane_plane(
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
)
y_axis = direction2
x_axis = (clip2 - clip1).normalized().to_3d()
z_axis = x_axis.cross(y_axis)
self.clippings.append(self.create_matrix(clip1, x_axis, y_axis, z_axis))
else:
# This is the standard L and T joints described by IFC
if connection1 == "ATEND":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
self.body[1] = intersect
else:
plane = self.get_profile_plane(
profile2,
furthest_plane if is_relating else closest_plane,
z_inwards=False if is_relating else True,
)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
self.clippings.append(plane)
elif connection1 == "ATSTART":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
self.body[0] = intersect
else:
plane = self.get_profile_plane(
profile2,
furthest_plane if is_relating else closest_plane,
z_inwards=False if is_relating else True,
)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
self.clippings.append(plane)
def get_max_bound_box_dimension(self, obj):
x = [v[0] for v in obj.bound_box]
y = [v[1] for v in obj.bound_box]
x_dim = max(x) - min(x)
y_dim = max(y) - min(y)
return x_dim if x_dim > y_dim else y_dim
def get_profile_plane(self, obj, plane, z_inwards=True):
# Get a matrix for one of the bounding planes of the profile to be used as cutting plane
# Here's an example of looking at the end of an I-Beam profile with a +Z extrusion out of the screen
# top
# +-----+ Y
# |=====| ^
# | | | |
# left | | | right Z->X
# | | |
# |=====|
# +-----+
# bottom
if plane == "top":
max_y = max([v[1] for v in obj.bound_box])
p = obj.matrix_world @ Vector((0, max_y, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
if z_inwards:
y_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
else:
y_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
elif plane == "bottom":
min_y = min([v[1] for v in obj.bound_box])
p = obj.matrix_world @ Vector((0, min_y, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
if z_inwards:
y_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
else:
y_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
elif plane == "right":
max_x = max([v[0] for v in obj.bound_box])
p = obj.matrix_world @ Vector((max_x, 0, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
if z_inwards:
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
else:
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
elif plane == "left":
min_x = min([v[0] for v in obj.bound_box])
p = obj.matrix_world @ Vector((min_x, 0, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
if z_inwards:
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
else:
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
return self.create_matrix(p, x_axis, y_axis, z_axis)
def create_matrix(self, p, x, y, z):
return Matrix(
(
(x[0], y[0], z[0], p[0]),
(x[1], y[1], z[1], p[1]),
(x[2], y[2], z[2], p[2]),
(0.0, 0.0, 0.0, 1.0),
)
)
def get_profile_axis(self, obj):
z_values = [v[2] for v in obj.bound_box]
return [
(obj.matrix_world @ Vector((0.0, 0.0, min(z_values)))),
(obj.matrix_world @ Vector((0.0, 0.0, max(z_values)))),
]
class RecalculateProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.recalculate_profile"
bl_label = "Recalculate Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
DumbProfileRecalculator().recalculate(context.selected_objects)
return {"FINISHED"}
class DumbProfileRecalculator:
def recalculate(self, profiles):
queue = set()
for profile in profiles:
element = tool.Ifc.get_entity(profile)
queue.add((element, profile))
for rel in getattr(element, "ConnectedTo", []):
queue.add((rel.RelatedElement, tool.Ifc.get_object(rel.RelatedElement)))
for rel in getattr(element, "ConnectedFrom", []):
queue.add((rel.RelatingElement, tool.Ifc.get_object(rel.RelatingElement)))
joiner = DumbProfileJoiner()
for element, profile in queue:
if element.is_a() in ("IfcColumn", "IfcBeam", "IfcMember") and profile:
joiner.recreate_profile(element, profile)
class ChangeProfileDepth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_profile_depth"
bl_label = "Change Profile Length"
bl_options = {"REGISTER", "UNDO"}
depth: bpy.props.FloatProperty()
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
joiner = DumbProfileJoiner()
for obj in context.selected_objects:
joiner.set_depth(obj, self.depth)
return {"FINISHED"}
class ChangeCardinalPoint(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_cardinal_point"
bl_label = "Change Cardinal Point"
bl_options = {"REGISTER", "UNDO"}
cardinal_point: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
objs = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
material = ifcopenshell.util.element.get_material(element, should_skip_usage=False)
if material.is_a("IfcMaterialProfileSetUsage"):
material.CardinalPoint = self.cardinal_point
objs.append(obj)
DumbProfileRecalculator().recalculate(objs)
return {"FINISHED"}
class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.rotate_90"
bl_label = "Rotate 90"
bl_options = {"REGISTER", "UNDO"}
axis: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
objs = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element.ConnectedTo or element.ConnectedFrom:
objs.append(obj)
rotate_matrix = Matrix.Rotation(pi / 2, 4, self.axis)
obj.matrix_world @= rotate_matrix
bpy.context.view_layer.update()
DumbProfileRecalculator().recalculate(objs)
return {"FINISHED"}
@@ -17,21 +17,10 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell.util.type
from blenderbim.bim.prop import ObjProperty
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.prop import StrProperty, Attribute
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
StringProperty,
EnumProperty,
BoolProperty,
IntProperty,
FloatProperty,
FloatVectorProperty,
CollectionProperty,
)
def get_ifc_class(self, context):
@@ -43,18 +32,174 @@ def get_ifc_class(self, context):
def get_relating_type(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
return AuthoringData.data["relating_types"]
return AuthoringData.data["relating_types_ids"]
def get_relating_type_browser(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
return AuthoringData.data["relating_types_ids_browser"]
def store_cursor_position(context, event, cursor=True, window=True):
browser_state = context.scene.BIMModelProperties.constr_browser_state
if cursor:
browser_state.cursor_x, browser_state.cursor_y = event.mouse_x, event.mouse_y
if window:
browser_state.window_x, browser_state.window_y = event.mouse_x, event.mouse_y
def update_icon_id(self, context, browser=False):
if context == "lost_context" or (context.region is not None and context.region.type != "TOOL_HEADER"):
ifc_class = self.ifc_class_browser if browser else self.ifc_class
relating_type_id = self.relating_type_id_browser if browser else self.relating_type_id
# relating_type = AuthoringData.relating_type_name_by_id(ifc_class, relating_type_id)
if ifc_class not in self.constr_classes or relating_type_id not in self.constr_classes[ifc_class].constr_types:
try:
AuthoringData.assetize_relating_type_from_selection(browser=browser)
except ConstrTypeEntityNotFound:
return
def set_icon(update_interval_seconds=1e-4):
if (ifc_class not in self.constr_classes
or relating_type_id not in self.constr_classes[ifc_class].constr_types):
return update_interval_seconds
else:
self.icon_id = self.constr_classes[ifc_class].constr_types[relating_type_id].icon_id
bpy.app.timers.register(set_icon)
def update_ifc_class(self, context):
AuthoringData.is_loaded = False
AuthoringData.load_ifc_classes()
AuthoringData.load_relating_types()
if not self.updating:
update_icon_id(self, context)
def update_ifc_class_browser(self, context):
if context.region is not None and context.region.type != "TOOL_HEADER":
AuthoringData.load_ifc_classes()
AuthoringData.load_relating_types_browser()
if self.updating:
return
ifc_class = self.ifc_class_browser
constr_class_info = AuthoringData.constr_class_info(ifc_class)
if constr_class_info is None or not constr_class_info.fully_loaded:
AuthoringData.assetize_constr_class(ifc_class)
def update_relating_type(self, context):
AuthoringData.load_relating_types()
if not self.updating:
update_icon_id(self, context)
def update_relating_type_browser(self, context):
AuthoringData.load_relating_types_browser()
if not self.updating:
update_icon_id(self, context, browser=True)
def update_relating_type_by_name(self, context):
AuthoringData.load_relating_types()
relating_type_id = AuthoringData.relating_type_id_by_name(self.ifc_class, self.relating_type)
if relating_type_id is not None:
self.relating_type_id = relating_type_id
def get_constr_class_info(props, ifc_class):
return props.constr_classes[ifc_class] if ifc_class in props.constr_classes else None
def update_preview_multiple(self, context):
if context.region is not None and context.region.type != "TOOL_HEADER":
if self.preview_multiple_constr_types:
ifc_class = self.ifc_class
constr_class_info = get_constr_class_info(self, ifc_class)
if constr_class_info is None or not constr_class_info.fully_loaded:
AuthoringData.assetize_constr_class(ifc_class)
else:
update_relating_type(self, context)
class ConstrTypeInfo(PropertyGroup):
name: bpy.props.StringProperty(name="Construction type ID")
icon_id: bpy.props.IntProperty(name="Icon ID")
object: bpy.props.PointerProperty(name="Object", type=bpy.types.Object)
class ConstrClassInfo(PropertyGroup):
name: bpy.props.StringProperty(name="Construction class")
constr_types: bpy.props.CollectionProperty(type=ConstrTypeInfo)
fully_loaded: bpy.props.BoolProperty(default=False)
class ConstrBrowserState(PropertyGroup):
cursor_x: bpy.props.IntProperty()
cursor_y: bpy.props.IntProperty()
window_x: bpy.props.IntProperty()
window_y: bpy.props.IntProperty()
far_away_x: bpy.props.IntProperty(default=10) # lower left corner to temporarily warp the mouse
far_away_y: bpy.props.IntProperty(default=10) # useful to close popup operators
updating: bpy.props.BoolProperty()
update_delay: bpy.props.FloatProperty(default=3e-2)
class BIMModelProperties(PropertyGroup):
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="IFC Class", update=update_ifc_class)
relating_type: bpy.props.EnumProperty(items=get_relating_type, name="Relating Type")
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
ifc_class_browser: bpy.props.EnumProperty(
items=get_ifc_class, name="Construction Class", update=update_ifc_class_browser
)
relating_type: bpy.props.StringProperty(update=update_relating_type_by_name)
relating_type_id: bpy.props.EnumProperty(
items=get_relating_type, name="Construction Type", update=update_relating_type
)
relating_type_id_browser: bpy.props.EnumProperty(
items=get_relating_type_browser, name="Construction Type", update=update_relating_type_browser
)
icon_id: bpy.props.IntProperty()
preview_multiple_constr_types: bpy.props.BoolProperty(default=False, update=update_preview_multiple)
updating: bpy.props.BoolProperty(default=False)
occurrence_name_style: bpy.props.EnumProperty(
items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")],
name="Occurrence Name Style",
)
occurrence_name_function: bpy.props.StringProperty(name="Occurrence Name Function")
getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type}
constr_classes: bpy.props.CollectionProperty(type=ConstrClassInfo)
constr_browser_state: bpy.props.PointerProperty(type=ConstrBrowserState)
extrusion_depth: bpy.props.FloatProperty(default=42.0)
cardinal_point: bpy.props.EnumProperty(
items=(
# TODO: complain to buildingSMART
("1", "bottom left", ""),
("2", "bottom centre", ""),
("3", "bottom right", ""),
("4", "mid-depth left", ""),
("5", "mid-depth centre", ""),
("6", "mid-depth right", ""),
("7", "top left", ""),
("8", "top centre", ""),
("9", "top right", ""),
("10", "geometric centroid", ""),
("11", "bottom in line with the geometric centroid", ""),
("12", "left in line with the geometric centroid", ""),
("13", "right in line with the geometric centroid", ""),
("14", "top in line with the geometric centroid", ""),
("15", "shear centre", ""),
("16", "bottom in line with the shear centre", ""),
("17", "left in line with the shear centre", ""),
("18", "right in line with the shear centre", ""),
("19", "top in line with the shear centre", ""),
),
name="Cardinal Point",
default="5",
)
length: bpy.props.FloatProperty(default=42.0)
openings: bpy.props.CollectionProperty(type=ObjProperty)
x: bpy.props.FloatProperty(name="X", default=0.5)
y: bpy.props.FloatProperty(name="Y", default=0.5)
z: bpy.props.FloatProperty(name="Z", default=0.5)

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