Compare commits

...

522 Commits

Author SHA1 Message Date
fbpyr 10bde73c1a choco nightly path (#2498)
* fix choco nightly scripts path

* fix choco nightly scripts path
2022-10-14 17:16:50 +11:00
Massimo Fabbro 670399f327 Add gross volume calculation (#2487)
* Now gross volume calculation doesn't consider related voids

* Qto gross volume calculation minor fixes

* No need to create a new entity and better mesh creation

* Use bmesh to calculate volume so it's not necessary to create a new object
2022-10-14 16:50:04 +11:00
Dion Moult 241c5b1f48 Fix bug where unnamed types didn't show up in the UI. 2022-10-14 10:08:52 +11:00
Dion Moult 9ef3b4389b Change pip name to ifcopenshell. See #2468. 2022-10-14 08:25:15 +11:00
Dion Moult 2d37d3c914 More robust type switching of parametric objects 2022-10-13 22:51:06 +11:00
Thomas Krijnen ede9c5fca5 Fixes to svgfill/cgal/mpfr/gmp for -shared build on nix 2022-10-13 09:52:22 +02:00
Thomas Krijnen 4cccfd08d3 #671 check for zero magnitude vector in newells method 2022-10-13 09:34:14 +02:00
Thomas Krijnen 153c5a06e7 #671 use new faceset_helper api in IfcFace 2022-10-13 09:22:49 +02:00
Thomas Krijnen 9a4d3e706a #671 api in faceset_helper to obtain multiple bounds from single loop in case of intersections 2022-10-13 09:22:28 +02:00
Thomas Krijnen 0b20d3ae2c #671 correct check for NO_WIRE_INTERSECTION_CHECK setting 2022-10-13 09:21:50 +02:00
Thomas Krijnen 92fb46558f #671 perform fix on wires after wire_intersections() 2022-10-13 09:20:44 +02:00
Dion Moult 8be1a913ec Basic support for mesh booleans 2022-10-13 17:04:53 +11:00
Dion Moult c354de7b83 Minor fix 2022-10-13 11:50:33 +11:00
Dion Moult 19b718cdf5 Thumbnails auto load when projects are loaded. 2022-10-13 10:19:09 +11:00
Dion Moult cdffeba4cc Map units are now supported in converting to and from local to global coordinates 2022-10-12 20:32:16 +11:00
fbpyr e6e8db8a00 choco yesterday (#2495)
* fix check for available bbim releases yesterday

* fix check yesterday bbim releases after midnight

* fix check all releases url
2022-10-12 17:39:50 +11:00
Thomas Krijnen 8dab1810c7 #2486 #1861 Don't fail on overwriting header values 2022-10-11 10:48:53 +02:00
Dion Moult c5c9a192b0 You can now duplicate existing types in the type manager 2022-10-11 17:51:01 +11:00
Dion Moult 4279576d3c Type manager can now browse to type classes that don't have any types yet 2022-10-11 17:34:17 +11:00
Dion Moult dff35c173d You can now delete a type when managing types 2022-10-11 15:31:31 +11:00
Dion Moult 38f563d4f4 New timerless thumbnailed type manager. 2022-10-11 13:49:46 +11:00
Dion Moult 36fdd3f7fc Bundle pillow as dependency for thumbnail generator 2022-10-11 10:54:08 +11:00
fbpyr 47e0ec43c2 fix get sha256sum directly from release download (#2493) 2022-10-11 09:40:00 +11:00
Thomas Krijnen 0446fca56a --json --spf option in validate.py 2022-10-10 14:25:45 +02:00
Thomas Krijnen 785dc2a300 Test case for string formatting 2022-10-10 14:20:21 +02:00
Thomas Krijnen 64ff046e96 #2482 Use u32string for character encoder, remove CODECVT_BOOST_FALLBACK 2022-10-10 14:19:42 +02:00
Dion Moult 16582cf57f Experimental new thumbnail with smarter layer / profile thumbnails and no timers. 2022-10-10 23:13:37 +11:00
Dion Moult a5517071cc Make remove opening more accessible in UI and refactor 2022-10-10 11:45:50 +11:00
Gorgious 5d5ab3dafd Fix bad indenting in last commit
Sorry, I'm a bit rusty !
2022-10-09 22:05:56 +02:00
Gorgious ccfbeab51e Creating a wall with a IfcMaterialLayerSet with the BIM tool now makes it active (yellow outline)
+ deduplicate code under new function
2022-10-09 22:04:01 +02:00
Dion Moult c9214bf04a The BIM Tool workspace now intelligently reflects the active object 2022-10-09 23:51:40 +11:00
Dion Moult 492a535312 You can now set a fixed RL for inserted windows 2022-10-09 23:29:06 +11:00
Dion Moult 278931f5db Minor fix 2022-10-09 22:40:20 +11:00
Dion Moult d847f358ba Minor fix 2022-10-09 22:26:38 +11:00
Dion Moult ab31fd7e48 Copying filled openings is now possible. 2022-10-09 22:25:55 +11:00
Dion Moult 32f0911479 Elements with filled openings don't have their openings copied, as it depends on the fill 2022-10-09 22:22:12 +11:00
Dion Moult bdbd874fa8 Fix #2488. Copying objects now copy deep instead of adding afresh (and losing parametric data and openings). 2022-10-09 18:09:22 +11:00
Dion Moult 232252f61d Copy deep can now exclude things, useful for copying representation graphs where you don't want to duplicate the context 2022-10-09 17:15:39 +11:00
Dion Moult 3ee4b1f0e3 Fix #2489. Prevent openings from being added twice and only save changed openings. 2022-10-09 15:22:10 +11:00
Thomas Krijnen cd27d48701 #2482 entity_instance.to_string() 2022-10-08 17:18:47 +02:00
Thomas Krijnen 94ba1a121d #2486 store provenance to file also in simple type instances 2022-10-08 15:16:53 +02:00
Dion Moult 4110b81270 Decomposition mode now selects all children (not parenting) to move a host and all children easily 2022-10-08 23:42:03 +11:00
Dion Moult 37028857b4 Suspicious undo stack fix 2022-10-08 22:29:13 +11:00
Dion Moult 7c1f02cafe You can now reinsert windows and doors into other hosts 2022-10-08 22:28:46 +11:00
Dion Moult 7d779130e1 Removing products now handles purging a bunch more inverse relationships 2022-10-08 21:15:01 +11:00
fbpyr fb6d24b3ed add choco alpha version tag, description fixes (#2484) 2022-10-07 22:14:04 +11:00
Dion Moult c6ee1d1679 Deleting a fill auto deletes the opening 2022-10-07 20:55:46 +11:00
Dion Moult 1db5d3b06c Begin purging old dynamic void feature 2022-10-07 20:47:13 +11:00
garylzimmer 3b81c158f3 Added auto renaming for null project name (#2478)
Sorry it took me almost a year to implement @aothms excellent suggestion!
https://github.com/IfcOpenShell/IfcOpenShell/issues/1954#issuecomment-1002463624
2022-10-07 20:36:25 +11:00
Dion Moult 34aff01eb2 New prototype operator to regen fill openings 2022-10-07 17:06:19 +11:00
Dion Moult 7d1a41c1c7 Minor fix 2022-10-07 16:08:21 +11:00
Dion Moult d187c588d3 Minor fix 2022-10-07 16:04:06 +11:00
Dion Moult 82f847f5d4 Basic adding of filled openings reimplemented with new opening system 2022-10-07 16:00:17 +11:00
Dion Moult 27dc81544c Minor fixes to IfcTester testcases 2022-10-07 13:34:19 +11:00
Dion Moult 1677a6c83f IfcTester now supports new entityType data in partof facet 2022-10-07 13:11:52 +11:00
Dion Moult 61f4c48a4a Write docs for basic description of what's in an IFC 2022-10-06 21:07:58 +11:00
Dion Moult 0259b75d4c Write documentation for IOS code examples 2022-10-06 19:56:53 +11:00
Dion Moult 0db28917a4 Write docs on IOS geometry processing. See #671. 2022-10-06 17:06:37 +11:00
Dion Moult 15b4ab543b Update geom settings with newly added settings, and add example code. See #671. 2022-10-06 15:52:12 +11:00
Dion Moult 72147e51f5 Migrate old Doxygen geom markdown to new Sphinx rst docs. See #671 2022-10-06 11:18:23 +11:00
Sigma Dimensions 835e3f00b4 fix the bug fix on commit #88893a8 2022-10-05 13:59:41 +01:00
Thomas Krijnen 2dee2afdfa #1861 test case 2022-10-05 13:07:16 +02:00
Thomas Krijnen 3d82791a99 #1861 update globalid map on attribute change 2022-10-05 13:03:52 +02:00
Dion Moult 2cb8eebf83 Profiles now retain manual booleans alongside autogenerated clipping planes 2022-10-05 21:52:07 +11:00
Dion Moult 49d3ee2f73 Copying an opening now tracks it as a special objects in the scene 2022-10-05 20:04:19 +11:00
Dion Moult aeaef641bf Bump IOS build 2022-10-05 19:52:24 +11:00
Dion Moult e97d427e9f Copying elements with openings now also copies openings 2022-10-05 19:44:32 +11:00
Dion Moult d525dfa46f Deleting voided objects now works with new opening system 2022-10-05 18:22:35 +11:00
Dion Moult 272a00941e Openings now move with profiles, but stay fixed along the local Z axis 2022-10-05 17:57:51 +11:00
Dion Moult 1590e8c0e7 Openings locations are fixed relative to slab profile editing 2022-10-05 16:39:10 +11:00
Dion Moult ac4e2f142b Openings now move with the host, but also intuitively stay along the local X axis of the host 2022-10-05 15:26:27 +11:00
Dion Moult 428ee648b4 Wall representations now distinguish between auto and manual booleans and manual booleans move with the wall 2022-10-05 13:09:18 +11:00
Thomas Krijnen fa6bbf2d4a A bunch of not so pretty validate.py performance improvements 2022-10-04 15:16:34 +02:00
Dion Moult 12aa66fdcb Fix nasty bug related to inconsistent behaviour of wall intersection math 2022-10-05 00:14:12 +11:00
Thomas Krijnen f68db3d3be Fixes after 698c708bf6 2022-10-04 12:38:40 +02:00
Dion Moult 1f56b36bfa Minor fix 2022-10-04 20:16:36 +11:00
Thomas Krijnen be805a3e96 Fixes after 698c708bf6 2022-10-04 11:14:13 +02:00
Thomas Krijnen 5d95475970 #2471 test case 2022-10-04 10:23:27 +02:00
Thomas Krijnen f41ad0d5de #2471 work around file gc with instance refs 2022-10-04 10:23:08 +02:00
Dion Moult f81b22240c Minor fix 2022-10-04 18:35:01 +11:00
Dion Moult 7d63b03dd3 Fix bug where linking an element twice caused stacked undos to fail. See #2200. 2022-10-04 14:23:02 +11:00
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
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
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
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
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
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
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
475 changed files with 26269 additions and 11728 deletions
+92
View File
@@ -0,0 +1,92 @@
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: "30 0 * * *" # 30min past 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: Fill chocolatey scripts on win with latest blender python
if: ${{steps.do_choco.outputs.choco_release}} == 'do_choco_release'
run: |
yesterday_non_iso="$(date --date='yesterday' +'%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 }}.$yesterday_non_iso" &&
export url_blenderbim_py310_win_zip="https://github.com/IfcOpenShell/IfcOpenShell/releases/download/blenderbim-$yesterday_non_iso/blenderbim-$yesterday_non_iso-$pyver-$target_os.zip" &&
wget $url_blenderbim_py310_win_zip --no-verbose &&
export sha256sum_blenderbim_py310_win_zip=$( sha256sum blenderbim-$yesterday_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."
@@ -53,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: "49 23 1 * *" # 11min 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
+13 -2
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'
+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/
+63 -45
View File
@@ -9,9 +9,15 @@ For more information, see
* [http://ifcopenshell.org](http://ifcopenshell.org)
* [http://academy.ifcopenshell.org](http://academy.ifcopenshell.org)
[![Build Status](https://travis-ci.com/IfcOpenShell/IfcOpenShell.svg?branch=v0.6.0)](https://travis-ci.com/IfcOpenShell/IfcOpenShell)
[![Financial Contributors](https://opencollective.com/opensourcebim/tiers/badge.svg)](https://opencollective.com/opensourcebim/)
| 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)](https://pypi.org/project/ifcopenshell/) |
| 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
-------------
@@ -25,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*
@@ -89,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.
@@ -163,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
----------------------------------
@@ -273,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-alpha</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/IfcOpenShell/IfcOpenShell/c6ee1d1679d1298de0d7447ff108c22709c68e54/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 ifc python opensource foss</tags>
<summary>opensource bim</summary>
<description>opensource bim addon for blender - source code at: https://github.com/IfcOpenShell/IfcOpenShell</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

+80
View File
@@ -0,0 +1,80 @@
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 - datetime.timedelta(days=1)).strftime("%y%m%d")
url = "https://github.com/IfcOpenShell/IfcOpenShell/releases"
resp = request_repo_info(url)
text = str(resp.read())
if blenderbim_date in text:
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-nightly\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-nightly\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.")
+55 -24
View File
@@ -53,7 +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)" ON)
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()
@@ -452,30 +452,60 @@ if(NOT MINIMAL_BUILD AND 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
@@ -590,7 +620,7 @@ 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", "4x3")
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4" "4x3")
endif()
foreach(s ${SCHEMA_VERSIONS})
@@ -823,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()
@@ -931,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
+15 -11
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"
@@ -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:
@@ -630,7 +634,7 @@ if "cgal" in targets:
build_dependency(
name=f"gmp-{GMP_VERSION}",
mode="autoconf",
build_tool_args=["--disable-shared", "--with-pic"],
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, "--with-pic"],
download_url="https://ftp.gnu.org/gnu/gmp/",
download_name=f"gmp-{GMP_VERSION}.tar.bz2"
)
@@ -638,7 +642,7 @@ if "cgal" in targets:
build_dependency(
name=f"mpfr-{MPFR_VERSION}",
mode="autoconf",
build_tool_args=["--disable-shared", f"--with-gmp={DEPS_DIR}/install/gmp-{GMP_VERSION}"],
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, f"--with-gmp={DEPS_DIR}/install/gmp-{GMP_VERSION}"],
download_url=f"http://www.mpfr.org/mpfr-{MPFR_VERSION}/",
download_name=f"mpfr-{MPFR_VERSION}.tar.bz2"
)
@@ -647,9 +651,9 @@ if "cgal" in targets:
name=f"cgal-{CGAL_VERSION}",
mode="cmake",
build_tool_args=[
f"-DGMP_LIBRARIES={DEPS_DIR}/install/gmp-{GMP_VERSION}/lib/libgmp.a",
f"-DGMP_LIBRARIES={DEPS_DIR}/install/gmp-{GMP_VERSION}/lib/libgmp.{LIBRARY_EXT}",
f"-DGMP_INCLUDE_DIR={DEPS_DIR}/install/gmp-{GMP_VERSION}/include",
f"-DMPFR_LIBRARIES={DEPS_DIR}/install/mpfr-{MPFR_VERSION}/lib/libmpfr.a" ,
f"-DMPFR_LIBRARIES={DEPS_DIR}/install/mpfr-{MPFR_VERSION}/lib/libmpfr.{LIBRARY_EXT}" ,
f"-DMPFR_INCLUDE_DIR={DEPS_DIR}/install/mpfr-{MPFR_VERSION}/include",
f"-DBoost_INCLUDE_DIR={DEPS_DIR}/install/boost-{BOOST_VERSION}",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/cgal-{CGAL_VERSION}/",
+123 -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,24 @@ 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
PILLOW_URL:=https://files.pythonhosted.org/packages/ed/d5/c2e84e1e36ab8ebea033921d5886a056c77e18bab5ab1051fcc22de2e8a2/Pillow-9.2.0-cp37-cp37m-manylinux_2_28_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
PILLOW_URL:=https://files.pythonhosted.org/packages/01/61/3ff85fb4bb596ce3d223c8fcf93c8df5c12bc8899dfb4fb3cb1c5b20dd5f/Pillow-9.2.0-cp39-cp39-manylinux_2_28_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
PILLOW_URL:=https://files.pythonhosted.org/packages/f6/51/320986ebd6d46a0e95c2240468ced73153b691ce07617078bcdf30c609ec/Pillow-9.2.0-cp310-cp310-manylinux_2_28_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 +86,85 @@ 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
PILLOW_URL:=https://files.pythonhosted.org/packages/88/49/c26fc3b5b0e82bdc9d8751d6b939da29327b0d98f7c3b95a575cbfed2743/Pillow-9.2.0-cp37-cp37m-macosx_10_10_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
PILLOW_URL:=https://files.pythonhosted.org/packages/88/7a/ddfe28b485b623361457d4783007c1f9ba83a87f93e7fec32f64793efb6c/Pillow-9.2.0-cp39-cp39-macosx_10_10_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
PILLOW_URL:=https://files.pythonhosted.org/packages/d8/60/b13c00d403f34110e96c1b5c0afa73ce461efe3fe960c3a7e3e7fe190d82/Pillow-9.2.0-cp310-cp310-macosx_10_10_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
PILLOW_URL:=https://files.pythonhosted.org/packages/aa/bc/21097cd891dd2fa02f2b3d767e02e883e026482e59d29975d1bc30024aa3/Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.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
PILLOW_URL:=https://files.pythonhosted.org/packages/aa/bc/21097cd891dd2fa02f2b3d767e02e883e026482e59d29975d1bc30024aa3/Pillow-9.2.0-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
PILLOW_URL:=https://files.pythonhosted.org/packages/0c/5f/117b653cad585f3aedfe0de996c292e67d4b020ed77f652e5a6c8c24f908/Pillow-9.2.0-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
PILLOW_URL:=https://files.pythonhosted.org/packages/69/f5/9e802159d78b2eaf26bf1f8b94648605993f5ca7247ac8870f065063fc40/Pillow-9.2.0-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
PILLOW_URL:=https://files.pythonhosted.org/packages/19/3f/b4d4bcf05dbcbe07f2e9613a8f4180c297395e73a91d8ad22c32c6624f8c/Pillow-9.2.0-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
PILLOW_URL:=https://files.pythonhosted.org/packages/02/55/67a3c17b9e7d972ed8c246f104da99ca4f3ea42fba566697e479011b84b6/Pillow-9.2.0-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 +182,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-cb8cfd7-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-fa6bbf2-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -138,11 +205,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 +230,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 +279,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 +307,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 +340,29 @@ 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 the BIM tool thumbnail generator
mkdir dist/working
cd dist/working && wget $(PILLOW_URL)
cd dist/working && cp *.whl pillow.zip && unzip pillow.zip
cp -r dist/working/PIL dist/blenderbim/libs/site/packages/
ifeq ($(PLATFORM), linux)
cp -r dist/working/Pillow.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 +373,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 +481,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 +576,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 +624,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
@@ -62,6 +62,7 @@ modules = {
"clash": None,
"lca": None,
"csv": None,
"tester": None,
"bimtester": None,
"diff": None,
"patch": None,
@@ -92,6 +93,7 @@ classes = [
operator.SelectIfcFile,
operator.SelectSchemaDir,
operator.SelectURIAttribute,
operator.EditBlenderCollection,
prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
prop.ObjProperty,
+56 -1
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
@@ -46,6 +47,11 @@ def draw_attribute(attribute, layout, copy_operator=None):
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:
@@ -78,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:
@@ -108,7 +115,7 @@ def export_attributes(props, callback=None):
return attributes
def prop_with_search(layout, data, prop_name, **kwargs):
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
@@ -118,6 +125,54 @@ def prop_with_search(layout, data, prop_name, **kwargs):
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
+44 -4
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():
@@ -68,7 +70,7 @@ class IfcStore:
IfcStore.last_transaction = ""
IfcStore.history = []
IfcStore.future = []
IfcStore.schema_identifiers = ["IFC4", "IFC2X3"]
IfcStore.schema_identifiers = ["IFC4", "IFC2X3", "IFC4X3"]
@staticmethod
def get_file():
@@ -223,7 +225,11 @@ class IfcStore:
try:
value.name
except:
obj = bpy.data.objects.get(IfcStore.undo_redo_stack_object_names[key])
# TODO not so sure about this obj_name check
obj_name = IfcStore.undo_redo_stack_object_names.get(key, None)
if not obj_name:
continue
obj = bpy.data.objects.get(obj_name)
if not obj or not obj.BIMObjectProperties.ifc_definition_id:
continue
element = file.by_id(obj.BIMObjectProperties.ifc_definition_id)
@@ -232,7 +238,6 @@ class IfcStore:
data["guid"] = element.GlobalId
IfcStore.commit_link_element(data)
@staticmethod
def relink_all_objects():
if not IfcStore.get_file():
@@ -276,6 +281,15 @@ class IfcStore:
@staticmethod
def link_element(element, obj):
existing_obj = IfcStore.id_map.get(element.id(), None)
if existing_obj == obj:
return
elif existing_obj:
try:
existing_obj.name
IfcStore.unlink_element(obj=existing_obj)
except:
pass
IfcStore.id_map[element.id()] = obj
if hasattr(element, "GlobalId"):
IfcStore.guid_map[element.GlobalId] = obj
@@ -322,6 +336,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:
@@ -357,6 +386,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)
+37 -43
View File
@@ -181,7 +181,6 @@ class IfcImporter:
self.elements = set()
self.type_collection = None
self.type_products = {}
self.openings = {}
self.meshes = {}
self.mesh_shapes = {}
self.time = 0
@@ -189,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)
@@ -206,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")
@@ -224,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()
@@ -245,8 +242,8 @@ class IfcImporter:
self.create_structural_items()
self.profile_code("Create structural items")
if not self.ifc_import_settings.is_coordinating:
self.create_type_products()
self.profile_code("Create type products")
self.create_element_types()
self.profile_code("Create element types")
self.place_objects_in_collections()
self.profile_code("Place objects in collections")
self.add_project_to_scene()
@@ -322,11 +319,21 @@ class IfcImporter:
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 and not e.is_a("IfcFeatureElement")]
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")
+ self.file.by_type("IfcDoorStyle")
+ self.file.by_type("IfcWindowStyle")
)
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:
@@ -395,6 +402,12 @@ class IfcImporter:
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):
@@ -551,8 +564,9 @@ class IfcImporter:
if grid.Representation:
shape = ifcopenshell.geom.create_shape(self.settings_2d, grid)
grid_obj = self.create_product(grid, shape)
grid_obj.lock_location = (True, True, True)
grid_obj.lock_rotation = (True, True, True)
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)
@@ -570,25 +584,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)
obj.lock_location = (True, True, True)
obj.lock_rotation = (True, True, True)
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
@@ -717,12 +726,12 @@ class IfcImporter:
self.incrementally_merge_objects()
shape = iterator.get()
if shape:
product = self.file.by_id(shape.guid)
product = self.file.by_id(shape.id)
if self.body_contexts:
self.create_product(product, shape)
results.add(product)
else:
if shape.context not in ["Body", "Facetation"] and IfcStore.get_element(shape.guid):
if shape.context not in ["Body", "Facetation"] and IfcStore.get_element(shape.id):
# We only load a single context, and we prioritise the Body context. See #1290.
pass
else:
@@ -872,7 +881,7 @@ class IfcImporter:
checkpoint = time.time()
shape = iterator.get()
if shape:
product = self.file.by_id(shape.guid)
product = self.file.by_id(shape.id)
self.create_product(product, shape)
results.add(product)
if not iterator.next():
@@ -891,7 +900,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:
@@ -921,10 +930,6 @@ 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):
@@ -1108,6 +1113,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):
@@ -1239,7 +1245,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):
@@ -1262,11 +1267,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:
@@ -1397,10 +1397,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)
@@ -1645,8 +1641,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"):
@@ -1957,7 +1951,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):
@@ -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)
@@ -69,9 +69,3 @@ class BIMBrickProperties(PropertyGroup):
libraries: EnumProperty(name="Libraries", items=get_libraries)
namespace: EnumProperty(name="Namespace", items=get_namespaces)
brick_equipment_class: EnumProperty(name="Brick Equipment Class", items=get_brick_equipment_classes)
getter_enum = {
"libraries": get_libraries,
"namespace": get_namespaces,
"brick_equipment_class": get_brick_equipment_classes,
}
@@ -17,14 +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.AlignViewToProfile,
operator.AddIfcArcIndexFillet,
operator.AddIfcCircle,
operator.AddRectangle,
operator.CadArcFrom2Points,
operator.CadArcFrom3Points,
operator.CadFillet,
operator.CadMitre,
operator.CadOffset,
operator.CadTrimExtend,
prop.BIMCadProperties,
workspace.CadHotkey,
)
@@ -32,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
@@ -22,6 +22,8 @@ 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",
@@ -40,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"}
@@ -69,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"}
@@ -86,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"}
@@ -115,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"}
@@ -204,9 +204,9 @@ 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)
mesh.update()
bm.free()
return {"FINISHED"}
@@ -255,7 +255,7 @@ class CadArcFrom2Points(bpy.types.Operator):
v = selected_verts[0]
bm.verts.remove(selected_verts[1])
axis = region_3d.view_rotation @ mathutils.Vector((0, 0, 1))
axis = region_3d.view_rotation @ Vector((0, 0, 1))
bmesh.ops.spin(
bm,
geom=[v],
@@ -297,49 +297,517 @@ class CadArcFrom3Points(bpy.types.Operator):
mesh = obj.data
bm = bmesh.from_edit_mesh(mesh)
selected_verts = [v for v in bm.verts if v.select]
if len(selected_verts) != 3:
arc = [v for v in bm.verts if v.select]
if len(arc) != 3:
return {"CANCELLED"}
pts = [v.co for v in selected_verts]
center = tool.Cad.get_center_of_arc(pts, obj)
if not center:
elif len([e for e in bm.edges if e.select]) != 2:
return {"CANCELLED"}
bpy.context.scene.cursor.location = center
if self.only_recalculate_center:
return {"FINISHED"}
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
center = obj.matrix_world.inverted() @ center
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)
def get_distance_to_other_points(vert):
other_verts = [v for v in selected_verts if v != vert]
total_vector = mathutils.Vector((0, 0, 0))
for other_vert in other_verts:
total_vector += other_vert.co - vert.co
return total_vector.length
bm.verts.index_update()
bm.edges.index_update()
# For three selected points that represent a start, end, and point on an
# arc, the start and end verts can be identified by being furthest away
# from the other 2 points.
arc_end_verts = sorted(selected_verts, key=get_distance_to_other_points)[-2:]
normal = mathutils.geometry.normal(pts)
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)]
dir1 = (arc_end_verts[0].co - center).normalized()
dir2 = (arc_end_verts[1].co - center).normalized()
bm.verts.remove(sorted_arc[1])
bmesh.ops.remove_doubles(bm, verts=new_verts + [sorted_arc[0], sorted_arc[2]], dist=1e-5)
# 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_verts[0].co - center).normalized()
y = z.cross(x)
arc_matrix = mathutils.Matrix([x, y, z]).transposed().to_4x4()
dir1 = ((arc_matrix.inverted() @ arc_end_verts[0].co) - (arc_matrix.inverted() @ center)).normalized()
dir2 = ((arc_matrix.inverted() @ arc_end_verts[1].co) - (arc_matrix.inverted() @ center)).normalized()
angle = dir1.xy.angle_signed(dir2.xy)
v = arc_end_verts[0]
bmesh.ops.spin(bm, geom=[v], axis=normal, cent=center, steps=self.resolution * 4, angle=-angle)
bmesh.update_edit_mesh(mesh)
mesh.update()
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,39 +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": []}),
# Enable Mesh Tools add-on to get this amazing tool
("mesh.offset_edges", {"type": "O", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.cad_arc_from_2_points", {"type": "C", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.cad_arc_from_3_points", {"type": "V", "value": "PRESS", "shift": True}, {"properties": []}),
("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="2-Point Arc", icon="EVENT_C")
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="3-Point Arc", icon="EVENT_V")
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):
@@ -77,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.props.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.active_cost_item_index < len(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]
@@ -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,11 +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
import blenderbim.bim.handler
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
class SelectDiffJsonFile(bpy.types.Operator):
@@ -29,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
@@ -49,17 +50,19 @@ class VisualiseDiff(bpy.types.Operator):
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)
obj.color = (1.0, 1.0, 1.0, 1.0)
element = tool.Ifc.get_entity(obj)
if not element:
continue
global_id = element.GlobalId
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"}
@@ -70,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):
@@ -85,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):
@@ -111,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)
@@ -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:
@@ -1195,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))
@@ -528,8 +528,9 @@ class SvgWriter:
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(
@@ -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"
)
@@ -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)
@@ -240,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()
@@ -342,12 +381,10 @@ class OverrideDeleteTrait:
self.remove_port(port)
def delete_opening_element(self, element):
obj = IfcStore.get_element(element.VoidsElements[0].RelatingBuildingElement.id())
bpy.ops.bim.remove_opening(opening_id=element.id(), obj=obj.name)
bpy.ops.bim.remove_opening(opening_id=element.id())
def remove_filling(self, element):
obj = IfcStore.get_element(element.id())
bpy.ops.bim.remove_filling(obj=obj.name)
bpy.ops.bim.remove_filling(filling=element.id())
def remove_port(self, port):
blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
@@ -494,6 +531,9 @@ class OverrideDuplicateMove(bpy.types.Operator):
def _execute(self, context):
self.new_active_obj = None
# Track decompositions so they can be recreated after the operation
relationships = tool.Root.get_decomposition_relationships(context.selected_objects)
old_to_new = {}
for obj in context.selected_objects:
new_obj = obj.copy()
if obj.data:
@@ -504,8 +544,12 @@ class OverrideDuplicateMove(bpy.types.Operator):
collection.objects.link(new_obj)
obj.select_set(False)
new_obj.select_set(True)
# This is the only difference
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
# Copy the actual class
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
if new:
old_to_new[tool.Ifc.get_entity(obj)] = new
# Recreate decompositions
tool.Root.recreate_decompositions(relationships, old_to_new)
bpy.ops.transform.translate("INVOKE_DEFAULT")
blenderbim.bim.handler.purge_module_data()
@@ -542,6 +586,9 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
def _execute(self, context):
self.new_active_obj = None
# Track decompositions so they can be recreated after the operation
relationships = tool.Root.get_decomposition_relationships(context.selected_objects)
old_to_new = {}
for obj in context.selected_objects:
new_obj = obj.copy()
if obj == context.active_object:
@@ -550,8 +597,12 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
collection.objects.link(new_obj)
obj.select_set(False)
new_obj.select_set(True)
# This is the only difference
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
# Copy the actual class
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
if new:
old_to_new[tool.Ifc.get_entity(obj)] = new
# Recreate decompositions
tool.Root.recreate_decompositions(relationships, old_to_new)
bpy.ops.transform.translate("INVOKE_DEFAULT")
blenderbim.bim.handler.purge_module_data()
return {"FINISHED"}
@@ -20,7 +20,7 @@ import bpy
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import prop_with_search
from blenderbim.bim.module.geometry.data import RepresentationsData, DerivedPlacementsData
from blenderbim.bim.module.geometry.data import RepresentationsData, ConnectionsData, DerivedPlacementsData
class BIM_PT_representations(Panel):
@@ -68,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"
@@ -90,18 +127,6 @@ class BIM_PT_mesh(Panel):
layout = self.layout
props = context.active_object.data.BIMMeshProperties
row = layout.row(align=True)
op = row.operator("bim.switch_representation", text="Bake Voids", icon="SELECT_SUBTRACT")
op.should_switch_all_meshes = True
op.should_reload = True
op.ifc_definition_id = props.ifc_definition_id
op.disable_opening_subtractions = False
op = row.operator("bim.switch_representation", text="Dynamic Voids", icon="SELECT_INTERSECT")
op.should_switch_all_meshes = True
op.should_reload = True
op.ifc_definition_id = props.ifc_definition_id
op.disable_opening_subtractions = True
row = layout.row()
row.operator("bim.copy_representation")
@@ -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
@@ -137,5 +137,3 @@ class BIMObjectMaterialProperties(PropertyGroup):
parameterized_profile_classes: EnumProperty(
items=getParameterizedProfileClasses, name="Parameterized Profile Classes"
)
getter_enum = {"material": get_materials}
@@ -51,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
@@ -189,7 +189,7 @@ 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":
prop_with_search(row, self.props, "material", text="")
row.operator("bim.assign_material", icon="ADD", text="")
@@ -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,61 @@
# 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.AddConstrTypeInstance,
product.AddEmptyType,
product.AddTypeInstance,
product.AlignProduct,
product.ChangeTypePage,
product.DisplayConstrTypes,
product.DynamicallyVoidProduct,
product.LoadTypeThumbnails,
product.ReinvokeOperator,
workspace.Hotkey,
wall.JoinWall,
wall.AlignWall,
wall.ChangeExtrusionDepth,
wall.ChangeLayerLength,
wall.FlipWall,
wall.JoinWall,
wall.MergeWall,
wall.RecalculateWall,
wall.SplitWall,
opening.AddElementOpening,
opening.AddBoolean,
opening.AddFilledOpening,
opening.AddPotentialHalfSpaceSolid,
opening.AddPotentialOpening,
opening.EditOpenings,
opening.HideBooleans,
opening.HideOpenings,
opening.RecalculateFill,
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.LaunchTypeManager,
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 +88,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 +105,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:
@@ -17,7 +17,12 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import math
import functools
import ifcopenshell
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
def refresh():
@@ -26,15 +31,130 @@ def refresh():
class AuthoringData:
data = {}
type_thumbnails = {}
types_per_page = 9
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {
"ifc_classes": cls.ifc_classes(),
"relating_types": cls.relating_types(),
}
cls.props = bpy.context.scene.BIMModelProperties
cls.load_ifc_classes()
cls.load_relating_types()
cls.load_relating_types_browser()
cls.data["type_class"] = cls.type_class()
cls.data["type_predefined_type"] = cls.type_predefined_type()
cls.data["total_types"] = cls.total_types()
cls.data["total_pages"] = cls.total_pages()
cls.data["next_page"] = cls.next_page()
cls.data["prev_page"] = cls.prev_page()
cls.data["paginated_relating_types"] = cls.paginated_relating_types()
cls.data["type_thumbnail"] = cls.type_thumbnail()
cls.data["is_voidable_element"] = cls.is_voidable_element()
cls.data["has_visible_openings"] = cls.has_visible_openings()
cls.data["active_class"] = cls.active_class()
@classmethod
def type_class(cls):
declaration = tool.Ifc.schema().declaration_by_name("IfcElementType")
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
names = [d.name() for d in declarations]
names.extend(("IfcDoorStyle", "IfcWindowStyle"))
return [(c, c, "") for c in sorted(names)]
@classmethod
def type_predefined_type(cls):
results = []
declaration = tool.Ifc().schema().declaration_by_name(cls.props.type_class)
for attribute in declaration.attributes():
if attribute.name() == "PredefinedType":
results.extend([(e, e, "") for e in attribute.type_of_attribute().declared_type().enumeration_items()])
break
return results
@classmethod
def type_thumbnail(cls):
if not cls.props.relating_type_id:
return 0
element = tool.Ifc.get().by_id(int(cls.props.relating_type_id))
return cls.type_thumbnails.get(element.id(), None) or 0
@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 total_types(cls):
type_class = cls.props.type_class
return len(tool.Ifc.get().by_type(type_class)) if type_class else 0
@classmethod
def total_pages(cls):
type_class = cls.props.type_class
total_types = len(tool.Ifc.get().by_type(type_class)) if type_class else 0
return math.ceil(total_types / cls.types_per_page)
@classmethod
def next_page(cls):
if cls.props.type_page < cls.total_pages():
return cls.props.type_page + 1
@classmethod
def prev_page(cls):
if cls.props.type_page > 1:
return cls.props.type_page - 1
@classmethod
def paginated_relating_types(cls):
type_class = cls.props.type_class
if not type_class:
return []
results = []
elements = sorted(tool.Ifc.get().by_type(type_class), key=lambda e: e.Name or "Unnamed")
elements = elements[(cls.props.type_page - 1) * cls.types_per_page : cls.props.type_page * cls.types_per_page]
for element in elements:
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
if predefined_type == "NOTDEFINED":
predefined_type = None
results.append(
{
"id": element.id(),
"ifc_class": element.is_a(),
"name": element.Name or "Unnamed",
"description": element.Description or "No Description",
"predefined_type": predefined_type,
"icon_id": cls.type_thumbnails.get(element.id(), None) or 0,
}
)
return results
@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 active_class(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
return element.is_a()
@classmethod
def ifc_classes(cls):
@@ -49,16 +169,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
@@ -55,15 +55,15 @@ class MepGenerator:
self.height = dimensions.get("NominalHeight")
self.length = 1
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((-self.width / 2, self.height / 2, 0)),
Vector((-self.width / 2, -self.height / 2, 0)),
@@ -89,10 +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[0] = math.pi / 2
obj.rotation_euler[2] = math.pi / 2
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,
@@ -114,5 +115,13 @@ class MepGenerator:
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)
obj.select_set(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,134 +17,109 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import gpu
import bgl
import bmesh
import logging
import blenderbim.bim.handler
import ifcopenshell
import ifcopenshell.util.representation
import blenderbim.tool as tool
import blenderbim.core.geometry
import blenderbim.bim.import_ifc as import_ifc
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
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
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()
class AddElementOpening(bpy.types.Operator):
bl_idname = "bim.add_element_opening"
bl_label = "Add Element Opening"
class AddFilledOpening(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_filled_opening"
bl_label = "Add Filled Opening"
bl_options = {"REGISTER", "UNDO"}
voided_building_element: bpy.props.StringProperty()
filling_building_element: bpy.props.StringProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
voided_obj: bpy.props.StringProperty()
filling_obj: bpy.props.StringProperty()
def _execute(self, context):
voided_obj = self.get_voided_building_element(context)
filling_obj = self.get_filling_building_element(context)
props = context.scene.BIMModelProperties
if not voided_obj:
voided_obj = bpy.data.objects.get(self.voided_obj)
filling_obj = bpy.data.objects.get(self.filling_obj)
filling = tool.Ifc.get_entity(filling_obj)
if not voided_obj or not filling_obj:
return {"FINISHED"}
element = IfcStore.get_file().by_id(voided_obj.BIMObjectProperties.ifc_definition_id)
local_location = voided_obj.matrix_world.inverted() @ context.scene.cursor.location
raycast = voided_obj.closest_point_on_mesh(local_location, distance=0.01)
element = tool.Ifc.get_entity(voided_obj)
target = context.scene.cursor.location
raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.01)
if not raycast[0]:
return {"FINISHED"}
target = filling_obj.matrix_world.col[3].to_3d().copy()
raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.5)
if not raycast[0]:
return {"FINISHED"}
if filling_obj:
opening = self.generate_opening_from_filling(filling_obj, voided_obj)
else:
# The opening shall be based on the smallest bounding dimension of the element
dimension = min(voided_obj.dimensions)
bpy.ops.mesh.primitive_cube_add(size=dimension * 2)
opening = context.selected_objects[0]
axis = [voided_obj.matrix_world @ Vector((0, 0, 0)), voided_obj.matrix_world @ Vector((1, 0, 0))]
# Place the opening in the middle of the element
global_location = voided_obj.matrix_world @ raycast[1]
normal = raycast[2]
normal.negate()
global_normal = voided_obj.matrix_world.to_quaternion() @ normal
opening.location = global_location + (global_normal * (dimension / 2))
opening.rotation_euler = voided_obj.rotation_euler
opening.name = "Opening"
# In this prototype, we assume openings are only added to axis-based elements
new_matrix = voided_obj.matrix_world.copy()
new_matrix.col[3] = tool.Cad.point_on_edge(target, axis).to_4d()
if not filling.is_a("IfcDoor"):
container = ifcopenshell.util.element.get_container(element)
if container:
container_obj = tool.Ifc.get_object(container)
if container_obj:
new_matrix[2][3] = container_obj.matrix_world[2][3] + props.rl
filling_obj.matrix_world = new_matrix
bpy.context.view_layer.update()
bpy.ops.bim.add_opening(opening=opening.name, obj=voided_obj.name)
if filling_obj:
bpy.ops.bim.add_filling(opening=opening.name, obj=filling_obj.name)
opening_obj = self.generate_opening_from_filling(filling_obj, voided_obj)
# Still prototyping, for now duplicating code from bpy.ops.bim.add_opening
if tool.Ifc.is_moved(voided_obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=voided_obj)
has_visible_openings = False
for opening in [r.RelatedOpeningElement for r in element.HasOpenings]:
if tool.Ifc.get_object(opening):
has_visible_openings = True
break
body_context = ifcopenshell.util.representation.get_context(IfcStore.get_file(), "Model", "Body")
opening = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=opening_obj,
ifc_class="IfcOpeningElement",
should_add_representation=True,
context=body_context,
)
ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=opening, element=element)
representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id)
blenderbim.core.geometry.switch_representation(
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
enable_dynamic_voids=False,
is_global=True,
should_sync_changes_first=False,
)
bpy.ops.bim.add_filling(opening=opening_obj.name, obj=filling_obj.name)
if not has_visible_openings:
tool.Ifc.unlink(obj=opening_obj)
bpy.data.objects.remove(opening_obj)
return {"FINISHED"}
def get_voided_building_element(self, context):
obj = None
if self.voided_building_element:
obj = bpy.data.objects.get(self.voided_building_element)
else:
total_selected = len(context.selected_objects)
if total_selected == 1 or total_selected == 2:
obj = context.active_object
if obj and obj.BIMObjectProperties.ifc_definition_id:
return obj
def get_filling_building_element(self, context):
obj = None
if self.filling_building_element:
obj = bpy.data.objects.get(self.filling_building_element)
else:
total_selected = len(context.selected_objects)
if total_selected == 2:
if context.selected_objects[0] == context.active_object:
obj = context.selected_objects[1]
else:
obj = context.selected_objects[0]
if obj and obj.BIMObjectProperties.ifc_definition_id:
return obj
def generate_opening_from_filling(self, filling_obj, voided_obj):
x, y, z = filling_obj.dimensions
dimension = min(voided_obj.dimensions)
@@ -194,38 +169,525 @@ class AddElementOpening(bpy.types.Operator):
mesh.from_pydata(verts, edges, faces)
obj = bpy.data.objects.new("Opening", mesh)
obj.matrix_world = filling_obj.matrix_world
filling_obj.rotation_euler = voided_obj.rotation_euler
obj.parent = filling_obj
obj.matrix_parent_inverse = filling_obj.matrix_world.inverted()
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 RecalculateFill(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.recalculate_fill"
bl_label = "Recalculate Fill"
bl_options = {"REGISTER", "UNDO"}
size: FloatProperty(name="Size", default=2)
@classmethod
def poll(cls, context):
return context.selected_objects
def execute(self, context):
add_object(self, context)
def _execute(self, context):
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.FillsVoids:
continue
openings = [r.RelatingOpeningElement for r in element.FillsVoids or []]
building_elements = []
for opening in openings:
building_elements.extend([r.RelatingBuildingElement for r in opening.VoidsElements or []])
for building_element in building_elements:
building_obj = tool.Ifc.get_object(building_element)
if tool.Ifc.is_moved(building_obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=building_obj)
for opening in openings:
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
ifcopenshell.api.run(
"geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=obj.matrix_world
)
for building_element in building_elements:
building_obj = tool.Ifc.get_object(building_element)
body = ifcopenshell.util.representation.get_representation(
building_element, "Model", "Body", "MODEL_VIEW"
)
if body:
blenderbim.core.geometry.switch_representation(
tool.Geometry,
obj=building_obj,
representation=body,
should_reload=True,
enable_dynamic_voids=False,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
def add_object_button(self, context):
self.layout.operator(BIM_OT_add_object.bl_idname, icon="PLUGIN")
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)
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):
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()
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"}
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)
if not obj2.data or len(obj2.data.polygons) <= 4: # It takes 4 faces to create a closed solid
mesh_data = {"type": "IfcHalfSpaceSolid", "matrix": obj1.matrix_world.inverted() @ obj2.matrix_world}
elif obj2.data:
mesh_data = {"type": "Mesh", "blender_obj": obj1, "blender_void": obj2}
ifcopenshell.api.run(
"geometry.add_boolean",
tool.Ifc.get(),
representation=representation,
operator="DIFFERENCE",
**mesh_data
)
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))
props = bpy.context.scene.BIMModelProperties
tool.Model.clear_scene_openings()
for boolean in booleans:
boolean_obj = None
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
else:
settings = ifcopenshell.geom.settings()
logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
shape = ifcopenshell.geom.create_shape(settings, boolean)
if shape:
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
ifc_importer.file = tool.Ifc.get()
mesh = ifc_importer.create_mesh(boolean, shape)
else:
mesh = None
boolean_obj = object_data_add(bpy.context, mesh, operator=self)
boolean_obj.name = "BooleanMesh"
boolean_obj.matrix_world = obj.matrix_world
if boolean_obj:
boolean_obj.data.BIMMeshProperties.ifc_boolean_id = boolean.id()
boolean_obj.data.BIMMeshProperties.obj = obj
new = props.openings.add()
new.obj = boolean_obj
if booleans:
DecorationsHandler.install(bpy.context)
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):
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"
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 and 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:
if tool.Ifc.is_edited(opening_obj):
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
elif tool.Ifc.is_moved(opening_obj):
blenderbim.core.geometry.edit_object_placement(
tool.Ifc, tool.Geometry, tool.Surveyor, 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,6 +18,7 @@
import bpy
import mathutils
from functools import reduce
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.system
@@ -29,9 +30,18 @@ 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
def select_and_activate_single_object(context, obj):
bpy.ops.object.select_all(action="DESELECT")
context.view_layer.objects.active = obj
obj.select_set(True)
class AddEmptyType(bpy.types.Operator, AddObjectHelper):
@@ -43,9 +53,7 @@ class AddEmptyType(bpy.types.Operator, AddObjectHelper):
obj = bpy.data.objects.new("TYPEX", None)
context.scene.collection.objects.link(obj)
context.scene.BIMRootProperties.ifc_product = "IfcElementType"
bpy.ops.object.select_all(action="DESELECT")
context.view_layer.objects.active = obj
obj.select_set(True)
select_and_activate_single_object(context, obj)
return {"FINISHED"}
@@ -53,13 +61,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)
@@ -67,29 +80,35 @@ 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):
select_and_activate_single_object(context, context.selected_objects[-1])
return {"FINISHED"}
if relating_type.is_a("IfcFlowSegmentType") and not relating_type.RepresentationMaps:
if mep.MepGenerator(relating_type).generate():
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 = [
@@ -114,23 +133,27 @@ 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)
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
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":
obj.location[2] = building_obj.location[2] - min([v[2] for v in obj.bound_box])
else:
bpy.ops.bim.add_filled_opening(voided_obj=building_obj.name, filling_obj=obj.name)
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])
@@ -145,12 +168,11 @@ class AddTypeInstance(bpy.types.Operator):
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
select_and_activate_single_object(context, 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")
@@ -163,15 +185,86 @@ 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 ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_type_page"
bl_label = "Change Type Page"
bl_options = {"REGISTER"}
page: bpy.props.IntProperty()
def _execute(self, context):
context.scene.BIMModelProperties.type_page = self.page
return {"FINISHED"}
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"
@@ -257,6 +350,123 @@ class DynamicallyVoidProduct(bpy.types.Operator):
return {"FINISHED"}
class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.load_type_thumbnails"
bl_label = "Load Type Thumbnails"
bl_options = {"REGISTER", "UNDO"}
ifc_class: bpy.props.StringProperty()
def _execute(self, context):
from PIL import Image, ImageDraw
processing = set()
# Only process at most one class at a time.
# Large projects have hundreds of types which can lead to unnecessary lag.
queue = tool.Ifc.get().by_type(self.ifc_class)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
while queue:
# if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation
element = queue.pop()
obj = tool.Ifc.get_object(element)
if not obj:
continue # Nothing to process
elif AuthoringData.type_thumbnails.get(element.id(), None):
continue # Already processed
elif obj.preview and obj.preview.icon_id:
AuthoringData.type_thumbnails[element.id()] = obj.preview.icon_id
continue
if obj.data:
obj.asset_generate_preview()
while not obj.preview:
pass
else:
size = 128
img = Image.new("RGBA", (size, size))
draw = ImageDraw.Draw(img)
material = ifcopenshell.util.element.get_material(element)
if material and material.is_a("IfcMaterialProfileSet"):
profile = material.MaterialProfiles[0].Profile
settings = ifcopenshell.geom.settings()
settings.set(settings.INCLUDE_CURVES, True)
shape = ifcopenshell.geom.create_shape(settings, profile)
verts = shape.verts
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1]] for i in range(0, len(verts), 3)]
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
max_x = max([v[0] for v in grouped_verts])
min_x = min([v[0] for v in grouped_verts])
max_y = max([v[1] for v in grouped_verts])
min_y = min([v[1] for v in grouped_verts])
dim_x = max_x - min_x
dim_y = max_y - min_y
max_dim = max([dim_x, dim_y])
scale = 100 / max_dim
for vert in grouped_verts:
vert[0] = round(scale * (vert[0] - min_x)) + ((size / 2) - scale * (dim_x / 2))
vert[1] = round(scale * (vert[1] - min_y)) + ((size / 2) - scale * (dim_y / 2))
for e in grouped_edges:
draw.line((tuple(grouped_verts[e[0]]), tuple(grouped_verts[e[1]])), fill="white", width=2)
elif material and material.is_a("IfcMaterialLayerSet"):
thicknesses = [l.LayerThickness for l in material.MaterialLayers]
total_thickness = sum(thicknesses)
si_total_thickness = total_thickness * unit_scale
if si_total_thickness <= 0.051:
width = 10
elif si_total_thickness <= 0.11:
width = 20
elif si_total_thickness <= 0.21:
width = 30
elif si_total_thickness <= 0.31:
width = 40
else:
width = 50
height = 100
if element.is_a("IfcSlabType"):
width, height = height, width
x_offset = (size / 2) - (width / 2)
y_offset = (size / 2) - (height / 2)
draw.rectangle([x_offset, y_offset, width + x_offset, height + y_offset], outline="white", width=2)
current_thickness = 0
del thicknesses[-1]
for thickness in thicknesses:
current_thickness += thickness
if element.is_a("IfcSlabType"):
y = (current_thickness / total_thickness) * height
line = [x_offset, y_offset + y, x_offset + width, y_offset + y]
else:
x = (current_thickness / total_thickness) * width
line = [x_offset + x, y_offset, x_offset + x, y_offset + height]
draw.line(line, fill="white", width=2)
else:
# For things like parametric duct segments
draw.line([0, 0, size, size], fill="red", width=2)
draw.line([0, size, size, 0], fill="red", width=2)
pixels = [item for sublist in img.getdata() for item in sublist]
obj.asset_generate_preview()
while not obj.preview:
pass
obj.preview.image_size = size, size
obj.preview.image_pixels_float = pixels
queue.append(element)
return {"FINISHED"}
def generate_box(usecase_path, ifc_file, settings):
box_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Box", "MODEL_VIEW")
if not box_context:
@@ -340,10 +550,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,118 @@ 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:
obj.location[2] = self.collection_obj.location[2]
self.collection.objects.link(obj)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=ifc_class, should_add_representation=False)
matrix_world = Matrix()
if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"]:
obj.rotation_euler[0] = math.pi / 2
obj.rotation_euler[2] = math.pi / 2
matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
matrix_world.col[3] = self.location.to_4d()
if link_to_scene and self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
matrix_world[2][3] = self.collection_obj.location[2]
if link_to_scene:
self.collection.objects.link(obj)
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
obj.matrix_world = matrix_world
bpy.context.view_layer.update()
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
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,
)
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)
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 +229,673 @@ 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().copy()
new_matrix.invert()
for clipping in self.clippings:
if clipping["operand_type"] == "IfcHalfSpaceSolid":
clipping["matrix"] = new_matrix @ clipping["matrix"]
self.clippings.extend(tool.Model.get_manual_booleans(element))
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
previous_matrix = obj.matrix_world.copy()
previous_origin = obj.location.copy()
obj.location[0], obj.location[1], obj.location[2] = self.body[0]
bpy.context.view_layer.update()
if tool.Ifc.is_moved(obj):
# Openings should move with the host overall ...
# ... except their position should stay the same along the local Z axis of the wall
for opening in [r.RelatedOpeningElement for r in element.HasOpenings]:
percent = tool.Cad.edge_percent(
self.body[0], (previous_origin, (previous_matrix @ Vector((0, 0, 1))))
)
is_z_offset_increased = True if percent < 0 else False
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]}
change_in_z = (self.body[0] - previous_origin).length / self.unit_scale
coordinates = list(opening.ObjectPlacement.RelativePlacement.Location.Coordinates)
if is_z_offset_increased:
coordinates[2] += change_in_z
else:
coordinates[2] -= change_in_z
opening.ObjectPlacement.RelativePlacement.Location.Coordinates = coordinates
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)
return final
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)
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 = 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 = 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(
{
"type": "IfcBooleanClippingResult",
"operand_type": "IfcHalfSpaceSolid",
"matrix": 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(
{
"type": "IfcBooleanClippingResult",
"operand_type": "IfcHalfSpaceSolid",
"matrix": 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(
{
"type": "IfcBooleanClippingResult",
"operand_type": "IfcHalfSpaceSolid",
"matrix": 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(
{
"type": "IfcBooleanClippingResult",
"operand_type": "IfcHalfSpaceSolid",
"matrix": 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):
@@ -40,21 +29,217 @@ def get_ifc_class(self, context):
return AuthoringData.data["ifc_classes"]
def get_type_class(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
return AuthoringData.data["type_class"]
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 get_type_predefined_type(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
return AuthoringData.data["type_predefined_type"]
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
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
AuthoringData.data["relating_types_ids"] = AuthoringData.relating_types()
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
def update_type_class(self, context):
AuthoringData.data["total_types"] = AuthoringData.total_types()
AuthoringData.data["total_pages"] = AuthoringData.total_pages()
AuthoringData.data["prev_page"] = AuthoringData.prev_page()
AuthoringData.data["next_page"] = AuthoringData.next_page()
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
AuthoringData.data["type_predefined_type"] = AuthoringData.type_predefined_type()
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()
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
def update_type_page(self, context):
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
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)
rl: bpy.props.FloatProperty(name="RL", default=1)
type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
type_template: bpy.props.EnumProperty(
items=(
("1", "Custom Mesh", ""),
("2", "Vertical Layers", ""),
("2", "Horizontal Layers", ""),
("3", "Extruded Profile", ""),
("4", "Non-Geometric Type", ""),
),
name="Type Template",
default="1",
)
type_class: bpy.props.EnumProperty(items=get_type_class, name="IFC Class", update=update_type_class)
type_predefined_type: bpy.props.EnumProperty(items=get_type_predefined_type, name="Predefined Type", default=None)
@@ -48,3 +48,8 @@ def sync_name(usecase_path, ifc_file, settings):
collection.name = new_name
obj.name = new_name
blenderbim.bim.handler.refresh_ui_data()
class ConstrTypeEntityNotFound(Exception):
pass
File diff suppressed because it is too large Load Diff
+235 -48
View File
@@ -16,65 +16,252 @@
# 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.module.type.prop as type_prop
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
import bpy
from bpy.types import Panel, Operator, Menu
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.prop import store_cursor_position
from blenderbim.bim.helper import prop_with_search, close_operator_panel
class LaunchTypeManager(bpy.types.Operator):
bl_idname = "bim.launch_type_manager"
bl_label = "Launch Type Manager"
bl_options = {"REGISTER"}
bl_description = "Display all available Construction Types to add new instances"
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
props = context.scene.BIMModelProperties
props.type_page = 1
if props.ifc_class:
props.type_class = props.ifc_class
bpy.ops.bim.load_type_thumbnails(ifc_class=props.ifc_class)
if not AuthoringData.is_loaded:
AuthoringData.load()
return context.window_manager.invoke_popup(self, width=550)
def draw(self, context):
props = context.scene.BIMModelProperties
row = self.layout.row()
row.prop(props, "type_class", text="")
columns = self.layout.column_flow(columns=3)
row = columns.row()
row.alignment = "LEFT"
row.label(text=f"{AuthoringData.data['total_types']} Types", icon="FILE_VOLUME")
row = columns.row(align=True)
row.alignment = "CENTER"
row.prop(props, "type_predefined_type", text="")
row.prop(props, "type_template", text="")
op = row.operator("bim.change_type_page", icon="ADD", text="")
row = columns.row(align=True)
row.alignment = "RIGHT"
if AuthoringData.data["total_pages"] > 1:
row.label(text=f"Page {props.type_page}/{AuthoringData.data['total_pages']} ")
if AuthoringData.data["prev_page"]:
op = row.operator("bim.change_type_page", icon="TRIA_LEFT", text="")
op.page = AuthoringData.data["prev_page"]
if AuthoringData.data["next_page"]:
op = row.operator("bim.change_type_page", icon="TRIA_RIGHT", text="")
op.page = AuthoringData.data["next_page"]
flow = self.layout.grid_flow(row_major=True, columns=3, even_columns=True, even_rows=True, align=True)
for relating_type in AuthoringData.data["paginated_relating_types"]:
outer_col = flow.column()
box = outer_col.box()
row = box.row()
row.alignment = "CENTER"
row.label(text=relating_type["name"], icon="FILE_3D")
row = box.row()
row.alignment = "CENTER"
row.label(text=relating_type["description"])
row = box.row()
if relating_type["icon_id"]:
row.template_icon(icon_value=relating_type["icon_id"], scale=4)
else:
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
op.ifc_class = props.type_class
row = box.row(align=True)
text = f"Add {relating_type['predefined_type']}" if relating_type["predefined_type"] else "Add"
op = row.operator("bim.add_constr_type_instance", icon="ADD", text=text)
op.from_invoke = True
op.ifc_class = relating_type["ifc_class"]
op.relating_type_id = relating_type["id"]
op = row.operator("bim.duplicate_type", icon="COPYDOWN", text="")
op.element = relating_type["id"]
op = row.operator("bim.remove_type", icon="X", text="")
op.element = relating_type["id"]
class BIM_PT_authoring(Panel):
bl_idname = "BIM_PT_authoring"
bl_label = "Authoring"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
@classmethod
def poll(cls, context):
return IfcStore.get_file()
def draw(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
props = context.scene.BIMModelProperties
col = self.layout.column(align=True)
enabled = True
if AuthoringData.data["ifc_classes"]:
col.prop(props, "ifc_class", text="", icon="FILE_VOLUME")
else:
col.label(text="No IFC Class", icon="FILE_VOLUME")
enabled = False
if AuthoringData.data["relating_types"]:
col.prop(props, "relating_type", text="", icon="FILE_3D")
else:
col.label(text="No Relating Type", icon="FILE_3D")
enabled = False
row = col.row()
row.operator("bim.add_type_instance", icon="ADD")
row.enabled = enabled
class BIM_PT_authoring_architectural(Panel):
bl_label = "Architectural"
bl_idname = "BIM_PT_authoring_architectural"
bl_options = {"DEFAULT_CLOSED"}
bl_idname = "BIM_PT_authoring"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
bl_parent_id = "BIM_PT_authoring"
def draw(self, context):
row = self.layout.row(align=True)
row.operator("bim.join_wall", icon="MOD_SKIN", text="T").join_type = "T"
row.operator("bim.join_wall", icon="MOD_SKIN", text="L").join_type = "L"
row.operator("bim.join_wall", icon="MOD_SKIN", text="V").join_type = "V"
row.operator("bim.join_wall", icon="X", text="").join_type = ""
row = self.layout.row(align=True)
row.operator("bim.align_wall", icon="ANCHOR_TOP", text="Ext.").align_type = "EXTERIOR"
row.operator("bim.align_wall", icon="ANCHOR_CENTER", text="C/L").align_type = "CENTERLINE"
row.operator("bim.align_wall", icon="ANCHOR_BOTTOM", text="Int.").align_type = "INTERIOR"
row = self.layout.row(align=True)
row.operator("bim.flip_wall", icon="ORIENTATION_NORMAL", text="Flip")
row.operator("bim.split_wall", icon="MOD_PHYSICS", text="Split")
class DisplayConstrTypesUI(Operator):
bl_idname = "bim.display_constr_types_ui"
bl_label = "Browse Construction Types"
bl_options = {"REGISTER"}
bl_description = "Display all available Construction Types to add new instances"
reinvoked: bpy.props.BoolProperty(default=False)
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
store_cursor_position(context, event, window=not self.reinvoked)
return context.window_manager.invoke_popup(self, width=550)
def reinvoke(self, context):
browser_state = context.scene.BIMModelProperties.constr_browser_state
def set_updating_transaction():
browser_state.updating = True
def run_operator():
bpy.ops.bim.reinvoke_operator("INVOKE_DEFAULT", operator="bim.display_constr_types_ui")
browser_state.updating = False
if not browser_state.updating:
bpy.app.timers.register(set_updating_transaction)
bpy.app.timers.register(run_operator, first_interval=browser_state.update_delay)
def draw(self, context):
props = context.scene.BIMModelProperties
if AuthoringData.data["ifc_classes"]:
row = self.layout.row()
row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, props, "ifc_class_browser", text="")
ifc_class = props.ifc_class_browser
num_cols = 3
self.layout.row().separator(factor=0.25)
flow = self.layout.grid_flow(row_major=True, columns=num_cols, even_columns=True, even_rows=True, align=True)
relating_types = AuthoringData.relating_types_browser()
num_types = len(relating_types)
for idx, (relating_type_id, name, desc) in enumerate(relating_types):
outer_col = flow.column()
box = outer_col.box()
row = box.row()
row.label(text=name, icon="FILE_3D")
row.alignment = "CENTER"
row = box.row()
if ifc_class in props.constr_classes:
constr_class_info = props.constr_classes[ifc_class]
constr_types_info = constr_class_info.constr_types
if relating_type_id in constr_types_info:
icon_id = constr_types_info[relating_type_id].icon_id
row.template_icon(icon_value=icon_id, scale=6.0)
else:
self.reinvoke(context)
return
row = box.row()
op = row.operator("bim.add_constr_type_instance", icon="ADD")
op.from_invoke = True
op.ifc_class = ifc_class
if relating_type_id.isnumeric():
op.relating_type_id = int(relating_type_id)
last_row_cols = num_types % num_cols
if last_row_cols != 0:
for _ in range(num_cols - last_row_cols):
flow.column()
row = self.layout.row()
row.alignment = "RIGHT"
row.operator("bim.help_relating_types", text="", icon="QUESTION")
class HelpConstrTypes(Operator):
bl_idname = "bim.help_relating_types"
bl_label = "Construction Types Help"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Click to read some contextual help"
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=510)
def draw(self, context):
layout = self.layout
layout.row().separator(factor=0.5)
row = layout.row()
row.alignment = "CENTER"
row.label(text="BlenderBIM Help", icon="BLENDER")
layout.row().separator(factor=0.5)
row = layout.row().row()
row.label(text="Overview:", icon="KEYTYPE_MOVING_HOLD_VEC")
self.draw_lines(layout, self.message_summary)
layout.row().separator()
row = layout.row().row()
row.label(text="Further support:", icon="KEYTYPE_MOVING_HOLD_VEC")
layout.row().separator(factor=0.5)
row = layout.row()
op = row.operator("bim.open_upstream", text="Homepage", icon="HOME")
op.page = "home"
op = row.operator("bim.open_upstream", text="Docs", icon="DOCUMENTS")
op.page = "docs"
op = row.operator("bim.open_upstream", text="Wiki", icon="CURRENT_FILE")
op.page = "wiki"
op = row.operator("bim.open_upstream", text="Community", icon="COMMUNITY")
op.page = "community"
layout.row().separator()
def draw_lines(self, layout, lines):
box = layout.box()
for line in lines:
row = box.row()
row.label(text=f" {line}")
@property
def message_summary(self):
return [
"The Construction Type Browser allows to preview and add new instances to the model.",
"For further support, please click on the Documentation link below.",
]
class BIM_MT_model(Menu):
bl_idname = "BIM_MT_model"
bl_label = "IFC Objects"
def draw(self, context):
layout = self.layout
layout.operator("bim.add_empty_type", text="Empty Type", icon="EMPTY_AXIS")
layout.operator("bim.add_potential_half_space_solid", text="Half Space Proxy", icon="ORIENTATION_NORMAL")
layout.operator("bim.add_potential_opening", text="Opening Proxy", icon="CUBE")
def add_menu(self, context):
self.layout.menu("BIM_MT_model", icon="FILE_3D")
File diff suppressed because it is too large Load Diff
@@ -18,10 +18,12 @@
import os
import bpy
import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
from blenderbim.bim.helper import prop_with_search, close_operator_panel
from bpy.types import WorkSpaceTool
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model import prop
class BimTool(WorkSpaceTool):
@@ -38,76 +40,230 @@ class BimTool(WorkSpaceTool):
# ("bim.wall_tool_op", {"type": 'MOUSEMOVE', "value": 'ANY'}, {"properties": []}),
# ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}),
("bim.hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
("bim.join_wall", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("join_type", "L")]}),
("bim.join_wall", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("join_type", "V")]}),
("bim.flip_wall", {"type": "F", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.split_wall", {"type": "S", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
("bim.hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
("bim.hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
("bim.hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
("bim.hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
("bim.hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
("bim.hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
("bim.hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}),
("bim.hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}),
("bim.hotkey", {"type": "S", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_S")]}),
("bim.hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
("bim.hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
("bim.hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
("bim.hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}),
("bim.hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}),
("bim.hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}),
)
def draw_settings(context, layout, tool):
if not AuthoringData.is_loaded and IfcStore.get_file():
AuthoringData.load()
def draw_settings(context, layout, something):
props = context.scene.BIMModelProperties
is_tool_header = context.region.type == "TOOL_HEADER"
is_sidebar = context.region.type == "UI"
row = layout.row(align=True)
if not IfcStore.get_file():
if not tool.Ifc.get():
row.label(text="No IFC Project", icon="ERROR")
return
props = context.scene.BIMModelProperties
if AuthoringData.data["ifc_classes"]:
row.prop(props, "ifc_class", text="")
else:
row.label(text="No IFC Class")
if AuthoringData.data["relating_types"]:
row.prop(props, "relating_type", text="")
else:
row.label(text="No Relating Type")
row.label(text="", icon="BLANK1")
if not AuthoringData.is_loaded:
AuthoringData.load()
ifc_classes = AuthoringData.data["ifc_classes"] if "ifc_classes" in AuthoringData.data else False
relating_types_ids = (
AuthoringData.data["relating_types_ids"] if "relating_types_ids" in AuthoringData.data else False
)
#if ifc_classes and relating_types_ids and not props.icon_id and not is_tool_header:
# # hack Dion won't like to show a preview also on the first time the sidebar is shown
# bpy.app.timers.register(lambda: prop.update_ifc_class(props, "lost_context"))
# bpy.app.timers.register(lambda: prop.update_relating_type(props, "lost_context"))
ifc_class = props.ifc_class
relating_type_id = props.relating_type_id
if is_tool_header:
row = layout.row(align=True)
if ifc_classes:
row.label(text="", icon="FILE_VOLUME")
row.prop(data=props, property="ifc_class", text="")
else:
row.label(text="No Construction Class", icon="FILE_VOLUME")
row = layout.row(align=True)
if relating_types_ids:
row.label(text="", icon="FILE_3D")
prop_with_search(row, props, "relating_type_id", text="")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="")
else:
row.label(text="No Construction Type", icon="FILE_3D")
if ifc_classes:
#row = layout.row()
#row.operator("bim.display_constr_types", icon="TRIA_DOWN", text="")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
op.ifc_class = ifc_class
if relating_type_id.isnumeric():
op.relating_type_id = int(relating_type_id)
elif is_sidebar:
row = layout.row(align=True)
if ifc_classes:
row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, props, "ifc_class", text="")
else:
row.label(text="No Construction Class", icon="FILE_VOLUME")
row = layout.row(align=True)
if relating_types_ids:
row.label(text="", icon="FILE_3D")
prop_with_search(row, props, "relating_type_id", text="")
else:
row.label(text="No Construction Type", icon="FILE_3D")
box = layout.box()
if AuthoringData.data["type_thumbnail"]:
box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5)
else:
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
op.ifc_class = props.ifc_class
if ifc_classes:
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
op.ifc_class = ifc_class
if relating_type_id.isnumeric():
op.relating_type_id = int(relating_type_id)
if AuthoringData.data["active_class"] in ("IfcWall", "IfcWallStandardCase"):
row = layout.row(align=True)
row.prop(data=props, property="extrusion_depth", text="Height")
op = row.operator("bim.change_extrusion_depth", icon="FILE_REFRESH", text="")
op.depth = props.extrusion_depth
row = layout.row(align=True)
row.prop(data=props, property="length", text="Length")
op = row.operator("bim.change_layer_length", icon="FILE_REFRESH", text="")
op.length = props.length
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.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.hotkey", text="Butt").hotkey = "S_T"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Y")
row.operator("bim.hotkey", text="Mitre").hotkey = "S_Y"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_M")
row.operator("bim.hotkey", text="Merge").hotkey = "S_M"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_F")
row.operator("bim.hotkey", text="Flip").hotkey = "S_F"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_S")
row.operator("bim.hotkey", text="Split").hotkey = "S_S"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
row.operator("bim.hotkey", text="Regen").hotkey = "S_G"
row.operator("bim.join_wall", icon="X", text="").join_type = ""
elif AuthoringData.data["active_class"] in ("IfcSlab", "IfcSlabStandardCase"):
if not context.active_object:
pass
elif context.active_object.mode == "OBJECT":
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Profile").hotkey = "S_E"
elif AuthoringData.data["active_class"] in (
"IfcColumn",
"IfcColumnStandardCase",
"IfcBeam",
"IfcBeamStandardCase",
"IfcMember",
"IfcMemberStandardCase",
):
row = layout.row(align=True)
row.prop(data=props, property="cardinal_point", text="Axis")
op = row.operator("bim.change_cardinal_point", icon="FILE_REFRESH", text="")
op.cardinal_point = int(props.cardinal_point)
row = layout.row(align=True)
label = "Height" if ifc_class == "IfcColumnType" else "Length"
row.prop(data=props, property="extrusion_depth", text=label)
op = row.operator("bim.change_profile_depth", icon="FILE_REFRESH", text="")
op.depth = props.extrusion_depth
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.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.hotkey", text="Butt").hotkey = "S_T"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Y")
row.operator("bim.hotkey", text="Mitre").hotkey = "S_Y"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_R")
row.operator("bim.hotkey", text="Rotate 90").hotkey = "S_R"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
row.operator("bim.hotkey", text="Regen").hotkey = "S_G"
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
elif AuthoringData.data["active_class"] in (
"IfcWindow",
"IfcWindowStandardCase",
"IfcDoor",
"IfcDoorStandardCase",
):
row = layout.row(align=True)
row.prop(data=props, property="rl", text="RL")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
row.operator("bim.hotkey", text="Regen").hotkey = "S_G"
if props.ifc_class in (
"IfcCableCarrierSegmentType",
"IfcCableSegmentType",
"IfcDuctSegmentType",
"IfcPipeSegmentType",
):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Extend", icon="EVENT_E")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Add Type Instance", icon="EVENT_A")
if AuthoringData.data["ifc_classes"]:
if props.ifc_class == "IfcWallType":
row = layout.row()
row.label(text="Join")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Extend", icon="EVENT_E")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Butt", icon="EVENT_T")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Mitre", icon="EVENT_Y")
row = layout.row()
row.label(text="Wall Tools")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Flip", icon="EVENT_F")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Split", icon="EVENT_S")
if props.ifc_class in ("IfcColumnType", "IfcBeamType", "IfcMemberType"):
row = layout.row()
row.label(text="Join")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Extend", icon="EVENT_E")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Opening", icon="EVENT_O")
row.label(text="", icon="EVENT_O")
if len(context.selected_objects) == 2:
row.operator("bim.add_opening", text="Apply Void")
else:
row.operator("bim.add_potential_opening", text="Add Void")
if AuthoringData.data["is_voidable_element"]:
if AuthoringData.data["has_visible_openings"]:
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
row.operator("bim.hide_openings", icon="CANCEL", text="")
else:
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
row = layout.row(align=True)
row.label(text="Align")
@@ -126,28 +282,36 @@ class BimTool(WorkSpaceTool):
row.label(text="Mode")
row = layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="Opening", icon="EVENT_O")
row.label(text="", icon="EVENT_O")
row.operator("bim.hotkey", text="Void").hotkey = "A_O"
row = layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="Decomposition", icon="EVENT_D")
row.label(text="", icon="EVENT_D")
row.operator("bim.hotkey", text="Decomposition").hotkey = "A_D"
class Hotkey(bpy.types.Operator):
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.hotkey"
bl_label = "Hotkey"
bl_options = {"REGISTER", "UNDO"}
hotkey: bpy.props.StringProperty()
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)
@classmethod
def poll(cls, context):
return IfcStore.get_file()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
return tool.Ifc.get()
def _execute(self, context):
self.props = context.scene.BIMModelProperties
self.has_ifc_class = True
self.active_class = None
element = tool.Ifc.get_entity(context.active_object)
if element:
self.active_class = element.is_a()
try:
self.has_ifc_class = bool(self.props.ifc_class)
except:
@@ -155,41 +319,139 @@ class Hotkey(bpy.types.Operator):
getattr(self, f"hotkey_{self.hotkey}")()
return {"FINISHED"}
def invoke(self, context, event):
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
self.props = context.scene.BIMModelProperties
self.x = self.props.x
self.y = self.props.y
self.z = self.props.z
return self.execute(context)
def draw(self, context):
props = context.scene.BIMModelProperties
if self.hotkey == "S_O":
row = self.layout.row()
row.prop(self, "x")
row = self.layout.row()
row.prop(self, "y")
row = self.layout.row()
row.prop(self, "z")
def hotkey_S_A(self):
bpy.ops.bim.add_type_instance()
bpy.ops.bim.add_constr_type_instance()
def hotkey_S_C(self):
if self.has_ifc_class and self.props.ifc_class == "IfcWallType":
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
bpy.ops.bim.align_wall(align_type="CENTERLINE")
else:
bpy.ops.bim.align_product(align_type="CENTERLINE")
def hotkey_S_E(self):
if not self.has_ifc_class:
return
if self.props.ifc_class == "IfcWallType":
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
bpy.ops.bim.join_wall(join_type="T")
elif self.props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]:
bpy.ops.bim.extend_profile()
elif self.active_class in ("IfcSlab", "IfcSlabStandardCase"):
if not bpy.context.active_object:
pass
elif bpy.context.active_object.mode == "OBJECT":
bpy.ops.bim.enable_editing_extrusion_profile()
elif self.active_class in (
"IfcColumn",
"IfcColumnStandardCase",
"IfcBeam",
"IfcBeamStandardCase",
"IfcMember",
"IfcMemberStandardCase",
):
bpy.ops.bim.extend_profile(join_type="T")
def hotkey_S_F(self):
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
bpy.ops.bim.flip_wall()
def hotkey_S_G(self):
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
bpy.ops.bim.recalculate_wall()
elif self.active_class in (
"IfcColumn",
"IfcColumnStandardCase",
"IfcBeam",
"IfcBeamStandardCase",
"IfcMember",
"IfcMemberStandardCase",
):
bpy.ops.bim.recalculate_profile()
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
bpy.ops.bim.recalculate_fill()
def hotkey_S_M(self):
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
bpy.ops.bim.merge_wall()
def hotkey_S_R(self):
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
bpy.ops.bim.rotate_90(axis="Z")
elif self.active_class in ("IfcColumn", "IfcColumnStandardCase"):
bpy.ops.bim.rotate_90(axis="Z")
elif self.active_class in ("IfcBeam", "IfcBeamStandardCase", "IfcMember", "IfcMemberStandardCase"):
bpy.ops.bim.rotate_90(axis="Y")
def hotkey_S_S(self):
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
bpy.ops.bim.split_wall()
def hotkey_S_T(self):
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
bpy.ops.bim.join_wall(join_type="L")
elif self.active_class in (
"IfcColumn",
"IfcColumnStandardCase",
"IfcBeam",
"IfcBeamStandardCase",
"IfcMember",
"IfcMemberStandardCase",
):
bpy.ops.bim.extend_profile(join_type="L")
def hotkey_S_V(self):
if self.has_ifc_class and self.props.ifc_class == "IfcWallType":
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
bpy.ops.bim.align_wall(align_type="INTERIOR")
else:
bpy.ops.bim.align_product(align_type="POSITIVE")
def hotkey_S_X(self):
if self.has_ifc_class and self.props.ifc_class == "IfcWallType":
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
if bpy.ops.bim.align_wall.poll():
bpy.ops.bim.align_wall(align_type="EXTERIOR")
else:
bpy.ops.bim.align_product(align_type="NEGATIVE")
def hotkey_S_Y(self):
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
bpy.ops.bim.join_wall(join_type="V")
elif self.active_class in (
"IfcColumn",
"IfcColumnStandardCase",
"IfcBeam",
"IfcBeamStandardCase",
"IfcMember",
"IfcMemberStandardCase",
):
bpy.ops.bim.extend_profile(join_type="V")
def hotkey_S_O(self):
bpy.ops.bim.add_element_opening(voided_building_element="", filling_building_element="")
if len(bpy.context.selected_objects) == 2:
bpy.ops.bim.add_opening()
else:
bpy.ops.bim.add_potential_opening(x=self.x, y=self.y, z=self.z)
self.props.x = self.x
self.props.y = self.y
self.props.z = self.z
def hotkey_A_D(self):
bpy.ops.bim.toggle_decomposition_parenting()
bpy.ops.bim.select_decomposition()
def hotkey_A_O(self):
bpy.ops.bim.toggle_opening_visibility()
if AuthoringData.data["has_visible_openings"]:
bpy.ops.bim.edit_openings()
else:
bpy.ops.bim.show_openings()
@@ -19,6 +19,7 @@
import os
import bpy
import json
import ifcopenshell
import blenderbim.tool as tool
import blenderbim.core.patch as core
import blenderbim.bim.handler
@@ -79,15 +80,15 @@ class ExecuteIfcPatch(bpy.types.Operator):
else:
arguments = [arg.get_value() for arg in props.ifc_patch_args_attr]
ifcpatch.execute(
output = ifcpatch.execute(
{
"input": props.ifc_patch_input,
"output": props.ifc_patch_output,
"input": ifcopenshell.open(props.ifc_patch_input),
"recipe": props.ifc_patch_recipes,
"arguments": arguments,
"log": os.path.join(context.scene.BIMProperties.data_dir, "process.log"),
}
)
ifcpatch.write(output, props.ifc_patch_output)
return {"FINISHED"}
@@ -66,7 +66,3 @@ class BIMPatchProperties(PropertyGroup):
ifc_patch_output: StringProperty(default="", name="IFC Patch Output IFC")
ifc_patch_args: StringProperty(default="", name="Arguments")
ifc_patch_args_attr: CollectionProperty(type=Attribute, name="Arguments")
getter_enum = {
"ifc_patch_recipes": get_ifcpatch_recipes,
}
@@ -36,7 +36,7 @@ classes = (
operator.RefreshLibrary,
operator.RewindLibrary,
operator.SaveLibraryFile,
operator.AppendEntiryLibrary,
operator.AppendEntireLibrary,
operator.SelectLibraryFile,
operator.ToggleFilterCategories,
operator.ToggleLinkVisibility,
@@ -56,6 +56,10 @@ class CreateProject(bpy.types.Operator):
props = context.scene.BIMProjectProperties
template = None if props.template_file == "0" else props.template_file
core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template)
# Load thumbnails for the first time
if tool.Ifc.get().by_type("IfcElementType"):
ifc_class = sorted(tool.Ifc.get().by_type("IfcElementType"), key=lambda e: e.is_a())[0].is_a()
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class)
def rollback(self, data):
IfcStore.file = None
@@ -107,9 +111,9 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector):
def commit(self, data):
IfcStore.library_path = data["filepath"]
IfcStore.library_file = ifcopenshell.open(data["filepath"])
def draw(self, context):
self.layout.prop(self, "append_all", text= "Append Entire Library")
self.layout.prop(self, "append_all", text="Append Entire Library")
class RefreshLibrary(bpy.types.Operator):
@@ -288,10 +292,10 @@ class SaveLibraryFile(bpy.types.Operator):
return {"FINISHED"}
class AppendEntiryLibrary(bpy.types.Operator):
class AppendEntireLibrary(bpy.types.Operator):
bl_idname = "bim.append_entire_library"
bl_label = "Append Entiry Library"
@classmethod
def poll(cls, context):
return IfcStore.get_file()
@@ -302,23 +306,29 @@ class AppendEntiryLibrary(bpy.types.Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
self.library = IfcStore.library_file
lib_elements = ifcopenshell.util.selector.Selector().parse(self.library, '.IfcTypeProduct | .IfcMaterial | .IfcCostSchedule| .IfcProfileDef')
lib_elements = ifcopenshell.util.selector.Selector().parse(
self.library, ".IfcTypeProduct | .IfcMaterial | .IfcCostSchedule| .IfcProfileDef"
)
for element in lib_elements:
bpy.ops.bim.append_library_element(definition= element.id())
bpy.ops.bim.append_library_element(definition=element.id())
return {"FINISHED"}
class AppendLibraryElement(bpy.types.Operator):
bl_idname = "bim.append_library_element"
bl_label = "Append Library Element"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Append element to the current project"
definition: bpy.props.IntProperty()
prop_index: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
return IfcStore.get_file()
poll = bool(IfcStore.get_file())
if bpy.app.version > (3, 0, 0) and not poll:
cls.poll_message_set("Please create or load a project first.")
return poll
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
@@ -374,7 +384,7 @@ class AppendLibraryElement(bpy.types.Operator):
ifc_importer.material_creator.load_existing_materials()
self.import_type_materials(element, ifc_importer)
self.import_type_styles(element, ifc_importer)
ifc_importer.create_type_product(element)
ifc_importer.create_element_type(element)
ifc_importer.place_objects_in_collections()
def import_type_materials(self, element, ifc_importer):
@@ -445,7 +455,7 @@ class EditHeader(bpy.types.Operator):
bl_idname = "bim.edit_header"
bl_label = "Edit Header"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Save header informations"
bl_description = "Save header information"
@classmethod
def poll(cls, context):
@@ -501,7 +511,7 @@ class DisableEditingHeader(bpy.types.Operator):
bl_idname = "bim.disable_editing_header"
bl_label = "Disable Editing Header"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Cancel unsaved header informations"
bl_description = "Cancel unsaved header information"
def execute(self, context):
context.scene.BIMProjectProperties.is_editing = False
@@ -585,6 +595,11 @@ class LoadProjectElements(bpy.types.Operator):
settings.logger.info("Import finished in {:.2f} seconds".format(time.time() - start))
print("Import finished in {:.2f} seconds".format(time.time() - start))
context.scene.BIMProjectProperties.is_loading = False
# Load thumbnails for the first time
if tool.Ifc.get().by_type("IfcElementType"):
ifc_class = sorted(tool.Ifc.get().by_type("IfcElementType"), key=lambda e: e.is_a())[0].is_a()
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class)
return {"FINISHED"}
def get_decomposition_elements(self):
@@ -660,16 +675,20 @@ class LinkIfc(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Link a Blender file"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement)
directory: bpy.props.StringProperty(subtype="DIR_PATH")
filter_glob: bpy.props.StringProperty(default="*.blend;*.blend1", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
def execute(self, context):
new = context.scene.BIMProjectProperties.links.add()
filepath = self.filepath
if self.use_relative_path:
filepath = os.path.relpath(filepath, bpy.path.abspath("//"))
new.name = filepath
bpy.ops.bim.load_link(filepath=self.filepath)
files = [f.name for f in self.files] if self.files else [self.filepath]
for filename in files:
filepath = os.path.join(self.directory, filename)
new = context.scene.BIMProjectProperties.links.add()
if self.use_relative_path:
filepath = os.path.relpath(filepath, bpy.path.abspath("//"))
new.name = filepath
bpy.ops.bim.load_link(filepath=filepath)
return {"FINISHED"}
def invoke(self, context, event):
@@ -735,7 +754,6 @@ class LoadLink(bpy.types.Operator):
continue
bpy.data.scenes[0].collection.children.link(child)
link = context.scene.BIMProjectProperties.links.get(filepath)
link.collection = child
link.is_loaded = True
return {"FINISHED"}
@@ -758,23 +776,25 @@ class ToggleLinkVisibility(bpy.types.Operator):
return {"FINISHED"}
def toggle_wireframe(self, link):
objs = filter(lambda obj: "IfcOpeningElement" not in obj.name, link.collection.all_objects)
for i, obj in enumerate(objs):
if i == 0:
if obj.display_type == "WIRE":
display_type = "TEXTURED"
else:
display_type = "WIRE"
obj.display_type = display_type
link.is_wireframe = display_type == "WIRE"
for collection in self.get_linked_collections():
objs = filter(lambda obj: "IfcOpeningElement" not in obj.name, collection.all_objects)
for i, obj in enumerate(objs):
if i == 0:
if obj.display_type == "WIRE":
display_type = "TEXTURED"
else:
display_type = "WIRE"
obj.display_type = display_type
link.is_wireframe = display_type == "WIRE"
def toggle_visibility(self, link):
linked_collections = self.get_linked_collections()
queue = [bpy.context.view_layer.layer_collection]
layer_collection = None
while queue:
layer = queue.pop()
if layer.collection == link.collection:
if layer.collection in linked_collections:
layer_collection = layer
break
queue.extend(list(layer.children))
@@ -783,6 +803,11 @@ class ToggleLinkVisibility(bpy.types.Operator):
layer_collection.exclude = not layer_collection.exclude
link.is_hidden = layer_collection.exclude
def get_linked_collections(self):
return [
c for c in bpy.data.collections if "IfcProject" in c.name and c.library and c.library.filepath == self.link
]
class ExportIFC(bpy.types.Operator):
bl_idname = "export_ifc.bim"
@@ -812,7 +837,16 @@ class ExportIFC(bpy.types.Operator):
return {"RUNNING_MODAL"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
if context.scene.BIMProjectProperties.should_disable_undo_on_save:
old_history_size = tool.Ifc.get().history_size
old_undo_steps = context.preferences.edit.undo_steps
tool.Ifc.get().history_size = 0
context.preferences.edit.undo_steps = 0
IfcStore.execute_ifc_operator(self, context)
if context.scene.BIMProjectProperties.should_disable_undo_on_save:
tool.Ifc.get().history_size = old_history_size
context.preferences.edit.undo_steps = old_undo_steps
return {"FINISHED"}
def _execute(self, context):
start = time.time()
@@ -92,7 +92,6 @@ class FilterCategory(PropertyGroup):
class Link(PropertyGroup):
name: StringProperty(name="Name")
collection: PointerProperty(name="Collection", type=bpy.types.Collection)
is_loaded: BoolProperty(name="Is Loaded", default=False)
is_wireframe: BoolProperty(name="Is Wireframe", default=False)
is_hidden: BoolProperty(name="Is Hidden", default=False)
@@ -160,6 +159,9 @@ class BIMProjectProperties(PropertyGroup):
false_origin: StringProperty(name="False Origin", default="0,0,0")
element_offset: IntProperty(name="Element Offset", default=0)
element_limit: IntProperty(name="Element Offset", default=30000)
should_disable_undo_on_save: BoolProperty(
name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False
)
links: CollectionProperty(name="Links", type=Link)
active_link_index: IntProperty(name="Active Link Index")
export_schema: EnumProperty(items=get_export_schema, name="IFC Schema")
@@ -167,28 +169,3 @@ class BIMProjectProperties(PropertyGroup):
def get_library_element_index(self, lib_element):
return next((i for i in range(len(self.library_elements)) if self.library_elements[i] == lib_element))
getter_enum = {
"collection_mode": lambda self, context: [
("DECOMPOSITION", "Decomposition", "Collections represent aggregates and spatial containers"),
("SPATIAL_DECOMPOSITION", "Spatial Decomposition", "Collections represent spatial containers"),
("IFC_CLASS", "IFC Class", "Collections represent IFC class"),
("NONE", "None", "No collections are created"),
],
"filter_mode": lambda self, context: [
("NONE", "None", "No filtering is performed"),
("DECOMPOSITION", "Decomposition", "Filter objects by decomposition"),
("IFC_CLASS", "IFC Class", "Filter objects by class"),
("IFC_TYPE", "IFC Type", "Filter objects by type"),
("WHITELIST", "Whitelist", "Filter objects using a custom whitelist query"),
("BLACKLIST", "Blacklist", "Filter objects using a custom blacklist query"),
],
"merge_mode": lambda self, context: [
("NONE", "None", "No objects are merged"),
("IFC_CLASS", "IFC Class", "One object per IFC class"),
("IFC_TYPE", "IFC Type", "One object per IFC construction type"),
("MATERIAL", "Material", "One object per material"),
],
"export_schema": get_export_schema,
"template_file": get_template_file,
}
@@ -27,6 +27,7 @@ classes = (
operator.EditPset,
operator.EnablePsetEditing,
operator.GuessQuantity,
operator.GuessAllQuantities,
operator.RemovePset,
operator.TogglePsetExpansion,
operator.BIM_OT_add_property_to_edit,
@@ -23,6 +23,7 @@ import ifcopenshell.util.unit
import ifcopenshell.util.pset
import ifcopenshell.util.attribute
import blenderbim.bim.schema
import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.pset as core
@@ -42,11 +43,9 @@ class Operator:
def get_pset_props(context, obj, obj_type):
if obj_type == "Object":
obj = bpy.data.objects.get(obj)
return obj.PsetProperties
return bpy.data.objects.get(obj).PsetProperties
elif obj_type == "Material":
obj = bpy.data.materials.get(obj)
return obj.PsetProperties
return bpy.data.materials.get(obj).PsetProperties
elif obj_type == "Task":
return context.scene.TaskPsetProperties
elif obj_type == "Resource":
@@ -57,29 +56,6 @@ def get_pset_props(context, obj, obj_type):
return context.scene.WorkSchedulePsetProperties
def get_pset_obj_ifc_definition_id(context, obj, obj_type):
if obj_type == "Object":
obj = bpy.data.objects.get(obj)
return obj.BIMObjectProperties.ifc_definition_id
elif obj_type == "Material":
obj = bpy.data.materials.get(obj)
return 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 == "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
class TogglePsetExpansion(bpy.types.Operator, Operator):
bl_idname = "bim.toggle_pset_expansion"
bl_label = "Toggle Pset Expansion"
@@ -102,7 +78,8 @@ class EnablePsetEditing(bpy.types.Operator):
def execute(self, context):
self.props = get_pset_props(context, self.obj, self.obj_type)
self.props.properties.clear()
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
Data.load(IfcStore.get_file(), ifc_definition_id)
data = Data.psets if self.pset_id in Data.psets else Data.qtos
pset_data = data[self.pset_id]
self.props.active_pset_name = pset_data["Name"]
@@ -124,43 +101,52 @@ class EnablePsetEditing(bpy.types.Operator):
continue # Other types not yet supported
if prop_template.TemplateType == "P_SINGLEVALUE":
self.load_single_value(prop_template, data)
elif prop_template.TemplateType.startswith("Q_"):
self.load_single_value(prop_template, data)
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
self.load_enumerated_value(prop_template, data)
def load_single_value(self, prop_template, data):
try:
data_type = ifcopenshell.util.attribute.get_primitive_type(
IfcStore.get_schema().declaration_by_name(prop_template.PrimaryMeasureType or "IfcLabel")
)
except:
# TODO: Occurs if the data type is something that exists in
# IFC4 and not in IFC2X3. To fully fix this we need to
# generate the IFC2X3 pset template definitions.
return
prop = self.props.properties.add()
prop.name = prop_template.Name
prop.value_type = "IfcPropertySingleValue"
metadata = prop.metadata
metadata.name = prop_template.Name
metadata.is_null = data.get(prop_template.Name, None) is None
metadata.is_optional = True
metadata.is_uri = prop_template.PrimaryMeasureType == "IfcURIReference"
metadata.data_type = data_type
metadata.data_type = self.get_data_type(prop_template)
if data_type == "string":
if metadata.data_type == "string":
metadata.string_value = "" if metadata.is_null else data[prop_template.Name]
elif data_type == "integer":
elif metadata.data_type == "integer":
metadata.int_value = 0 if metadata.is_null else data[prop_template.Name]
elif data_type == "float":
elif metadata.data_type == "float":
metadata.float_value = 0.0 if metadata.is_null else data[prop_template.Name]
elif data_type == "boolean":
elif metadata.data_type == "boolean":
metadata.bool_value = False if metadata.is_null else data[prop_template.Name]
def get_data_type(self, prop_template):
if prop_template.TemplateType in ["Q_LENGTH", "Q_AREA", "Q_VOLUME", "Q_WEIGHT", "Q_TIME"]:
return "float"
elif prop_template.TemplateType == "Q_COUNT":
return "integer"
try:
return ifcopenshell.util.attribute.get_primitive_type(
IfcStore.get_schema().declaration_by_name(prop_template.PrimaryMeasureType or "IfcLabel")
)
except:
# TODO: Occurs if the data type is something that exists in
# IFC4 and not in IFC2X3. To fully fix this we need to
# generate the IFC2X3 pset template definitions.
pass
def load_enumerated_value(self, prop_template, data):
enum_items = [v.wrappedValue for v in prop_template.Enumerators.EnumerationValues]
selected_enum_items = data.get(prop_template.Name, [])
prop = self.props.properties.add()
prop.name = prop_template.Name
prop.value_type = "IfcPropertyEnumeratedValue"
metadata = prop.metadata
metadata.name = prop_template.Name
@@ -200,8 +186,8 @@ class EnablePsetEditing(bpy.types.Operator):
new.is_selected = enum in selected_enum_items
else:
value = prop["NominalValue"]
prop = self.props.properties.add()
metadata = prop.metadata
new_prop = self.props.properties.add()
metadata = new_prop.metadata
metadata.set_value(value)
metadata.name = prop["Name"]
metadata.is_null = value is None
@@ -233,7 +219,7 @@ class EditPset(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
ifc_definition_id = get_pset_obj_ifc_definition_id(context, self.obj, self.obj_type)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
properties = {}
pset_id = self.pset_id or props.active_pset_id
@@ -275,6 +261,7 @@ class EditPset(bpy.types.Operator, Operator):
},
)
CostData.purge()
bpy.ops.bim.load_cost_item_quantities()
Data.load(IfcStore.get_file(), ifc_definition_id)
bpy.ops.bim.disable_pset_editing(obj=self.obj, obj_type=self.obj_type)
@@ -288,18 +275,24 @@ class RemovePset(bpy.types.Operator, Operator):
obj_type: bpy.props.StringProperty()
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
ifc_definition_id = get_pset_obj_ifc_definition_id(context, self.obj, self.obj_type)
ifcopenshell.api.run(
"pset.remove_pset",
self.file,
**{
"product": self.file.by_id(ifc_definition_id),
"pset": self.file.by_id(self.pset_id),
},
)
Data.load(IfcStore.get_file(), ifc_definition_id)
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]
pset_name = tool.Ifc.get().by_id(self.pset_id).Name
for obj in objects:
props = get_pset_props(context, obj, self.obj_type)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
element = tool.Ifc.get().by_id(ifc_definition_id)
pset = ifcopenshell.util.element.get_psets(element, should_inherit=False).get(pset_name, None)
if pset:
ifcopenshell.api.run(
"pset.remove_pset", tool.Ifc.get(), product=element, pset=tool.Ifc.get().by_id(pset["id"])
)
Data.load(IfcStore.get_file(), ifc_definition_id)
class AddPset(bpy.types.Operator, Operator):
@@ -313,11 +306,14 @@ class AddPset(bpy.types.Operator, Operator):
self.file = IfcStore.get_file()
pset_name = get_pset_props(context, self.obj, self.obj_type).pset_name
if self.obj_type == "Object":
objects = [o.name for o in context.selected_objects]
if context.selected_objects:
objects = [o.name for o in context.selected_objects]
else:
objects = [context.active_object.name]
else:
objects = [self.obj]
for obj in objects:
ifc_definition_id = get_pset_obj_ifc_definition_id(context, obj, self.obj_type)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
if not ifc_definition_id:
continue
element = tool.Ifc.get().by_id(ifc_definition_id)
@@ -336,7 +332,7 @@ class AddQto(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
ifc_definition_id = get_pset_obj_ifc_definition_id(context, self.obj, self.obj_type)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
ifcopenshell.api.run(
"pset.add_qto",
self.file,
@@ -358,7 +354,7 @@ class GuessQuantity(bpy.types.Operator):
self.qto_calculator = QtoCalculator()
obj = context.active_object
prop = obj.PsetProperties.properties.get(self.prop)
prop.float_value = self.guess_quantity(obj, context)
prop.metadata.float_value = self.guess_quantity(obj, context)
return {"FINISHED"}
def guess_quantity(self, obj, context):
@@ -394,6 +390,33 @@ class GuessQuantity(bpy.types.Operator):
return unit_settings.length_unit[0 : -len("METERS")], "METRE"
class GuessAllQuantities(bpy.types.Operator):
bl_idname = "bim.guess_all_quantities"
bl_label = "Guess All Quantities"
bl_options = {"REGISTER", "UNDO"}
pset_id: bpy.props.IntProperty()
obj_name: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
self.qto_calculator = QtoCalculator()
obj = context.active_object
bpy.ops.bim.enable_pset_editing(pset_id=self.pset_id, obj=self.obj_name, obj_type=self.obj_type)
for prop in obj.PsetProperties.properties:
if (
"length" in prop.name.lower()
or "area" in prop.name.lower()
or "volume" in prop.name.lower()
or "width" in prop.name.lower()
or "height" in prop.name.lower()
or "depth" in prop.name.lower()
or "perimeter" in prop.name.lower()
):
bpy.ops.bim.guess_quantity(prop=prop.name)
bpy.ops.bim.edit_pset(obj=self.obj_name, obj_type=self.obj_type)
return {"FINISHED"}
class CopyPropertyToSelection(bpy.types.Operator, Operator):
bl_idname = "bim.copy_property_to_selection"
bl_label = "Copy Property To Selection"
@@ -402,7 +425,7 @@ class CopyPropertyToSelection(bpy.types.Operator, Operator):
def _execute(self, context):
is_pset = tool.Ifc.get().by_id(context.active_object.PsetProperties.active_pset_id).is_a("IfcPropertySet")
pset_name = context.active_object.PsetProperties.active_pset_name
prop_value = context.active_object.PsetProperties.properties.get(self.name).get_value()
prop_value = context.active_object.PsetProperties.properties.get(self.name).metadata.get_value()
for obj in context.selected_objects:
core.copy_property_to_selection(
tool.Ifc,
@@ -158,30 +158,25 @@ class PsetProperties(PropertyGroup):
pset_name: EnumProperty(items=get_pset_names, name="Pset Name")
qto_name: EnumProperty(items=get_qto_names, name="Qto Name")
getter_enum = {
"qto_name": get_qto_names,
"pset_name": get_pset_names,
}
class MaterialPsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
properties: CollectionProperty(name="Properties", type=Attribute)
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=getMaterialPsetNames, name="Pset Name")
class TaskPsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
properties: CollectionProperty(name="Properties", type=Attribute)
properties: CollectionProperty(name="Properties", type=IfcProperty)
qto_name: EnumProperty(items=getTaskQtoNames, name="Qto Name")
class ResourcePsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
properties: CollectionProperty(name="Properties", type=Attribute)
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=getResourcePsetNames, name="Pset Name")
qto_name: EnumProperty(items=getResourceQtoNames, name="Qto Name")
@@ -189,14 +184,14 @@ class ResourcePsetProperties(PropertyGroup):
class ProfilePsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
properties: CollectionProperty(name="Properties", type=Attribute)
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=getProfilePsetNames, name="Pset Name")
class WorkSchedulePsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
properties: CollectionProperty(name="Properties", type=Attribute)
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=getWorkSchedulePsetNames, name="Pset Name")
@@ -223,14 +218,6 @@ class AddEditProperties(PropertyGroup):
)
enum_values: CollectionProperty(name="Enum Values", type=Attribute)
getter_enum = {
"primary_measure_type": get_primary_measure_type,
"template_type": lambda self, context: [
("IfcPropertySingleValue", "IfcPropertySingleValue", "IfcPropertySingleValue"),
("IfcPropertyEnumeratedValue", "IfcPropertyEnumeratedValue", "IfcPropertyEnumeratedValue"),
],
}
def get_value_name(self):
ifc_data_type = IfcStore.get_schema().declaration_by_name(self.primary_measure_type)
data_type = ifcopenshell.util.attribute.get_primitive_type(ifc_data_type)
@@ -1,5 +1,5 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, Vukas Pajic <vulepajic@gmail.om>
#
# This file is part of BlenderBIM Add-on.
#
@@ -16,7 +16,15 @@
# 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 mathutils import Vector
import bpy, bmesh
import mathutils
from mathutils import Vector, Matrix
from mathutils.bvhtree import BVHTree
import math
from shapely.geometry import Polygon
from shapely.ops import unary_union
import blenderbim.tool as tool
import ifcopenshell
class QtoCalculator:
@@ -36,18 +44,25 @@ class QtoCalculator:
elif "perimeter" in prop_name:
return self.get_perimeter(obj)
elif "area" in prop_name and ("footprint" in prop_name or "section" in prop_name or "floor" in prop_name):
return self.get_footprint_area(obj)
return self.get_net_footprint_area(obj)
elif "area" in prop_name and "side" in prop_name:
return self.get_side_area(obj)
elif "area" in prop_name:
return self.get_area(obj)
elif "volume" in prop_name:
return self.get_volume(obj)
return self.get_total_surface_area(obj)
elif "volume" in prop_name and "gross" in prop_name:
return self.get_gross_volume(obj)
elif "volume" in prop_name :
return self.get_net_volume(obj)
def get_units(self, o, vg_index):
return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]])
def get_linear_length(self, o):
"""_summary_: Returns the length of the longest edge of the object bounding box
:param blender-object o: Blender Object
:return float: Length
"""
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
@@ -72,11 +87,21 @@ class QtoCalculator:
return length
def get_width(self, o):
"""_summary_: Returns the width of the object bounding box
:param blender-object o: blender object
:return float: width
"""
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
return min(x, y)
def get_height(self, o):
"""_summary_: Returns the height of the object bounding box
:param blender-object o: blender object
:return float: height
"""
return (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
def get_perimeter(self, o):
@@ -110,18 +135,52 @@ class QtoCalculator:
lowest_z = z
return lowest_polygons
def get_highest_polygons(self, o):
highest_polygons = []
highest_z = None
for polygon in o.data.polygons:
z = round(polygon.center[2], 3)
if highest_z is None:
highest_z = z
if z > highest_z:
continue
elif z == highest_z:
highest_polygons.append(polygon)
elif z < highest_z:
highest_polygons = [polygon]
highest_z = z
return highest_polygons
def get_edge_key_distance(self, obj, edge_key):
return (obj.data.vertices[edge_key[1]].co - obj.data.vertices[edge_key[0]].co).length
def get_edge_distance(self, obj, edge):
return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length
def get_footprint_area(self, o):
def get_net_footprint_area(self, o):
"""_summary_: Returns the area of the footprint of the object, excluding any holes
:param blender-object o: blender object
:return float: footprint area
"""
area = 0
for polygon in self.get_lowest_polygons(o):
area += polygon.area
return area
def get_net_roofprint_area(self, o):
# Is roofprint the right word? Couldn't think of anything better - vulevukusej
"""_summary_: Returns the area of the net roofprint of the object, excluding any holes
:param blender-object o: Blender Object
:return float: Area
"""
area = 0
for polygon in self.get_highest_polygons(o):
area += polygon.area
return area
def get_side_area(self, o):
# There are a few dumb options for this, but this seems the dumbest
# until I get more practical experience on what works best.
@@ -130,7 +189,7 @@ class QtoCalculator:
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return max(x * z, y * z)
def get_area(self, o, vg_index=None):
def get_total_surface_area(self, o, vg_index=None):
if vg_index is None:
area = 0
for polygon in o.data.polygons:
@@ -149,29 +208,594 @@ class QtoCalculator:
return False
return True
def get_volume(self, o, vg_index=None):
volume = 0
ob_mat = o.matrix_world
me = o.data
me.calc_loop_triangles()
for tf in me.loop_triangles:
tfv = tf.vertices
if len(tf.vertices) == 3:
tf_tris = ((me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]),)
else:
tf_tris = (
(me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]),
(
me.vertices[tfv[2]],
me.vertices[tfv[3]],
me.vertices[tfv[0]],
),
def get_net_volume(self, o):
o_mesh = bmesh.new()
o_mesh.from_mesh(o.data)
return o_mesh.calc_volume()
def get_gross_volume(self, o):
element = tool.Ifc.get_entity(o)
if not element:
print(f"Object {o.name} hasn't an IFC instance so gross volume is equal to net volume")
return self.get_net_volume
settings = ifcopenshell.geom.settings()
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
shape = ifcopenshell.geom.create_shape(settings, element)
faces = shape.geometry.faces
verts = shape.geometry.verts
mesh = bpy.data.meshes.new("myBeautifulMesh")
num_vertices = len(verts) // 3
total_faces = len(faces)
loop_start = range(0, total_faces, 3)
num_loops = total_faces // 3
loop_total = [3] * num_loops
num_vertex_indices = len(faces)
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
mesh.loops.add(num_vertex_indices)
mesh.loops.foreach_set("vertex_index", faces)
mesh.polygons.add(num_loops)
mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.update()
# An alternative to foreach is
# mesh.from_pydata(grouped_verts, edges, grouped_faces)
# look at import_ifc.py in blenderbim
bm = bmesh.new()
bm.from_mesh(mesh)
gross_volume = bm.calc_volume()
bm.free()
return gross_volume
# The following is @Moult's older code. Keeping it here just in case the bmesh function is buggy. -vulevukusej
# def get_volume(self, o, vg_index=None):
# volume = 0
# ob_mat = o.matrix_world
# me = o.data
# me.calc_loop_triangles()
# for tf in me.loop_triangles:
# tfv = tf.vertices
# if len(tf.vertices) == 3:
# tf_tris = ((me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]),)
# else:
# tf_tris = (
# (me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]),
# (
# me.vertices[tfv[2]],
# me.vertices[tfv[3]],
# me.vertices[tfv[0]],
# ),
# )
# for tf_iter in tf_tris:
# v1 = ob_mat @ tf_iter[0].co
# v2 = ob_mat @ tf_iter[1].co
# v3 = ob_mat @ tf_iter[2].co
# volume += v1.dot(v2.cross(v3)) / 6.0
# return volume
def get_opening_type(self, opening, obj):
"""_summary_: Returns the opening type - OPENING / RECESS
:param blender-object opening: blender opening object
:param blender-object obj: blender object
:return string: "OPENING" or "RECESS"
"""
polygons = opening.data.polygons
ray_intersections = 0
for polygon in polygons:
normal_vector = (polygon.normal.x, polygon.normal.y, polygon.normal.z)
polygon_centre = (polygon.center.x, polygon.center.y, polygon.center.z)
if obj.ray_cast(polygon_centre, normal_vector)[0]:
ray_intersections += 1
# If an odd number of face-normal vectors intersect with the object, then the void is a recess, otherwise it's an opening
return "OPENING" if ray_intersections % 2 == 0 else "RECESS"
def get_opening_area(
self, obj, angle_z1: int = 45, angle_z2: int = 135, min_area: int = 0, ignore_recesses: bool = False
):
"""_summary_: Returns the lateral area of the openings in the object.
:param obj: blender object
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector lower than this value will be ignored, defaults to 45
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector greater than this value will be ignored,defaults to 135
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored, defaults to 0
:param bool ignore_recesses: Toggle whether recess areas should be considered, defaults to False
:return float: Opening Area
"""
total_opening_area = 0
ifc = tool.Ifc.get()
ifc_element = ifc.by_id(obj.BIMObjectProperties.ifc_definition_id)
if len(openings := ifc_element.HasOpenings) != 0:
for opening in openings:
opening_id = opening.RelatedOpeningElement.GlobalId
ifc_opening_element = ifc.by_guid(opening_id)
bl_opening_obj = tool.Ifc.get_object(ifc_opening_element)
opening_type = (
ifc_opening_element.PredefinedType
if ifc_opening_element.PredefinedType is not None
else self.get_opening_type(bl_opening_obj, obj)
)
for tf_iter in tf_tris:
v1 = ob_mat @ tf_iter[0].co
v2 = ob_mat @ tf_iter[1].co
v3 = ob_mat @ tf_iter[2].co
if ignore_recesses and opening_type == "RECESS":
continue
opening_area = self.get_lateral_area(
self.get_OBB_object(bl_opening_obj), angle_z1=angle_z1, angle_z2=angle_z2, exclude_end_areas=True
)
if opening_area >= min_area:
total_opening_area += opening_area
return total_opening_area
def get_lateral_area(
self,
obj,
subtract_openings: bool = True,
exclude_end_areas: bool = False,
exclude_side_areas: bool = False,
angle_z1: int = 45,
angle_z2: int = 135,
):
"""_summary_
:param blender-object obj: blender object, bpy.types.Object
:param bool subtract_openings: Toggle whether opening-areas should be subtracted, defaults to True
:param bool exclude_end_areas: , defaults to False
:param bool exclude_side_areas: , defaults to False
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the area. Openings with a normal_vector lower than this value will be ignored, defaults to 45
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the area. Openings with a normal_vector greater than this value will be ignored, defaults to 135
:return float: Lateral Area
"""
x_axis = [1, 0, 0]
y_axis = [0, 1, 0]
z_axis = [0, 0, 1]
area = 0
total_opening_area = (
0 if subtract_openings else self.get_opening_area(obj, angle_z1=angle_z1, angle_z2=angle_z2)
)
polygons = obj.data.polygons
for polygon in polygons:
angle_to_z_axis = math.degrees(polygon.normal.rotation_difference(Vector(z_axis)).angle)
if angle_to_z_axis < angle_z1 or angle_to_z_axis > angle_z2:
continue
if exclude_end_areas:
angle_to_x_axis = math.degrees(polygon.normal.rotation_difference(Vector(x_axis)).angle)
if angle_to_x_axis < 45 or angle_to_x_axis > 135:
continue
if exclude_side_areas:
angle_to_y_axis = math.degrees(polygon.normal.rotation_difference(Vector(y_axis)).angle)
if angle_to_y_axis < 45 or angle_to_y_axis > 135:
continue
area += polygon.area
return area + total_opening_area
def get_gross_top_area(self, obj, angle: int = 45):
"""_summary_: Returns the gross top area of the object.
:param blender-object obj: blender object
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area. Values lower than this will be ignored, defaults to 45
:return float: Gross Top Area
"""
z_axis = (0, 0, 1)
area = 0
opening_area = 0
polygons = obj.data.polygons
ifc = tool.Ifc.get()
ifc_element = ifc.by_id(obj.BIMObjectProperties.ifc_definition_id)
if len(openings := ifc_element.HasOpenings) != 0:
for opening in openings:
if opening.RelatedOpeningElement.PredefinedType == "OPENING":
opening_id = opening.RelatedOpeningElement.GlobalId
entity = ifc.by_guid(opening_id)
open_obj = tool.Ifc.get_object(entity)
opening_area += self.get_net_top_area(open_obj, angle=angle)
else:
continue
for polygon in polygons:
normal_vector = (polygon.normal.x, polygon.normal.y, polygon.normal.z)
angle_to_z_axis = math.degrees(polygon.normal.rotation_difference(Vector(z_axis)).angle)
if angle_to_z_axis < angle:
area += polygon.area
return area + opening_area
# curently net top area is larger then projected area, because its taking into account internal polygons, or window sills
def get_net_top_area(self, obj, angle: int = 45, ignore_internal: bool = True):
"""_summary_: Returns the net top area of the object.
:param blender-object obj: blender object
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area. Values lower than this will be ignored, defaults to 45
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills), defaults to True
:return float: Net Top Area
"""
z_axis = (0, 0, 1)
area = 0
polygons = obj.data.polygons
for polygon in polygons:
normal_vector = (polygon.normal.x, polygon.normal.y, polygon.normal.z)
angle_to_z_axis = math.degrees(polygon.normal.rotation_difference(Vector(z_axis)).angle)
if angle_to_z_axis < angle:
# offset the raycast, otherwise the raycast will always collide with the object.
offset = polygon.center+Vector((0,0,0.01))
if ignore_internal and obj.ray_cast(offset, (0,0,1))[0]:
continue
area += polygon.area
return area
def get_projected_area(self, obj, projection_axis: str = "z", is_gross: bool = True):
"""_summary_: Returns the projected area of the object.
:param blender-object obj: blender object
:param str projection_axis: Axis to project the area onto. Can be "X", "Y" or "Z"
:param bool is_gross: if True, the projected area will include openings, if False, the projected area will exclude openings
:return float: Projected Area
"""
odata = obj.data
polygons = obj.data.polygons
shapely_polygons = []
axes = {"x": ["y", "z"], "y": ["x", "z"], "z": ["x", "y"]}[projection_axis]
for polygon in polygons:
if getattr(polygon.normal, projection_axis) == 0:
continue
polygon_tuples = []
for loop_index in polygon.loop_indices:
loop = odata.loops[loop_index]
a = getattr(odata.vertices[loop.vertex_index].co, axes[0])
b = getattr(odata.vertices[loop.vertex_index].co, axes[1])
polygon_tuples.append((a, b))
pgon = Polygon(polygon_tuples)
shapely_polygons.append(pgon)
projected_polygon = unary_union(shapely_polygons)
if is_gross:
void_area = 0
voids = projected_polygon.interiors
for void in voids:
void_polygon = Polygon(void)
void_area += void_polygon.area
return projected_polygon.area + void_area
return projected_polygon.area
def get_OBB_object(self, obj):
"""_summary_: Returns the Oriented-Bounding-Box (OBB) of the object.
:param blender-object obj: Blender Object
:return blender-object: OBB of the Object
"""
ifc_id = obj.BIMObjectProperties.ifc_definition_id
bbox = obj.bound_box
# matrix transformation to go from obj coordinates to world coordinates:
obb = [Vector(v) for v in bbox]
obb_mesh = bpy.data.meshes.new(f"OBB_{ifc_id}")
# list of faces, with each tuple referring to an vertex-index in obb
faces = [
(0, 1, 2, 3),
(7, 6, 5, 4),
(5, 6, 2, 1),
(0, 3, 7, 4),
(0, 4, 5, 1),
(2, 6, 7, 3),
]
obb_mesh.from_pydata(vertices=obb, edges=[], faces=faces)
# obb_mesh.transform(obj.matrix_world)
# create a new object from the mesh
new_OBB_object = bpy.data.objects.new(f"OBB_{ifc_id}", obb_mesh)
new_OBB_object.matrix_world = obj.matrix_world
# create new collection for QtoCalculator
collection = bpy.data.collections.get("QtoCalculator", bpy.data.collections.new("QtoCalculator"))
if not bpy.context.scene.collection.children.get(collection.name):
bpy.context.scene.collection.children.link(collection)
# add object to scene collection and then hide them.
collection.objects.get(new_OBB_object.name, collection.objects.link(new_OBB_object))
new_OBB_object.hide_set(True)
return new_OBB_object
def get_AABB_object(self, obj):
"""_summary_: Returns the Axis-Aligned-Bounding-Box (AABB) of the object.
:param blender-object obj: Blender Object
:return blender-object: AABB of the Object
"""
ifc_id = obj.BIMObjectProperties.ifc_definition_id
aabb_mesh = bpy.data.meshes.new(f"OBB_{ifc_id}")
x = [v.co.x for v in obj.data.vertices]
y = [v.co.y for v in obj.data.vertices]
z = [v.co.z for v in obj.data.vertices]
min_x, max_x, min_y, max_y, min_z, max_z = min(x), max(x), min(y), max(y), min(z), max(z)
vertices = [
(min_x, min_y, min_z),
(min_x, min_y, max_z),
(min_x, max_y, max_z),
(min_x, max_y, min_z),
(max_x, min_y, min_z),
(max_x, min_y, max_z),
(max_x, max_y, max_z),
(max_x, max_y, min_z),
]
faces = [
(0, 1, 2, 3),
(7, 6, 5, 4),
(5, 6, 2, 1),
(0, 3, 7, 4),
(0, 4, 5, 1),
(2, 6, 7, 3),
]
aabb_mesh.from_pydata(vertices=vertices, edges=[], faces=faces)
aabb_mesh.update()
# create a new object from the mesh
new_AABB_object = bpy.data.objects.new(f"OBB_{ifc_id}", aabb_mesh)
new_AABB_object.matrix_world = obj.matrix_world
# create new collection for QtoCalculator
collection = bpy.data.collections.get("QtoCalculator", bpy.data.collections.new("QtoCalculator"))
if not bpy.context.scene.collection.children.get(collection.name):
bpy.context.scene.collection.children.link(collection)
# add object to scene collection and then hide them.
collection.objects.link(new_AABB_object)
new_AABB_object.hide_set(True)
return new_AABB_object
def get_bisected_obj(
self,
obj,
plane_co_pos,
plane_no_pos,
plane_co_neg,
plane_no_neg,
):
"""_summary_: Returns the object bisected by two planes.
:param blender-object obj: Blender Object
:param tuple(x,y,z) plane_co_pos: Point on upper bisection plane. Example: (0,0,0)
:param tuple(x,y,z) plane_no_pos: Tuple describing the normal vector of the upper bisection plane. Example: (0,0,1)
:param tuple(x,y,z) plane_co_neg: Point on lower bisection plane. Example: (0,0,0)
:param tuple(x,y,z) plane_no_neg: Tuple describing the normal vector of the lower bisection plane. Example: (0,0,-1)
:return _type_: _description_
"""
ifc_id = obj.BIMObjectProperties.ifc_definition_id
bis_obj = obj.copy()
bis_obj.data = obj.data.copy()
bis_obj.name = f"Bisected_{ifc_id}"
collection = bpy.data.collections.get("QtoCalculator", bpy.data.collections.new("QtoCalculator"))
if not bpy.context.scene.collection.children.get(collection.name):
bpy.context.scene.collection.children.link(collection)
collection.objects.link(bis_obj)
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = bis_obj
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.bisect(plane_co=plane_co_pos, plane_no=plane_no_pos, use_fill=True, clear_outer=True)
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.bisect(plane_co=plane_co_neg, plane_no=plane_no_neg, use_fill=True, clear_outer=True)
bpy.ops.object.editmode_toggle()
bis_obj.hide_set(True)
return bis_obj
def get_total_contact_area(self, obj, class_filter: str = ["IfcElement"]):
"""_summary_: Returns the total contact area of the object with other objects.
:param blender-object obj: Blender Object
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:return float: Total contact area of the object with other objects.
"""
total_contact_area = 0
touching_objects = self.get_touching_objects(obj, class_filter)
for o in touching_objects:
total_contact_area += self.get_contact_area(obj, o)
return total_contact_area
def get_touching_objects(self, obj, class_filter):
"""_summary_: Returns a list of objects that are touching the object.
:param blender-object obj: Blender Object
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:return list: List of touching objects
"""
# rotate the object ever so slightly, otherwise bvhtree.overlap won't work properly. https://blender.stackexchange.com/a/275244/130742
# I still prefer using bhvtree over ifcclash simply because of the considerable speed improvement @vulevukusej
obj.rotation_euler[0] += math.radians(0.001)
obj.rotation_euler[1] += math.radians(0.001)
bpy.context.evaluated_depsgraph_get().update()
obj_mesh = bmesh.new()
obj_mesh.from_mesh(obj.data)
obj_mesh.transform(obj.matrix_world)
obj_tree = BVHTree.FromBMesh(obj_mesh)
touching_objects = []
filtered_objects = []
ifc = tool.Ifc.get()
for f in class_filter:
filtered_objects += ifc.by_type(f)
for o in filtered_objects:
blender_o = tool.Ifc.get_object(o)
if blender_o == obj:
continue
o_mesh = bmesh.new()
try:
o_mesh.from_mesh(blender_o.data)
except:
# i'm too tired to debug this properly. Not sure what causes this error. @vulevukusej
continue
o_mesh.transform(blender_o.matrix_world)
o_tree = BVHTree.FromBMesh(o_mesh)
if len(obj_tree.overlap(o_tree)) > 0:
touching_objects.append(blender_o)
# return the objects to their original states
blender_o.rotation_euler[0] -= math.radians(0.001)
blender_o.rotation_euler[1] -= math.radians(0.001)
bpy.context.evaluated_depsgraph_get().update()
return touching_objects
def get_contact_area(self, object1, object2):
"""_summary_: Returns the contact area between two objects.
:param blender-object obj: Blender Object
:param blender-object obj: Blender Object
:return float: contact area between the two objects.
"""
# list of tuples, each tuple containing the index of the polygon in object1 and object2 that are touching
total_area = 0
for poly1 in object1.data.polygons:
for poly2 in object2.data.polygons:
total_area += self.get_intersection_between_polygons(object1, poly1, object2, poly2)
return total_area
def get_intersection_between_polygons(self, object1, poly1, object2, poly2):
"""_summary_: Returns the intersection between two polygons.
:param blender-object object1: Blender Object
:param blender-polygon poly1: Blender Polygon
:param blender-object object1: Blender Object
:param blender-polygon poly1: Blender Polygon
:return float: intersection area of the two polygons.
"""
# get normal vectors according to world axis
normal1 = object1.rotation_euler.to_matrix() @ poly1.normal
center1 = object1.matrix_world @ poly1.center
normal2 = object2.rotation_euler.to_matrix() @ poly2.normal
center2 = object2.matrix_world @ poly2.center
angle_between_normals = normal1.rotation_difference(normal2).angle
if math.degrees(angle_between_normals) < 178:
return 0
# touching polygons should be coplanar:
plane_intersection = mathutils.geometry.intersect_plane_plane(
center1,
normal1,
center2,
normal2
)
# sometimes coplanar planes will interesect far off into the distance. This is a crude way of filtering out those intersections.
if plane_intersection[0] is None or (plane_intersection[0] - center1).magnitude > 20:
return 0
# calculate rotation between face and vertical Z-axis. This makes it easier to calculate intersection area later
rotation_to_z = normal1.rotation_difference(Vector((0, 0, 1)))
center_of_rotation = center1
# rotation around face.center in world space / https://blender.stackexchange.com/a/12324/130742
trans_matrix = Matrix.Translation(center_of_rotation) @ rotation_to_z.to_matrix().to_4x4()
pgon1 = self.create_shapely_polygon(object1, poly1, trans_matrix)
pgon2 = self.create_shapely_polygon(object2, poly2, trans_matrix)
try:
return pgon1.intersection(pgon2).area
except:
# TopologicalError - Generated Geometry might be invalid
return 0
def create_shapely_polygon(self, obj, polygon, trans_matrix):
"""_summary_: Create a shapely polygon
:param blender-object obj: Blender Object
:param blender-polygon polygon: Blender Polygon
:param matrix trans_matrix: Matrix that rotates the polygon to face upwards
:return Shapely Polygon: Shapely Polygon
"""
polygon_tuples = []
odata = obj.data
for loop_index in polygon.loop_indices:
loop = odata.loops[loop_index]
coords = obj.matrix_world @ odata.vertices[loop.vertex_index].co
rotated_coords = trans_matrix @ coords
x = rotated_coords.x
y = rotated_coords.y
polygon_tuples.append((x, y))
return Polygon(polygon_tuples)
# # Following code is here temporarily to test newly created functions:
#qto = QtoCalculator()
#o = bpy.context.active_object
#sel = bpy.context.selected_objects
#
#nl = '\n'
# print(
# f"get_linear_length: {qto.get_linear_length(o)}{nl}{nl}"
# f"get_width: {qto.get_width(o)}{nl}{nl}"
# f"get_height: {qto.get_height(o)}{nl}{nl}"
# f"get_perimeter: {qto.get_perimeter(o)}{nl}{nl}"
# f"get_lowest_polygons: {qto.get_lowest_polygons(o)}{nl}{nl}"
# f"get_highest_polygons: {qto.get_highest_polygons(o)}{nl}{nl}"
# f"get_net_footprint_area: {qto.get_net_footprint_area(o)}{nl}{nl}"
# f"get_net_roofprint_area: {qto.get_net_roofprint_area(o)}{nl}{nl}"
# f"get_side_area: {qto.get_side_area(o)}{nl}{nl}"
# f"get_total_surface_area: {qto.get_total_surface_area(o)}{nl}{nl}"
# f"get_volume: {qto.get_volume(o)}{nl}{nl}"
# f"get_opening_area(o, angle_z1=45, angle_z2=135, min_area=0, ignore_recesses=False): {qto.get_opening_area(o, angle_z1=45, angle_z2=135, min_area=0, ignore_recesses=False)}{nl}{nl}"
# f"get_lateral_area(o, subtract_openings=True, exclude_end_areas=False, exclude_side_areas=False, angle_z1=45, angle_z2=135): {qto.get_lateral_area(o, subtract_openings=True, exclude_end_areas=False, exclude_side_areas=False, angle_z1=45, angle_z2=135)}{nl}{nl}"
# f"get_gross_top_area: {qto.get_gross_top_area(o, angle=45)}{nl}{nl}"
# f"get_net_top_area(o, angle=45, ignore_internal=True): {qto.get_net_top_area(o, angle=45, ignore_internal=True)}{nl}{nl}"
# f"get_projected_area(o, projection_axis='z', is_gross=True): {qto.get_projected_area(o, projection_axis='z', is_gross=True)}{nl}{nl}"
# f"get_OBB_object: {qto.get_OBB_object(o)}{nl}{nl}"
# f"get_AABB_object: {qto.get_AABB_object(o)}{nl}{nl}"
# f"get_bisected_obj(o, plane_co_pos=(0,0,1), plane_no_pos=(0,0,1), plane_co_neg=(0,0,1), plane_no_neg=(0,0,1)): {qto.get_bisected_obj(o, plane_co_pos=(0,0,1), plane_no_pos=(0,0,1), plane_co_neg=(0,0,1), plane_no_neg=(0,0,1))}{nl}{nl}"
# f"get_total_contact_area(o, class_filter=['IfcWall', 'IfcSlab']): {qto.get_total_contact_area(o, class_filter=['IfcWall', 'IfcSlab'])}{nl}{nl}"
# f"get_touching_objects(o, ['IfcElement']): {qto.get_touching_objects(o, ['IfcElement'])}{nl}{nl}"
# #f"get_contact_area: {qto.get_contact_area(o)}{nl}{nl}"
# )
volume += v1.dot(v2.cross(v3)) / 6.0
return volume
@@ -41,7 +41,7 @@ def draw_property(prop, layout, copy_operator=None):
def draw_single_property(prop, layout, copy_operator=None):
value_name = prop.metadata.get_value_name()
if not value_name:
layout.label(text=prop.metadata.name)
layout.label(text=prop["Name"])
return
layout.prop(
prop.metadata,
@@ -88,6 +88,10 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
obj_name = get_active_pset_obj_name(context, obj_type)
if not props.active_pset_id:
row.label(text=pset["Name"], icon="COPY_ID")
op = row.operator("bim.guess_all_quantities", icon="FILE_REFRESH", text="")
op.pset_id = pset_id
op.obj_name = obj_name
op.obj_type = obj_type
op = row.operator("bim.enable_pset_editing", icon="GREASEPENCIL", text="")
op.pset_id = pset_id
op.obj = obj_name
@@ -252,7 +256,7 @@ class BIM_PT_material_psets(Panel):
props = context.active_object.active_material.PsetProperties
row = self.layout.row(align=True)
row.prop(props, "pset_name", text="")
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
op.obj = context.active_object.active_material.name
op.obj_type = "Material"
@@ -348,7 +352,7 @@ class BIM_PT_resource_psets(Panel):
props = context.scene.ResourcePsetProperties
row = self.layout.row(align=True)
row.prop(props, "pset_name", text="")
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
op.obj_type = "Resource"
@@ -381,7 +385,7 @@ class BIM_PT_profile_psets(Panel):
props = context.scene.ProfilePsetProperties
row = self.layout.row(align=True)
row.prop(props, "pset_name", text="")
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
op.obj_type = "Profile"
@@ -410,7 +414,7 @@ class BIM_PT_work_schedule_psets(Panel):
props = context.scene.WorkSchedulePsetProperties
row = self.layout.row(align=True)
row.prop(props, "pset_name", text="")
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
op.obj_type = "WorkSchedule"
@@ -450,7 +454,7 @@ class BIM_PT_rename_parameters(Panel):
if props:
for index, prop in enumerate(props):
row = layout.row(align=True)
row.prop(prop, "pset_name", text="")
prop_with_search(row, prop, "pset_name", text="")
row.prop(prop, "existing_property_name", text="")
row.prop(prop, "new_property_name", text="")
op = row.operator("bim.remove_property_to_edit", icon="X", text="")
@@ -485,12 +489,12 @@ class BIM_PT_add_edit_custom_properties(Panel):
if props:
for index, prop in enumerate(props):
row = layout.row(align=True)
row.prop(prop, "pset_name", text="")
prop_with_search(row, prop, "pset_name", text="")
row.prop(prop, "property_name", text="")
if prop.template_type == "IfcPropertySingleValue":
row.prop(prop, prop.get_value_name(), text="")
prop_with_search(row, prop, "primary_measure_type", text="")
prop_with_search(row, prop, "template_type", text="")
row.prop(prop, "template_type", text="")
op = row.operator("bim.remove_property_to_edit", icon="X", text="")
op.index = index
op.option = "AddEditProperties"
@@ -537,7 +541,7 @@ class BIM_PT_delete_psets(Panel):
if props:
for index, prop in enumerate(props):
row = layout.row(align=True)
row.prop(prop, "pset_name", text="")
prop_with_search(row, prop, "pset_name", text="")
op = row.operator("bim.remove_property_to_edit", icon="X", text="")
op.index = index
op.option = "DeletePsets"
@@ -20,21 +20,20 @@ import bpy
from . import ui, prop, operator
classes = (
operator.RefreshPsetTemplates,
operator.AddPsetFile,
operator.SavePsetTemplateFile,
operator.AddPsetTemplate,
operator.EditPsetTemplate,
operator.RemovePsetTemplate,
operator.AddPropTemplate,
operator.EditPropTemplate,
operator.RemovePropTemplate,
operator.EnableEditingPsetTemplate,
operator.DisableEditingPsetTemplate,
operator.EnableEditingPropTemplate,
operator.DeletePropEnum,
operator.AddPropEnum,
operator.AddPropTemplate,
operator.AddPsetFile,
operator.AddPsetTemplate,
operator.DeletePropEnum,
operator.DisableEditingPropTemplate,
operator.DisableEditingPsetTemplate,
operator.EditPropTemplate,
operator.EditPsetTemplate,
operator.EnableEditingPropTemplate,
operator.EnableEditingPsetTemplate,
operator.RemovePropTemplate,
operator.RemovePsetTemplate,
operator.SavePsetTemplateFile,
prop.PsetTemplate,
prop.EnumerationValues,
prop.PropTemplate,
@@ -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 os
import bpy
import ifcopenshell
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
def refresh():
@@ -30,10 +33,51 @@ class PsetTemplatesData:
@classmethod
def load(cls):
cls.data = {"primary_measure_type": cls.primary_measure_type()}
cls.is_loaded = True
cls.data["primary_measure_type"] = cls.primary_measure_type()
cls.data["pset_template_files"] = cls.pset_template_files()
cls.data["pset_templates"] = cls.pset_templates()
cls.data["pset_template"] = cls.pset_template()
cls.data["prop_templates"] = cls.prop_templates()
@classmethod
def primary_measure_type(cls):
schema = tool.Ifc.schema()
return [(t, t, "") for t in sorted([d.name() for d in schema.declarations() if hasattr(d, "declared_type")])]
@classmethod
def pset_template_files(cls):
files = os.listdir(os.path.join(bpy.context.scene.BIMProperties.data_dir, "pset"))
return [(f.replace(".ifc", ""), f.replace(".ifc", ""), "") for f in files]
@classmethod
def pset_templates(cls):
if not IfcStore.pset_template_file:
IfcStore.pset_template_path = os.path.join(
bpy.context.scene.BIMProperties.data_dir,
"pset",
bpy.context.scene.BIMPsetTemplateProperties.pset_template_files + ".ifc",
)
IfcStore.pset_template_file = ifcopenshell.open(IfcStore.pset_template_path)
return [(str(t.id()), t.Name, "") for t in IfcStore.pset_template_file.by_type("IfcPropertySetTemplate")]
@classmethod
def pset_template(cls):
props = bpy.context.scene.BIMPsetTemplateProperties
template_id = props.pset_templates
if not template_id:
return {}
info = IfcStore.pset_template_file.by_id(int(template_id)).get_info()
del info["id"]
del info["type"]
del info["OwnerHistory"]
del info["HasPropertyTemplates"]
return info
@classmethod
def prop_templates(cls):
props = bpy.context.scene.BIMPsetTemplateProperties
template_id = props.pset_templates
if not template_id:
return []
return [e.get_info() for e in IfcStore.pset_template_file.by_id(int(template_id)).HasPropertyTemplates or []]
@@ -22,24 +22,26 @@ import ifcopenshell
import ifcopenshell.api
import blenderbim.bim.schema
import blenderbim.bim.handler
from blenderbim.bim.module.pset_template.prop import purge as purge_templates, updatePsetTemplates, getPsetTemplates
from blenderbim.bim.module.pset.prop import purge as purge_psets
from ifcopenshell.api.pset_template.data import Data
from blenderbim.bim.ifc import IfcStore
# This is just a temporary operator until
# @Moult performs some of his refactor-magic ;) - vulevukusej
class RefreshPsetTemplates(bpy.types.Operator):
bl_idname = "bim.refresh_psettemplates"
bl_label = "Refresh the data for PsetTemplates"
bl_options = {"REGISTER", "UNDO"}
class Operator:
def execute(self, context):
purge_templates()
purge_psets()
IfcStore.begin_transaction(self)
IfcStore.pset_template_file.begin_transaction()
result = self._execute(context)
IfcStore.pset_template_file.end_transaction()
IfcStore.add_transaction_operation(self)
IfcStore.end_transaction(self)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
def rollback(self, data):
IfcStore.pset_template_file.undo()
def commit(self, data):
IfcStore.pset_template_file.redo()
class AddPsetFile(bpy.types.Operator):
bl_idname = "bim.add_pset_file"
@@ -76,52 +78,22 @@ class AddPsetFile(bpy.types.Operator):
return {"FINISHED"}
class AddPsetTemplate(bpy.types.Operator):
class AddPsetTemplate(bpy.types.Operator, Operator):
bl_idname = "bim.add_pset_template"
bl_label = "Add Pset Template"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
IfcStore.begin_transaction(self)
IfcStore.pset_template_file.begin_transaction()
result = self._execute(context)
IfcStore.pset_template_file.end_transaction()
IfcStore.add_transaction_operation(self)
IfcStore.end_transaction(self)
return result
def _execute(self, context):
ifcopenshell.api.run("pset_template.add_pset_template", IfcStore.pset_template_file)
Data.load(IfcStore.pset_template_file)
updatePsetTemplates(self, context)
return {"FINISHED"}
def rollback(self, data):
IfcStore.pset_template_file.undo()
def commit(self, data):
IfcStore.pset_template_file.redo()
template = ifcopenshell.api.run("pset_template.add_pset_template", IfcStore.pset_template_file)
blenderbim.bim.handler.refresh_ui_data()
context.scene.BIMPsetTemplateProperties.pset_templates = str(template.id())
class RemovePsetTemplate(bpy.types.Operator):
class RemovePsetTemplate(bpy.types.Operator, Operator):
bl_idname = "bim.remove_pset_template"
bl_label = "Remove Pset Template"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
props = context.scene.BIMPsetTemplateProperties
return bool(getPsetTemplates(props, context))
def execute(self, context):
IfcStore.begin_transaction(self)
IfcStore.pset_template_file.begin_transaction()
result = self._execute(context)
IfcStore.pset_template_file.end_transaction()
IfcStore.add_transaction_operation(self)
IfcStore.end_transaction(self)
return result
def _execute(self, context):
props = context.scene.BIMPsetTemplateProperties
if props.active_pset_template_id == int(props.pset_templates):
@@ -131,15 +103,6 @@ class RemovePsetTemplate(bpy.types.Operator):
IfcStore.pset_template_file,
**{"pset_template": IfcStore.pset_template_file.by_id(int(props.pset_templates))}
)
Data.load(IfcStore.pset_template_file)
updatePsetTemplates(self, context)
return {"FINISHED"}
def rollback(self, data):
IfcStore.pset_template_file.undo()
def commit(self, data):
IfcStore.pset_template_file.redo()
class EnableEditingPsetTemplate(bpy.types.Operator):
@@ -147,20 +110,15 @@ class EnableEditingPsetTemplate(bpy.types.Operator):
bl_label = "Enable Editing Pset Template"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
props = context.scene.BIMPsetTemplateProperties
return bool(getPsetTemplates(props, context))
def execute(self, context):
props = context.scene.BIMPsetTemplateProperties
props.active_pset_template_id = int(props.pset_templates)
template = Data.pset_templates[props.active_pset_template_id]
props.active_pset_template.global_id = template["GlobalId"]
props.active_pset_template.name = template["Name"] or ""
props.active_pset_template.description = template["Description"] or ""
props.active_pset_template.template_type = template["TemplateType"]
props.active_pset_template.applicable_entity = template["ApplicableEntity"] or ""
template = IfcStore.pset_template_file.by_id(props.active_pset_template_id)
props.active_pset_template.global_id = template.GlobalId
props.active_pset_template.name = template.Name or ""
props.active_pset_template.description = template.Description or ""
props.active_pset_template.template_type = template.TemplateType
props.active_pset_template.applicable_entity = template.ApplicableEntity or ""
return {"FINISHED"}
@@ -184,20 +142,19 @@ class EnableEditingPropTemplate(bpy.types.Operator):
def execute(self, context):
props = context.scene.BIMPsetTemplateProperties
props.active_prop_template_id = self.prop_template
template = Data.prop_templates[props.active_prop_template_id]
props.active_prop_template.name = template["Name"] or ""
props.active_prop_template.description = template["Description"] or ""
props.active_prop_template.primary_measure_type = template["PrimaryMeasureType"]
props.active_prop_template.template_type = template["TemplateType"]
template = IfcStore.pset_template_file.by_id(props.active_prop_template_id)
props.active_prop_template.name = template.Name or ""
props.active_prop_template.description = template.Description or ""
props.active_prop_template.primary_measure_type = template.PrimaryMeasureType
props.active_prop_template.template_type = template.TemplateType
props.active_prop_template.enum_values.clear()
if template["Enumerators"]:
if template.Enumerators:
props.active_prop_template.enum_values.clear()
data_type = props.active_prop_template.get_value_name()
for e in template["Enumerators"].EnumerationValues:
for e in template.Enumerators.EnumerationValues:
new = props.active_prop_template.enum_values.add()
setattr(new, data_type, e.wrappedValue)
return {"FINISHED"}
@@ -236,20 +193,11 @@ class DisableEditingPropTemplate(bpy.types.Operator):
return {"FINISHED"}
class EditPsetTemplate(bpy.types.Operator):
class EditPsetTemplate(bpy.types.Operator, Operator):
bl_idname = "bim.edit_pset_template"
bl_label = "Edit Pset Template"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
IfcStore.begin_transaction(self)
IfcStore.pset_template_file.begin_transaction()
result = self._execute(context)
IfcStore.pset_template_file.end_transaction()
IfcStore.add_transaction_operation(self)
IfcStore.end_transaction(self)
return result
def _execute(self, context):
props = context.scene.BIMPsetTemplateProperties
ifcopenshell.api.run(
@@ -265,16 +213,7 @@ class EditPsetTemplate(bpy.types.Operator):
},
}
)
Data.load(IfcStore.pset_template_file)
updatePsetTemplates(self, context)
bpy.ops.bim.disable_editing_pset_template()
return {"FINISHED"}
def rollback(self, data):
IfcStore.pset_template_file.undo()
def commit(self, data):
IfcStore.pset_template_file.redo()
class SavePsetTemplateFile(bpy.types.Operator):
@@ -288,20 +227,11 @@ class SavePsetTemplateFile(bpy.types.Operator):
return {"FINISHED"}
class AddPropTemplate(bpy.types.Operator):
class AddPropTemplate(bpy.types.Operator, Operator):
bl_idname = "bim.add_prop_template"
bl_label = "Add Prop Template"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
IfcStore.begin_transaction(self)
IfcStore.pset_template_file.begin_transaction()
result = self._execute(context)
IfcStore.pset_template_file.end_transaction()
IfcStore.add_transaction_operation(self)
IfcStore.end_transaction(self)
return result
def _execute(self, context):
props = context.scene.BIMPsetTemplateProperties
pset_template_id = props.active_pset_template_id or int(props.pset_templates)
@@ -310,31 +240,15 @@ class AddPropTemplate(bpy.types.Operator):
IfcStore.pset_template_file,
**{"pset_template": IfcStore.pset_template_file.by_id(pset_template_id)}
)
Data.load(IfcStore.pset_template_file)
return {"FINISHED"}
def rollback(self, data):
IfcStore.pset_template_file.undo()
def commit(self, data):
IfcStore.pset_template_file.redo()
bpy.ops.bim.disable_editing_prop_template()
class RemovePropTemplate(bpy.types.Operator):
class RemovePropTemplate(bpy.types.Operator, Operator):
bl_idname = "bim.remove_prop_template"
bl_label = "Remove Prop Template"
bl_options = {"REGISTER", "UNDO"}
prop_template: bpy.props.IntProperty()
def execute(self, context):
IfcStore.begin_transaction(self)
IfcStore.pset_template_file.begin_transaction()
result = self._execute(context)
IfcStore.pset_template_file.end_transaction()
IfcStore.add_transaction_operation(self)
IfcStore.end_transaction(self)
return result
def _execute(self, context):
props = context.scene.BIMPsetTemplateProperties
ifcopenshell.api.run(
@@ -342,30 +256,13 @@ class RemovePropTemplate(bpy.types.Operator):
IfcStore.pset_template_file,
**{"prop_template": IfcStore.pset_template_file.by_id(self.prop_template)}
)
Data.load(IfcStore.pset_template_file)
return {"FINISHED"}
def rollback(self, data):
IfcStore.pset_template_file.undo()
def commit(self, data):
IfcStore.pset_template_file.redo()
class EditPropTemplate(bpy.types.Operator):
class EditPropTemplate(bpy.types.Operator, Operator):
bl_idname = "bim.edit_prop_template"
bl_label = "Edit Prop Template"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
IfcStore.begin_transaction(self)
IfcStore.pset_template_file.begin_transaction()
result = self._execute(context)
IfcStore.pset_template_file.end_transaction()
IfcStore.add_transaction_operation(self)
IfcStore.end_transaction(self)
return result
def _execute(self, context):
props = context.scene.BIMPsetTemplateProperties
if props.active_prop_template.template_type == "P_ENUMERATEDVALUE":
@@ -386,9 +283,7 @@ class EditPropTemplate(bpy.types.Operator):
},
}
)
Data.load(IfcStore.pset_template_file)
bpy.ops.bim.disable_editing_prop_template()
return {"FINISHED"}
# TODO -This will need to go into the
# api code at some point - vulevukusej
@@ -400,13 +295,7 @@ class EditPropTemplate(bpy.types.Operator):
"IFCPROPERTYENUMERATION",
Name=prop.name,
EnumerationValues=tuple(
self.file.create_entity(prop.primary_measure_type, ev[data_type]) for ev in prop.enum_values
self.file.create_entity(prop.primary_measure_type, getattr(ev, data_type)) for ev in prop.enum_values
),
)
return prop_enum
def rollback(self, data):
IfcStore.pset_template_file.undo()
def commit(self, data):
IfcStore.pset_template_file.redo()
@@ -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.
#
@@ -33,59 +33,30 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from ifcopenshell.api.pset_template.data import Data
psettemplatefiles_enum = []
psettemplates_enum = []
def purge():
global psettemplatefiles_enum
global psettemplates_enum
psettemplatefiles_enum = []
psettemplates_enum = []
def updatePsetTemplateFiles(self, context):
global psettemplates_enum
IfcStore.pset_template_path = os.path.join(
context.scene.BIMProperties.data_dir,
"pset",
context.scene.BIMPsetTemplateProperties.pset_template_files + ".ifc",
)
IfcStore.pset_template_file = ifcopenshell.open(IfcStore.pset_template_path)
updatePsetTemplates(self, context)
IfcStore.pset_template_file = None
PsetTemplatesData.data["pset_template_files"] = PsetTemplatesData.pset_template_files()
PsetTemplatesData.data["pset_templates"] = PsetTemplatesData.pset_templates()
PsetTemplatesData.data["prop_templates"] = PsetTemplatesData.prop_templates()
def getPsetTemplateFiles(self, context):
global psettemplatefiles_enum
if len(psettemplatefiles_enum) < 1:
files = os.listdir(os.path.join(context.scene.BIMProperties.data_dir, "pset"))
psettemplatefiles_enum.extend([(f.replace(".ifc", ""), f.replace(".ifc", ""), "") for f in files])
return psettemplatefiles_enum
if not PsetTemplatesData.is_loaded:
PsetTemplatesData.load()
return PsetTemplatesData.data["pset_template_files"]
def updatePsetTemplates(self, context):
global psettemplates_enum
psettemplates_enum.clear()
getPsetTemplates(self, context)
PsetTemplatesData.data["pset_template"] = PsetTemplatesData.pset_template()
PsetTemplatesData.data["prop_templates"] = PsetTemplatesData.prop_templates()
def getPsetTemplates(self, context):
global psettemplates_enum
if len(psettemplates_enum) < 1:
if not IfcStore.pset_template_file:
IfcStore.pset_template_path = os.path.join(
context.scene.BIMProperties.data_dir,
"pset",
context.scene.BIMPsetTemplateProperties.pset_template_files + ".ifc",
)
IfcStore.pset_template_file = ifcopenshell.open(IfcStore.pset_template_path)
templates = IfcStore.pset_template_file.by_type("IfcPropertySetTemplate")
psettemplates_enum.extend([(str(t.id()), t.Name, "") for t in templates])
Data.load(IfcStore.pset_template_file)
return psettemplates_enum
if not PsetTemplatesData.is_loaded:
PsetTemplatesData.load()
return PsetTemplatesData.data["pset_templates"]
def get_primary_measure_type(self, context):
@@ -146,10 +117,6 @@ class PsetTemplate(PropertyGroup):
template_type: EnumProperty(items=get_template_type, name="Template Type")
applicable_entity: StringProperty(name="Applicable Entity")
getter_enum = {
"template_type": get_template_type,
}
class EnumerationValues(PropertyGroup):
string_value: StringProperty(name="Value")
@@ -168,9 +135,6 @@ class PropTemplate(PropertyGroup):
name="Template Type",
)
enum_values: CollectionProperty(type=EnumerationValues)
getter_enum = {
"primary_measure_type": get_primary_measure_type,
}
def get_value_name(self):
ifc_data_type = IfcStore.get_schema().declaration_by_name(self.primary_measure_type)
@@ -189,14 +153,9 @@ class BIMPsetTemplateProperties(PropertyGroup):
pset_template_files: EnumProperty(
items=getPsetTemplateFiles, name="Pset Template Files", update=updatePsetTemplateFiles
)
pset_templates: EnumProperty(items=getPsetTemplates, name="Pset Template Files")
pset_templates: EnumProperty(items=getPsetTemplates, name="Pset Template Files", update=updatePsetTemplates)
active_pset_template_id: IntProperty(name="Active Pset Template Id")
active_prop_template_id: IntProperty(name="Active Prop Template Id")
active_pset_template: PointerProperty(type=PsetTemplate)
active_prop_template: PointerProperty(type=PropTemplate)
new_template_filename: StringProperty("New TemplateFileName")
getter_enum = {
"pset_template_files": getPsetTemplateFiles,
"pset_templates": getPsetTemplates,
}
@@ -18,12 +18,8 @@
import bpy
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import prop_with_search
from blenderbim.bim.module.pset_template.prop import (
getPsetTemplates,
)
from ifcopenshell.api.pset_template.data import Data
from blenderbim.bim.module.pset_template.data import PsetTemplatesData
class BIM_PT_pset_template(Panel):
@@ -36,23 +32,24 @@ class BIM_PT_pset_template(Panel):
bl_parent_id = "BIM_PT_project_setup"
def draw(self, context):
layout = self.layout
props = context.scene.BIMPsetTemplateProperties
if not PsetTemplatesData.is_loaded:
PsetTemplatesData.load()
row = layout.row(align=True)
prop_with_search(row, props, "pset_template_files", text="")
self.props = context.scene.BIMPsetTemplateProperties
row = self.layout.row(align=True)
prop_with_search(row, self.props, "pset_template_files", text="")
row.operator("bim.save_pset_template_file", text="", icon="EXPORT")
row.operator("bim.add_pset_file", icon="ADD", text="")
row.operator("bim.refresh_psettemplates", icon="FILE_REFRESH", text="")
row = layout.row(align=True)
row = self.layout.row(align=True)
if bool(getPsetTemplates(props, context)):
prop_with_search(row, props, "pset_templates", text="", icon="COPY_ID")
if PsetTemplatesData.data["pset_templates"]:
prop_with_search(row, self.props, "pset_templates", text="", icon="COPY_ID")
else:
row.label(text="No Pset Templates", icon="COPY_ID")
if props.active_pset_template_id:
if self.props.active_pset_template_id:
row.operator("bim.edit_pset_template", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_pset_template", text="", icon="CANCEL")
else:
@@ -60,49 +57,39 @@ class BIM_PT_pset_template(Panel):
row.operator("bim.enable_editing_pset_template", text="", icon="GREASEPENCIL")
row.operator("bim.remove_pset_template", text="", icon="X")
if not Data.is_loaded and props.pset_template_files:
Data.load(IfcStore.pset_template_file)
if PsetTemplatesData.data["pset_template"]:
self.draw_pset_template()
if not Data.pset_templates:
return
if props.active_pset_template_id:
pset_template = Data.pset_templates[int(props.active_pset_template_id)]
row = layout.row()
row.prop(props.active_pset_template, "name")
row = layout.row()
row.prop(props.active_pset_template, "description")
prop_with_search(layout, props.active_pset_template, "template_type")
row = layout.row()
row.prop(props.active_pset_template, "applicable_entity")
def draw_pset_template(self):
if self.props.active_pset_template_id:
row = self.layout.row()
row.prop(self.props.active_pset_template, "name")
row = self.layout.row()
row.prop(self.props.active_pset_template, "description")
prop_with_search(self.layout, self.props.active_pset_template, "template_type")
row = self.layout.row()
row.prop(self.props.active_pset_template, "applicable_entity")
else:
pset_template = Data.pset_templates[int(props.pset_templates)]
for name, value in pset_template.items():
if name == "id" or name == "type" or name == "HasPropertyTemplates":
continue
row = layout.row(align=True)
for name, value in PsetTemplatesData.data["pset_template"].items():
row = self.layout.row(align=True)
row.label(text=name)
row.label(text=str(value))
row = layout.row(align=True)
row = self.layout.row(align=True)
row.label(text="Property Templates", icon="COPY_ID")
row.operator("bim.add_prop_template", icon="ADD", text="")
row = layout.row()
row.label(text="Name | Description | PrimaryMeasureType | TemplateType")
for prop_template in PsetTemplatesData.data["prop_templates"]:
row = self.layout.row(align=True)
for prop_template_id in pset_template["HasPropertyTemplates"]:
prop_template = Data.prop_templates[prop_template_id]
row = layout.row(align=True)
if self.props.active_prop_template_id and self.props.active_prop_template_id == prop_template["id"]:
row.prop(self.props.active_prop_template, "name", text="")
row.prop(self.props.active_prop_template, "description", text="")
prop_with_search(row, self.props.active_prop_template, "primary_measure_type", text="")
row.prop(self.props.active_prop_template, "template_type", text="")
if props.active_prop_template_id and props.active_prop_template_id == prop_template_id:
row.prop(props.active_prop_template, "name", text="")
row.prop(props.active_prop_template, "description", text="")
prop_with_search(row, props.active_prop_template, "primary_measure_type", text="")
row.prop(props.active_prop_template, "template_type", text="")
if props.active_prop_template.template_type == "P_ENUMERATEDVALUE":
row = layout.row(align=True)
if self.props.active_prop_template.template_type == "P_ENUMERATEDVALUE":
row = self.layout.row(align=True)
split = row.split(factor=0.2)
c = split.column()
c.label(
@@ -112,10 +99,10 @@ class BIM_PT_pset_template(Panel):
c = split.column()
box = c.box()
grid = box.column_flow(columns=4)
value_name = props.active_prop_template.get_value_name()
value_name = self.props.active_prop_template.get_value_name()
r = grid.row()
r.operator("bim.add_prop_enum", text="Add Enum")
for k, v in enumerate(props.active_prop_template.enum_values):
for k, v in enumerate(self.props.active_prop_template.enum_values):
r = grid.row(align=True)
r.prop(v, value_name, text="")
op = r.operator("bim.delete_prop_enum", icon="X", text="")
@@ -127,12 +114,12 @@ class BIM_PT_pset_template(Panel):
row.label(text=prop_template["PrimaryMeasureType"])
row.label(text=prop_template["TemplateType"])
if props.active_prop_template_id and props.active_prop_template_id == prop_template_id:
if self.props.active_prop_template_id and self.props.active_prop_template_id == prop_template["id"]:
op = row.operator("bim.edit_prop_template", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_prop_template", icon="CANCEL", text="")
elif props.active_prop_template_id and props.active_prop_template_id != prop_template_id:
row.operator("bim.remove_prop_template", icon="X", text="").prop_template = prop_template_id
elif self.props.active_prop_template_id and self.props.active_prop_template_id != prop_template["id"]:
row.operator("bim.remove_prop_template", icon="X", text="").prop_template = prop_template["id"]
else:
op = row.operator("bim.enable_editing_prop_template", icon="GREASEPENCIL", text="")
op.prop_template = prop_template_id
row.operator("bim.remove_prop_template", icon="X", text="").prop_template = prop_template_id
op.prop_template = prop_template["id"]
row.operator("bim.remove_prop_template", icon="X", text="").prop_template = prop_template["id"]
@@ -26,6 +26,8 @@ classes = (
operator.CalculateObjectVolumes,
operator.ExecuteQtoMethod,
operator.QuantifyObjects,
operator.AssignPsetQto,
operator.CalculateAllQtos,
prop.BIMQtoProperties,
ui.BIM_PT_qto_utilities,
)
@@ -24,6 +24,7 @@ import blenderbim.core.qto as core
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.qto import helper
from ifcopenshell.api.pset.data import Data as PsetData
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
class CalculateCircleRadius(bpy.types.Operator):
@@ -107,6 +108,26 @@ class ExecuteQtoMethod(bpy.types.Operator):
result = helper.calculate_formwork_area(selected_mesh_objects, context)
elif props.qto_methods == "SIDE_FORMWORK":
result = helper.calculate_side_formwork_area(selected_mesh_objects, context)
elif props.qto_methods == "NetFootprintArea":
result = QtoCalculator().get_net_footprint_area(selected_mesh_objects[0])
elif props.qto_methods == "NetRoofprintArea":
result = QtoCalculator().get_net_roofprint_area(selected_mesh_objects[0])
elif props.qto_methods == "LateralArea":
result = QtoCalculator().get_lateral_area(selected_mesh_objects[0])
elif props.qto_methods == "TotalSurfaceArea":
result = QtoCalculator().get_total_surface_area(selected_mesh_objects[0])
elif props.qto_methods == "OpeningArea":
result = QtoCalculator().get_opening_area(selected_mesh_objects[0])
elif props.qto_methods == "GrossTopArea":
result = QtoCalculator().get_gross_top_area(selected_mesh_objects[0])
elif props.qto_methods == "NetTopArea":
result = QtoCalculator().get_net_top_area(selected_mesh_objects[0])
elif props.qto_methods == "ProjectedArea":
result = QtoCalculator().get_projected_area(selected_mesh_objects[0])
elif props.qto_methods == "TotalContactArea":
result = QtoCalculator().get_total_contact_area(selected_mesh_objects[0])
elif props.qto_methods == "ContactArea":
result = QtoCalculator().get_contact_area(selected_mesh_objects[0], selected_mesh_objects[1])
props.qto_result = str(round(result, 3))
return {"FINISHED"}
@@ -150,3 +171,31 @@ class QuantifyObjects(bpy.types.Operator):
ifcopenshell.api.run("pset.edit_qto", self.file, qto=qto, properties={props.prop_name: result})
PsetData.load(self.file, obj.BIMObjectProperties.ifc_definition_id)
return {"FINISHED"}
class AssignPsetQto(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_pset_qto"
bl_label = "Assign Pset Qto"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Assign pset qto to selected object"
@classmethod
def poll(cls, context):
return context.active_object and context.selected_objects
def _execute(self, context):
core.assign_pset_qto(tool.Qto, selected_objects = context.selected_objects)
return {"FINISHED"}
class CalculateAllQtos(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.calculate_all_qtos"
bl_label = "Calculate All Qtos"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Calculate all pset qtos properties of objects assigned pset qto"
@classmethod
def poll(cls, context):
return context.active_object and context.selected_objects
def _execute(self, context):
core.calculate_all_qtos(tool.Qto, selected_objects = context.selected_objects)
return {"FINISHED"}

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