Compare commits

..

205 Commits

Author SHA1 Message Date
Ryan Schultz 05cd11ae12 Added some troubleshooting measures to determine if another plugin is interfering with BB. 2022-12-13 08:12:21 -06:00
fbpyr 307f9a2322 bump python version
bumping python version, so it works with our ubuntu latest image and `actions/setup-python@v2`
2022-12-13 21:35:42 +11:00
CyrilWaechter 3d05dc234a Ignore mypy cache, swig and compiled files 2022-12-11 22:49:44 +01:00
CyrilWaechter 1f87165601 Remove bloat line 2022-12-11 22:46:05 +01:00
CyrilWaechter a513e1b56a Remove duplicate line 2022-12-10 19:45:17 +01:00
CyrilWaechter ec0c8f509e Move boundary origin to space origin
Ensure that boundary origin is same as space origin to assign correct
geometry into IFC.
2022-12-10 19:44:28 +01:00
Ryan Schultz a23bac6ecb where to look to see if a dependency has changed. 2022-12-09 10:31:57 -06:00
Kristoffer Andersen 569c19abca Merge pull request #2642 from CyrilWaechter/v0.7.0
Remove unecessary line which make build failing
2022-12-09 10:52:01 +01:00
CyrilWaechter dfeac601e4 Remove unecessary line which make build failing 2022-12-09 09:44:19 +01:00
Dion Moult 53cecb3546 Write documentation for the nest API module. 2022-12-07 18:56:44 +11:00
Dion Moult cf87852d9c Add API documentation for the material module 2022-12-07 17:30:08 +11:00
Dion Moult 7da3d9ee70 Add docs for library API module 2022-12-07 12:20:29 +11:00
Dion Moult 9650bbb5ee Add docs to layer API 2022-12-07 08:59:24 +11:00
Dion Moult 45953c6b49 Add API documentation for group module 2022-12-06 16:42:47 +11:00
Dion Moult b34caa1b5f Add documentation for grid API module 2022-12-05 17:10:22 +11:00
Ryan Schultz ea8433b187 Added more examples to the Code Examples page 2022-12-05 15:44:38 +11:00
Andrej730 390f23a30c Added ReloadSelectedIfcFile operator
Added ReloadSelectedIfcFile operator and added it to IFC Quality Control -> IFC Debug, it's useful when you're debugging and doing a lot of iterations on the same file.
2022-12-05 14:59:58 +11:00
Massimo Fabbro c8ba2b53c9 Mapping quantities (see #2533) (#2627)
* Finish mapping Qto_PileBaseQuantities

* Finish mapping IfcPipeSegmentBaseQuantities

* Finish mapping IfcVehicleBaseQuantities

* Finish mapping Qto_OpeningElementBaseQuantities

* Finish mapping Qto_PlateBaseQuantities

* Finish Qto_MarineFacilityBaseQuantities

* Finish Qto_FacilityPartBaseQuantities

* Finish Qto_ProjectionElementBaseQuantities

* Better syntax
2022-12-02 18:19:43 +01:00
Thomas Krijnen dbbbb52e9e #2624 2022-12-02 13:43:44 +01:00
Dion Moult 052d6e2254 Add API docs for georeferencing module 2022-11-28 10:30:51 +11:00
Massimo Fabbro b05c711fb9 Merge pull request #2619 from maxfb87/mapping_quantities
Finish mapping quantities Qto_SpaceBaseQuantities (see #2533)
2022-11-27 08:17:32 +01:00
CyrilWaechter eb0322759c Allow ifcpatch to work with .ifcZIP | .ifcXML
Closes #2350
2022-11-26 05:37:00 +01:00
CyrilWaechter 25a7a95284 Allow to write .ifcZIP | .ifcXML #2350
Transparently write any .ifc | .ifcXML | .ifcZIP formatted file
 with any extension :
- by guessing format from its extension
(eg. .zip | .ifczip | .ifcZIP | .ifcXML | .ifcxml | .IFC | .ifc)
zipped file can file can be of any supported format
- by specifying a format
- by specifying if it should be zipped
2022-11-26 05:19:01 +01:00
Ryan Schultz 8ec1b7bb3e misspellings with outer_boundary 2022-11-25 12:29:00 -06:00
CyrilWaechter 5f094f6a95 Fix IfcSystem assignment bug introduced in f0b1935
Fixing my own bug…
Note : always run all test before pushing not only the one which used to fail…
2022-11-25 19:01:07 +01:00
CyrilWaechter f0b1935861 Add system assignment check
@aothms Is there a way to check against IFC formal propositions ?
Example for [IfcZone](http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcZone.htm#5.4.3.82.4-Formal-propositions)
@Moult Should I move is_assignable to an util ?
2022-11-25 18:52:58 +01:00
Massimo Fabbro 2540ba3467 Finish mapping quantities Qto_SpaceBaseQuantities 2022-11-25 17:22:13 +01:00
CyrilWaechter 88093728a7 Add IfcSpatialElementType to assignable classes
Required for IFC4+ to assign IfcSpaceType and IfcSpatialZoneType
Solves #2615
2022-11-25 15:55:32 +01:00
Thomas Krijnen af6734d7b0 IfcOpenShell WASM module build script (#2617) 2022-11-25 13:16:36 +01:00
Thomas Krijnen 8a108f37c3 Moar schema indices 2022-11-24 09:36:02 +01:00
Andrej730 d88510b919 Added new angle annotations operator
New operator should be more intuitive to work with. I kept the old one too for now.
2022-11-24 12:05:54 +06:00
Thomas Krijnen afe9f7602f Test cases for #2586 2022-11-23 13:46:15 +01:00
Thomas Krijnen ff2b79a41f Distinguish between nil and derived in validate.py 2022-11-23 13:45:44 +01:00
Thomas Krijnen 631722303f https://raw.githubusercontent.com/buildingSMART/IFC4.3-html/sep-13-release/IFC/RELEASE/IFC4x3/HTML/IFC4X3_TC1.exp 2022-11-23 13:23:49 +01:00
Dion Moult 4b20a407e9 Add API docs for drawing module 2022-11-23 11:23:40 +09:00
Dion Moult b23f445946 Add API documentation for document module 2022-11-23 10:56:41 +09:00
CyrilWaechter 09c8268494 Fix path for ci (root at src/ifcopenshell-python) 2022-11-22 23:25:11 +01:00
CyrilWaechter 0bc3543914 Update test/input submodule to https://github.com/IfcOpenShell/files/commit/20a17fcf3b1be2bc48af0273f7604af5b7cc4068
#2350
2022-11-22 21:23:26 +01:00
CyrilWaechter 0412eb48a0 Fix type hints #2350 for python <3.10 2022-11-22 01:00:04 +01:00
CyrilWaechter e7d5add13d Add test suite for ifcopenshell.open #2350
Fix open function: format argument was unused
2022-11-22 00:40:27 +01:00
Thomas Krijnen 25948a608a Missing parse updates after 4e0a19e 2022-11-21 09:43:13 +01:00
Dion Moult 11f772b5a3 Add documentation for cost module 2022-11-21 12:05:58 +09:00
CyrilWaechter 412cf7c6cb Add stable AUR package 2022-11-21 01:42:26 +01:00
CyrilWaechter b2d5580c37 Allow to open .ifcZIP | .ifcXML #2350
Transparently open any .ifc | .ifcXML | .ifcZIP formatted file
 with any extension :
- by guessing format from its extension
(eg. .zip | .ifczip | .ifcZIP | .ifcXML | .ifcxml | .IFC | .ifc)
zipped file can file can be of any supported format
-  by specifying a format
2022-11-21 01:32:12 +01:00
CyrilWaechter 8386aa0e0a IfcZone: Allow to remove an object from an IfcZone 2022-11-20 17:48:30 +01:00
CyrilWaechter ae0f2a9de6 IfcZone : Allow basic selection/deletion/creation 2022-11-20 17:35:27 +01:00
Thomas Krijnen 4e0a19e013 https://raw.githubusercontent.com/buildingSMART/IFC4.3-html/v4.3.1.0/IFC/RELEASE/IFC4x3/HTML/IFC4X3_ADD1.exp 2022-11-20 16:52:46 +01:00
Thomas Krijnen 9a840174d2 Fix test case 2022-11-20 14:38:17 +01:00
Massimo Fabbro 83528ba80a Merge pull request #2612 from maxfb87/Mapping_quantities
Finish mapping Qto_CoveringBaseQuantities (see #2533)
2022-11-19 23:05:29 +01:00
Massimo Fabbro eb23372fd0 Finish mapping Qto_CoveringBaseQuantities 2022-11-19 21:44:51 +01:00
Massimo Fabbro 286e9dfda9 Merge pull request #2605 from maxfb87/mapping_quantities
Mapping quantities (see #2533)
2022-11-19 17:28:29 +01:00
Vukas Pajic f0b9c9ad7b #2602 get_properties no longer throws error when EnumerationValues = None 2022-11-18 14:35:57 +01:00
Kristoffer Andersen afd79ee16d bump conda daily build number 2022-11-18 13:52:35 +01:00
Ryan Schultz caa4445eb0 Added diffuse_color inside IFC Style panel 2022-11-18 12:36:10 +11:00
Andrej730 f850bd5741 documented create_simple_2dcurve() 2022-11-17 12:03:28 +06:00
Massimo Fabbro 8c5883ffaa Finish mapping Qto_DoorBaseQuantities and Qto_WindowBaseQuantities 2022-11-16 22:28:10 +01:00
Massimo Fabbro 8f8940de2a Minor fix 2022-11-16 21:56:29 +01:00
Massimo Fabbro bf59871db5 Fix get_gross_side_area for Qto_WallBaseQuantities 2022-11-16 21:50:52 +01:00
Dion Moult 22eb0bf2e4 Write API documentation for control module 2022-11-16 18:38:51 +09:00
Dion Moult 68838a37e5 Add API documentation for context module 2022-11-16 18:18:22 +09:00
Dion Moult 22b4bfe07d Add API docs for constraint module 2022-11-16 08:57:25 +09:00
Andrej730 ff2714d991 Added c-, z-profiles with lips to AU steel library
Added c- and z-profiles with lips to IFC AU Steel profile library (based on data from https://github.com/boltsparts/boltsparts/pull/15).

For z-profiles I've created simple 2D curve generation function that can be reused later - it requires list of points and list of points with fillets and the results are list of curve points, segments and IfcIndexedPolyCurve.

Example:

    points, segments, ifc_curve = create_simple_curve_from_coords(coords,
        fillets =     (0,   1,   4, 5, 6,   7,   10, 11),
        fillet_radius=(r+t, r+t, r, r, r+t, r+t, r, r),
        closed=True, ifc_file=ifc_file)
2022-11-15 13:35:44 +06:00
Dion Moult 308ee7f7a9 Add API documentation for classification module 2022-11-15 16:11:47 +09:00
Dion Moult c87924c4ba Add API docs for boundary module 2022-11-15 08:27:26 +09:00
Thomas Krijnen 8f33c2ca9d Missing file 2022-11-14 09:38:51 +01:00
Thomas Krijnen 9351038009 Refactoring 2022-11-14 09:23:59 +01:00
Dion Moult 66281ec1f1 Add docs for attribute API 2022-11-14 07:55:50 +11:00
Dion Moult ae843e6faf Add docs for aggregate api 2022-11-14 07:48:20 +11:00
Massimo Fabbro 5ca0a254ff Cleaning around in qto_calculator 2022-11-14 06:50:33 +11:00
Thomas Krijnen c18458b3db Refactoring 2022-11-13 11:21:58 +01:00
Thomas Krijnen 3736a8b4b4 Refactoring 2022-11-13 11:04:16 +01:00
Dion Moult 6558f68cd4 Minor fix 2022-11-13 15:32:24 +11:00
Dion Moult 4fd5806f70 Minor documentation tweaks 2022-11-13 15:30:35 +11:00
Thomas Krijnen b1c730c04d draw.include_projection setting 2022-11-12 15:49:29 +01:00
Dion Moult bdec443410 Minor fixes to IfcTester 2022-11-12 14:08:01 +11:00
Erik Abrahamsson d9afb0f8a2 Fix translation of days in Powerproject importer 2022-11-12 08:32:19 +11:00
Andrej730 9dfd7e03f7 IFC Stair - added "Wood / Steel" stair type
Added new stair type "Wood / Steel" to IFC Stair modifier. It's less sophisticated than "Concrete" stair type - it acts more like a "Dumb stair" operator we have.
2022-11-11 17:32:57 +06:00
Andrej730 31b633b8b6 IFC Stair - cosmetic adjustments 2022-11-11 16:22:55 +06:00
Massimo Fabbro 20f4737ffd Fix some errors and update notes 2022-11-11 12:00:08 +11:00
Massimo Fabbro 4d4ab63404 Finish mapping Qto_BeamBaseQuantities 2022-11-11 12:00:08 +11:00
Massimo Fabbro 056f7610c1 Fix some get_lateral_area function errors 2022-11-11 12:00:08 +11:00
Massimo Fabbro bfb2f06e44 Finish mapping Qto_BeamBaseQuantities 2022-11-11 12:00:08 +11:00
Dion Moult a7febaa9b0 Fix astapowerproject import to ifc which seemed mostly broken for some reason 2022-11-11 11:38:09 +11:00
Sigma Dimensions ff984f62af BBIM feature to load nested tasks outputs, and select all nested task outputs 2022-11-10 18:05:26 +01:00
Otso Alho d005524c4c catch missing instances one by one. do not throw away whole relation. 2022-11-10 14:18:50 +01:00
Dion Moult a2362668a3 The profile manager now only manages named profiles 2022-11-10 23:32:15 +11:00
Andrej730 277317e6fd Added "IFC Stair" to object modifiers
1) Added "IFC Stair" to object modifiers - it works in similar way as "IFC Array". It creates a stair from current object (if it's IfcStairFlight) and writes it's parameters to property set "BBIM_Stair" and also to IfcStairFlight parameters.
Then you can edit it - modifying stair parameters and immediately see the changes, then you can save those changes or discard them.
2) "Clever Stair" operator (in Shift+A menu) works a bit different now - it creates new IfcStairFlight with IFC Stair modifier activated.
2022-11-10 15:57:08 +06:00
Dion Moult b3f0b9b37b Get cross section area from profile directly rather than mesh parsing 2022-11-10 17:37:14 +11:00
Dion Moult 6de1e73e4f Distinguish between calculating gross and net surface area 2022-11-10 17:02:11 +11:00
Thomas Krijnen c6cdf4ab77 Be a bit more careful in throwing NoSuchObject in case of triangulated face handling https://github.com/IfcOpenShell/wasm-preview/issues/1 2022-11-09 20:32:53 +01:00
Dion Moult 21f8aca00f Elementary fixes to the BCF module just so that things load (but lots are still broken) 2022-11-09 23:35:47 +11:00
Thomas Krijnen 1d6c3b3fc2 Update build-all.py 2022-11-09 10:58:38 +01:00
Thomas Krijnen 4362163ea9 Update build-all.py 2022-11-09 10:57:42 +01:00
Dion Moult 67808b8384 Fix issue where sometimes world coordinate system would trigger the georeferencing distance threshold 2022-11-09 18:37:16 +11:00
Andrea Ghensi 2cbc7a10ab [BCF] New BCF XML handler with better performances (#2355)
BREAKING CHANGE: incompatible methods and objects with previous version.

closes #2321
2022-11-09 08:54:23 +11:00
Massimo Fabbro d97594df96 Finish Qto_SlabBaseQuantities quantities (#2567) 2022-11-08 22:21:00 +11:00
Dion Moult c7eca135de Document how you can get the matrix of elements from IfcOpenShell Python. 2022-11-08 20:18:08 +11:00
Dion Moult e63ec83325 Fix some bugs in IfcTester that prevented tests running on IFC2X3 2022-11-08 20:05:56 +11:00
Dion Moult 1ddae8a6f5 Fix #2577. Bug where system editing UI appears multiple times. 2022-11-08 20:04:59 +11:00
Andrej730 03781bdf9b Updated EU, AU steel profiles libraries
1) Updated AU profiles based on more correct data (https://github.com/boltsparts/boltsparts/pull/4) - also added welded i-beam profiles
2) Added inner and outer fillet radius to hollow profiles since this data was added to https://github.com/boltsparts/boltsparts
3) Added more data to ifc profiles that was available on bolts: FilletRadius for I-beams, C-beams; FilletRadius and EdgeRadius for I-beams. Sovles #2556
4) Set names IfcProfileDef names too. Solves #2556
2022-11-08 11:29:43 +06:00
Dion Moult 62e5c3684b Fix #2564. Bug where profiles were not reused across copies. 2022-11-07 22:37:53 +11:00
Dion Moult ff90728a22 New unassign container API function 2022-11-07 18:47:15 +11:00
Dion Moult dadf23681b Fix #2123. Bug where mesh to extrusion conversion didn't work for thin extrusions. 2022-11-07 17:45:05 +11:00
Dion Moult 516bd37518 Fix #2550. Bug where material styles weren't included when appending from a library 2022-11-07 16:20:23 +11:00
Dion Moult 521ec1b124 Support styles defined somewhere along a boolean result stack 2022-11-07 11:24:13 +11:00
Thomas Krijnen 4881e4c686 Error in type checking introduced in ca98e77b40 2022-11-06 14:45:00 +01:00
Thomas Krijnen a57cac5f9c #2558 make conditional 2022-11-06 14:39:01 +01:00
Thomas Krijnen ca98e77b40 #2471 Fix refcount when setting aggregates 2022-11-05 16:24:04 +01:00
Dion Moult cbd9c898c3 Fix failing tests 2022-11-04 17:08:41 +11:00
Massimo Fabbro f57731a10a Mapping quantities #2533 (#2565)
* Implement some Qto_WallBaseQuantities quantities

* finish Qto_WallBaseQuantities
2022-11-04 08:56:38 +11:00
Dion Moult 0142bface5 You can now edit parameterised profiles again in the material panel 2022-11-04 00:05:09 +11:00
Dion Moult 5acbf4658f Material UI now prompts to add a profile if none exist in your library 2022-11-03 23:44:47 +11:00
Dion Moult 32e00f6d49 Refactor material UI to not depend on IOS API data class 2022-11-03 23:36:16 +11:00
Gorgious56 4e3ce192cc Custom property set attributes now show their description when right clicking the field
+ Add explicit numerical constraint members to attribute class #2562
+ Fix error in numerical value callback #2562
+ Fix error in setting attribute values because of wrong type cast
2022-11-03 09:54:35 +01:00
Gorgious56 841bb59d18 Fix error when the user selects a class suggestion that is not a predefined type 2022-11-03 08:21:55 +01:00
Gorgious56 8dd8a23af9 The predefined type suggestions now fetch their values from the ifcopenshell.util.doc module instead of parsing the file data 2022-11-03 08:16:40 +01:00
Dion Moult adc59de2bf Minor fix to accommodate null descriptions and parents with no attributes
# Conflicts:
#	src/ifcopenshell-python/ifcopenshell/util/doc.py
2022-11-03 17:13:56 +11:00
Gorgious56 20198bccdb Numerical Attribute validation callback (#2562)
* Numerical attributes can now be clamped

* #2545 : IfcMaterialLayer Priority is now clamped between 0 and 100

* Run black
2022-11-03 14:41:47 +11:00
Andrej730 c0909dace0 black.py formatting 2022-11-02 17:35:51 +06:00
Andrej730 bd054d4baf Added more advanced stair generating operator 2022-11-02 17:31:27 +06:00
Dion Moult 35a70a4ae3 Fix #2559. Don't consider door and window styles in IFC4X3. 2022-11-02 17:31:10 +11:00
Dion Moult a1d30d157b Minor fix. 2022-11-02 17:27:18 +11:00
Kristoffer Andersen 621d89c80e Update ci-ifcopenshell-conda-daily.yml
Bump GA versions away from deprecated versions
2022-11-01 20:58:59 +01:00
Gorgious 2fa3793622 Selecting an ifc predefined type in addition to the ifc class in the object ifc class selector now correctly assigns it 2022-11-01 17:34:29 +01:00
Kristoffer Andersen 872c057c4f bump conda daily to python v 3.10 and 3.11 2022-11-01 16:16:28 +01:00
Kristoffer Andersen f6a530e5e3 remove cap on boost version 2022-11-01 15:17:32 +01:00
Dion Moult 2043a629c7 Material profile sets UI cleanup and now lets you choose from a profile in your library instead of creating new profiles on the fly all the time 2022-11-01 23:39:07 +11:00
Kristoffer Andersen a7f9d548cf fix: add conditional #include <windows.h> for certain files breaking … (#2558)
* fix: add conditional #include <windows.h> for certain files breaking with boost v1.78+

* Revert "fix: add conditional #include <windows.h> for certain files breaking with boost v1.78+"

This reverts commit 7cd5c6292b.

* fix: add only `BOOST_REGEX_NO_W32` to fix issues boost v1.78+
2022-11-01 13:30:07 +01:00
Dion Moult 1391dd38ec Update README.md 2022-11-01 13:29:20 +01:00
Dion Moult 38f6b7aca0 Clean up README
This new proposed readme makes it more obvious to readers that there is a large collection of tools available, a brief glance of what they do, and directs users to the dedicated websites and documentation for further instructions on installation and so on.
2022-11-01 13:29:20 +01:00
Dion Moult f0e4397ef9 Change PyPI package readme. See #2531. 2022-11-01 09:08:13 +11:00
Dion Moult 6b7ffc8ca6 Merge pull request #2544 from Gorgious56/Tooltips
Blenderbim : Implement tooltips and online docs in the interface
2022-11-01 08:46:51 +11:00
Gorgious 5458f4f9dd Slightly optimise the display of the description popup 2022-10-31 22:22:46 +01:00
Gorgious 0c3269d9e1 Revert "Squashed commit of the following:"
This reverts commit f07cf1d1ef.
2022-10-31 19:53:19 +01:00
Gorgious 84dde4c5c0 Property set templates attributes now show a tooltip on hover 2022-10-31 19:34:59 +01:00
Gorgious 7eefb3f62f Pset nominal values now show their description on right click 2022-10-31 19:09:58 +01:00
Gorgious 90369bf7f7 Directly use predefined type doc function instead of wrapper 2022-10-31 18:10:03 +01:00
Gorgious eff3318399 Directly use attribute doc functions instead of wrapper 2022-10-31 18:01:59 +01:00
Gorgious ee59d366dc Directly use entity doc functions instead of wrappers 2022-10-31 17:57:24 +01:00
Gorgious c0b91848a3 Refactor right click context menu 2022-10-31 17:30:33 +01:00
Gorgious d05d430642 Directly use pset doc functions instead of wrapped ones 2022-10-31 17:30:18 +01:00
Gorgious e8a73e008e Pset types and template types enums now show tooltips on hover 2022-10-31 17:15:58 +01:00
Thomas Krijnen 259bb4f36a Let's try #2551 2022-10-31 13:39:01 +01:00
Dion Moult b87191a358 Forgot to commit new profile import export in model tool 2022-10-31 22:34:48 +11:00
Gorgious a359230181 Actually fix #2553 : Add safeguard to unregister custom right click context menu 2022-10-31 11:29:04 +01:00
Gorgious a95e12c0b2 Pset enums now display a tooltip on hover 2022-10-31 11:09:31 +01:00
Gorgious 2ac27c6fdd Add additional line to the ifc description popup to display the attribute type 2022-10-31 10:43:50 +01:00
Gorgious 757578a74b Fix error fetching the description of a nameless attribute 2022-10-31 10:34:18 +01:00
Gorgious 3466a38a44 Removed hardcoded descriptions thanks to 937f39c 2022-10-31 10:32:49 +01:00
Gorgious f07cf1d1ef Squashed commit of the following:
commit 7fb4cdaa16
Author: Gorgious <nathan.hild@gmail.com>
Date:   Mon Oct 31 09:35:06 2022 +0100

    Fix #2546 : Editing a material now automatically ends the edition of its material set item if necessary

commit 427411e758
Author: Dion Moult <dion@thinkmoult.com>
Date:   Mon Oct 31 19:04:57 2022 +1100

    You can now edit arbitrary profiles in the profile manager

commit cc056d5d53
Author: Gorgious <nathan.hild@gmail.com>
Date:   Mon Oct 31 08:52:11 2022 +0100

    Fix #2553 : Right click documentation is now only available for Blender 3.3+
    Also fix an error in the console when right clicking a button

commit 31600033bf
Author: ArturTomczak <artomczak@gmail.com>
Date:   Sun Oct 30 23:16:08 2022 +0100

    add BCF reporter to IDS (#2552)

    * grammar

    * reason not always a list

    * extend readme with instruction

    * remove microseconds from date

    * add BCF reporter

commit 0338825175
Author: Dion Moult <dion@thinkmoult.com>
Date:   Sun Oct 30 23:26:59 2022 +1100

    You can now add new profile definitions in the profile manager

commit d9f8aa4e7b
Author: Andrej730 <azhilenkov@gmail.com>
Date:   Sun Oct 30 11:29:42 2022 +0600

    Added ifc types schema for Ifc2x3 and Ifc4

    Types schema located in separate json file, it's structure:
    { ifc_type_name: {"description": type_description, "spec_url": type_spec_url} }

    I've also added new API function to get type data - `get_type_doc(version, ifc_type)`

commit 6803788676
Author: Dion Moult <dion@thinkmoult.com>
Date:   Sun Oct 30 11:02:18 2022 +1100

    Load thumbnails in a paginated manner to allow to scale to larger models

commit d421cc0bbb
Author: Massimo Fabbro <79401028+maxfb87@users.noreply.github.com>
Date:   Sun Oct 30 00:07:56 2022 +0200

    No mapped quantities in qto calculator  has None value (#2549)

commit bf57c8b1bf
Author: Dion Moult <dion@thinkmoult.com>
Date:   Sat Oct 29 23:45:04 2022 +1100

    Refactor profile module to use data class

commit 9a25db6584
Author: Andrej730 <azhilenkov@gmail.com>
Date:   Sat Oct 29 18:31:40 2022 +0600

    get_attribute_doc get_entity_doc now use recursive method by default

commit 17ccd0adf7
Author: Andrej730 <azhilenkov@gmail.com>
Date:   Sat Oct 29 18:27:51 2022 +0600

    Simplified getting entity parent information for schema api

    Removed unnecssary parent information from json schema - now API is getting it from ifcopenshell schema.

commit 937f39c898
Author: Andrej730 <azhilenkov@gmail.com>
Date:   Sat Oct 29 17:35:43 2022 +0600

    Added information about parent entity to schema

    Added information about parent entity to schema (it's located in "parent_entity" entity parameter inside schema), both for Ifc2x3 and Ifc4.

    Modified get_entity_doc and get_attribute_doc - now you can use additional optional parameter `recursive=True` with those functions - then parent entities attributes will be included to the list of entity's attributes. For example IfcWindow will also have attributes from IfcBuildingElement, IfcElement, IfcProduct etc...

commit 0b5ccd88a5
Author: Andrej730 <azhilenkov@gmail.com>
Date:   Sat Oct 29 16:31:54 2022 +0600

    Added descriptions for psets and qsets to ifc2x3, ifc4 schema

    Example of getting pset description: `get_property_set_doc("IFC4", "Pset_ZoneCommon")['description']`

commit ebf3cd8909
Author: Andrej730 <azhilenkov@gmail.com>
Date:   Sat Oct 29 10:32:26 2022 +0600

    Added IFC libraries for AU and EU profiles

    Also added script that was used to generate those libraries (the script is using data from https://github.com/boltsparts/BOLTS).

    Types of steel profiles included:
    I profiles, Z profiles, C profiles (known as U profiles in IFC), L profiles (both equal and unequal), hollow profiles (circle, square and rectangular).

commit 08afec3461
Author: Dion Moult <dion@thinkmoult.com>
Date:   Fri Oct 28 22:32:15 2022 +1100

    Support material layer set usage negative direction sense for parametric walls

commit 4408a23873
Author: Dion Moult <dion@thinkmoult.com>
Date:   Fri Oct 28 22:30:43 2022 +1100

    Merge duplicate types IfcPatch recipe can now merge based on any attribute
2022-10-31 09:51:05 +01:00
Gorgious 7fb4cdaa16 Fix #2546 : Editing a material now automatically ends the edition of its material set item if necessary 2022-10-31 09:35:06 +01:00
Dion Moult 427411e758 You can now edit arbitrary profiles in the profile manager 2022-10-31 19:05:04 +11:00
Gorgious cc056d5d53 Fix #2553 : Right click documentation is now only available for Blender 3.3+
Also fix an error in the console when right clicking a button
2022-10-31 08:52:11 +01:00
ArturTomczak 31600033bf add BCF reporter to IDS (#2552)
* grammar

* reason not always a list

* extend readme with instruction

* remove microseconds from date

* add BCF reporter
2022-10-31 09:16:08 +11:00
Dion Moult 0338825175 You can now add new profile definitions in the profile manager 2022-10-30 23:27:11 +11:00
Andrej730 d9f8aa4e7b Added ifc types schema for Ifc2x3 and Ifc4
Types schema located in separate json file, it's structure:
{ ifc_type_name: {"description": type_description, "spec_url": type_spec_url} }

I've also added new API function to get type data - `get_type_doc(version, ifc_type)`
2022-10-30 11:33:20 +06:00
Dion Moult 6803788676 Load thumbnails in a paginated manner to allow to scale to larger models 2022-10-30 11:02:31 +11:00
Massimo Fabbro d421cc0bbb No mapped quantities in qto calculator has None value (#2549) 2022-10-30 09:07:56 +11:00
Dion Moult bf57c8b1bf Refactor profile module to use data class 2022-10-29 23:45:12 +11:00
Andrej730 9a25db6584 get_attribute_doc get_entity_doc now use recursive method by default 2022-10-29 18:31:40 +06:00
Andrej730 17ccd0adf7 Simplified getting entity parent information for schema api
Removed unnecssary parent information from json schema - now API is getting it from ifcopenshell schema.
2022-10-29 18:27:51 +06:00
Andrej730 937f39c898 Added information about parent entity to schema
Added information about parent entity to schema (it's located in "parent_entity" entity parameter inside schema), both for Ifc2x3 and Ifc4.

Modified get_entity_doc and get_attribute_doc - now you can use additional optional parameter `recursive=True` with those functions - then parent entities attributes will be included to the list of entity's attributes. For example IfcWindow will also have attributes from IfcBuildingElement, IfcElement, IfcProduct etc...
2022-10-29 17:35:43 +06:00
Andrej730 0b5ccd88a5 Added descriptions for psets and qsets to ifc2x3, ifc4 schema
Example of getting pset description: `get_property_set_doc("IFC4", "Pset_ZoneCommon")['description']`
2022-10-29 16:31:54 +06:00
Andrej730 ebf3cd8909 Added IFC libraries for AU and EU profiles
Also added script that was used to generate those libraries (the script is using data from https://github.com/boltsparts/BOLTS).

Types of steel profiles included:
I profiles, Z profiles, C profiles (known as U profiles in IFC), L profiles (both equal and unequal), hollow profiles (circle, square and rectangular).
2022-10-29 10:32:26 +06:00
Gorgious56 d9b1c26bf0 Fix bug when prop_with_search is used with a prop that is not an enum 2022-10-28 16:50:11 +02:00
Gorgious56 490da99792 Pset primary measure type should now display a tooltip on hover
Awaiting for ifc measure descriptions to be added to the docs
2022-10-28 16:32:17 +02:00
Gorgious56 528df00027 Organization and person now show a tooltip on hover 2022-10-28 16:30:04 +02:00
Gorgious56 bc10f934d6 Add tooltips to attributes to guide users to their description 2022-10-28 15:46:02 +02:00
Gorgious56 819db11db4 Simplify attribute description 2022-10-28 15:44:31 +02:00
Gorgious56 37a0854d40 Material set profile attributes now show a description on right click
+ added link to docs to Material set profile attributes and Material set item
2022-10-28 14:39:36 +02:00
Gorgious56 b984268b5d parameterized profile classes now shot a tooltip on hover 2022-10-28 14:34:57 +02:00
Gorgious56 e4d2ae185d Fix bug when an entity doesn't have any predefined type to fetch in the docs 2022-10-28 14:26:08 +02:00
Gorgious56 345098b4af Fix bug when attribute enum items descriptions are not filled in 2022-10-28 13:51:52 +02:00
Gorgious56 98b517954f Material layer set item attributes now show a description on right click 2022-10-28 13:47:42 +02:00
Gorgious56 dba8d2df9d Fix bug when right clicking an operator button in the interface 2022-10-28 13:43:41 +02:00
Gorgious56 6756ba7022 Ifc profile classes enum now shows a tooltip on hover 2022-10-28 13:42:51 +02:00
Dion Moult 08afec3461 Support material layer set usage negative direction sense for parametric walls 2022-10-28 22:32:15 +11:00
Dion Moult 4408a23873 Merge duplicate types IfcPatch recipe can now merge based on any attribute 2022-10-28 22:30:57 +11:00
Gorgious56 c33b5aa4b7 Extract attribute description fetching 2022-10-28 12:08:28 +02:00
Gorgious56 0ebbc62a06 Add popup window when clicking Ifc description in right click context menu 2022-10-28 11:54:07 +02:00
Gorgious56 0494373155 Store Ifc class in attribute
This shouldn't be necessary ? but it's easier for now
2022-10-28 11:53:23 +02:00
Gorgious56 c897cb3ea0 Show descriptions for attributes in right click context menu
This is hacky, looking for a solution to get tooltips on hover over field
2022-10-28 11:10:12 +02:00
Gorgious56 f38e8f26f5 You can now access property sets online documentation through the right click context menu 2022-10-28 09:07:33 +02:00
Gorgious56 7164103a80 Search button now only displays when an enum has more than 10 items
To reduce visual clutter for enums with only a few values
Also tweak the online docs ui function
2022-10-28 08:33:56 +02:00
Gorgious56 5fb7f33fb0 You can now access the ifc classes online docs from the right click context menu
+ Remove the ugly buttons in the interface
2022-10-28 08:19:35 +02:00
Gorgious56 e602f16ed3 Add description to more ifc entities enums
- Constraint type
- Material type
- ObjectMaterial type
- Actor class and type
- Structural load type and boundary condition type
- Style type
- System type
- Relating type
- Unit type
2022-10-27 17:19:24 +02:00
Gorgious56 17a872a8d8 Use new get_predefined_type_doc function #1982 2022-10-27 16:00:12 +02:00
Dion Moult 4533b74d65 Minor IfcTester fixes 2022-10-27 23:37:01 +11:00
Dion Moult 62229b032e You can now array IFC elements parametrically 2022-10-27 23:36:09 +11:00
Andrej730 f2c2fa8dce Added entities predefined types to schema
Both for ifc2x3 and ifc4 but there is still a problem with ifc2x3 predefined types description - it's empty since constants have no description in ifc2x3. For temporary solution I've used description available on ifc4 for ifc2x3 too.

I've also added API function to get entity's predefined type description: `get_predefined_type_doc(version, entity, predefined_type)`

Example for the same constant that has no description in ifc2x3 and has one in ifc4:
https://github.com/buildingSMART/IFC/blob/Ifc2.3.0.1/Constants/s/STEAMINJECTION_1b6zZZM99EevT2LVdQ7B%24V.xml
https://github.com/buildingSMART/IFC/blob/Ifc4.0.2.1/Constants/s/STEAMINJECTION_1b6zZZM99EevT2LVdQ7B$V.xml
2022-10-27 16:23:46 +06:00
Dion Moult ae170bbfac Fix #2541. Forgot that wall body must be based off the axis reference line. 2022-10-27 19:08:37 +11:00
Dion Moult 706b925f4c FM documents now use the identification as the key, and refer to the doc info instead of doc reference 2022-10-27 09:46:11 +11:00
Dion Moult 65288eac04 Minor fix. See #2533. 2022-10-27 09:45:28 +11:00
Massimo Fabbro bf5a9bdac6 Start quantity calculator implementation (related to #2533) (#2534)
* Start quantity calculator implementation

* Fix get qto_name
2022-10-26 23:51:52 +11:00
Dion Moult e22ebfbf94 Fix #2523. Extrusion depth now reflects perpendicular depth 2022-10-26 22:24:18 +11:00
Dion Moult fb2989c17f Fix #2528. Setting x angle on slabs now only affects the X euler angle 2022-10-26 18:11:54 +11:00
Dion Moult ffbf6483b5 You can now disconnect a wall from a roof clipping 2022-10-26 17:53:20 +11:00
Dion Moult 5c5488fa49 You can now clip walls by sloped slabs / roofs. 2022-10-25 23:13:44 +11:00
Dion Moult 7ff1b8a34d New API for connecting and disconnecting elements 2022-10-25 17:54:24 +11:00
Dion Moult fb786bb90c Batch reassignments now take advantage of inverse indices for a slight speedup. See #2408. 2022-10-25 16:27:13 +11:00
Dion Moult fad80eacaf Bump IOS 2022-10-25 14:03:25 +11:00
Gorgious56 84020c9832 Type Manager : Add search to ifc class and descriptions to class an predefined type enums 2022-10-24 15:20:52 +02:00
Dion Moult 59bce590cc Fix #2529. Bulk copying decomposed elements to multiple storeys now maintains decomposition. 2022-10-24 22:57:23 +11:00
Dion Moult a7ae78ec2a Fix crash when deleting opening. See #2529. 2022-10-24 21:49:44 +11:00
Dion Moult 523a97329e Fix door appearing on wrong RL when not on ground floor. See #2529. 2022-10-24 20:34:09 +11:00
395 changed files with 233432 additions and 8608 deletions
+5 -6
View File
@@ -19,20 +19,19 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.x'
python-version: '3.10'
- name: Build package
run: |
cd src/bcf
pip install build
python -m build
- name: Test
run: |
cd src/bcf
make test
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_TOKEN }}
packages_dir: src/bcf/dist
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- 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
python-version: '3.10.9' # 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 }}
@@ -61,7 +61,6 @@ jobs:
- 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
@@ -26,16 +26,17 @@ jobs:
fail-fast: false
matrix:
pyver: [
{ name: py39, distver: '3.9' },
{ name: py310, distver: '3.10'}
# { name: py39, distver: '3.9' },
{ name: py310, distver: '3.10'},
{ name: py311, distver: '3.11'}
]
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: Linux, distver: ubuntu-20.04, upload: 'true' },
{ name: Linux, distver: ubuntu-22.04, upload: 'false' },
{ name: macOS, distver: macos-11, upload: 'true' },
{ name: macOS, distver: macos-10.15, upload: 'false' }
{ name: macOS, distver: macos-12, upload: 'false' }
]
steps:
- uses: actions/checkout@v2
+14
View File
@@ -73,3 +73,17 @@ _build/
# IDS Docs
src/ifcopenshell-python/test/build
# tox cache
.tox/
*.egg-info/
# mypy cache
.mypy_cache
# blenderbim libs
src/blenderbim/blenderbim/libs
# ifcopenshell swig and compiled files
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
+55 -306
View File
@@ -1,325 +1,74 @@
IfcOpenShell
============
IfcOpenShell is an open source ([LGPL]) software library for working with the Industry Foundation Classes ([IFC])
file format. Extensive geometric support is implemented for the IFC releases [IFC2x3 TC1] and [IFC4 Add2 TC1].
Support for parsing is provided for IFC4x1, IFC4x2, and the IFC4x3 release candidates. Extending with support for
arbitrary IFC schemas is possible at compile-time when using C++ and at run-time when using Python.
For more information, see
* [http://ifcopenshell.org](http://ifcopenshell.org)
* [http://academy.ifcopenshell.org](http://academy.ifcopenshell.org)
IfcOpenShell is an open source ([LGPL]) software library for working with Industry Foundation Classes ([IFC]). Complete
parsing support is provided for [IFC2x3 TC1], [IFC4 Add2 TC1], IFC4x1, IFC4x3, and IFC4x3. Extensive geometric support
is implemented for the IFC releases [IFC2x3 TC1] and [IFC4 Add2 TC1]. Extending with support for arbitrary IFC schemas
is possible at compile-time when using C++ and at run-time when using Python.
In addition to a C++ and Python API, IfcOpenShell comes with an ecosystem of tools, notably including IfcConvert (an application to convert IFC models to
other formats), the BlenderBIM Add-on (an add-on to Blender providing a graphical IFC authoring platform), and many
other libraries, CLI apps, and more. Support is also provided for auxiliary standards such as BCF and IDS.
For more information, see:
* [IfcOpenShell Website](http://ifcopenshell.org)
* [IfcOpenShell Documentation](http://blenderbim.org/docs-python)
* [IfcOpenShell C++ Installation](https://blenderbim.org/docs-python/ifcopenshell/installation.html)
* [IfcOpenShell Python Installation](https://blenderbim.org/docs-python/ifcopenshell-python/installation.html)
* [IfcOpenShell Python Hello World Tutorial](https://blenderbim.org/docs-python/ifcopenshell-python/hello_world.html)
* [BlenderBIM Add-on Website](https://blenderbim.org)
* [BlenderBIM Add-on Documentation](http://blenderbim.org/docs)
* [Add-on Installation](https://blenderbim.org/docs/users/installation.html)
* [Exploring an IFC model](https://blenderbim.org/docs/users/exploring_an_ifc_model.html)
| 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) |
| ArchLinux AUR Package Stable | [![AUR Badge](https://img.shields.io/aur/version/ifcopenshell)](https://aur.archlinux.org/packages/ifcopenshell) |
| ArchLinux AUR Package git | [![AUR Badge](https://img.shields.io/aur/version/ifcopenshell-git)](https://aur.archlinux.org/packages/ifcopenshell-git) |
| BlenderBIM Add-on Chocolatey (under moderation) | [![Chocolatey Badge](https://img.shields.io/chocolatey/v/blenderbim-nightly)](https://community.chocolatey.org/packages/blenderbim-nightly/) |
| Sponsor development on OpenCollective | [![Financial Contributors](https://opencollective.com/opensourcebim/tiers/badge.svg)](https://opencollective.com/opensourcebim/) |
| Docker hub | [![Docker Pulls](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell)](https://hub.docker.com/r/aecgeeks/ifcopenshell) |
Prerequisites
-------------
* Git
* CMake (3.1.3 or newer)
* Windows: [Visual Studio] 2008 to 2019 (2022 not yet supported by dependency CMake) with C++ toolset (or [Visual C++ Build Tools]) or [MSYS2] + MinGW
* *nix: GCC 4.7 or newer, or Clang (any version)
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 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*
For building the IfcOpenShell Python interface and the Blender add-on
* [3ds Max SDK](http://www.autodesk.com/products/3ds-max/free-trial) - *optional*
For building the 3ds Max plug-in.
All recent versions of 3ds Max (2014 and newer) are 64-bit only, so a 64-bit installation is assumed.
Building IfcOpenShell
---------------------
Note 1: The path where the source code is cloned to can contain spaces but non-ASCII characters are very likely to cause problems with the build.
Note 2: If you had not used `git clone --recursive https://github.com/IfcOpenShell/IfcOpenshell.git`, update the submodules by running `git submodule init & git submodule update`.
Note 3: Be careful with special characters is the path when using the nix or win build scripts, because the OpenCASCADE build will fail on paths containing ++ and likely other situations.
### Compiling on Windows
The preferred way to fetch and build this project's dependencies is to use the build scripts
in win/ folder. **See [win/readme.md] for more information**.
#### Using Visual Studio
Instructions in a nutshell (**assuming Visual Studio 2015 x64 environment variables set**):
> cd IfcOpenShell\win
> build-deps.cmd
> run-cmake.bat
NB: `build-deps.cmd` need to be ran from the directory containing it, i.e. the `./win` folder.
You can now open and build the solution file in Visual Studio:
> ..\build-vs2015-x64\IfcOpenShell.sln
As the scripts default to using the `RelWithDebInfo` configuration, and a freshly created solution by CMake defaults
to `Debug`, make sure to switch the used build configuration. Build the `INSTALL` project (right-click -> Project
Only) to deploy the headers and binaries into a single location if wanted/needed.
Alternatively, one can use the utility batch file(s) to build and install the project easily from the command-line
(installing a project will build it also, if required):
> install-ifcopenshell.bat
#### Using MSYS2 + MinGW
Start the MSYS2 Shell and then:
$ cd IfcOpenShell/win
$ ./build-deps.sh
$ ./run-cmake.sh
$ ./install-ifcopenshell.sh
#### Using Bash on Ubuntu on Windows
Start Bash on Ubuntu on Windows and follow the instructions below. Compiling on Ubuntu 14.04.4 LTS using GCC 4.8.4
or Clang 3.5 has been confirmed to work.
### Compiling on *nix
The following instructions are for Ubuntu, modify as required for other operating systems. [nix/build-all.py] script
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 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.
**1)** Install most of the prerequisites and dependencies:
$ sudo apt-get install git cmake gcc g++ libboost-all-dev libcgal-dev
**2a)** Either use an OCCT package from your operating system's software repository
$ sudo apt-get install libocct-data-exchange-dev libocct-draw-dev libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev
**2b)** or, if OCCT is not available or the latest code is wanted, obtain and compile OCCT from https://dev.opencascade.org/release
**2c)** or, if you'd like to try using OCE, use the package from your operating system's software repository
$ sudo apt-get install liboce-foundation-dev liboce-modeling-dev liboce-ocaf-dev liboce-visualization-dev liboce-ocaf-lite-dev
**2d)** or if OCE is not available, or the latest code is wanted, compile OCE yourself (note that the build takes a long time)
$ sudo apt-get install libftgl-dev libtbb2 libtbb-dev libgl1-mesa-dev libfreetype6-dev
$ git clone https://github.com/tpaviot/oce.git
$ cd oce
$ mkdir build && cd build
$ cmake ..
$ make -j
$ sudo make install
**3)** For building IfcConvert with COLLADA (.dae) support (on by default), OpenCOLLADA is needed:
$ sudo apt-get install libpcre3-dev libxml2-dev
$ git clone https://github.com/KhronosGroup/OpenCOLLADA.git
$ cd OpenCOLLADA
Using a known good revision, but HEAD should work too:
$ git checkout 064a60b65c2c31b94f013820856bc84fb1937cc6
$ mkdir build && cd build
$ cmake ..
$ make -j
$ sudo make install
**4)** For building the IfcPython wrapper (on by default), SWIG and Python development are needed, if not already available:
$ sudo apt-get install python-all-dev swig
**5)** To build IfcOpenShell please take the following steps. Alternatively use environment variables for setting the
dependencies' paths. `OCC_INCLUDE_DIR` might be needed to set also. `OPENCOLLADA_INCLUDE_DIR` and `OPENCOLLADA_LIBRARY_DIR`
(and potentially `PCRE_LIBRARY_DIR`) are needed if building with COLLADA support. (`-DCOLLADA_SUPPORT=0` disables it).
$ cd /path/to/IfcOpenShell
$ mkdir build && cd build
$ cmake ../cmake -DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu/ \
-DOPENCOLLADA_INCLUDE_DIR="/usr/local/include/opencollada" \
-DOPENCOLLADA_LIBRARY_DIR="/usr/local/lib/opencollada" \
-DPCRE_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu/ \
-DCGAL_INCLUDE_DIR=/usr/include \
-DGMP_INCLUDE_DIR=/usr/include \
-DMPFR_INCLUDE_DIR=/usr/include \
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DHDF5_SUPPORT=Off
$ make -j
If all worked out correctly you can now use IfcOpenShell. See the examples below.
**6)** Install the project if wanted:
$ sudo make install
### Installing on MacOS Using Homebrew
**1)** Install all dependencies using [Homebrew](https://brew.sh/)
```{shell}
$ 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/``) 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
# 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 the XCode provided Apple Clang compiler, installed with `xcode-select --install
`, rather than a ```brew``` installed C/C++ compiler.
Installing IfcOpenShell with Conda
----------------------------------
Another option for building and installing IfcOpenShell is to use the popular
[Anaconda Python Distribution](https://www.anaconda.com/download).
The requirements are spread across a number of channels.
You can add these channels to your configuration, or specify them all on the command line:
$ conda install -c conda-forge -c oce -c dlr-sc -c ifcopenshell ifcopenshell
Usage examples
--------------
**Invoking IfcConvert from the command line**
$ wget -O duplex.zip https://portal.nibs.org/files/wl/\?id=4DsTgHFQAcOXzFetxbpRCECPbbfUqpgo
$ unzip duplex.zip
$ ./IfcConvert Duplex_A_20110907_optimized.ifc
$ less Duplex_A_20110907_optimized.obj
**Using the IfcOpenShell Python interface**
$ wget -O duplex.zip https://portal.nibs.org/files/wl/\?id=4DsTgHFQAcOXzFetxbpRCECPbbfUqpgo
$ unzip duplex.zip
$ python
>>> import ifcopenshell
>>> f = ifcopenshell.open("Duplex_A_20110907_optimized.ifc")
>>>
>>> # Accessing entity instances by type:
>>> f.by_type("ifcwall")[:2]
[#91=IfcWallStandardCase('2O2Fr$t4X7Zf8NOew3FL9r',#1,'Basic Wall:Interior - Partition (92mm Stud):144586',$,'Basic Wall:Interior - Partition (92mm Stud):128360',#5198,#18806,'144586'), #92=IfcWallStandardCase('2O2Fr$t4X7Zf8NOew3FLIE',#1,'Basic Wall:Interior - Partition (92mm Stud):143921',$,'Basic Wall:Interior - Partition (92mm Stud):128360',#5206,#18805,'143921')]
>>> wall = _[0]
>>> len(wall) # number of EXPRESS attributes
8
>>>
>>> # Accessing EXPRESS attributes by name:
>>> wall.GlobalId
'2O2Fr$t4X7Zf8NOew3FL9r'
>>> wall.Name = "My wall"
>>> wall.NonExistingAttr
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File ".\ifcopenshell.py", line 14, in __getattr__
except: raise AttributeError("entity instance of type '%s' has no attribute'%s'"%(self.wrapped_data.is_a(), name)) from None
AttributeError: entity instance of type 'IfcWallStandardCase' has no attribute 'NonExistingAttr'
>>> wall.GlobalId = 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File ".\ifcopenshell.py", line 26, in __setattr__
self[self.wrapped_data.get_argument_index(key)] = value
File ".\ifcopenshell.py", line 30, in __setitem__
self.wrapped_data.set_argument(idx, entity_instance.map_value(value))
File ".\ifc_wrapper.py", line 118, in <lambda>
set_argument = lambda self,x,y: self._set_argument(x) if y is None else self
._set_argument(x,y)
File ".\ifc_wrapper.py", line 114, in _set_argument
def _set_argument(self, *args): return _ifc_wrapper.entity_instance__set_argument(self, *args)
RuntimeError: INT is not a valid type for 'GlobalId'
>>> # Creating new entity instances
>>> f.createIfcCartesianPoint(Coordinates=(1.0,1.5,2.0))
#27530=IfcCartesianPoint((1.,1.5,2.))
>>>
>>> # Working with GlobalId attributes:
>>> import uuid
>>> ifcopenshell.guid.compress(uuid.uuid1().hex)
'3x4C8Q_6qHuv$P$FYkANRX'
>>> new_guid = _
>>> owner_hist = f.by_type("IfcOwnerHistory")[0]
>>> new_wall = f.createIfcWallStandardCase(new_guid, owner_hist, None, None, Tag='my_tag')
>>> new_wall.ObjectType = ''
>>> new_wall.ObjectPlacement = new_wall.Representation = None
>>>
>>> # Accessing entity instances by instance id or GlobalId:
>>> f[92]
#92=IfcWallStandardCase('2O2Fr$t4X7Zf8NOew3FLIE',#1,'Basic Wall:Interior - Partition (92mm Stud):143921',$,'Basic Wall:Interior - Partition (92mm Stud):128360',#5206,#18805,'143921')
>>> f['2O2Fr$t4X7Zf8NOew3FLIE']
#92=IfcWallStandardCase('2O2Fr$t4X7Zf8NOew3FLIE',#1,'Basic Wall:Interior - Partition (92mm Stud):143921',$,'Basic Wall:Interior - Partition (92mm Stud):128360',#5206,#18805,'143921')
>>>
>>> # Writing IFC-SPF files to disk:
>>> f.write("out.ifc")
Extra tools
-----------
Also available are a series of utilities that are based on or related to IfcOpenShell.
Contents
--------
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 | Description | License |
| ------------------------- | ----------------------------------------------------------------------| ------------------- |
| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later |
| blenderbim | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later |
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later |
| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later |
| ifc5d | Report and optimise cost information from IFC | LGPL-3.0-or-later |
| ifcbimtester | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
| ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later |
| ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later |
| ifccobie | Extract IFC data for COBie handover requirements | LGPL-3.0-or-later |
| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* |
| ifccsv | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later |
| ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later |
| ifcfm | Extract IFC data for FM handover requirements | LGPL-3.0-or-later |
| ifcgeom | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| ifcgeom\_schema\_agnostic | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| ifcgeomserver | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| ifcjni | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| ifcmax | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* |
| ifcopenshell-python | Python library for IFC manipulation | LGPL-3.0-or-later\* |
| ifcparse | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| ifcpatch | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later |
| ifcsverchok | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later |
| ifctester | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later |
| ifcwrap | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| qtviewer | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| serializers | Internal library for IfcOpenShell | 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"
+64 -14
View File
@@ -54,6 +54,7 @@ OPTION(USE_VLD "Use Visual Leak Detector for debugging memory leaks, MSVC-only."
OPTION(USE_MMAP "Adds a command line options to parse IFC files from memory mapped files using Boost.Iostreams" OFF)
OPTION(BUILD_SHARED_LIBS "Build IfcParse and IfcGeom as shared libs (SO/DLL)." OFF)
OPTION(MSVC_PARALLEL_BUILD "Multi-threaded compilation in Microsoft Visual Studio (/MP)" OFF)
OPTION(WASM_BUILD OFF)
if (${HAS_MAX})
OPTION(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
endif()
@@ -149,6 +150,14 @@ UNIFY_ENVVARS_AND_CACHE(MPFR_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_LIBRARY_DIR)
endif()
if(WASM_BUILD)
# when using the nix/build-all.py build script we should not
# look into the sysroot for most of the dependencies but rather
# in the designated build/ folder created by the script.
set(CMAKE_FIND_ROOT_PATH_BACKUP "${CMAKE_FIND_ROOT_PATH}")
set(CMAKE_FIND_ROOT_PATH "")
endif()
if (NOT MINIMAL_BUILD AND GLTF_SUPPORT AND BUILD_CONVERT)
UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR)
FIND_FILE(json_hpp "json.hpp" ${JSON_INCLUDE_DIR}/nlohmann)
@@ -199,7 +208,14 @@ ELSE()
ENDIF()
ENDIF()
set(BOOST_COMPONENTS system program_options regex thread date_time)
if (WASM_BUILD)
set(BOOST_COMPONENTS)
else()
# @todo review this, shouldn't this be all possible header-only now?
# ... or rewritten using C++17 features?
set(BOOST_COMPONENTS system program_options regex thread date_time)
endif()
if(USE_MMAP)
if(MSVC)
# filesystem is necessary for the utf-16 wpath
@@ -210,6 +226,10 @@ if(USE_MMAP)
add_definitions(-DUSE_MMAP)
endif()
FIND_PACKAGE(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS})
MESSAGE(STATUS "Boost include files found in ${Boost_INCLUDE_DIRS}")
MESSAGE(STATUS "Boost libraries found in ${Boost_LIBRARY_DIRS}")
if (NOT MINIMAL_BUILD)
# libxml2 is required for IFCXML (optional) and SVGFILL (mandatory)
find_package(LibXml2 REQUIRED)
@@ -220,11 +240,6 @@ if (NOT MINIMAL_BUILD AND IFCXML_SUPPORT)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_IFCXML)
endif()
FIND_PACKAGE(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS})
MESSAGE(STATUS "Boost include files found in ${Boost_INCLUDE_DIRS}")
MESSAGE(STATUS "Boost libraries found in ${Boost_LIBRARY_DIRS}")
# Usage:
# set(SOME_LIRARIES foo bar)
# add_debug_variants(SOME_LIRARIES "${SOME_LIRARIES}" d)
@@ -343,10 +358,21 @@ if("${libTKernelExt}" STREQUAL ".a")
set(OCCT_STATIC ON)
endif()
if(WASM_BUILD)
set(CMAKE_FIND_ROOT_PATH "${CMAKE_FIND_ROOT_PATH_BACKUP}")
endif()
if(OCCT_STATIC)
find_package(Threads)
# OPENCASCADE_LIBRARIES repeated three times below in order to fix cyclic dependencies - use --start-group ... --end-group instead?
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
if(WASM_BUILD)
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
# OPENCASCADE_LIBRARIES repeated N times below in order to fix cyclic dependencies - use --start-group ... --end-group instead?
# tfk: --start-group ... --end-group didn't work on the apple linker when last tested
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
endif()
if (NOT APPLE AND NOT WIN32)
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} "rt")
endif()
@@ -620,7 +646,12 @@ 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")
if(WASM_BUILD)
# super arbitrarily try to keep size down at least a little bit
set(SCHEMA_VERSIONS "2x3" "4")
else()
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4" "4x3" "4x3_tc1" "4x3_add1")
endif()
endif()
foreach(s ${SCHEMA_VERSIONS})
@@ -697,7 +728,11 @@ if (BUILD_IFCGEOM)
foreach(s ${SCHEMA_VERSIONS})
set(IFCGEOM_SCHEMA_LIBRARIES ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom_ifc${s})
endforeach()
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
if (WASM_BUILD)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
else()
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
endif()
endif()
if (BUILD_CONVERT OR BUILD_IFCPYTHON)
foreach(s ${SCHEMA_VERSIONS})
@@ -741,7 +776,11 @@ set(IFCPARSE_FILES ${IFCPARSE_CPP_FILES} ${IFCPARSE_H_FILES})
add_library(IfcParse ${IFCPARSE_FILES})
set_target_properties(IfcParse PROPERTIES COMPILE_FLAGS -DIFC_PARSE_EXPORTS VERSION "0.6.0" SOVERSION "0.6")
TARGET_LINK_LIBRARIES(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
if (WASM_BUILD)
TARGET_LINK_LIBRARIES(IfcParse ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
else()
TARGET_LINK_LIBRARIES(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
endif()
if (BUILD_IFCGEOM)
@@ -753,7 +792,9 @@ set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES})
foreach(s ${SCHEMA_VERSIONS})
add_library(IfcGeom_ifc${s} STATIC ${IFCGEOM_FILES})
set_target_properties(IfcGeom_ifc${s} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${s}")
TARGET_LINK_LIBRARIES(IfcGeom_ifc${s} IfcParse ${OPENCASCADE_LIBRARIES})
if (NOT WASM_BUILD)
TARGET_LINK_LIBRARIES(IfcGeom_ifc${s} IfcParse ${OPENCASCADE_LIBRARIES})
endif()
endforeach()
# IfcGeom (schema agnostic)
@@ -768,7 +809,11 @@ if (UNIX)
find_package(Threads)
endif()
TARGET_LINK_LIBRARIES(IfcGeom IfcParse ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
if (WASM_BUILD)
TARGET_LINK_LIBRARIES(IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
TARGET_LINK_LIBRARIES(IfcGeom IfcParse ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
endif()
endif(BUILD_IFCGEOM)
@@ -785,7 +830,12 @@ set(SERIALIZERS_S_FILES ${SERIALIZERS_S_H_FILES} ${SERIALIZERS_S_CPP_FILES})
foreach(s ${SCHEMA_VERSIONS})
add_library(Serializers_ifc${s} STATIC ${SERIALIZERS_S_FILES})
set_target_properties(Serializers_ifc${s} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${s} ${CONVERT_PRECISION}")
TARGET_LINK_LIBRARIES(Serializers_ifc${s} IfcGeom ${OPENCASCADE_LIBRARIES} ${HDF5_LIBRARIES})
if (WASM_BUILD)
TARGET_LINK_LIBRARIES(Serializers_ifc${s} ${HDF5_LIBRARIES})
else()
TARGET_LINK_LIBRARIES(Serializers_ifc${s} IfcGeom ${OPENCASCADE_LIBRARIES} ${HDF5_LIBRARIES})
endif()
endforeach()
add_library(Serializers ${SERIALIZERS_FILES})
+3 -2
View File
@@ -13,6 +13,7 @@ source:
build:
binary_relocation: false [osx]
number: 1
requirements:
build:
@@ -24,7 +25,7 @@ requirements:
host:
- python
- boost-cpp <=1.74.0
- boost-cpp
- occt
- libxml2
- cgal-cpp
@@ -36,7 +37,7 @@ requirements:
run:
- python
- boost-cpp <=1.74.0
- boost-cpp
- occt
- libxml2
- cgal-cpp
+165 -70
View File
@@ -26,7 +26,7 @@
# * cmake * git * bzip2 * tar * c(++) compilers * autoconf #
# #
# if building with USE_OCCT additionally: #
# * freetype * glx.h #
# * glx.h #
# #
# if building with OCCT 7.4.0 additionally: #
# * libfontconfig1-dev #
@@ -39,14 +39,14 @@
# #
# on debian 7.8 these can be obtained with: #
# $ apt-get install git gcc g++ autoconf bison bzip2 cmake #
# libfreetype6-dev mesa-common-dev libffi-dev libfontconfig1-dev #
# mesa-common-dev libffi-dev libfontconfig1-dev #
# #
# on ubuntu 14.04: #
# $ apt-get install git gcc g++ autoconf bison make cmake #
# libfreetype6-dev mesa-common-dev libffi-dev libfontconfig1-dev #
# mesa-common-dev libffi-dev libfontconfig1-dev #
# #
# on OS X El Capitan with homebrew: #
# $ brew install git bison autoconf automake freetype libffi cmake #
# $ brew install git bison autoconf automake libffi cmake #
# #
###############################################################################
import logging
@@ -72,11 +72,11 @@ PROJECT_NAME = "IfcOpenShell"
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
PYTHON_VERSIONS = ["3.6.14", "3.7.13", "3.8.13", "3.9.11", "3.10.3"]
PYTHON_VERSIONS = ["3.6.14", "3.7.13", "3.8.13", "3.9.11", "3.10.3", "3.11.0"]
JSON_VERSION = "v3.6.1"
OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.5.3"
BOOST_VERSION = "1.71.0"
BOOST_VERSION = "1.80.0"
PCRE_VERSION = "8.41"
LIBXML2_VERSION = "2.9.11"
SWIG_VERSION = "4.0.2"
@@ -103,6 +103,9 @@ curl = "curl"
wget = "wget"
strip = "strip"
explicit_targets = [s for s in sys.argv[1:] if not s.startswith("-")]
flags = set(s.lstrip('-') for s in sys.argv[1:] if s.startswith("-"))
# Helper function for coloured printing
NO_COLOR = "\033[0m" # <ref>http://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux</ref>
@@ -135,9 +138,11 @@ if platform.system() == "Darwin":
IFCOS_NUM_BUILD_PROCS = os.getenv("IFCOS_NUM_BUILD_PROCS", multiprocessing.cpu_count() + 1)
CMAKE_DIR = os.path.realpath(os.path.join("..", "cmake"))
CMAKE_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "cmake"))
path = ["..", "build", platform.system(), platform.machine()]
build_dir = os.environ.get("BUILD_DIR", os.path.join(os.path.dirname(__file__), "..", "build"))
path = [build_dir, platform.system(), "wasm" if "wasm" in flags else platform.machine()]
if TOOLSET:
path.append(TOOLSET)
DEFAULT_DEPS_DIR = os.path.realpath(os.path.join(*path))
@@ -186,11 +191,12 @@ dependency_tree = {
'boost': (),
'libxml2': (),
'python': (),
'occ': (),
'occ': ('freetype',),
'pcre': (),
'json': (),
'hdf5': (),
'cgal': ()
'cgal': (),
'freetype': (),
}
def v(dep):
@@ -199,10 +205,12 @@ def v(dep):
for x in v(d):
yield x
tgts = [s for s in sys.argv[1:] if not s.startswith("-")]
flags = set(s for s in sys.argv[1:] if s.startswith("-"))
if "v" in flags:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
BUILD_STATIC = not "-shared" in flags
BUILD_STATIC = "shared" not in flags
ENABLE_FLAG = "--enable-static" if BUILD_STATIC else "--enable-shared"
DISABLE_FLAG = "--disable-shared" if BUILD_STATIC else "--disable-static"
LINK_TYPE = "static" if BUILD_STATIC else "shared"
@@ -210,10 +218,15 @@ LINK_TYPE_UCFIRST = LINK_TYPE[0].upper() + LINK_TYPE[1:]
LIBRARY_EXT = "a" if BUILD_STATIC else "so"
PIC = "-fPIC" if BUILD_STATIC else ""
if len(tgts):
targets = set(sum((list(v(target)) for target in tgts), []))
if any(f.startswith("py-") for f in flags):
PYTHON_VERSIONS = [pyv for pyv in PYTHON_VERSIONS if "py-%s" % "".join(pyv.split('.')[0:2]) in flags]
if len(explicit_targets):
targets = set(sum((list(v(target)) for target in explicit_targets), []))
else:
targets = set(dependency_tree.keys())
targets = set(t for t in targets if 'without-%s' % t.lower() not in flags)
print("Building:", *sorted(targets, key=lambda t: len(list(v(t)))))
@@ -237,7 +250,7 @@ if not os.path.exists(LOG_FILE):
open(LOG_FILE, "w").close()
logger.info(f"using command log file '{LOG_FILE}'")
def run(cmds, cwd=None):
def run(cmds, cwd=None, can_fail=False):
"""
Wraps `subprocess.Popen.communicate()` and logs the command being executed,
@@ -254,7 +267,7 @@ def run(cmds, cwd=None):
log_file_handle.close()
logger.debug(f"command returned {proc.returncode}")
if proc.returncode != 0:
if proc.returncode != 0 and not can_fail:
print("-" * 70)
print(stderr)
print("-" * 70)
@@ -266,6 +279,9 @@ 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)]
if run(["sw_vers", "-productVersion"]) < "10.16":
# Apparently not supported
PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) < (3, 11)]
BOOST_VERSION_UNDERSCORE = BOOST_VERSION.replace(".", "_")
@@ -281,7 +297,12 @@ def run_autoconf(arg1, configure_args, cwd):
run([bash, "./autogen.sh"], cwd=os.path.realpath(os.path.join(cwd, ".."))) # only run autogen.sh in the directory it is located and use cwd to achieve that in order to not mess up things
# Using `sh` over `bash` fixes issues with building swig
prefix = os.path.realpath(f"{DEPS_DIR}/install/{arg1}")
run(["/bin/sh", "../configure"] + configure_args + [f"--prefix={prefix}"], cwd=cwd)
wasm = []
if "wasm" in flags:
wasm.append("emconfigure")
run([*wasm, "/bin/sh", "../configure"] + configure_args + [f"--prefix={prefix}"], cwd=cwd)
def run_cmake(arg1, cmake_args, cmake_dir=None, cwd=None):
@@ -289,7 +310,12 @@ def run_cmake(arg1, cmake_args, cmake_dir=None, cwd=None):
P = ".."
else:
P = cmake_dir
run(["cmake", P] + cmake_args + [f"-DCMAKE_BUILD_TYPE={BUILD_CFG}"], cwd=cwd)
wasm = []
if "wasm" in flags:
wasm.append("emcmake")
run([*wasm, "cmake", P, *cmake_args, f"-DCMAKE_BUILD_TYPE={BUILD_CFG}"], cwd=cwd)
def git_clone_or_pull_repository(clone_url, target_dir, revision=None):
@@ -371,12 +397,15 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
urlretrieve(url, os.path.join(extract_dir, path))
if patch is not None:
patch_abs = os.path.abspath(os.path.join(os.path.dirname(__file__), patch))
if os.path.exists(patch_abs):
try: run(["patch", "-p1", "--batch", "--forward", "-i", patch_abs], cwd=extract_dir)
except Exception as e:
# Assert that the patch has already been applied
run(["patch", "-p1", "--batch", "--reverse", "--dry-run", "-i", patch_abs], cwd=extract_dir)
if isinstance(patch, str):
patch = [patch]
for p in patch:
patch_abs = os.path.abspath(os.path.join(os.path.dirname(__file__), p))
if os.path.exists(patch_abs):
try: run(["patch", "-p1", "--batch", "--forward", "-i", patch_abs], cwd=extract_dir)
except Exception as e:
# Assert that the patch has already been applied
run(["patch", "-p1", "--batch", "--reverse", "--dry-run", "-i", patch_abs], cwd=extract_dir)
if mode == "ctest":
run(["ctest", "-S", "HDF5config.cmake,BUILD_GENERATOR=Unix", "-C", BUILD_CFG, "-V", "-O", "hdf5.log"], cwd=extract_dir)
@@ -407,7 +436,7 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
logger.info(f"\rConfiguring {name}...")
run([bash, "./bootstrap.sh"], cwd=extract_dir)
logger.info(f"\rBuilding {name}... ")
run(["./b2", f"-j{IFCOS_NUM_BUILD_PROCS}"] + build_tool_args, cwd=extract_dir)
run(["./b2", f"-j{IFCOS_NUM_BUILD_PROCS}"] + build_tool_args, cwd=extract_dir, can_fail="wasm" in flags)
logger.info(f"\rInstalling {name}... ")
shutil.copytree(os.path.join(extract_dir, "boost"), os.path.join(DEPS_DIR, "install", f"boost-{BOOST_VERSION}", "boost"))
logger.info(f"\rInstalled {name} \n")
@@ -418,11 +447,13 @@ cecho("Collecting dependencies:", GREEN)
# TODO: This is untested
ADDITIONAL_ARGS = []
BOOST_ADDRESS_MODEL = []
if platform.system() == "Darwin":
ADDITIONAL_ARGS = [f"-mmacosx-version-min={TOOLSET}"] + ADDITIONAL_ARGS
if "wasm" in flags:
ADDITIONAL_ARGS.extend(("-sWASM_BIGINT", "-fexceptions"))
# If the linker supports GC sections, set it up to reduce binary file size
# -fPIC is required for the shared libraries to work
@@ -431,7 +462,7 @@ CFLAGS = os.environ.get("CFLAGS", "")
LDFLAGS = os.environ.get("LDFLAGS", "")
ADDITIONAL_ARGS_STR = " ".join(ADDITIONAL_ARGS)
if sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev/null"]) != 0:
if "wasm" not in flags and sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev/null"]) != 0:
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
if BUILD_STATIC:
@@ -501,8 +532,27 @@ if "swig" in targets:
download_tool=download_tool_git,
revision=f"rel-{SWIG_VERSION}"
)
if "freetype" in targets:
build_dependency(
name=f"freetype",
mode="cmake",
build_tool_args=[
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/freetype"
],
download_url = "https://github.com/freetype/freetype",
download_name = "freetype2",
download_tool=download_tool_git,
)
if USE_OCCT and "occ" in targets:
patches = []
if OCCT_VERSION < "7.4":
patches.append("./patches/occt/enable-exception-handling.patch")
if "wasm" in flags:
patches.append("./patches/occt/no_em_js.patch")
build_dependency(
name=f"occt-{OCCT_VERSION}",
mode="cmake",
@@ -510,12 +560,13 @@ if USE_OCCT and "occ" in targets:
f"-DINSTALL_DIR={DEPS_DIR}/install/occt-{OCCT_VERSION}",
f"-DBUILD_LIBRARY_TYPE={LINK_TYPE_UCFIRST}",
"-DBUILD_MODULE_Draw=0",
"-DBUILD_RELEASE_DISABLE_EXCEPTIONS=Off"
"-DBUILD_RELEASE_DISABLE_EXCEPTIONS=Off",
f"-D3RDPARTY_FREETYPE_DIR={DEPS_DIR}/install/freetype"
],
download_url = "https://github.com/Open-Cascade-SAS/OCCT",
download_name = "occt",
download_tool=download_tool_git,
patch=None if OCCT_VERSION >= "7.4" else "./patches/occt/enable-exception-handling.patch",
patch=patches,
revision="V" + OCCT_VERSION.replace('.', '_')
)
elif "occ" in targets:
@@ -566,11 +617,11 @@ if "OpenCOLLADA" in targets:
download_url="https://github.com/KhronosGroup/OpenCOLLADA.git",
download_name="OpenCOLLADA",
download_tool=download_tool_git,
patch="./patches/opencollada/pr622_and_disable_subdirs.patch",
patch=("./patches/opencollada/pr622_and_disable_subdirs.patch", "./patches/opencollada/remove_tr1.patch"),
revision=OPENCOLLADA_VERSION
)
if "python" in targets and not USE_CURRENT_PYTHON_VERSION:
if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flags:
# Python should not be built with -fvisibility=hidden, from experience that introduces segfaults
OLD_CXX_FLAGS = os.environ["CXXFLAGS"]
OLD_C_FLAGS = os.environ["CFLAGS"]
@@ -609,6 +660,9 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION:
if "boost" in targets:
str_concat = lambda prefix: lambda postfix: "" if postfix.strip() == "" else "=".join((prefix, postfix.strip()))
toolset = []
if "wasm" in flags:
toolset.append("toolset=emscripten")
build_dependency(
f"boost-{BOOST_VERSION}",
mode="bjam",
@@ -620,21 +674,30 @@ if "boost" in targets:
"--with-thread",
"--with-date_time",
"--with-iostreams",
f"link={LINK_TYPE}"
] + \
BOOST_ADDRESS_MODEL + \
list(map(str_concat("cxxflags"), CXXFLAGS.strip().split(' '))) + \
list(map(str_concat("linkflags"), LDFLAGS.strip().split(' '))) + \
["stage", "-s", "NO_BZIP2=1"],
f"link={LINK_TYPE}",
*toolset,
*map(str_concat("cxxflags"), CXXFLAGS.strip().split(' ')),
*map(str_concat("linkflags"), LDFLAGS.strip().split(' ')),
"stage", "-s", "NO_BZIP2=1"],
download_url=BOOST_LOCATION,
patch="./patches/boost/boostorg_regex_62.patch",
download_name=f"boost_{BOOST_VERSION_UNDERSCORE}.tar.bz2"
)
if "wasm" in flags:
# only supported on nix for now
run(("find", ".", "-name", "*.bc", "-exec", "bash", "-c", "emar q ${1%.bc}.a $1", "bash", "{}", ";"), cwd=f"{DEPS_DIR}/install/boost-{BOOST_VERSION}/lib")
if "cgal" in targets:
gmp_args = []
mpfr_args = []
if "wasm" in flags:
gmp_args.extend(("--disable-assembly", "--host", "none", "--enable-cxx"))
mpfr_args.extend(("--host", "none"))
build_dependency(
name=f"gmp-{GMP_VERSION}",
mode="autoconf",
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, "--with-pic"],
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, "--with-pic", *gmp_args],
download_url="https://ftp.gnu.org/gnu/gmp/",
download_name=f"gmp-{GMP_VERSION}.tar.bz2"
)
@@ -642,7 +705,7 @@ if "cgal" in targets:
build_dependency(
name=f"mpfr-{MPFR_VERSION}",
mode="autoconf",
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, f"--with-gmp={DEPS_DIR}/install/gmp-{GMP_VERSION}"],
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, *mpfr_args, 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"
)
@@ -676,18 +739,8 @@ os.makedirs(IFCOS_DIR, exist_ok=True)
executables_dir = os.path.join(IFCOS_DIR, "executables")
os.makedirs(executables_dir, exist_ok=True)
logger.info("\rConfiguring executables...")
OFF_ON = ["OFF", "ON"]
exec_args = [
"-DBUILD_IFCGEOM=" +OFF_ON["IfcGeom" in targets],
"-DBUILD_GEOMSERVER=" +OFF_ON["IfcGeomServer" in targets],
"-DBUILD_CONVERT=" +OFF_ON["IfcConvert" in targets],
"-DBUILD_IFCPYTHON=" "OFF",
"-DCMAKE_INSTALL_PREFIX=" f"{DEPS_DIR}/install/ifcopenshell",
]
cmake_args = [
"-DUSE_MMAP=" "OFF",
"-DBUILD_EXAMPLES=" "OFF",
@@ -699,6 +752,11 @@ cmake_args = [
"-DADD_COMMIT_SHA=" +("On" if ADD_COMMIT_SHA else "Off")
]
if "wasm" in flags:
# Boost is built by the build script so should not be found
# inside of the sysroot set by the emscriptem toolchain
cmake_args.append("-DWASM_BUILD=On")
if "cgal" in targets:
cmake_args.extend([
"-DCGAL_INCLUDE_DIR=" f"{DEPS_DIR}/install/cgal-{CGAL_VERSION}/include",
@@ -750,20 +808,36 @@ if "hdf5" in targets:
"-DHDF5_INCLUDE_DIR=" f"{DEPS_DIR}/install/hdf5-{HDF5_VERSION}/include",
"-DHDF5_LIBRARY_DIR=" f"{DEPS_DIR}/install/hdf5-{HDF5_VERSION}/lib"
])
else:
cmake_args.append("-DHDF5_SUPPORT=Off")
run_cmake("", exec_args + cmake_args, cmake_dir=CMAKE_DIR, cwd=executables_dir)
if not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets):
logger.info("\rConfiguring executables...")
logger.info("\rBuilding executables... ")
exec_args = [
"-DBUILD_IFCGEOM=" +OFF_ON["IfcGeom" in targets],
"-DBUILD_GEOMSERVER=" +OFF_ON["IfcGeomServer" in targets],
"-DBUILD_CONVERT=" +OFF_ON["IfcConvert" in targets],
"-DBUILD_IFCPYTHON=" "OFF",
"-DCMAKE_INSTALL_PREFIX=" f"{DEPS_DIR}/install/ifcopenshell",
]
run_cmake("", exec_args + cmake_args, cmake_dir=CMAKE_DIR, cwd=executables_dir)
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}"], cwd=executables_dir)
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=executables_dir)
logger.info("\rBuilding executables... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}"], cwd=executables_dir)
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=executables_dir)
if "IfcOpenShell-Python" in targets:
# On OSX the actual Python library is not linked against.
ADDITIONAL_ARGS = ""
if platform.system() == "Darwin":
ADDITIONAL_ARGS = "-Wl,-flat_namespace,-undefined,suppress"
if "wasm" in flags:
ADDITIONAL_ARGS = f"-Wl,-undefined,suppress -sSIDE_MODULE=2 -sEXPORTED_FUNCTIONS=_PyInit__ifcopenshell_wrapper"
os.environ["CXXFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CFLAGS"] = f"{CFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["LDFLAGS"] = f"{LDFLAGS} {ADDITIONAL_ARGS}"
@@ -780,36 +854,57 @@ if "IfcOpenShell-Python" in targets:
os.environ["PYTHON_LIBRARY_BASENAME"] = os.path.basename(python_library)
swig_when_built = []
if "swig" in targets:
swig_when_built.append(f"-DSWIG_EXECUTABLE={DEPS_DIR}/install/swig/bin/swig")
run_cmake("",
cmake_args + [
"-DPYTHON_LIBRARY=" +python_library,
"-DPYTHON_EXECUTABLE=" +python_executable,
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
# *([f"-DPYTHON_MODULE_INSTALL_DIR={os.environ['PYTHONPATH']}/ifcopenshell"] if "wasm" in flags else []),
*(["-DPYTHON_MODULE_INSTALL_DIR="+os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "package"))] if "wasm" in flags else []),
"-DPYTHON_INCLUDE_DIR=" +python_include,
"-DSWIG_EXECUTABLE=" f"{DEPS_DIR}/install/swig/bin/swig",
"-DCMAKE_INSTALL_PREFIX=" f"{DEPS_DIR}/install/ifcopenshell/tmp",
"-DUSERSPACE_PYTHON_PREFIX=" +["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}]
], cmake_dir=CMAKE_DIR, cwd=python_dir)
"-DUSERSPACE_PYTHON_PREFIX=" +["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
*swig_when_built],
cmake_dir=CMAKE_DIR, cwd=python_dir)
logger.info(f"\rBuilding python {python_version} wrapper... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "_ifcopenshell_wrapper"], cwd=python_dir)
run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
module_dir = os.path.dirname(run([python_executable, "-c", "import inspect, ifcopenshell; print(inspect.getfile(ifcopenshell))"]))
if python_executable:
module_dir = os.path.dirname(run([python_executable, "-c", "import inspect, ifcopenshell; print(inspect.getfile(ifcopenshell))"]))
if platform.system() != "Darwin":
# TODO: This symbol name depends on the Python version?
run([strip, "-s", "-K", "PyInit__ifcopenshell_wrapper", "_ifcopenshell_wrapper.so"], cwd=module_dir)
return module_dir
if platform.system() != "Darwin":
# TODO: This symbol name depends on the Python version?
run([strip, "-s", "-K", "PyInit__ifcopenshell_wrapper", "_ifcopenshell_wrapper.so"], cwd=module_dir)
return module_dir
if USE_CURRENT_PYTHON_VERSION:
python_info = sysconfig.get_paths()
python_lib = os.path.join(
sysconfig.get_config_var('LIBDIR'),
sysconfig.get_config_var('multiarchsubdir').replace("/", ""),
sysconfig.get_config_var("INSTSONAME")
if "wasm" in flags:
compile_python_wrapper(
f"{os.environ['PYMAJOR']}.{os.environ['PYMINOR']}.{os.environ['PYMICRO']}",
f"{os.environ['TARGETINSTALLDIR']}/lib/libpython{os.environ['PYMAJOR']}.{os.environ['PYMINOR']}.a",
os.environ['PYTHONINCLUDE'],
None
)
elif USE_CURRENT_PYTHON_VERSION:
python_info = sysconfig.get_paths()
py_path_components = [
sysconfig.get_config_var('LIBDIR'),
sysconfig.get_config_var("INSTSONAME")
]
if sysconfig.get_config_var('multiarchsubdir'):
py_path_components.insert(1, sysconfig.get_config_var('multiarchsubdir').replace("/", ""))
python_lib = os.path.join(*py_path_components)
compile_python_wrapper(platform.python_version(), python_lib, python_info["include"], sys.executable)
else:
for python_version in PYTHON_VERSIONS:
+20
View File
@@ -0,0 +1,20 @@
--- a/tools/build/src/tools/emscripten.jam
+++ b/tools/build/src/tools/emscripten.jam
@@ -6,6 +6,7 @@
import feature ;
import os ;
import toolset ;
+import generators ;
import common ;
import gcc ;
import type ;
@@ -52,6 +53,8 @@
<debug-symbols>off <debug-symbols>on
<rtti>off <rtti>on
;
+generators.override builtin.lib-generator : emscripten.prebuilt ;
+generators.override emscripten.searched-lib-generator : searched-lib-generator ;
type.set-generated-target-suffix EXE : <toolset>emscripten : "js" ;
type.set-generated-target-suffix OBJ : <toolset>emscripten : "bc" ;
+99
View File
@@ -0,0 +1,99 @@
diff --git a/src/Message/Message_PrinterSystemLog.cxx b/src/Message/Message_PrinterSystemLog.cxx
index 0c82c2167..a2e9e6d68 100644
--- a/src/Message/Message_PrinterSystemLog.cxx
+++ b/src/Message/Message_PrinterSystemLog.cxx
@@ -55,27 +55,6 @@
return ANDROID_LOG_DEBUG;
}
#elif defined(__EMSCRIPTEN__)
- #include <emscripten/emscripten.h>
-
- //! Print message to console.debug().
- EM_JS(void, occJSConsoleDebug, (const char* theStr), {
- console.debug(UTF8ToString(theStr));
- });
-
- //! Print message to console.info().
- EM_JS(void, occJSConsoleInfo, (const char* theStr), {
- console.info(UTF8ToString(theStr));
- });
-
- //! Print message to console.warn().
- EM_JS(void, occJSConsoleWarn, (const char* theStr), {
- console.warn(UTF8ToString(theStr));
- });
-
- //! Print message to console.error().
- EM_JS(void, occJSConsoleError, (const char* theStr), {
- console.error(UTF8ToString(theStr));
- });
#else
#include <syslog.h>
@@ -169,16 +148,6 @@ void Message_PrinterSystemLog::send (const TCollection_AsciiString& theString,
#elif defined(__ANDROID__)
__android_log_write (getAndroidLogPriority (theGravity), myEventSourceName.ToCString(), theString.ToCString());
#elif defined(__EMSCRIPTEN__)
- // don't use bogus emscripten_log() corrupting UNICODE strings
- switch (theGravity)
- {
- case Message_Trace: occJSConsoleDebug(theString.ToCString()); return;
- case Message_Info: occJSConsoleInfo (theString.ToCString()); return;
- case Message_Warning: occJSConsoleWarn (theString.ToCString()); return;
- case Message_Alarm: occJSConsoleError(theString.ToCString()); return;
- case Message_Fail: occJSConsoleError(theString.ToCString()); return;
- }
- occJSConsoleWarn (theString.ToCString());
#else
syslog (getSysLogPriority (theGravity), "%s", theString.ToCString());
#endif
diff --git a/src/OSD/OSD_MemInfo.cxx b/src/OSD/OSD_MemInfo.cxx
index 08a939beb..7c1fc79b3 100644
--- a/src/OSD/OSD_MemInfo.cxx
+++ b/src/OSD/OSD_MemInfo.cxx
@@ -37,15 +37,6 @@
#include <OSD_MemInfo.hxx>
-#if defined(__EMSCRIPTEN__)
- #include <emscripten.h>
-
- //! Return WebAssembly heap size in bytes.
- EM_JS(size_t, OSD_MemInfo_getModuleHeapLength, (), {
- return Module.HEAP8.length;
- });
-#endif
-
// =======================================================================
// function : OSD_MemInfo
// purpose :
@@ -156,29 +147,6 @@ void OSD_MemInfo::Update()
}
#elif defined(__EMSCRIPTEN__)
- if (IsActive (MemHeapUsage)
- || IsActive (MemWorkingSet)
- || IsActive (MemWorkingSetPeak))
- {
- // /proc/%d/status is not emulated - get more info from mallinfo()
- const struct mallinfo aMI = mallinfo();
- if (IsActive (MemHeapUsage))
- {
- myCounters[MemHeapUsage] = aMI.uordblks;
- }
- if (IsActive (MemWorkingSet))
- {
- myCounters[MemWorkingSet] = aMI.uordblks;
- }
- if (IsActive (MemWorkingSetPeak))
- {
- myCounters[MemWorkingSetPeak] = aMI.usmblks;
- }
- }
- if (IsActive (MemVirtual))
- {
- myCounters[MemVirtual] = OSD_MemInfo_getModuleHeapLength();
- }
#elif (defined(__linux__) || defined(__linux))
if (IsActive (MemHeapUsage))
{
+81
View File
@@ -0,0 +1,81 @@
diff --git a/COLLADABaseUtils/include/COLLADABUhash_map.h b/COLLADABaseUtils/include/COLLADABUhash_map.h
index 8ab0fb9b..12503bfb 100644
--- a/COLLADABaseUtils/include/COLLADABUhash_map.h
+++ b/COLLADABaseUtils/include/COLLADABUhash_map.h
@@ -32,9 +32,9 @@
#include <unordered_map>
#include <unordered_set>
- #define COLLADABU_HASH_MAP std::tr1::unordered_map
- #define COLLADABU_HASH_MULTIMAP std::tr1::unordered_multimap
- #define COLLADABU_HASH_SET std::tr1::unordered_set
+ #define COLLADABU_HASH_MAP std::unordered_map
+ #define COLLADABU_HASH_MULTIMAP std::unordered_multimap
+ #define COLLADABU_HASH_SET std::unordered_set
#define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
#define COLLADABU_HASH_NAMESPACE_CLOSE }
#define COLLADABU_HASH_FUN hash
@@ -50,12 +50,12 @@
#define COLLADABU_HASH_FUN hash
#endif
#elif defined(__MINGW32__) || defined(__MINGW64__)
- #include <tr1/unordered_map>
- #include <tr1/unordered_set>
+ #include <unordered_map>
+ #include <unordered_set>
- #define COLLADABU_HASH_MAP std::tr1::unordered_map
- #define COLLADABU_HASH_MULTIMAP std::tr1::unordered_multimap
- #define COLLADABU_HASH_SET std::tr1::unordered_set
+ #define COLLADABU_HASH_MAP std::unordered_map
+ #define COLLADABU_HASH_MULTIMAP std::unordered_multimap
+ #define COLLADABU_HASH_SET std::unordered_set
#define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
#define COLLADABU_HASH_NAMESPACE_CLOSE }
#define COLLADABU_HASH_FUN hash
@@ -107,12 +107,12 @@
#define COLLADABU_HASH_NAMESPACE_CLOSE
#define COLLADABU_HASH_FUN hash
#else
- #include <tr1/unordered_map>
- #include <tr1/unordered_set>
+ #include <unordered_map>
+ #include <unordered_set>
- #define COLLADABU_HASH_MAP std::tr1::unordered_map
- #define COLLADABU_HASH_MULTIMAP std::tr1::unordered_multimap
- #define COLLADABU_HASH_SET std::tr1::unordered_set
+ #define COLLADABU_HASH_MAP std::unordered_map
+ #define COLLADABU_HASH_MULTIMAP std::unordered_multimap
+ #define COLLADABU_HASH_SET std::unordered_set
#define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
#define COLLADABU_HASH_NAMESPACE_CLOSE }
#define COLLADABU_HASH_FUN hash
diff --git a/common/libBuffer/include/CommonFWriteBufferFlusher.h b/common/libBuffer/include/CommonFWriteBufferFlusher.h
index c7af45b2..fac4f133 100644
--- a/common/libBuffer/include/CommonFWriteBufferFlusher.h
+++ b/common/libBuffer/include/CommonFWriteBufferFlusher.h
@@ -15,12 +15,12 @@
#if (defined(WIN64) || defined(_WIN64) || defined(__WIN64__)) || (defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) || defined(__APPLE__))
#if defined(__GNUC__) && !defined(_LIBCPP_VERSION)
-# include <tr1/unordered_map>
+# include <unordered_map>
#else
# include <unordered_map>
#endif
#else
-# include <tr1/unordered_map>
+# include <unordered_map>
#endif
#ifdef _LIBCPP_VERSION
@@ -58,7 +58,7 @@ namespace Common
#else
typedef __int64 FilePosType;
#endif
- typedef std::tr1::unordered_map<MarkId, FilePosType > MarkIdToFilePos;
+ typedef std::unordered_map<MarkId, FilePosType > MarkIdToFilePos;
public:
static const size_t DEFAUL_BUFFER_SIZE = 64*1024;
+18
View File
@@ -0,0 +1,18 @@
package:
name: ifcopenshell
version: 0.7.0
source:
path: IfcOpenShell
build:
script: |
python nix/build-all.py --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py310 IfcOpenShell-Python
cp pyodide/setup.py .
about:
home: http://ifcopenshell.org
license: LGPL-3.0-or-later
summary: |
IfcOpenShell is an open source (LGPL) software library for
working with the Industry Foundation Classes (IFC) file format.
+11
View File
@@ -0,0 +1,11 @@
from setuptools import setup, find_packages
setup(name='IfcOpenShell',
version='0.7.0',
description='IfcOpenShell is an open source (LGPL) software library for working with the Industry Foundation Classes (IFC) file format.',
author='Thomas Krijnen',
author_email='thomas@aecgeeks.com',
url='http://ifcopenshell.org',
packages=find_packages(),
package_data={'': ['*.so']},
)
+1
View File
@@ -0,0 +1 @@
.tox
+37
View File
@@ -0,0 +1,37 @@
.PHONY: test
test:
tox
.PHONY: qa
qa:
black .
isort .
pylint ./src --output-format=colorized
.PHONY: typecheck
typecheck:
mypy src/bcf
.PHONY: license
license:
copyright-header --license LGPL3 --copyright-holder "Andrea Ghensi <andrea.ghensi@gmail.com>" --copyright-year "2022" --copyright-software "IfcOpenShell" --copyright-software-description "BCF XML file handling" -a ./ -o ./
.PHONY: coverage
coverage:
coverage run --source bcf -m pytest tests
coverage html
xdg-open htmlcov/index.html
.PHONY: clean
clean:
rm -rf htmlcov
# TODO: make this based on xsd file presence
.PHONY: models
models:
cd src && xsdata generate -p bcf.v2.model --unnest-classes --kw-only --slots -ds Google bcf/v2/xsd
cd src && xsdata generate -p bcf.v3.model --unnest-classes --kw-only --slots -ds Google bcf/v3/xsd
# .PHONY
# api:
# openapi-python-client generate --url https://api.swaggerhub.com/apis/buildingSMART/BCF/3.0
+31 -48
View File
@@ -1,64 +1,54 @@
# bcf
A simple Python implementation of BCF. The data model is described in `data.py`.
A simple Python implementation of BCF.
Manipulation of BCF-XML is available via `bcfxml.py` and manipulation of BCF-API
is available via `bcfapi.py`.
- BCF-XML version 2.1: Fully supported
- BCF-API version 2.1: Not supported, will probably tackle this after BCF-API v3.0
- BCF-XML version 3.0: Almost fully supported, except for the documents module
- BCF-API version 3.0: Almost fully supported, except for two requests.
It tries to support BCF-XML version 2.1 and 3.0, and BCF-API 3.0.
## bcfxml
The `bcfxml` module lets you interact with the BCF-XML standard.
The `bcfxml.load` function lets you read a BCF-XML file.
It takes care of using the right version based on the "bcf.version" file contained in the BCF package.
The BCF files are extracted and parsed on-demand, and edits are stored in memory until you call the `save` method.
```python
from bcf import bcfxml
from bcf.bcfxml import load
# Load a project
bcfxml = bcfxml.load("/path/to/file.bcf")
with load("/path/to/file.bcf") as bcfxml:
project = bcfxml.project
print(project.name)
# To edit a project, just modify the object directly
bcfxml.project.name = "New name"
# The project is also stored in the module
# project == bcfxml.project
project=bcfxml.get_project()
print(project.name)
# Get a dictionary of topics
topics = bcfxml.topics
# To edit a project, just modify the object directly
bcfxml.project.name = "New name"
bcfxml.edit_project()
for topic_handler in bcfxml.topics:
topic = topic_handler.topic
print("Topic guid is", topic.guid)
print("Topic title is", topic.title)
# The BCF file is extracted to this temporary directory
print(bcfxml.filepath)
# Fetch extra data about a topic
header = topic_handler.header
comments = topic_handler.comments
viewpoints = topic_handler.viewpoints
# Get a dictionary of topics
topics = bcfxml.get_topics()
for comment in comments:
print(comment.guid)
print(comment.comment)
print(comment.author)
# Note: topics == bcfxml.topics
for guid, topic in bcfxml.topics.items():
print("Topic guid is", guid)
print("Topic guid is", topic.guid)
print("Topic title is", topic.title)
# Get a particular topic
topic = bcfxml.get_topic(guid)
# Fetch extra data about a topic
header = bcfxml.get_header(guid)
comments = bcfxml.get_comments(guid)
viewpoints = bcfxml.get_viewpoints(guid)
# Modify a topic
topic.title = "New title"
# Note: comments == topic.comments, and so on
for comment_guid, comment in comments.items():
print(comment_guid)
print(comment.comment)
print(comment.author)
# Get a particular topic
topic = bcfxml.get_topic(guid)
# Modify a topic
topic.title = "New title"
bcfxml.edit_topic(topic)
bcfxml.save()
```
## bcfapi
@@ -92,10 +82,3 @@ print(data)
data = bcf_client.get_extensions(project_id)
print(data)
```
## Todo List
The remaining work that needs to be completed in `bcfxml.py` and `bcfapi.py`.
- For `bcfxml.py` two xsds support is remaining namely 'documents.xsd`and`extensions.xsd`.
- For `bcfapi.py` two requests that are `get_topics` and `get_comments` are remaining.
+7
View File
@@ -0,0 +1,7 @@
name: bcf-client
channels:
- conda-forge
dependencies:
- ifcopenshell
- xsdata
- numpy
+116 -2
View File
@@ -1,12 +1,126 @@
[build-system]
requires = [
"setuptools>=42",
"setuptools>=61",
"wheel"
]
build-backend = "setuptools.build_meta"
[project]
name = "bcf-client"
# author = "IfcOpenShell"
description = "BCF-XML file handler."
readme = "README.md"
requires-python = ">=3.8"
keywords = ["IFC", "BCF", "BIM", "eingineering"]
dependencies = [
"xsdata",
"numpy",
]
version = "0.0.1"
classifiers = [
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering",
"Topic :: Utilities",
]
[project.urls]
Source = "https://github.com/IfcOpenShell/IfcOpenShell"
Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues"
[tool.black]
line-length = 120
extend-exclude = "model"
[tool.isort]
profile = "black"
profile = "black"
extend_skip_glob = ["src/bcf/*/model/*"]
[tool.coverage.paths]
source = ["src"]
[tool.coverage.run]
branch = true
source = ["bcf"]
omit = ["*/model/*"]
[tool.coverage.report]
show_missing = true
fail_under = 65
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING",
"if __name__ == .__main__.:",
"Protocol",
]
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py310
isolated_build = true
skip_missing_interpreters = true
requires = tox-conda
[testenv]
conda_deps =
pytest
pytest-cov
coverage
conda_channels =
conda-forge
conda_env = environment.yml
commands = pytest --cov --cov-report=term tests
"""
[tool.mypy]
check_untyped_defs = true
disallow_any_generics = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_defs = true
no_implicit_optional = true
#no_implicit_reexport = true
show_column_numbers = true
show_error_codes = true
show_error_context = true
strict_equality = true
strict_optional = true
warn_redundant_casts = true
#warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
warn_unused_ignores = true
exclude= "src/bcf/v(2|3)/model"
plugins = "numpy.typing.mypy_plugin"
[[tool.mypy.overrides]]
module = "tests"
disallow_untyped_decorators = false
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = [
"pytest",
"pytest_mock",
"ifcopenshell",
"ifcopenshell.*",
]
ignore_missing_imports = true
[tool.pylint.main]
ignore = ["model"]
ignored-modules = ["bcf.v2.model", "bcf.v3.model", "xsdata"]
jobs = 0
disable="all"
enable="E" # B,B9,BLK,C,D,E,F,I,N,S,W
[tool.pylint.design]
max-args = 10
max-attributes = 10
[tool.pylint.format]
expected-line-ending-format = "LF"
max-line-length = 120
+7
View File
@@ -0,0 +1,7 @@
black
mypy
pylint
isort
xsdata
tox
tox-conda
-1
View File
@@ -1 +0,0 @@
xmlschema
-37
View File
@@ -1,37 +0,0 @@
[metadata]
name=bcf-client
version=0.0.1
author = Ifcopenshell
description = A simple Python implementation of BCF
url = https://github.com/IfcOpenShell/IfcOpenShell
project_urls =
Code=https://github.com/IfcOpenShell/IfcOpenShell
Issues=https://github.com/IfcOpenShell/IfcOpenShell/issues
long_description = file: README.md
long_description_content_type = text/markdown
classifiers =
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Operating System :: OS Independent
Programming Language :: Python :: 3
Topic :: Scientific/Engineering
Topic :: Utilities
keywords =
Python
file formats
engineering
[options]
package_dir =
= src
packages = find:
python_requires = >=3
install_requires=
xmlschema
include_package_data = True
[options.packages.find]
where = src
[flake8]
max-line-length = 120
ignore = E24, E121, E123, E126, E203, E226, E704, E741, W503, W504
+3
View File
@@ -0,0 +1,3 @@
from setuptools import setup
setup()
+58 -50
View File
@@ -1,61 +1,69 @@
# BCF - BCF Python library
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
#
# This file is part of BCF.
#
# BCF is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BCF 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
"""
BCF - BCF Python library
Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
Copyright (C) 2022 Andrea Ghensi <andrea.ghensi@gmail.com>
This file is part of BCF.
import os.path
BCF is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
BCF 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 Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with BCF. If not, see <http://www.gnu.org/licenses/>.
"""
import zipfile
import tempfile
from xml.dom import minidom
from pathlib import Path
from typing import Optional, Union
from bcf.v2.bcfxml import BcfXml as BcfXml2
from bcf.v3.bcfxml import BcfXml as BcfXml3
from bcf.v3.model import Version
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
def load(filepath):
filepath = extract_project(filepath)
if os.path.isfile(os.path.join(filepath, "bcf.version")):
version_path = os.path.join(filepath, "bcf.version")
version_id = get_version(version_path)
# TODO: we actually coded it for 2.1, let's check the difference between 2.0 and 2.1
if version_id == "2.1" or version_id == "2.0":
from bcf.v2.bcfxml import BcfXml
def load(
filepath: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None
) -> Optional[Union[BcfXml2, BcfXml3]]:
"""
Load a BCF file.
bcfxml = BcfXml()
bcfxml.filepath = filepath
return bcfxml
elif version_id == "3.0":
from bcf.v3.bcfxml import BcfXml
Args:
filepath: The path to the BCF file.
bcfxml = BcfXml()
bcfxml.filepath = filepath
return bcfxml
else:
raise Exception(f"Version {version_id} not supported.")
Returns:
The loaded BCF file.
Raises:
ValueError: If the BCF version is not supported.
"""
xml_handler = xml_handler or XmlParserSerializer()
version_id = _get_version(filepath, xml_handler)
if version_id in {"2.1", "2.0"}:
return BcfXml2.load(filepath, xml_handler)
if version_id == "3.0":
return BcfXml3.load(filepath, xml_handler)
raise ValueError(f"Version {version_id} not supported.")
def get_version(version_path):
xmlparse = minidom.parse(version_path)
version_el = xmlparse.getElementsByTagName("Version")[0]
version = version_el.getAttribute("VersionId")
return version
def _get_version(filepath: Union[str, Path], xml_handler: Optional[AbstractXmlParserSerializer] = None) -> str:
"""
Returns the version of the BCF file.
Args:
filepath: The path to the BCF file.
xml_handler: The XML handler. If none is given, XmlParserSerializer is used.
def extract_project(filepath):
if not filepath:
return
zip_file = zipfile.ZipFile(filepath)
filepath = tempfile.mkdtemp()
zip_file.extractall(filepath)
return filepath
Returns:
The version of the BCF file.
"""
xml_handler = xml_handler or XmlParserSerializer()
with zipfile.ZipFile(filepath) as bcf_zip:
version = xml_handler.parse(bcf_zip.read("bcf.version"), Version)
return version.version_id
+44
View File
@@ -0,0 +1,44 @@
import numpy as np
from numpy.typing import NDArray
def calc_camera_vectors(
elem_placement: NDArray[np.float_],
) -> tuple[NDArray[np.float_], NDArray[np.float_], NDArray[np.float_]]:
"""
Calculate the vectors of a camera pointing to an element.
Args:
elem_placement: Placement matrix of an element.
Returns:
Camera position, direction and up vectors
"""
target_position = elem_placement[:3, 3]
camera_position = target_position + np.array((5, 5, 5))
camera_direction = unit_vector(camera_position - target_position)
camera_right = unit_vector(np.cross(np.array([0.0, 0.0, 1.0]), camera_direction))
camera_up = unit_vector(np.cross(camera_direction, camera_right))
rotation_transform = np.eye(4)
rotation_transform[0, :3] = camera_right
rotation_transform[1, :3] = camera_up
rotation_transform[2, :3] = camera_direction
translation_transform = np.eye(4)
translation_transform[:3, -1] = -camera_position
look_at_transform = np.matmul(rotation_transform, translation_transform)
mat = np.linalg.inv(look_at_transform)
return camera_position, -mat[:3, 2], mat[:3, 1]
def unit_vector(v: NDArray[np.float_]) -> NDArray[np.float_]:
"""
Return the unit vector of a vector.
Args:
v: vector
Returns:
unit vector.
"""
norm = np.linalg.norm(v)
return v if norm == 0 else v / norm
+55
View File
@@ -0,0 +1,55 @@
"""
In Memory Zip File management, taken from ruamel.std.zipfile
Copyright (c) 2017-2020 Anthon van der Neut, Ruamel bvba
original idea from https://stackoverflow.com/a/19722365/1307905
"""
import zipfile
from io import BytesIO
from os import PathLike
from pathlib import Path
from typing import Any, Optional, Protocol
class ZipFileInterface(Protocol):
def writestr(self, filename_in_zip: str | zipfile.ZipInfo, file_contents: bytes | str) -> None:
...
class InMemoryZipFile:
def __init__(
self, file_name: Optional[str | Path] = None, compression: int = zipfile.ZIP_DEFLATED, debug: int = 0
) -> None:
# Create the in-memory file-like object
self._file_name: Optional[str | Path] = str(file_name) if hasattr(file_name, "_from_parts") else file_name
self.in_memory_data = BytesIO()
# Create the in-memory zipfile
self.in_memory_zip = zipfile.ZipFile(self.in_memory_data, "w", compression, False)
self.in_memory_zip.debug = debug
def writestr(self, filename_in_zip: str | zipfile.ZipInfo, file_contents: bytes | str) -> None:
"""Appends a file with name filename_in_zip and contents of
file_contents to the in-memory zip."""
self.in_memory_zip.writestr(filename_in_zip, file_contents)
def write_to_file(self, filename: str | bytes | PathLike[str] | PathLike[bytes] | int) -> None:
"""Writes the in-memory zip to a file."""
# Mark the files as having been created on Windows so that
# Unix permissions are not inferred as 0000
for zfile in self.in_memory_zip.filelist:
zfile.create_system = 0
self.in_memory_zip.close()
with open(filename, "wb") as f:
f.write(self.data)
@property
def data(self) -> bytes:
return self.in_memory_data.getvalue()
def __enter__(self) -> "InMemoryZipFile":
return self
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
if self._file_name:
self.write_to_file(self._file_name)
View File
-2
View File
@@ -1,4 +1,3 @@
# BCF - BCF Python library
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -16,4 +15,3 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
File diff suppressed because it is too large Load Diff
-198
View File
@@ -1,198 +0,0 @@
# BCF - BCF Python library
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BCF.
#
# BCF is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BCF 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
class Project:
def __init__(self):
self.project_id = ""
self.name = ""
self.extension_schema = ""
class BimSnippet:
def __init__(self):
self.snippet_type = None
self.is_external = False
self.reference = None
self.reference_schema = None
class DocumentReference:
def __init__(self):
self.referenced_document = None
self.description = None
self.guid = None
self.is_external = False
class RelatedTopic:
def __init__(self):
self.guid = None
class HeaderFile:
def __init__(self):
self.filename = None
self.date = None
self.reference = None
self.ifc_project = None
self.ifc_spatial_structure_element = None
self.is_external = True
class Header:
def __init__(self):
self.files = []
class Topic:
def __init__(self):
self.reference_links = []
self.title = ""
self.priority = None
self.index = None # Deprecated, stored, but ignored
self.labels = []
self.creation_date = None
self.creation_author = None
self.modified_date = None
self.modified_author = None
self.due_date = None
self.assigned_to = None
self.stage = None
self.description = None
self.bim_snippet = None
self.document_references = []
self.related_topics = []
self.topic_status = None
self.topic_type = None
self.guid = None
self.header = None
self.comments = {}
self.viewpoints = {}
class Comment:
def __init__(self):
self.guid = None
self.date = None
self.author = None
self.comment = None
self.viewpoint = None
self.modified_date = None
self.modified_author = None
self.topic_guid = None # Part of BCF-API
class ViewSetupHints:
def __init__(self):
self.spaces_visible = False
self.space_boundaries_visible = False
self.openings_visible = False
class Component:
def __init__(self):
self.originating_system = None
self.authoring_tool_id = None
self.ifc_guid = None
class ComponentVisibility:
def __init__(self):
self.exceptions = []
self.default_visibility = False
class Color:
def __init__(self):
self.color = None
self.components = []
class Components:
def __init__(self):
self.view_setup_hints = None
self.selection = []
self.visibility = None
self.coloring = []
class Point:
def __init__(self):
self.x = 0
self.y = 0
self.z = 0
class Direction(Point):
pass
class OrthogonalCamera:
def __init__(self):
self.camera_view_point = Point()
self.camera_direction = Direction()
self.camera_up_vector = Direction()
self.view_to_world_scale = 1.0
class PerspectiveCamera:
def __init__(self):
self.camera_view_point = Point()
self.camera_direction = Direction()
self.camera_up_vector = Direction()
self.field_of_view = 60.0
class Line:
def __init__(self):
self.start_point = Point()
self.end_point = Point()
class ClippingPlane:
def __init__(self):
self.location = Point()
self.direction = Direction()
class Bitmap:
def __init__(self):
self.reference = "" # Only in BCF-XML
self.bitmap_data = None # Only in BCF-API
self.bitmap_format = "PNG" # Enum of png or jpg
self.location = Point()
self.normal = Direction()
self.up = Direction()
self.height = 1.0
class Viewpoint:
def __init__(self):
self.guid = None
self.viewpoint = None
self.snapshot = None
self.index = None
self.components = None # It's not a list, despite the plural name
self.orthogonal_camera = None
self.perspective_camera = None
self.lines = []
self.clipping_planes = []
self.bitmaps = []
+70
View File
@@ -0,0 +1,70 @@
from bcf.v2.model.markup import (
BimSnippet,
Comment,
CommentViewpoint,
Header,
HeaderFile,
Markup,
Topic,
TopicDocumentReference,
TopicRelatedTopic,
ViewPoint,
)
from bcf.v2.model.project import Project, ProjectExtension
from bcf.v2.model.version import Version
from bcf.v2.model.visinfo import (
BitmapFormat,
ClippingPlane,
Component,
ComponentColoring,
ComponentColoringColor,
Components,
ComponentSelection,
ComponentVisibility,
ComponentVisibilityExceptions,
Direction,
Line,
OrthogonalCamera,
PerspectiveCamera,
Point,
ViewSetupHints,
VisualizationInfo,
VisualizationInfoBitmap,
VisualizationInfoClippingPlanes,
VisualizationInfoLines,
)
__all__ = [
"BimSnippet",
"Comment",
"CommentViewpoint",
"Header",
"HeaderFile",
"Markup",
"Topic",
"TopicDocumentReference",
"TopicRelatedTopic",
"ViewPoint",
"Project",
"ProjectExtension",
"Version",
"BitmapFormat",
"ClippingPlane",
"Component",
"ComponentColoring",
"ComponentColoringColor",
"ComponentSelection",
"ComponentVisibility",
"ComponentVisibilityExceptions",
"Components",
"Direction",
"Line",
"OrthogonalCamera",
"PerspectiveCamera",
"Point",
"ViewSetupHints",
"VisualizationInfo",
"VisualizationInfoBitmap",
"VisualizationInfoClippingPlanes",
"VisualizationInfoLines",
]
+461
View File
@@ -0,0 +1,461 @@
from dataclasses import dataclass, field
from typing import List, Optional
from xsdata.models.datatype import XmlDateTime
@dataclass(slots=True, kw_only=True)
class BimSnippet:
reference: str = field(
metadata={
"name": "Reference",
"type": "Element",
"namespace": "",
"required": True,
}
)
reference_schema: str = field(
metadata={
"name": "ReferenceSchema",
"type": "Element",
"namespace": "",
"required": True,
}
)
snippet_type: str = field(
metadata={
"name": "SnippetType",
"type": "Attribute",
"required": True,
}
)
is_external: bool = field(
default=False,
metadata={
"name": "isExternal",
"type": "Attribute",
}
)
@dataclass(slots=True, kw_only=True)
class CommentViewpoint:
class Meta:
global_type = False
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
}
)
@dataclass(slots=True, kw_only=True)
class HeaderFile:
class Meta:
global_type = False
filename: Optional[str] = field(
default=None,
metadata={
"name": "Filename",
"type": "Element",
"namespace": "",
}
)
date: Optional[XmlDateTime] = field(
default=None,
metadata={
"name": "Date",
"type": "Element",
"namespace": "",
}
)
reference: Optional[str] = field(
default=None,
metadata={
"name": "Reference",
"type": "Element",
"namespace": "",
}
)
ifc_project: Optional[str] = field(
default=None,
metadata={
"name": "IfcProject",
"type": "Attribute",
"length": 22,
"pattern": r"[0-9,A-Z,a-z,_$]*",
}
)
ifc_spatial_structure_element: Optional[str] = field(
default=None,
metadata={
"name": "IfcSpatialStructureElement",
"type": "Attribute",
"length": 22,
"pattern": r"[0-9,A-Z,a-z,_$]*",
}
)
is_external: bool = field(
default=True,
metadata={
"name": "isExternal",
"type": "Attribute",
}
)
@dataclass(slots=True, kw_only=True)
class TopicDocumentReference:
class Meta:
global_type = False
referenced_document: Optional[str] = field(
default=None,
metadata={
"name": "ReferencedDocument",
"type": "Element",
"namespace": "",
}
)
description: Optional[str] = field(
default=None,
metadata={
"name": "Description",
"type": "Element",
"namespace": "",
}
)
guid: Optional[str] = field(
default=None,
metadata={
"name": "Guid",
"type": "Attribute",
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
}
)
is_external: bool = field(
default=False,
metadata={
"name": "isExternal",
"type": "Attribute",
}
)
@dataclass(slots=True, kw_only=True)
class TopicRelatedTopic:
class Meta:
global_type = False
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
}
)
@dataclass(slots=True, kw_only=True)
class ViewPoint:
viewpoint: Optional[str] = field(
default=None,
metadata={
"name": "Viewpoint",
"type": "Element",
"namespace": "",
}
)
snapshot: Optional[str] = field(
default=None,
metadata={
"name": "Snapshot",
"type": "Element",
"namespace": "",
}
)
index: Optional[int] = field(
default=None,
metadata={
"name": "Index",
"type": "Element",
"namespace": "",
}
)
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
}
)
@dataclass(slots=True, kw_only=True)
class Comment:
date: XmlDateTime = field(
metadata={
"name": "Date",
"type": "Element",
"namespace": "",
"required": True,
}
)
author: str = field(
metadata={
"name": "Author",
"type": "Element",
"namespace": "",
"required": True,
}
)
comment: str = field(
metadata={
"name": "Comment",
"type": "Element",
"namespace": "",
"required": True,
}
)
viewpoint: Optional[CommentViewpoint] = field(
default=None,
metadata={
"name": "Viewpoint",
"type": "Element",
"namespace": "",
}
)
modified_date: Optional[XmlDateTime] = field(
default=None,
metadata={
"name": "ModifiedDate",
"type": "Element",
"namespace": "",
}
)
modified_author: Optional[str] = field(
default=None,
metadata={
"name": "ModifiedAuthor",
"type": "Element",
"namespace": "",
}
)
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
}
)
@dataclass(slots=True, kw_only=True)
class Header:
file: List[HeaderFile] = field(
default_factory=list,
metadata={
"name": "File",
"type": "Element",
"namespace": "",
"min_occurs": 1,
}
)
@dataclass(slots=True, kw_only=True)
class Topic:
reference_link: List[str] = field(
default_factory=list,
metadata={
"name": "ReferenceLink",
"type": "Element",
"namespace": "",
}
)
title: str = field(
metadata={
"name": "Title",
"type": "Element",
"namespace": "",
"required": True,
}
)
priority: Optional[str] = field(
default=None,
metadata={
"name": "Priority",
"type": "Element",
"namespace": "",
}
)
index: Optional[int] = field(
default=None,
metadata={
"name": "Index",
"type": "Element",
"namespace": "",
}
)
labels: List[str] = field(
default_factory=list,
metadata={
"name": "Labels",
"type": "Element",
"namespace": "",
}
)
creation_date: XmlDateTime = field(
metadata={
"name": "CreationDate",
"type": "Element",
"namespace": "",
"required": True,
}
)
creation_author: str = field(
metadata={
"name": "CreationAuthor",
"type": "Element",
"namespace": "",
"required": True,
}
)
modified_date: Optional[XmlDateTime] = field(
default=None,
metadata={
"name": "ModifiedDate",
"type": "Element",
"namespace": "",
}
)
modified_author: Optional[str] = field(
default=None,
metadata={
"name": "ModifiedAuthor",
"type": "Element",
"namespace": "",
}
)
due_date: Optional[XmlDateTime] = field(
default=None,
metadata={
"name": "DueDate",
"type": "Element",
"namespace": "",
}
)
assigned_to: Optional[str] = field(
default=None,
metadata={
"name": "AssignedTo",
"type": "Element",
"namespace": "",
}
)
stage: Optional[str] = field(
default=None,
metadata={
"name": "Stage",
"type": "Element",
"namespace": "",
}
)
description: Optional[str] = field(
default=None,
metadata={
"name": "Description",
"type": "Element",
"namespace": "",
}
)
bim_snippet: Optional[BimSnippet] = field(
default=None,
metadata={
"name": "BimSnippet",
"type": "Element",
"namespace": "",
}
)
document_reference: List[TopicDocumentReference] = field(
default_factory=list,
metadata={
"name": "DocumentReference",
"type": "Element",
"namespace": "",
}
)
related_topic: List[TopicRelatedTopic] = field(
default_factory=list,
metadata={
"name": "RelatedTopic",
"type": "Element",
"namespace": "",
}
)
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
}
)
topic_type: Optional[str] = field(
default=None,
metadata={
"name": "TopicType",
"type": "Attribute",
}
)
topic_status: Optional[str] = field(
default=None,
metadata={
"name": "TopicStatus",
"type": "Attribute",
}
)
@dataclass(slots=True, kw_only=True)
class Markup:
header: Optional[Header] = field(
default=None,
metadata={
"name": "Header",
"type": "Element",
"namespace": "",
}
)
topic: Topic = field(
metadata={
"name": "Topic",
"type": "Element",
"namespace": "",
"required": True,
}
)
comment: List[Comment] = field(
default_factory=list,
metadata={
"name": "Comment",
"type": "Element",
"namespace": "",
}
)
viewpoints: List[ViewPoint] = field(
default_factory=list,
metadata={
"name": "Viewpoints",
"type": "Element",
"namespace": "",
}
)
+41
View File
@@ -0,0 +1,41 @@
from dataclasses import dataclass, field
from typing import Optional
@dataclass(slots=True, kw_only=True)
class Project:
name: Optional[str] = field(
default=None,
metadata={
"name": "Name",
"type": "Element",
"namespace": "",
}
)
project_id: str = field(
metadata={
"name": "ProjectId",
"type": "Attribute",
"required": True,
}
)
@dataclass(slots=True, kw_only=True)
class ProjectExtension:
project: Optional[Project] = field(
default=None,
metadata={
"name": "Project",
"type": "Element",
"namespace": "",
}
)
extension_schema: str = field(
metadata={
"name": "ExtensionSchema",
"type": "Element",
"namespace": "",
"required": True,
}
)
+21
View File
@@ -0,0 +1,21 @@
from dataclasses import dataclass, field
from typing import Optional
@dataclass(slots=True, kw_only=True)
class Version:
detailed_version: Optional[str] = field(
default=None,
metadata={
"name": "DetailedVersion",
"type": "Element",
"namespace": "",
}
)
version_id: Optional[str] = field(
default=None,
metadata={
"name": "VersionId",
"type": "Attribute",
}
)
+476
View File
@@ -0,0 +1,476 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional
class BitmapFormat(Enum):
PNG = "PNG"
JPG = "JPG"
@dataclass(slots=True, kw_only=True)
class Component:
originating_system: Optional[str] = field(
default=None,
metadata={
"name": "OriginatingSystem",
"type": "Element",
}
)
authoring_tool_id: Optional[str] = field(
default=None,
metadata={
"name": "AuthoringToolId",
"type": "Element",
}
)
ifc_guid: Optional[str] = field(
default=None,
metadata={
"name": "IfcGuid",
"type": "Attribute",
"length": 22,
"pattern": r"[0-9,A-Z,a-z,_$]*",
}
)
@dataclass(slots=True, kw_only=True)
class Direction:
x: float = field(
metadata={
"name": "X",
"type": "Element",
"required": True,
}
)
y: float = field(
metadata={
"name": "Y",
"type": "Element",
"required": True,
}
)
z: float = field(
metadata={
"name": "Z",
"type": "Element",
"required": True,
}
)
@dataclass(slots=True, kw_only=True)
class Point:
x: float = field(
metadata={
"name": "X",
"type": "Element",
"required": True,
}
)
y: float = field(
metadata={
"name": "Y",
"type": "Element",
"required": True,
}
)
z: float = field(
metadata={
"name": "Z",
"type": "Element",
"required": True,
}
)
@dataclass(slots=True, kw_only=True)
class ViewSetupHints:
spaces_visible: Optional[bool] = field(
default=None,
metadata={
"name": "SpacesVisible",
"type": "Attribute",
}
)
space_boundaries_visible: Optional[bool] = field(
default=None,
metadata={
"name": "SpaceBoundariesVisible",
"type": "Attribute",
}
)
openings_visible: Optional[bool] = field(
default=None,
metadata={
"name": "OpeningsVisible",
"type": "Attribute",
}
)
@dataclass(slots=True, kw_only=True)
class ClippingPlane:
location: Point = field(
metadata={
"name": "Location",
"type": "Element",
"required": True,
}
)
direction: Direction = field(
metadata={
"name": "Direction",
"type": "Element",
"required": True,
}
)
@dataclass(slots=True, kw_only=True)
class ComponentColoringColor:
class Meta:
global_type = False
component: List[Component] = field(
default_factory=list,
metadata={
"name": "Component",
"type": "Element",
"min_occurs": 1,
}
)
color: Optional[str] = field(
default=None,
metadata={
"name": "Color",
"type": "Attribute",
"pattern": r"[0-9,a-f,A-F]{6}([0-9,a-f,A-F]{2})?",
}
)
@dataclass(slots=True, kw_only=True)
class ComponentSelection:
component: List[Component] = field(
default_factory=list,
metadata={
"name": "Component",
"type": "Element",
"min_occurs": 1,
}
)
@dataclass(slots=True, kw_only=True)
class ComponentVisibilityExceptions:
class Meta:
global_type = False
component: List[Component] = field(
default_factory=list,
metadata={
"name": "Component",
"type": "Element",
"min_occurs": 1,
}
)
@dataclass(slots=True, kw_only=True)
class Line:
start_point: Point = field(
metadata={
"name": "StartPoint",
"type": "Element",
"required": True,
}
)
end_point: Point = field(
metadata={
"name": "EndPoint",
"type": "Element",
"required": True,
}
)
@dataclass(slots=True, kw_only=True)
class OrthogonalCamera:
"""
Attributes
camera_view_point:
camera_direction:
camera_up_vector:
view_to_world_scale: view's visible size in meters
"""
camera_view_point: Point = field(
metadata={
"name": "CameraViewPoint",
"type": "Element",
"required": True,
}
)
camera_direction: Direction = field(
metadata={
"name": "CameraDirection",
"type": "Element",
"required": True,
}
)
camera_up_vector: Direction = field(
metadata={
"name": "CameraUpVector",
"type": "Element",
"required": True,
}
)
view_to_world_scale: float = field(
metadata={
"name": "ViewToWorldScale",
"type": "Element",
"required": True,
}
)
@dataclass(slots=True, kw_only=True)
class PerspectiveCamera:
"""
Attributes
camera_view_point:
camera_direction:
camera_up_vector:
field_of_view: It is currently limited to a value between 45 and
60 degrees. This limitation will be dropped in the next
release and viewers should be expect values outside this
range in current implementations.
"""
camera_view_point: Point = field(
metadata={
"name": "CameraViewPoint",
"type": "Element",
"required": True,
}
)
camera_direction: Direction = field(
metadata={
"name": "CameraDirection",
"type": "Element",
"required": True,
}
)
camera_up_vector: Direction = field(
metadata={
"name": "CameraUpVector",
"type": "Element",
"required": True,
}
)
field_of_view: float = field(
metadata={
"name": "FieldOfView",
"type": "Element",
"required": True,
"min_inclusive": 1.0,
"max_inclusive": 170.0,
}
)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoBitmap:
class Meta:
global_type = False
bitmap: BitmapFormat = field(
metadata={
"name": "Bitmap",
"type": "Element",
"required": True,
}
)
reference: str = field(
metadata={
"name": "Reference",
"type": "Element",
"required": True,
}
)
location: Point = field(
metadata={
"name": "Location",
"type": "Element",
"required": True,
}
)
normal: Direction = field(
metadata={
"name": "Normal",
"type": "Element",
"required": True,
}
)
up: Direction = field(
metadata={
"name": "Up",
"type": "Element",
"required": True,
}
)
height: float = field(
metadata={
"name": "Height",
"type": "Element",
"required": True,
}
)
@dataclass(slots=True, kw_only=True)
class ComponentColoring:
color: List[ComponentColoringColor] = field(
default_factory=list,
metadata={
"name": "Color",
"type": "Element",
"min_occurs": 1,
}
)
@dataclass(slots=True, kw_only=True)
class ComponentVisibility:
exceptions: Optional[ComponentVisibilityExceptions] = field(
default=None,
metadata={
"name": "Exceptions",
"type": "Element",
}
)
default_visibility: Optional[bool] = field(
default=None,
metadata={
"name": "DefaultVisibility",
"type": "Attribute",
}
)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoClippingPlanes:
class Meta:
global_type = False
clipping_plane: List[ClippingPlane] = field(
default_factory=list,
metadata={
"name": "ClippingPlane",
"type": "Element",
}
)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoLines:
class Meta:
global_type = False
line: List[Line] = field(
default_factory=list,
metadata={
"name": "Line",
"type": "Element",
"min_occurs": 1,
}
)
@dataclass(slots=True, kw_only=True)
class Components:
view_setup_hints: Optional[ViewSetupHints] = field(
default=None,
metadata={
"name": "ViewSetupHints",
"type": "Element",
}
)
selection: Optional[ComponentSelection] = field(
default=None,
metadata={
"name": "Selection",
"type": "Element",
}
)
visibility: ComponentVisibility = field(
metadata={
"name": "Visibility",
"type": "Element",
"required": True,
}
)
coloring: Optional[ComponentColoring] = field(
default=None,
metadata={
"name": "Coloring",
"type": "Element",
}
)
@dataclass(slots=True, kw_only=True)
class VisualizationInfo:
"""
VisualizationInfo documentation.
"""
components: Optional[Components] = field(
default=None,
metadata={
"name": "Components",
"type": "Element",
}
)
orthogonal_camera: Optional[OrthogonalCamera] = field(
default=None,
metadata={
"name": "OrthogonalCamera",
"type": "Element",
}
)
perspective_camera: Optional[PerspectiveCamera] = field(
default=None,
metadata={
"name": "PerspectiveCamera",
"type": "Element",
}
)
lines: Optional[VisualizationInfoLines] = field(
default=None,
metadata={
"name": "Lines",
"type": "Element",
}
)
clipping_planes: Optional[VisualizationInfoClippingPlanes] = field(
default=None,
metadata={
"name": "ClippingPlanes",
"type": "Element",
}
)
bitmap: List[VisualizationInfoBitmap] = field(
default_factory=list,
metadata={
"name": "Bitmap",
"type": "Element",
}
)
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
}
)
+240
View File
@@ -0,0 +1,240 @@
"""BCF XML V2 Topic handler."""
import datetime
import uuid
import zipfile
from pathlib import Path
from typing import Any, Optional
from ifcopenshell import entity_instance
from xsdata.models.datatype import XmlDateTime
import bcf.v2.model as mdl
from bcf.inmemory_zipfile import ZipFileInterface
from bcf.v2.visinfo import VisualizationInfoHandler
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
class TopicHandler:
"""BCF Topic and related objects handler."""
def __init__(
self,
topic_dir: Optional[zipfile.Path] = None,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> None:
self._markup: Optional[mdl.Markup] = None
self._viewpoints: dict[str, VisualizationInfoHandler] = {}
self._reference_files: dict[str, bytes] = {}
self._document_references: dict[str, bytes] = {}
self._bim_snippet: Optional[bytes] = None
self._xml_handler = xml_handler or XmlParserSerializer()
self._topic_dir = topic_dir
@property
def markup(self) -> Optional[mdl.Markup]:
if not self._markup:
markup_path = self._topic_dir.joinpath("markup.bcf")
if markup_path.exists():
self._markup = self._xml_handler.parse(markup_path.read_bytes(), mdl.Markup)
return self._markup
@markup.setter
def markup(self, value: mdl.Markup) -> None:
self._markup = value
@property
def topic(self) -> mdl.Topic:
"""Return the Topic object."""
return self.markup.topic
@property
def guid(self) -> str:
"""Return the GUID of the topic."""
if self._markup:
return self.topic.guid
return self._topic_dir.name if self._topic_dir else ""
@property
def header(self) -> Optional[mdl.Header]:
"""Return the header of the topic."""
return self.markup.header if self.markup else None
@property
def comments(self) -> list[mdl.Comment]:
"""Return the comments of the topic."""
return self.markup.comment if self.markup else []
@property
def bim_snippet(self) -> Optional[bytes]:
if not self._bim_snippet and self._topic_dir:
self._bim_snippet = self._load_bim_snippet()
return self._bim_snippet
@bim_snippet.setter
def bim_snippet(self, value: bytes) -> None:
self._bim_snippet = value
@property
def viewpoints(self) -> dict[str, VisualizationInfoHandler]:
if not self._viewpoints and self._topic_dir:
self._viewpoints = self._load_viewpoints()
return self._viewpoints
@property
def reference_files(self) -> dict[str, bytes]:
if self._reference_files or not self.header:
return self._reference_files
for ref in self.header.file:
if ref.is_external:
continue
real_path = self._topic_dir
for path_part in ref.reference.split("/"):
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
self._reference_files[ref.reference] = real_path.read_bytes()
return self._reference_files
@property
def document_references(self) -> dict[str, bytes]:
if self._document_references or not self.topic:
return self._document_references
for doc in self.topic.document_reference:
if doc.is_external or not doc.referenced_document:
continue
real_path = self._topic_dir
for path_part in doc.referenced_document.split("/"):
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
self._document_references[doc.referenced_document] = real_path.read_bytes()
return self._document_references
def _load_bim_snippet(self) -> Optional[bytes]:
bim_snippet_obj = self.topic.bim_snippet
if bim_snippet_obj and not bim_snippet_obj.is_external:
bim_snippet_path = self._topic_dir.joinpath(bim_snippet_obj.reference)
if bim_snippet_path.exists():
return bim_snippet_path.read_bytes()
return None
def _load_viewpoints(self) -> dict[str, VisualizationInfoHandler]:
if self.markup and (viewpoints := self.markup.viewpoints):
return VisualizationInfoHandler.from_topic_viewpoints(self._topic_dir, viewpoints)
return {}
@classmethod
def create_new(
cls,
title: str,
description: str,
author: str,
topic_type: str = "",
topic_status: str = "",
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> "TopicHandler":
"""
Create a new BCF topic.
Args:
title: The title of the topic.
description: The description of the topic.
author: The author of the topic.
topic_type: The type of the topic.
topic_status: The status of the topic.
xml_handler: The XML parser/serializer to use.
Returns:
The BCF topic definition.
"""
creation_date = XmlDateTime.from_datetime(datetime.datetime.now())
guid = str(uuid.uuid4())
topic = mdl.Topic(
title=title,
description=description,
creation_author=author,
creation_date=creation_date,
guid=guid,
topic_type=topic_type,
topic_status=topic_status,
)
markup = mdl.Markup(topic=topic)
obj = cls(topic_dir=Path(guid), xml_handler=xml_handler or XmlParserSerializer())
obj.markup = markup
return obj
def save(self, destination_zip: ZipFileInterface) -> None:
"""
Save the topic to a BCF zip file.
Args:
bcf_zip: The BCF zip file to save to.
"""
topic_dir = self.guid
self._save_xml(destination_zip, self._markup, "markup.bcf")
self._save_viewpoints(destination_zip, topic_dir)
self._save_bim_snippet(destination_zip)
self._save_reference_files(destination_zip)
self._save_document_references(destination_zip)
def _save_viewpoints(self, destination_zip: ZipFileInterface, topic_dir: str) -> None:
if not self.markup or not (viewpoints := self.markup.viewpoints):
return
for vpt in viewpoints:
if vpt.viewpoint:
self.viewpoints[vpt.viewpoint].save(destination_zip, topic_dir, vpt)
def _save_xml(self, destination_zip: ZipFileInterface, item: Any, target: str) -> None:
if self._topic_dir is None:
return
to_write = self._xml_handler.serialize(item) if item else self._topic_dir.joinpath(target).read_bytes()
destination_zip.writestr(f"{self._topic_dir.name}/{target}", to_write)
def _save_bim_snippet(self, destination_zip: ZipFileInterface) -> None:
snippet = self.topic.bim_snippet
if not snippet or snippet.is_external:
return
ref_filename = Path(snippet.reference).name
if self.bim_snippet:
destination_zip.writestr(f"{self.topic.guid}/{ref_filename}", self.bim_snippet)
def _save_reference_files(self, destination_zip: ZipFileInterface) -> None:
if not self.header:
return
for ref in self.header.file:
if ref.is_external or not ref.reference:
continue
real_path = self._topic_dir
for path_part in ref.reference.split("/"):
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
destination_zip.writestr(real_path.at, self.reference_files[ref.reference])
def _save_document_references(self, destination_zip: ZipFileInterface) -> None:
if not self.topic:
return
for doc in self.topic.document_reference:
if doc.is_external or not doc.referenced_document:
continue
real_path = self._topic_dir
for path_part in doc.referenced_document.split("/"):
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
destination_zip.writestr(real_path.at, self.document_references[doc.referenced_document])
def add_viewpoint(self, element: entity_instance) -> None:
"""
Add a viewpoint tergeting an IFC element to the topic.
Args:
element: The IFC element.
"""
new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler)
self.add_visinfo_handler(new_viewpoint)
def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None:
self.viewpoints[new_viewpoint.guid] = new_viewpoint
self.markup.viewpoints.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid))
def __eq__(self, other: object) -> bool:
if not isinstance(other, TopicHandler):
raise TypeError("Equality needs a BcfXml object.")
return (
self.markup == other.markup
and self.viewpoints == other.viewpoints
and self.bim_snippet == other.bim_snippet
)
+246
View File
@@ -0,0 +1,246 @@
import uuid
import zipfile
from functools import lru_cache
from typing import Any, Iterable, Optional
import numpy as np
from ifcopenshell import entity_instance
from ifcopenshell.util import placement
from numpy.typing import NDArray
import bcf.v2.model as mdl
from bcf.geometry import calc_camera_vectors
from bcf.inmemory_zipfile import ZipFileInterface
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
class VisualizationInfoHandler:
"""Handle the VisualizationInfo and related objects."""
def __init__(
self,
visualization_info: mdl.VisualizationInfo,
snapshot: Optional[bytes] = None,
bitmaps: Optional[dict[str, bytes]] = None,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> None:
self.visualization_info = visualization_info
self.snapshot = snapshot
self.bitmaps = bitmaps or {}
self._xml_handler = xml_handler or XmlParserSerializer()
@property
def guid(self) -> str:
"""Return the GUID of the visualization info."""
return self.visualization_info.guid
@classmethod
def from_topic_viewpoints(
cls,
topic_dir: zipfile.Path,
vps: Iterable[mdl.ViewPoint],
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> dict[str, "VisualizationInfoHandler"]:
"""Create VisualizationInfoHandler objects of a Topic's ViewPoints."""
viewpoints = {}
for vpt in vps:
visinfo = cls.load(topic_dir, vpt, xml_handler)
if visinfo and vpt.viewpoint:
viewpoints[vpt.viewpoint] = visinfo
return viewpoints
@classmethod
def load(
cls,
topic_dir: zipfile.Path,
vpt: mdl.ViewPoint,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> Optional["VisualizationInfoHandler"]:
"""
Load the VisualizationInfo and related objects from a BCF zip file.
Args:
topic_dir: The directory in the BCF zip file to load from.
vpt: The ViewPoint to load.
xml_handler: The XML handler to use to parse the VisualizationInfo.
Returns:
The VisualizationInfoHandler object.
"""
visinfo = cls._load_visinfo(topic_dir, vpt.viewpoint, xml_handler)
if not visinfo:
return None
snapshot = cls._load_snapshot(topic_dir, vpt.snapshot)
bitmaps = cls._load_bitmaps(topic_dir, visinfo)
return cls(visinfo, snapshot, bitmaps, xml_handler)
@staticmethod
def _load_visinfo(
topic_dir: zipfile.Path,
vp_name: Optional[str],
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> Optional[mdl.VisualizationInfo]:
if not vp_name:
return None
vp_path = topic_dir.joinpath(vp_name)
if vp_path.exists():
xml_handler = xml_handler or XmlParserSerializer()
return xml_handler.parse(vp_path.read_bytes(), mdl.VisualizationInfo)
return None
@staticmethod
def _load_snapshot(topic_dir: zipfile.Path, vp_snapshot: Optional[str]) -> Optional[bytes]:
if vp_snapshot:
snapshot_path = topic_dir.joinpath(vp_snapshot)
if snapshot_path.exists():
return snapshot_path.read_bytes()
return None
@staticmethod
def _load_bitmaps(topic_dir: zipfile.Path, visinfo: Optional[mdl.VisualizationInfo]) -> dict[str, bytes]:
if not visinfo or not (bitmaps := visinfo.bitmap):
return {}
bitmaps_dict = {}
for bitmap in bitmaps:
if not bitmap.reference:
continue
bitmap_path = topic_dir.joinpath(bitmap.reference)
if bitmap_path.exists():
bitmaps_dict[bitmap.reference] = bitmap_path.read_bytes()
return bitmaps_dict
def save(
self,
bcf_zip: ZipFileInterface,
topic_dir: str,
vpt: mdl.ViewPoint,
) -> None:
"""
Save the VisualizationInfo and related objects to a BCF zip file.
Args:
bcf_zip: The BCF zip file to save to.
topic_dir: The directory in the BCF zip file to save to.
vpt: The ViewPoint to save.
"""
if not (vp_name := vpt.viewpoint):
return
self._save_visinfo(bcf_zip, topic_dir, vp_name)
self._save_snapshot(bcf_zip, topic_dir, vpt.snapshot)
self._save_bitmaps(bcf_zip, topic_dir)
def _save_snapshot(self, bcf_zip: ZipFileInterface, topic_dir: str, filename: Optional[str]) -> None:
if self.snapshot and filename:
bcf_zip.writestr(f"{topic_dir}/{filename}", self.snapshot)
def _save_visinfo(self, bcf_zip: ZipFileInterface, topic_dir: str, vp_name: str) -> None:
bcf_zip.writestr(
f"{topic_dir}/{vp_name}",
self._xml_handler.serialize(self.visualization_info),
)
def _save_bitmaps(self, bcf_zip: ZipFileInterface, topic_dir: str) -> None:
if not self.bitmaps:
return
if not (bitmaps_defs := self.visualization_info.bitmap):
return
for bitmap_def in bitmaps_defs:
if not (bitmap_name := bitmap_def.reference):
continue
if bitmap_name in self.bitmaps:
bcf_zip.writestr(f"{topic_dir}/{bitmap_name}", self.bitmaps[bitmap_name])
@classmethod
def create_new(
cls,
element: entity_instance,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> "VisualizationInfoHandler":
"""
Create a new VisualizationInfoHandler object from an IFC element.
Args:
element: The IFC element to point at.
xml_handler: The XML handler to use.
Returns:
The VisualizationInfoHandler object.
"""
xml_handler = xml_handler or XmlParserSerializer()
return cls(visualization_info=build_viewpoint(element), xml_handler=xml_handler)
@lru_cache(maxsize=None)
def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
"""
Return a BCF viewpoint of an IFC element.
This function is cached to speedudp the creation of multiple BCF topics regarding the same element.
Args:
element: The IFC element to point at.
Returns:
The BCF viewpoint definition.
"""
elem_placement = placement.get_local_placement(element.ObjectPlacement)
return mdl.VisualizationInfo(
guid=str(uuid.uuid4()),
components=build_components(element.GlobalId),
perspective_camera=build_camera(elem_placement),
)
def build_components(guid: str) -> mdl.Components:
"""
Return the BCF components from an IFC element GUID.
Args:
guid: The IFC element GUID.
Returns:
The BCF components definition.
"""
return mdl.Components(
selection=mdl.ComponentSelection(component=[mdl.Component(ifc_guid=guid)]),
visibility=mdl.ComponentVisibility(default_visibility=True),
)
def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera:
"""
Return a BCF camera for an IFC element placement matrix.
Args:
elem_placement: The IFC element placement as a rototranslation matrix.
Returns:
The BCF camera definition.
"""
return build_camera_from_vectors(*calc_camera_vectors(elem_placement))
def build_camera_from_vectors(
camera_position: NDArray[np.float_], camera_dir: NDArray[np.float_], camera_up: NDArray[np.float_]
) -> mdl.PerspectiveCamera:
"""
Return a BCF camera for an IFC element placement matrix.
Args:
camera_position: camera position array
camera_dir: camera direction versor
camera_up_vector: camera up versor
Returns:
The BCF camera definition.
"""
camera_viewpoint = mdl.Point(x=camera_position[0], y=camera_position[1], z=camera_position[2])
camera_direction = mdl.Direction(x=camera_dir[0], y=camera_dir[1], z=camera_dir[2])
camera_up_vector = mdl.Direction(x=camera_up[0], y=camera_up[1], z=camera_up[2])
return mdl.PerspectiveCamera(
camera_view_point=camera_viewpoint,
camera_direction=camera_direction,
camera_up_vector=camera_up_vector,
field_of_view=60.0,
)
+1 -19
View File
@@ -1,19 +1 @@
# BCF - BCF Python library
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
#
# This file is part of BCF.
#
# BCF is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BCF 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
"""BCF XML v3 handler."""
+172 -184
View File
@@ -1,4 +1,3 @@
# BCF - BCF Python library
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
#
@@ -17,25 +16,27 @@
# You should have received a copy of the GNU Lesser General Public License
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
import uuid
import time
import json
import urllib
import requests
import webbrowser
import http.server
import base64
import tempfile
import http.server
import os
import tempfile
import time
import urllib
import uuid
import webbrowser
from re import A
from typing import Any, Optional, Tuple
import requests
client_id, client_secret = "", ""
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
def do_GET(self):
def do_GET(self) -> None:
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
self.server.auth_code = query.get("code", [""])[0]
self.server.auth_state = query.get("state", [""])[0]
self.server.auth_code = query.get("code", [""])[0] # type:ignore
self.server.auth_state = query.get("state", [""])[0] # type:ignore
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
@@ -43,20 +44,21 @@ class OAuthReceiver(http.server.BaseHTTPRequestHandler):
class FoundationClient:
def __init__(self, client_id, client_secret, base_url=None, redirect_subdir=None):
def __init__(
self, client_id: str, client_secret: str, base_url: Optional[str] = None, redirect_subdir: Optional[str] = None
) -> None:
self.baseurl = base_url
self.access_token = ""
self.refresh_token = ""
self.access_token_expires_on = time.time()
self.refresh_token_expires_on = float("inf")
self.auth_endpoint = None
self.token_endpoint = None
self.token_endpoint = ""
self.client_id = client_id
self.client_secret = client_secret
self.auth_method = None
self.auth_method: Optional[str] = None
self.redirect_subdir = redirect_subdir
def get_access_token(self):
def get_access_token(self) -> str:
if self.access_token and self.access_token_expires_on > time.time():
return self.access_token
elif self.refresh_token and self.refresh_token_expires_on > time.time():
@@ -65,18 +67,18 @@ class FoundationClient:
self.login()
return self.access_token
def get_auth_methods(self):
def get_auth_methods(self) -> list[Any]:
resp = requests.get(f"{self.baseurl}foundation/1.0/auth")
return resp.json()["supported_oauth2_flows"]
def get_versions(self):
def get_versions(self) -> list[Any]:
resp = requests.get(f"{self.baseurl}foundation/versions")
return resp.json()["versions"]
def login(self):
def login(self) -> None:
resp = requests.get(f"{self.baseurl}foundation/1.0/auth")
values = resp.json()
self.auth_endpoint = values["oauth2_auth_url"]
auth_endpoint = values["oauth2_auth_url"]
self.token_endpoint = values["oauth2_token_url"]
with http.server.HTTPServer(("", 8080), OAuthReceiver) as server:
@@ -89,25 +91,22 @@ class FoundationClient:
"redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_subdir}",
}
)
if "?" in self.auth_endpoint:
webbrowser.open(f"{self.auth_endpoint}&{query}")
if "?" in auth_endpoint:
webbrowser.open(f"{auth_endpoint}&{query}")
else:
webbrowser.open(f"{self.auth_endpoint}?{query}")
webbrowser.open(f"{auth_endpoint}?{query}")
server.timeout = 100
server.state = state
server.handle_request()
if server.auth_code and server.auth_state == state:
if server.auth_code and server.auth_state == state: # pylint: disable=E1101
data = {
"grant_type": "authorization_code",
"code": server.auth_code,
"code": server.auth_code, # pylint: disable=E1101 type:ignore
"redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_subdir}",
}
auth_string = f"{self.client_id}:{self.client_secret}"
header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
headers = {"Authorization": f"Basic {header_string}"}
headers = self._get_access_token_headers()
self.set_tokens_from_response(requests.post(self.token_endpoint, data=data, headers=headers))
def get_refresh_token(self):
def get_refresh_token(self) -> None:
self.set_tokens_from_response(
requests.post(
self.token_endpoint,
@@ -118,10 +117,8 @@ class FoundationClient:
).json()
)
def get_new_access_token(self):
auth_string = f"{self.client_id}:{self.client_secret}"
header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
headers = {"Authorization": f"Basic {header_string}"}
def get_new_access_token(self) -> None:
headers = self._get_access_token_headers()
self.set_tokens_from_response(
requests.post(
self.token_endpoint,
@@ -133,35 +130,41 @@ class FoundationClient:
).json()
)
def set_auth_method(self, method="authorization_code_grant"):
def _get_access_token_headers(self) -> dict[str, str]:
auth_string = f"{self.client_id}:{self.client_secret}"
header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
return {"Authorization": f"Basic {header_string}"}
def set_auth_method(self, method: str = "authorization_code_grant") -> None:
if method != "authorization_code_grant":
raise NotImplementedError(f"{method} not supported")
else:
self.auth_method = method
def set_tokens_from_response(self, response):
response = response.json()
self.access_token = response["access_token"]
self.refresh_token = response["refresh_token"]
self.access_token_expires_on = time.time() + response["expires_in"]
if "refresh_token_expires_in" in response:
self.refresh_token_expires_on = time.time() + response["refresh_token_expires_in"]
def set_tokens_from_response(self, response: requests.Response) -> None:
response_dict = response.json()
self.access_token = response_dict["access_token"]
self.refresh_token = response_dict["refresh_token"]
self.access_token_expires_on = time.time() + response_dict["expires_in"]
if "refresh_token_expires_in" in response_dict:
self.refresh_token_expires_on = time.time() + response_dict["refresh_token_expires_in"]
class BcfClient:
def __init__(self, foundation_client):
def __init__(self, foundation_client: FoundationClient) -> None:
self.foundation_client = foundation_client
self.version_id = None
self.baseurl = None
self.version_id: Optional[str] = None
self.baseurl: Optional[str] = None
self.filepath = tempfile.mkdtemp()
def set_version(self, version):
def set_version(self, version: dict[str, str]) -> None:
self.version_id = version["version_id"]
self.baseurl = version["api_base_url"]
def get(self, endpoint, params=None, is_auth_required=False):
def get(self, endpoint: str, params: Any = None, is_auth_required: bool = False) -> Any:
# TODO: handle error http status codes and raise exception. Follow error.json standard.
headers = {"Authorization": "Bearer " + self.foundation_client.get_access_token()}
headers = {"Authorization": f"Bearer {self.foundation_client.get_access_token()}"}
response = requests.get(f"{self.baseurl}{endpoint}", headers=headers, params=params or None)
try:
response = requests.get(f"{self.baseurl}{endpoint}", headers=headers, params=params or None)
@@ -171,68 +174,62 @@ class BcfClient:
except requests.exceptions.HTTPError as e:
print(f"message: {response.reason}' '{response.status_code}' '{ e }")
def post(self, endpoint, data=None, params=None):
def post(self, endpoint: str, data: Any = None, params: Any = None) -> Tuple[int, str]:
headers = {
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/json",
}
try:
response = requests.post(
f"{self.baseurl}{endpoint}",
headers=headers,
params=params or None,
data=data or None,
f"{self.baseurl}{endpoint}", headers=headers, params=params or None, data=data or None
)
if response.status_code == 201:
return response.status_code, response.text
response.raise_for_status()
if response.status_code != 201:
response.raise_for_status()
return response.status_code, response.text
except requests.exceptions.HTTPError as errh:
print(f"message: {response.reason}' '{response.status_code}, {errh}")
return response.status_code, response.reason
def put(self, endpoint, data=None, params=None):
def put(self, endpoint: str, data: Any = None, params: Any = None) -> Tuple[int, str]:
headers = {
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/json",
}
try:
response = requests.put(
f"{self.baseurl}{endpoint}",
headers=headers,
params=params or None,
data=data or None,
f"{self.baseurl}{endpoint}", headers=headers, params=params or None, data=data or None
)
if response.status_code == 200:
return response.status_code, response.text
response.raise_for_status()
if response.status_code != 200:
response.raise_for_status()
return response.status_code, response.text
except requests.exceptions.HTTPError as errh:
print(f"message: {response.reason}' '{response.status_code}, {errh}")
return response.status_code, response.reason
def delete(self, endpoint, params=None):
def delete(self, endpoint: str, params: Any = None) -> Tuple[int, str]:
headers = {
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/json",
}
try:
response = requests.delete(
f"{self.baseurl}{endpoint}",
headers=headers,
params=params or None,
)
if response.status_code == 200:
return response.status_code, response.text
response.raise_for_status()
response = requests.delete(f"{self.baseurl}{endpoint}", headers=headers, params=params or None)
if response.status_code != 200:
response.raise_for_status()
return response.status_code, response.text
except requests.exceptions.HTTPError as errh:
print(f"message: {response.reason}' '{response.status_code}, {errh}")
return response.status_code, response.reason
def get_projects(self) -> list:
return self.get(
f"/projects",
)
def get_projects(self) -> list[Any]:
return self.get("/projects")
def get_project(
self,
project_id="",
) -> dict:
def get_project(self, project_id: str = "") -> dict[str, Any]:
return self.get(
f"/projects/{project_id}",
{
@@ -240,16 +237,13 @@ class BcfClient:
},
)
def update_project(self, project_id="", data=None) -> dict:
def update_project(self, project_id: str = "", data: Any = None) -> Tuple[int, str]:
url = f"{self.baseurl}/projects/{project_id}"
headers = {"Authorization": "Bearer " + self.foundation_client.get_access_token()}
headers = {"Authorization": f"Bearer {self.foundation_client.get_access_token()}"}
resp = requests.put(url, headers=headers, data=data)
return resp.status_code, resp.text
def get_extensions(
self,
project_id="",
) -> dict:
def get_extensions(self, project_id: str = "") -> dict[str, Any]:
return self.get(
f"/projects/{project_id}/extensions",
{
@@ -259,10 +253,10 @@ class BcfClient:
def get_topics(
self,
project_id="",
topics="",
query_string=None,
) -> list:
project_id: str = "",
topics: str = "",
query_string: Optional[str] = None,
) -> list[Any]:
# return self.get(
# f"/projects/{project_id}/topics",
# {
@@ -273,7 +267,7 @@ class BcfClient:
# )
pass
def get_topic(self, project_id="", topic_id="") -> dict:
def get_topic(self, project_id: str = "", topic_id: str = "") -> dict[str, Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}",
{
@@ -282,42 +276,40 @@ class BcfClient:
},
)
def create_topic(self, project_id="", data=None):
def create_topic(self, project_id: str = "", data: Any = None) -> Tuple[int, str]:
return self.post(f"/projects/{project_id}/topics", data=data)
def update_topic(self, project_id="", topic_id="", data=None) -> dict:
def update_topic(self, project_id: str = "", topic_id: str = "", data: Any = None) -> Tuple[int, str]:
return self.put(f"/projects/{project_id}/topics/{topic_id}", data=data)
def delete_topic(self, project_id="", topic_id=""):
def delete_topic(self, project_id: str = "", topic_id: str = "") -> Tuple[int, str]:
return self.delete(f"/projects/{project_id}/topics/{topic_id}")
def get_snippet(self, project_id="", topic_id="") -> str:
def get_snippet(self, project_id: str = "", topic_id: str = "") -> Tuple[int, str]:
headers = {
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/octet-stream",
}
response = requests.get(
f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet",
headers=headers,
)
# TODO: write to tmpdir
with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_snippet.txt"), "wb") as f:
f.write(response.content.decode("utf-8"))
return response.status_code, response.content
def update_snippet(self, project_id="", topic_id="", files=None, data=None):
response = requests.get(f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet", headers=headers)
content = response.content.decode("utf-8")
with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_snippet.txt"), "w") as f:
f.write(content)
return response.status_code, content
def update_snippet(self, project_id: str = "", topic_id: str = "", files: Any = None, data: Any = None) -> int:
headers = {
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/octet-stream",
}
response = requests.put(
f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet",
headers=headers,
files=files,
f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet", headers=headers, files=files
)
return response.status_code
def get_files_information(self, project_id="") -> list:
def get_files_information(self, project_id: str = "") -> list[Any]:
return self.get(
f"/projects/{project_id}/files_information",
{
@@ -325,7 +317,7 @@ class BcfClient:
},
)
def get_files(self, project_id="", topic_id="") -> list:
def get_files(self, project_id: str = "", topic_id: str = "") -> list[Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/files",
{
@@ -336,32 +328,32 @@ class BcfClient:
def update_files(
self,
project_id="",
topic_id="",
data=None,
params=None,
):
project_id: str = "",
topic_id: str = "",
data: Any = None,
params: Any = None,
) -> Tuple[int, str]:
return self.put(
f"/projects/{project_id}/topics/{topic_id}/files",
data=data,
)
def get_comments(self, project_id="", topic_id="") -> list:
def get_comments(self, project_id: str = "", topic_id: str = "") -> None:
pass
def create_comments(
self,
project_id="",
topic_id="",
data=None,
params=None,
):
project_id: str = "",
topic_id: str = "",
data: Any = None,
params: Any = None,
) -> Tuple[int, str]:
return self.post(
f"/projects/{project_id}/topics/{topic_id}/comments",
data=data,
)
def get_comment(self, project_id="", topic_id="", comment_id="") -> dict:
def get_comment(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> dict[str, Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
{
@@ -371,22 +363,22 @@ class BcfClient:
},
)
def delete_comment(self, project_id="", topic_id="", comment_id=""):
def delete_comment(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> Tuple[int, str]:
return self.delete(f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}")
def update_comment(
self,
project_id="",
topic_id="",
comment_id="",
data=None,
):
project_id: str = "",
topic_id: str = "",
comment_id: str = "",
data: Any = None,
) -> Tuple[int, str]:
return self.put(
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
data=data,
)
def get_viewpoints(self, project_id="", topic_id="") -> list:
def get_viewpoints(self, project_id: str = "", topic_id: str = "") -> list[Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints",
{
@@ -395,13 +387,13 @@ class BcfClient:
},
)
def create_viewpoints(self, project_id="", topic_id="", data=None):
def create_viewpoints(self, project_id: str = "", topic_id: str = "", data: Any = None) -> Tuple[int, str]:
return self.post(
f"/projects/{project_id}/topics/{topic_id}/viewpoints",
data=data,
)
def get_viewpoint(self, project_id="", topic_id="", viewpoint_id="") -> dict:
def get_viewpoint(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> dict[str, Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
{
@@ -413,15 +405,15 @@ class BcfClient:
def delete_viewpoint(
self,
project_id="",
topic_id="",
viewpoint_id="",
):
project_id: str = "",
topic_id: str = "",
viewpoint_id: str = "",
) -> Tuple[int, str]:
return self.delete(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
)
def get_snapshot(self, project_id="", topic_id="", viewpoint_id="") -> str:
def get_snapshot(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> str:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/snapshot",
{
@@ -431,7 +423,7 @@ class BcfClient:
},
)
def get_bitmap(self, project_id="", topic_id="", viewpoint_id="", bitmap_id="") -> str:
def get_bitmap(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "", bitmap_id: str = "") -> str:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/bitmaps/{bitmap_id}",
{
@@ -442,7 +434,7 @@ class BcfClient:
},
)
def get_selection(self, project_id="", topic_id="", viewpoint_id="") -> dict:
def get_selection(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> dict[str, Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/selection",
{
@@ -452,7 +444,7 @@ class BcfClient:
},
)
def get_coloring(self, project_id="", topic_id="", viewpoint_id="") -> dict:
def get_coloring(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> dict[str, Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/coloring",
{
@@ -462,7 +454,7 @@ class BcfClient:
},
)
def get_visibility(self, project_id="", topic_id="", viewpoint_id="") -> dict:
def get_visibility(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> dict[str, Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/visibility",
{
@@ -472,7 +464,7 @@ class BcfClient:
},
)
def get_related_topics(self, project_id="", topic_id="") -> list:
def get_related_topics(self, project_id: str = "", topic_id: str = "") -> list[Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/related_topics",
{
@@ -483,16 +475,16 @@ class BcfClient:
def update_related_topics(
self,
project_id="",
topic_id="",
data=None,
):
project_id: str = "",
topic_id: str = "",
data: Any = None,
) -> Tuple[int, str]:
return self.put(
f"/projects/{project_id}/topics/{topic_id}/related_topics",
data=data,
)
def get_document_references(self, project_id="", topic_id="") -> list:
def get_document_references(self, project_id: str = "", topic_id: str = "") -> list[Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/document_references",
{
@@ -503,10 +495,10 @@ class BcfClient:
def create_document_reference(
self,
project_id="",
topic_id="",
data=None,
):
project_id: str = "",
topic_id: str = "",
data: Any = None,
) -> Tuple[int, str]:
return self.post(
f"/projects/{project_id}/topics/{topic_id}/document_references",
data=data,
@@ -514,17 +506,17 @@ class BcfClient:
def update_document_references(
self,
project_id="",
topic_id="",
document_reference_id="",
data=None,
):
project_id: str = "",
topic_id: str = "",
document_reference_id: str = "",
data: Any = None,
) -> Tuple[int, str]:
return self.put(
f"/projects/{project_id}/topics/{topic_id}/document_references/{document_reference_id}",
data=data,
)
def get_documents(self, project_id="", topic_id="") -> list:
def get_documents(self, project_id: str = "", topic_id: str = "") -> list[Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/documents",
{
@@ -534,40 +526,36 @@ class BcfClient:
)
def create_document(
self,
project_id="",
topic_id="",
guid=None,
files=None,
data=None,
):
self, project_id: str = "", topic_id: str = "", guid: Optional[str] = None, files: Any = None, data: Any = None
) -> int:
headers = {
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/octet-stream",
}
response = requests.post(
f"/projects/{project_id}/topics/{topic_id}/documents",
data=data,
params={guid},
params={"guid": guid},
files=files,
headers=headers,
)
return response.status_code
def get_document(self, project_id="", topic_id="", document_id="") -> str:
def get_document(self, project_id: str = "", topic_id: str = "", document_id: str = "") -> Tuple[int, str]:
headers = {
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/octet-stream",
}
response = requests.get(
f"{self.baseurl}/projects/{project_id}/topics/documents/{document_id}",
headers=headers,
)
with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_{document_id}_document.txt"), "wb") as f:
f.write(response.content.decode("utf-8"))
return response.status_code, response.content
def get_topics_events(self, project_id="") -> list:
response = requests.get(f"{self.baseurl}/projects/{project_id}/topics/documents/{document_id}", headers=headers)
content = response.content.decode("utf-8")
with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_{document_id}_document.txt"), "w") as f:
f.write(content)
return response.status_code, content
def get_topics_events(self, project_id: str = "") -> list[Any]:
return self.get(
f"/projects/{project_id}/topics/events",
{
@@ -575,7 +563,7 @@ class BcfClient:
},
)
def get_topic_events(self, project_id="", topic_id="") -> list:
def get_topic_events(self, project_id: str = "", topic_id: str = "") -> list[Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/events",
{
@@ -584,7 +572,7 @@ class BcfClient:
},
)
def get_comments_events(self, project_id="") -> list:
def get_comments_events(self, project_id: str = "") -> list[Any]:
return self.get(
f"/projects/{project_id}/topics/comments/events",
{
@@ -592,7 +580,7 @@ class BcfClient:
},
)
def get_comment_events(self, project_id="", topic_id="", comment_id="") -> list:
def get_comment_events(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> list[Any]:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}/events",
{
File diff suppressed because it is too large Load Diff
-200
View File
@@ -1,200 +0,0 @@
# BCF - BCF Python library
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
#
# This file is part of BCF.
#
# BCF is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BCF 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
class Project:
def __init__(self):
self.project_id = ""
self.name = ""
class BimSnippet:
def __init__(self):
self.snippet_type = None
self.is_external = False
self.reference = None
self.reference_schema = None
class DocumentReference:
def __init__(self):
self.description = None
self.document_guid = None
self.url = None
self.guid = None
class RelatedTopic:
def __init__(self):
self.guid = None
class HeaderFile:
def __init__(self):
self.filename = ""
self.date = None
self.reference = ""
self.ifc_project = None
self.ifc_spatial_structure_element = None
self.is_external = True
class Header:
def __init__(self):
self.files = []
class Topic:
def __init__(self):
self.reference_links = []
self.title = ""
self.priority = None
self.index = None # Deprecated, stored, but ignored
self.labels = []
self.creation_date = None
self.creation_author = None
self.modified_date = None
self.modified_author = None
self.due_date = None
self.assigned_to = None
self.stage = None
self.description = None
self.bim_snippet = None
self.document_references = []
self.related_topics = []
self.topic_status = None
self.topic_type = None
self.guid = None
self.header = None
self.comments = {}
self.viewpoints = {}
self.server_assigned_id = ""
class Comment:
def __init__(self):
self.guid = None
self.date = None
self.author = ""
self.comment = ""
self.viewpoint = None
self.modified_date = None
self.modified_author = ""
class ViewSetupHints:
def __init__(self):
self.spaces_visible = False
self.space_boundaries_visible = False
self.openings_visible = False
class Component:
def __init__(self):
self.originating_system = None
self.authoring_tool_id = None
self.ifc_guid = None
class ComponentVisibility:
def __init__(self):
self.exceptions = []
self.default_visibility = False
self.view_setup_hints = None
class Color:
def __init__(self):
self.color = None
self.components = []
class Components:
def __init__(self):
self.selection = []
self.visibility = None
self.coloring = []
class Point:
def __init__(self):
self.x = 0
self.y = 0
self.z = 0
class Direction(Point):
pass
class OrthogonalCamera:
def __init__(self):
self.camera_view_point = Point()
self.camera_direction = Direction()
self.camera_up_vector = Direction()
self.view_to_world_scale = 1.0
self.aspect_ratio = 1.0
class PerspectiveCamera:
def __init__(self):
self.camera_view_point = Point()
self.camera_direction = Direction()
self.camera_up_vector = Direction()
self.field_of_view = 60.0
self.aspect_ratio = 1.0
class Line:
def __init__(self):
self.start_point = Point()
self.end_point = Point()
class ClippingPlane:
def __init__(self):
self.location = Point()
self.direction = Direction()
class Bitmap:
def __init__(self):
self.reference = "" # Only in BCF-XML
self.bitmap_data = None # Only in BCF-API
self.bitmap_format = "PNG" # Enum of png or jpg
self.location = Point()
self.normal = Direction()
self.up = Direction()
self.height = 1.0
class Viewpoint:
def __init__(self):
self.guid = None
self.viewpoint = None
self.snapshot = None
self.index = None
self.components = None # It's not a list, despite the plural name
self.orthogonal_camera = None
self.perspective_camera = None
self.lines = []
self.clipping_planes = []
self.bitmaps = []
+61
View File
@@ -0,0 +1,61 @@
"""BCF XML V3 Documents handler."""
import zipfile
from typing import Any, Optional
import bcf.v3.model as mdl
from bcf.inmemory_zipfile import ZipFileInterface
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
class DocumentsHandler:
"""BCF documents handler."""
def __init__(
self,
definition: mdl.DocumentInfo,
documents: Optional[dict[str, bytes]] = None,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> None:
self.definition = definition
self.documents = documents or {}
self._xml_handler = xml_handler or XmlParserSerializer()
@classmethod
def load(
cls,
zip_file: zipfile.ZipFile,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> Optional["DocumentsHandler"]:
"""
Loads the documents from the given zip file directory.
Args:
zip_path: The directory path inside the zip file.
xml_handler: The xml parser/serializer to use.
Returns:
The documents handler.
"""
xml_handler = xml_handler or XmlParserSerializer()
file_to_open = zipfile.Path(zip_file, "documents.xml")
if not file_to_open.exists():
return None
definition = xml_handler.parse(file_to_open.read_bytes(), mdl.DocumentInfo)
documents = {}
if def_docs := definition.documents:
for document in def_docs.document:
document_path = zipfile.Path(zip_file, f"documents/{document.guid}")
if document_path.exists():
documents[document.filename] = document_path.read_bytes()
return cls(definition, documents=documents)
def save(self, bcf_zip: ZipFileInterface) -> None:
"""Save the documents to the zip file."""
bcf_zip.writestr("documents.xml", self._xml_handler.serialize(self.definition))
if documents := self.definition.documents:
for doc in documents.document:
if doc.filename in self.documents:
bcf_zip.writestr(
f"documents/{doc.guid}",
self.documents[doc.filename],
)
+110
View File
@@ -0,0 +1,110 @@
from bcf.v3.model.documents import Document, DocumentInfo, DocumentInfoDocuments
from bcf.v3.model.extensions import (
Extensions,
ExtensionsPriorities,
ExtensionsSnippetTypes,
ExtensionsStages,
ExtensionsTopicLabels,
ExtensionsTopicStatuses,
ExtensionsTopicTypes,
ExtensionsUsers,
)
from bcf.v3.model.markup import (
BimSnippet,
Comment,
CommentViewpoint,
DocumentReference,
File,
Header,
HeaderFiles,
Markup,
Topic,
TopicComments,
TopicDocumentReferences,
TopicLabels,
TopicReferenceLinks,
TopicRelatedTopics,
TopicRelatedTopicsRelatedTopic,
TopicViewpoints,
ViewPoint,
)
from bcf.v3.model.project import Project, ProjectInfo
from bcf.v3.model.version import Version
from bcf.v3.model.visinfo import (
Bitmap,
BitmapFormat,
ClippingPlane,
Component,
ComponentColoring,
ComponentColoringColor,
ComponentColoringColorComponents,
Components,
ComponentSelection,
ComponentVisibility,
ComponentVisibilityExceptions,
Direction,
Line,
OrthogonalCamera,
PerspectiveCamera,
Point,
ViewSetupHints,
VisualizationInfo,
VisualizationInfoBitmaps,
VisualizationInfoClippingPlanes,
VisualizationInfoLines,
)
__all__ = [
"Document",
"DocumentInfo",
"DocumentInfoDocuments",
"Extensions",
"ExtensionsPriorities",
"ExtensionsSnippetTypes",
"ExtensionsStages",
"ExtensionsTopicLabels",
"ExtensionsTopicStatuses",
"ExtensionsTopicTypes",
"ExtensionsUsers",
"BimSnippet",
"Comment",
"CommentViewpoint",
"DocumentReference",
"File",
"Header",
"HeaderFiles",
"Markup",
"Topic",
"TopicComments",
"TopicDocumentReferences",
"TopicLabels",
"TopicReferenceLinks",
"TopicRelatedTopics",
"TopicRelatedTopicsRelatedTopic",
"TopicViewpoints",
"ViewPoint",
"Project",
"ProjectInfo",
"Version",
"Bitmap",
"BitmapFormat",
"ClippingPlane",
"Component",
"ComponentColoring",
"ComponentColoringColor",
"ComponentColoringColorComponents",
"ComponentSelection",
"ComponentVisibility",
"ComponentVisibilityExceptions",
"Components",
"Direction",
"Line",
"OrthogonalCamera",
"PerspectiveCamera",
"Point",
"ViewSetupHints",
"VisualizationInfo",
"VisualizationInfoBitmaps",
"VisualizationInfoClippingPlanes",
"VisualizationInfoLines",
]
+61
View File
@@ -0,0 +1,61 @@
from dataclasses import dataclass, field
from typing import List, Optional
@dataclass(slots=True, kw_only=True)
class Document:
filename: str = field(
metadata={
"name": "Filename",
"type": "Element",
"namespace": "",
"required": True,
"min_length": 1,
"white_space": "collapse",
}
)
description: Optional[str] = field(
default=None,
metadata={
"name": "Description",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
}
)
@dataclass(slots=True, kw_only=True)
class DocumentInfoDocuments:
class Meta:
global_type = False
document: List[Document] = field(
default_factory=list,
metadata={
"name": "Document",
"type": "Element",
"namespace": "",
}
)
@dataclass(slots=True, kw_only=True)
class DocumentInfo:
documents: Optional[DocumentInfoDocuments] = field(
default=None,
metadata={
"name": "Documents",
"type": "Element",
"namespace": "",
}
)
+181
View File
@@ -0,0 +1,181 @@
from dataclasses import dataclass, field
from typing import List, Optional
@dataclass(slots=True, kw_only=True)
class ExtensionsPriorities:
class Meta:
global_type = False
priority: List[str] = field(
default_factory=list,
metadata={
"name": "Priority",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
@dataclass(slots=True, kw_only=True)
class ExtensionsSnippetTypes:
class Meta:
global_type = False
snippet_type: List[str] = field(
default_factory=list,
metadata={
"name": "SnippetType",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
@dataclass(slots=True, kw_only=True)
class ExtensionsStages:
class Meta:
global_type = False
stage: List[str] = field(
default_factory=list,
metadata={
"name": "Stage",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicLabels:
class Meta:
global_type = False
topic_label: List[str] = field(
default_factory=list,
metadata={
"name": "TopicLabel",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicStatuses:
class Meta:
global_type = False
topic_status: List[str] = field(
default_factory=list,
metadata={
"name": "TopicStatus",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicTypes:
class Meta:
global_type = False
topic_type: List[str] = field(
default_factory=list,
metadata={
"name": "TopicType",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
@dataclass(slots=True, kw_only=True)
class ExtensionsUsers:
class Meta:
global_type = False
user: List[str] = field(
default_factory=list,
metadata={
"name": "User",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
@dataclass(slots=True, kw_only=True)
class Extensions:
topic_types: Optional[ExtensionsTopicTypes] = field(
default=None,
metadata={
"name": "TopicTypes",
"type": "Element",
"namespace": "",
}
)
topic_statuses: Optional[ExtensionsTopicStatuses] = field(
default=None,
metadata={
"name": "TopicStatuses",
"type": "Element",
"namespace": "",
}
)
priorities: Optional[ExtensionsPriorities] = field(
default=None,
metadata={
"name": "Priorities",
"type": "Element",
"namespace": "",
}
)
topic_labels: Optional[ExtensionsTopicLabels] = field(
default=None,
metadata={
"name": "TopicLabels",
"type": "Element",
"namespace": "",
}
)
users: Optional[ExtensionsUsers] = field(
default=None,
metadata={
"name": "Users",
"type": "Element",
"namespace": "",
}
)
snippet_types: Optional[ExtensionsSnippetTypes] = field(
default=None,
metadata={
"name": "SnippetTypes",
"type": "Element",
"namespace": "",
}
)
stages: Optional[ExtensionsStages] = field(
default=None,
metadata={
"name": "Stages",
"type": "Element",
"namespace": "",
}
)
+616
View File
@@ -0,0 +1,616 @@
from dataclasses import dataclass, field
from typing import List, Optional
from xsdata.models.datatype import XmlDateTime
@dataclass(slots=True, kw_only=True)
class BimSnippet:
reference: str = field(
metadata={
"name": "Reference",
"type": "Element",
"namespace": "",
"required": True,
"min_length": 1,
"white_space": "collapse",
}
)
reference_schema: str = field(
metadata={
"name": "ReferenceSchema",
"type": "Element",
"namespace": "",
"required": True,
"min_length": 1,
"white_space": "collapse",
}
)
snippet_type: str = field(
metadata={
"name": "SnippetType",
"type": "Attribute",
"required": True,
"min_length": 1,
"white_space": "collapse",
}
)
is_external: bool = field(
default=False,
metadata={
"name": "IsExternal",
"type": "Attribute",
}
)
@dataclass(slots=True, kw_only=True)
class CommentViewpoint:
class Meta:
global_type = False
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
}
)
@dataclass(slots=True, kw_only=True)
class DocumentReference:
document_guid: Optional[str] = field(
default=None,
metadata={
"name": "DocumentGuid",
"type": "Element",
"namespace": "",
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
}
)
url: Optional[str] = field(
default=None,
metadata={
"name": "Url",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
description: Optional[str] = field(
default=None,
metadata={
"name": "Description",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
}
)
@dataclass(slots=True, kw_only=True)
class File:
filename: Optional[str] = field(
default=None,
metadata={
"name": "Filename",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
date: Optional[XmlDateTime] = field(
default=None,
metadata={
"name": "Date",
"type": "Element",
"namespace": "",
}
)
reference: Optional[str] = field(
default=None,
metadata={
"name": "Reference",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
ifc_project: Optional[str] = field(
default=None,
metadata={
"name": "IfcProject",
"type": "Attribute",
"length": 22,
"pattern": r"[0-9A-Za-z_$]*",
}
)
ifc_spatial_structure_element: Optional[str] = field(
default=None,
metadata={
"name": "IfcSpatialStructureElement",
"type": "Attribute",
"length": 22,
"pattern": r"[0-9A-Za-z_$]*",
}
)
is_external: bool = field(
default=True,
metadata={
"name": "IsExternal",
"type": "Attribute",
}
)
@dataclass(slots=True, kw_only=True)
class TopicLabels:
class Meta:
global_type = False
label: List[str] = field(
default_factory=list,
metadata={
"name": "Label",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
@dataclass(slots=True, kw_only=True)
class TopicReferenceLinks:
class Meta:
global_type = False
reference_link: List[str] = field(
default_factory=list,
metadata={
"name": "ReferenceLink",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
@dataclass(slots=True, kw_only=True)
class TopicRelatedTopicsRelatedTopic:
class Meta:
global_type = False
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
}
)
@dataclass(slots=True, kw_only=True)
class ViewPoint:
viewpoint: Optional[str] = field(
default=None,
metadata={
"name": "Viewpoint",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
snapshot: Optional[str] = field(
default=None,
metadata={
"name": "Snapshot",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
index: Optional[int] = field(
default=None,
metadata={
"name": "Index",
"type": "Element",
"namespace": "",
}
)
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
}
)
@dataclass(slots=True, kw_only=True)
class Comment:
date: XmlDateTime = field(
metadata={
"name": "Date",
"type": "Element",
"namespace": "",
"required": True,
}
)
author: str = field(
metadata={
"name": "Author",
"type": "Element",
"namespace": "",
"required": True,
"min_length": 1,
"white_space": "collapse",
}
)
comment: Optional[str] = field(
default=None,
metadata={
"name": "Comment",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
viewpoint: Optional[CommentViewpoint] = field(
default=None,
metadata={
"name": "Viewpoint",
"type": "Element",
"namespace": "",
}
)
modified_date: Optional[XmlDateTime] = field(
default=None,
metadata={
"name": "ModifiedDate",
"type": "Element",
"namespace": "",
}
)
modified_author: Optional[str] = field(
default=None,
metadata={
"name": "ModifiedAuthor",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
}
)
@dataclass(slots=True, kw_only=True)
class HeaderFiles:
class Meta:
global_type = False
file: List[File] = field(
default_factory=list,
metadata={
"name": "File",
"type": "Element",
"namespace": "",
}
)
@dataclass(slots=True, kw_only=True)
class TopicDocumentReferences:
class Meta:
global_type = False
document_reference: List[DocumentReference] = field(
default_factory=list,
metadata={
"name": "DocumentReference",
"type": "Element",
"namespace": "",
}
)
@dataclass(slots=True, kw_only=True)
class TopicRelatedTopics:
class Meta:
global_type = False
related_topic: List[TopicRelatedTopicsRelatedTopic] = field(
default_factory=list,
metadata={
"name": "RelatedTopic",
"type": "Element",
"namespace": "",
}
)
@dataclass(slots=True, kw_only=True)
class TopicViewpoints:
class Meta:
global_type = False
view_point: List[ViewPoint] = field(
default_factory=list,
metadata={
"name": "ViewPoint",
"type": "Element",
"namespace": "",
}
)
@dataclass(slots=True, kw_only=True)
class Header:
files: Optional[HeaderFiles] = field(
default=None,
metadata={
"name": "Files",
"type": "Element",
"namespace": "",
}
)
@dataclass(slots=True, kw_only=True)
class TopicComments:
class Meta:
global_type = False
comment: List[Comment] = field(
default_factory=list,
metadata={
"name": "Comment",
"type": "Element",
"namespace": "",
}
)
@dataclass(slots=True, kw_only=True)
class Topic:
reference_links: Optional[TopicReferenceLinks] = field(
default=None,
metadata={
"name": "ReferenceLinks",
"type": "Element",
"namespace": "",
}
)
title: str = field(
metadata={
"name": "Title",
"type": "Element",
"namespace": "",
"required": True,
"min_length": 1,
"white_space": "collapse",
}
)
priority: Optional[str] = field(
default=None,
metadata={
"name": "Priority",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
index: Optional[int] = field(
default=None,
metadata={
"name": "Index",
"type": "Element",
"namespace": "",
}
)
labels: Optional[TopicLabels] = field(
default=None,
metadata={
"name": "Labels",
"type": "Element",
"namespace": "",
}
)
creation_date: XmlDateTime = field(
metadata={
"name": "CreationDate",
"type": "Element",
"namespace": "",
"required": True,
}
)
creation_author: str = field(
metadata={
"name": "CreationAuthor",
"type": "Element",
"namespace": "",
"required": True,
"min_length": 1,
"white_space": "collapse",
}
)
modified_date: Optional[XmlDateTime] = field(
default=None,
metadata={
"name": "ModifiedDate",
"type": "Element",
"namespace": "",
}
)
modified_author: Optional[str] = field(
default=None,
metadata={
"name": "ModifiedAuthor",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
due_date: Optional[XmlDateTime] = field(
default=None,
metadata={
"name": "DueDate",
"type": "Element",
"namespace": "",
}
)
assigned_to: Optional[str] = field(
default=None,
metadata={
"name": "AssignedTo",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
stage: Optional[str] = field(
default=None,
metadata={
"name": "Stage",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
description: Optional[str] = field(
default=None,
metadata={
"name": "Description",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
bim_snippet: Optional[BimSnippet] = field(
default=None,
metadata={
"name": "BimSnippet",
"type": "Element",
"namespace": "",
}
)
document_references: Optional[TopicDocumentReferences] = field(
default=None,
metadata={
"name": "DocumentReferences",
"type": "Element",
"namespace": "",
}
)
related_topics: Optional[TopicRelatedTopics] = field(
default=None,
metadata={
"name": "RelatedTopics",
"type": "Element",
"namespace": "",
}
)
comments: Optional[TopicComments] = field(
default=None,
metadata={
"name": "Comments",
"type": "Element",
"namespace": "",
}
)
viewpoints: Optional[TopicViewpoints] = field(
default=None,
metadata={
"name": "Viewpoints",
"type": "Element",
"namespace": "",
}
)
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
}
)
server_assigned_id: Optional[str] = field(
default=None,
metadata={
"name": "ServerAssignedId",
"type": "Attribute",
"min_length": 1,
"white_space": "collapse",
}
)
topic_type: str = field(
metadata={
"name": "TopicType",
"type": "Attribute",
"required": True,
"min_length": 1,
"white_space": "collapse",
}
)
topic_status: str = field(
metadata={
"name": "TopicStatus",
"type": "Attribute",
"required": True,
"min_length": 1,
"white_space": "collapse",
}
)
@dataclass(slots=True, kw_only=True)
class Markup:
header: Optional[Header] = field(
default=None,
metadata={
"name": "Header",
"type": "Element",
"namespace": "",
}
)
topic: Topic = field(
metadata={
"name": "Topic",
"type": "Element",
"namespace": "",
"required": True,
}
)
+37
View File
@@ -0,0 +1,37 @@
from dataclasses import dataclass, field
from typing import Optional
@dataclass(slots=True, kw_only=True)
class Project:
name: Optional[str] = field(
default=None,
metadata={
"name": "Name",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
}
)
project_id: str = field(
metadata={
"name": "ProjectId",
"type": "Attribute",
"required": True,
"min_length": 1,
"white_space": "collapse",
}
)
@dataclass(slots=True, kw_only=True)
class ProjectInfo:
project: Project = field(
metadata={
"name": "Project",
"type": "Element",
"namespace": "",
"required": True,
}
)
+12
View File
@@ -0,0 +1,12 @@
from dataclasses import dataclass, field
@dataclass(slots=True, kw_only=True)
class Version:
version_id: str = field(
metadata={
"name": "VersionId",
"type": "Attribute",
"required": True,
}
)
+524
View File
@@ -0,0 +1,524 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional
class BitmapFormat(Enum):
PNG = "png"
JPG = "jpg"
@dataclass(slots=True, kw_only=True)
class Component:
originating_system: Optional[str] = field(
default=None,
metadata={
"name": "OriginatingSystem",
"type": "Element",
"min_length": 1,
"white_space": "collapse",
}
)
authoring_tool_id: Optional[str] = field(
default=None,
metadata={
"name": "AuthoringToolId",
"type": "Element",
"min_length": 1,
"white_space": "collapse",
}
)
ifc_guid: Optional[str] = field(
default=None,
metadata={
"name": "IfcGuid",
"type": "Attribute",
"length": 22,
"pattern": r"[0-9A-Za-z_$]*",
}
)
@dataclass(slots=True, kw_only=True)
class Direction:
x: float = field(
metadata={
"name": "X",
"type": "Element",
"required": True,
}
)
y: float = field(
metadata={
"name": "Y",
"type": "Element",
"required": True,
}
)
z: float = field(
metadata={
"name": "Z",
"type": "Element",
"required": True,
}
)
@dataclass(slots=True, kw_only=True)
class Point:
x: float = field(
metadata={
"name": "X",
"type": "Element",
"required": True,
}
)
y: float = field(
metadata={
"name": "Y",
"type": "Element",
"required": True,
}
)
z: float = field(
metadata={
"name": "Z",
"type": "Element",
"required": True,
}
)
@dataclass(slots=True, kw_only=True)
class ViewSetupHints:
spaces_visible: bool = field(
default=False,
metadata={
"name": "SpacesVisible",
"type": "Attribute",
}
)
space_boundaries_visible: bool = field(
default=False,
metadata={
"name": "SpaceBoundariesVisible",
"type": "Attribute",
}
)
openings_visible: bool = field(
default=False,
metadata={
"name": "OpeningsVisible",
"type": "Attribute",
}
)
@dataclass(slots=True, kw_only=True)
class Bitmap:
format: BitmapFormat = field(
metadata={
"name": "Format",
"type": "Element",
"required": True,
}
)
reference: str = field(
metadata={
"name": "Reference",
"type": "Element",
"required": True,
"min_length": 1,
"white_space": "collapse",
}
)
location: Point = field(
metadata={
"name": "Location",
"type": "Element",
"required": True,
}
)
normal: Direction = field(
metadata={
"name": "Normal",
"type": "Element",
"required": True,
}
)
up: Direction = field(
metadata={
"name": "Up",
"type": "Element",
"required": True,
}
)
height: float = field(
metadata={
"name": "Height",
"type": "Element",
"required": True,
}
)
@dataclass(slots=True, kw_only=True)
class ClippingPlane:
location: Point = field(
metadata={
"name": "Location",
"type": "Element",
"required": True,
}
)
direction: Direction = field(
metadata={
"name": "Direction",
"type": "Element",
"required": True,
}
)
@dataclass(slots=True, kw_only=True)
class ComponentColoringColorComponents:
class Meta:
global_type = False
component: List[Component] = field(
default_factory=list,
metadata={
"name": "Component",
"type": "Element",
"min_occurs": 1,
}
)
@dataclass(slots=True, kw_only=True)
class ComponentSelection:
component: List[Component] = field(
default_factory=list,
metadata={
"name": "Component",
"type": "Element",
}
)
@dataclass(slots=True, kw_only=True)
class ComponentVisibilityExceptions:
class Meta:
global_type = False
component: List[Component] = field(
default_factory=list,
metadata={
"name": "Component",
"type": "Element",
}
)
@dataclass(slots=True, kw_only=True)
class Line:
start_point: Point = field(
metadata={
"name": "StartPoint",
"type": "Element",
"required": True,
}
)
end_point: Point = field(
metadata={
"name": "EndPoint",
"type": "Element",
"required": True,
}
)
@dataclass(slots=True, kw_only=True)
class OrthogonalCamera:
"""
Attributes
camera_view_point:
camera_direction:
camera_up_vector:
view_to_world_scale: view's visible vertical size in meters
aspect_ratio: Proportional relationship between the width and
the height of the view (w/h).
"""
camera_view_point: Point = field(
metadata={
"name": "CameraViewPoint",
"type": "Element",
"required": True,
}
)
camera_direction: Direction = field(
metadata={
"name": "CameraDirection",
"type": "Element",
"required": True,
}
)
camera_up_vector: Direction = field(
metadata={
"name": "CameraUpVector",
"type": "Element",
"required": True,
}
)
view_to_world_scale: float = field(
metadata={
"name": "ViewToWorldScale",
"type": "Element",
"required": True,
}
)
aspect_ratio: float = field(
metadata={
"name": "AspectRatio",
"type": "Element",
"required": True,
"min_exclusive": 0.0,
}
)
@dataclass(slots=True, kw_only=True)
class PerspectiveCamera:
"""
Attributes
camera_view_point:
camera_direction:
camera_up_vector:
field_of_view: Vertical field of view, in degrees. It is
currently limited to a value between 45 and 60 degrees. This
limitation will be dropped in the next release and viewers
should be expect values outside this range in current
implementations.
aspect_ratio: Proportional relationship between the width and
the height of the view (w/h).
"""
camera_view_point: Point = field(
metadata={
"name": "CameraViewPoint",
"type": "Element",
"required": True,
}
)
camera_direction: Direction = field(
metadata={
"name": "CameraDirection",
"type": "Element",
"required": True,
}
)
camera_up_vector: Direction = field(
metadata={
"name": "CameraUpVector",
"type": "Element",
"required": True,
}
)
field_of_view: float = field(
metadata={
"name": "FieldOfView",
"type": "Element",
"required": True,
"min_exclusive": 0.0,
"max_exclusive": 180.0,
}
)
aspect_ratio: float = field(
metadata={
"name": "AspectRatio",
"type": "Element",
"required": True,
"min_exclusive": 0.0,
}
)
@dataclass(slots=True, kw_only=True)
class ComponentColoringColor:
class Meta:
global_type = False
components: ComponentColoringColorComponents = field(
metadata={
"name": "Components",
"type": "Element",
"required": True,
}
)
color: str = field(
metadata={
"name": "Color",
"type": "Attribute",
"required": True,
"pattern": r"[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?",
}
)
@dataclass(slots=True, kw_only=True)
class ComponentVisibility:
view_setup_hints: Optional[ViewSetupHints] = field(
default=None,
metadata={
"name": "ViewSetupHints",
"type": "Element",
}
)
exceptions: Optional[ComponentVisibilityExceptions] = field(
default=None,
metadata={
"name": "Exceptions",
"type": "Element",
}
)
default_visibility: bool = field(
default=False,
metadata={
"name": "DefaultVisibility",
"type": "Attribute",
}
)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoBitmaps:
class Meta:
global_type = False
bitmap: List[Bitmap] = field(
default_factory=list,
metadata={
"name": "Bitmap",
"type": "Element",
}
)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoClippingPlanes:
class Meta:
global_type = False
clipping_plane: List[ClippingPlane] = field(
default_factory=list,
metadata={
"name": "ClippingPlane",
"type": "Element",
}
)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoLines:
class Meta:
global_type = False
line: List[Line] = field(
default_factory=list,
metadata={
"name": "Line",
"type": "Element",
}
)
@dataclass(slots=True, kw_only=True)
class ComponentColoring:
color: List[ComponentColoringColor] = field(
default_factory=list,
metadata={
"name": "Color",
"type": "Element",
}
)
@dataclass(slots=True, kw_only=True)
class Components:
selection: Optional[ComponentSelection] = field(
default=None,
metadata={
"name": "Selection",
"type": "Element",
}
)
visibility: Optional[ComponentVisibility] = field(
default=None,
metadata={
"name": "Visibility",
"type": "Element",
}
)
coloring: Optional[ComponentColoring] = field(
default=None,
metadata={
"name": "Coloring",
"type": "Element",
}
)
@dataclass(slots=True, kw_only=True)
class VisualizationInfo:
"""
VisualizationInfo documentation.
"""
components: Optional[Components] = field(
default=None,
metadata={
"name": "Components",
"type": "Element",
}
)
orthogonal_camera: Optional[OrthogonalCamera] = field(
default=None,
metadata={
"name": "OrthogonalCamera",
"type": "Element",
}
)
perspective_camera: Optional[PerspectiveCamera] = field(
default=None,
metadata={
"name": "PerspectiveCamera",
"type": "Element",
}
)
lines: Optional[VisualizationInfoLines] = field(
default=None,
metadata={
"name": "Lines",
"type": "Element",
}
)
clipping_planes: Optional[VisualizationInfoClippingPlanes] = field(
default=None,
metadata={
"name": "ClippingPlanes",
"type": "Element",
}
)
bitmaps: Optional[VisualizationInfoBitmaps] = field(
default=None,
metadata={
"name": "Bitmaps",
"type": "Element",
}
)
guid: str = field(
metadata={
"name": "Guid",
"type": "Attribute",
"required": True,
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
}
)
+188
View File
@@ -0,0 +1,188 @@
"""BCF XML V3 Topic handler."""
import datetime
import uuid
import zipfile
from pathlib import Path
from typing import Any, Optional
from ifcopenshell import entity_instance
from xsdata.models.datatype import XmlDateTime
import bcf.v3.model as mdl
from bcf.inmemory_zipfile import ZipFileInterface
from bcf.v3.visinfo import VisualizationInfoHandler
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
class TopicHandler:
"""BCF Topic and related objects handler."""
def __init__(
self,
topic_dir: Optional[zipfile.Path] = None,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> None:
self._markup: Optional[mdl.Markup] = None
self._viewpoints: dict[str, VisualizationInfoHandler] = {}
self._bim_snippet: Optional[bytes] = None
self._xml_handler = xml_handler or XmlParserSerializer()
self._topic_dir = topic_dir
@property
def markup(self) -> Optional[mdl.Markup]:
if not self._markup and self._topic_dir:
markup_path = self._topic_dir.joinpath("markup.bcf")
if markup_path.exists():
self._markup = self._xml_handler.parse(markup_path.read_bytes(), mdl.Markup)
return self._markup
@markup.setter
def markup(self, value: mdl.Markup) -> None:
self._markup = value
@property
def topic(self) -> mdl.Topic:
"""Return the Topic object."""
return self.markup.topic
@property
def guid(self) -> Optional[str]:
"""Return the GUID of the topic."""
if self._markup:
return self.topic.guid
return self._topic_dir.name if self._topic_dir else None
@property
def header(self) -> Optional[mdl.Header]:
"""Return the header of the topic."""
return self.markup.header
@property
def comments(self) -> list[mdl.Comment]:
"""Return the comments of the topic."""
return self.topic.comments.comment if self.topic.comments else []
@property
def bim_snippet(self) -> Optional[bytes]:
if not self._bim_snippet and self._topic_dir:
self._bim_snippet = self._load_bim_snippet()
return self._bim_snippet
@bim_snippet.setter
def bim_snippet(self, value: bytes) -> None:
self._bim_snippet = value
@property
def viewpoints(self) -> Optional[VisualizationInfoHandler]:
if (
not self._viewpoints
and self._topic_dir
and self.topic.viewpoints
and (viewpoints := self.topic.viewpoints.view_point)
):
self._viewpoints = VisualizationInfoHandler.from_topic_viewpoints(self._topic_dir, viewpoints)
return self._viewpoints
def _load_bim_snippet(self) -> Optional[bytes]:
bim_snippet_obj = self.topic.bim_snippet
if bim_snippet_obj and not bim_snippet_obj.is_external and self._topic_dir:
bim_snippet_path = self._topic_dir.joinpath(bim_snippet_obj.reference)
if bim_snippet_path.exists():
return bim_snippet_path.read_bytes()
return None
@classmethod
def create_new(
cls,
title: str,
description: str,
author: str,
topic_type: str = "",
topic_status: str = "",
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> "TopicHandler":
"""
Create a new BCF topic.
Args:
title: The title of the topic.
description: The description of the topic.
author: The author of the topic.
topic_type: The type of the topic.
topic_status: The status of the topic.
xml_handler: The XML parser/serializer to use.
Returns:
The BCF topic definition.
"""
creation_date = XmlDateTime.from_datetime(datetime.datetime.now())
guid = str(uuid.uuid4())
topic = mdl.Topic(
title=title,
description=description,
creation_author=author,
creation_date=creation_date,
guid=guid,
topic_type=topic_type,
topic_status=topic_status,
)
markup = mdl.Markup(topic=topic)
obj = cls(topic_dir=Path(guid), xml_handler=xml_handler or XmlParserSerializer())
obj.markup = markup
return obj
def save(self, destination_zip: ZipFileInterface) -> None:
"""
Save the topic to a BCF zip file.
Args:
bcf_zip: The BCF zip file to save to.
"""
topic_dir = self.guid
self._save_xml(destination_zip, self._markup, "markup.bcf")
self._save_viewpoints(destination_zip, topic_dir)
self._save_bim_snippet(destination_zip)
def _save_viewpoints(self, destination_zip, topic_dir) -> None:
if not self.topic.viewpoints or not (viewpoints := self.topic.viewpoints.view_point):
return
for vpt in viewpoints:
if vpt.viewpoint:
self.viewpoints[vpt.viewpoint].save(destination_zip, topic_dir, vpt)
def _save_xml(self, destination_zip: ZipFileInterface, item: Any, target: str) -> None:
to_write = self._xml_handler.serialize(item) if item else self._topic_dir.joinpath(target).read_bytes()
destination_zip.writestr(f"{self._topic_dir.name}/{target}", to_write)
def _save_bim_snippet(self, destination_zip: ZipFileInterface) -> None:
snippet = self.topic.bim_snippet
if not snippet or snippet.is_external:
return
ref_filename = Path(snippet.reference).name
if self.bim_snippet:
destination_zip.writestr(f"{self.topic.guid}/{ref_filename}", self.bim_snippet)
def add_viewpoint(self, element: entity_instance) -> None:
"""
Add a viewpoint tergeting an IFC element to the topic.
Args:
element: The IFC element.
"""
new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler)
self.add_visinfo_handler(new_viewpoint)
def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None:
self.viewpoints[new_viewpoint.guid] = new_viewpoint
if self.topic.viewpoints is None:
self.topic.viewpoints = mdl.TopicViewpoints()
self.topic.viewpoints.view_point.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid))
def __eq__(self, other: object) -> bool:
if not isinstance(other, TopicHandler):
raise TypeError("Equality needs a BcfXml object.")
return (
self.markup == other.markup
and self.viewpoints == other.viewpoints
and self.bim_snippet == other.bim_snippet
)
+247
View File
@@ -0,0 +1,247 @@
import uuid
import zipfile
from functools import lru_cache
from typing import Any, Iterable, Optional
import numpy as np
from ifcopenshell import entity_instance
from ifcopenshell.util import placement
from numpy.typing import NDArray
import bcf.v3.model as mdl
from bcf.geometry import calc_camera_vectors
from bcf.inmemory_zipfile import ZipFileInterface
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
class VisualizationInfoHandler:
"""Handle the VisualizationInfo and related objects."""
def __init__(
self,
visualization_info: mdl.VisualizationInfo,
snapshot: Optional[bytes] = None,
bitmaps: Optional[dict[str, bytes]] = None,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> None:
self.visualization_info = visualization_info
self.snapshot = snapshot
self.bitmaps = bitmaps or {}
self._xml_handler = xml_handler or XmlParserSerializer()
@property
def guid(self) -> str:
"""Return the GUID of the visualization info."""
return self.visualization_info.guid
@classmethod
def from_topic_viewpoints(
cls,
topic_dir: zipfile.Path,
vps: Iterable[mdl.ViewPoint],
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> dict[str, "VisualizationInfoHandler"]:
"""Create VisualizationInfoHandler objects of a Topic's ViewPoints."""
viewpoints = {}
for vpt in vps:
visinfo = cls.load(topic_dir, vpt, xml_handler)
if visinfo and vpt.viewpoint:
viewpoints[vpt.viewpoint] = visinfo
return viewpoints
@classmethod
def load(
cls,
topic_dir: zipfile.Path,
vpt: mdl.ViewPoint,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> Optional["VisualizationInfoHandler"]:
"""
Load the VisualizationInfo and related objects from a BCF zip file.
Args:
topic_dir: The directory in the BCF zip file to load from.
vpt: The ViewPoint to load.
xml_handler: The XML handler to use to parse the VisualizationInfo.
Returns:
The VisualizationInfoHandler object.
"""
visinfo = cls._load_visinfo(topic_dir, vpt.viewpoint, xml_handler)
if not visinfo:
return None
snapshot = cls._load_snapshot(topic_dir, vpt.snapshot)
bitmaps = cls._load_bitmaps(topic_dir, visinfo)
return cls(visinfo, snapshot, bitmaps, xml_handler)
@staticmethod
def _load_visinfo(
topic_dir: zipfile.Path,
vp_name: Optional[str],
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> Optional[mdl.VisualizationInfo]:
if not vp_name:
return None
vp_path = topic_dir.joinpath(vp_name)
if vp_path.exists():
xml_handler = xml_handler or XmlParserSerializer()
return xml_handler.parse(vp_path.read_bytes(), mdl.VisualizationInfo)
return None
@staticmethod
def _load_snapshot(topic_dir: zipfile.Path, vp_snapshot: Optional[str]) -> Optional[bytes]:
if vp_snapshot:
snapshot_path = topic_dir.joinpath(vp_snapshot)
if snapshot_path.exists():
return snapshot_path.read_bytes()
return None
@staticmethod
def _load_bitmaps(topic_dir: zipfile.Path, visinfo: Optional[mdl.VisualizationInfo]) -> dict[str, bytes]:
if not visinfo or not (bitmaps := visinfo.bitmaps):
return {}
bitmaps_dict = {}
for bitmap in bitmaps.bitmap:
if not bitmap.reference:
continue
bitmap_path = topic_dir.joinpath(bitmap.reference)
if bitmap_path.exists():
bitmaps_dict[bitmap.reference] = bitmap_path.read_bytes()
return bitmaps_dict
def save(
self,
bcf_zip: ZipFileInterface,
topic_dir: str,
vpt: mdl.ViewPoint,
) -> None:
"""
Save the VisualizationInfo and related objects to a BCF zip file.
Args:
bcf_zip: The BCF zip file to save to.
topic_dir: The directory in the BCF zip file to save to.
vpt: The ViewPoint to save.
"""
if not (vp_name := vpt.viewpoint):
return
self._save_visinfo(bcf_zip, topic_dir, vp_name)
self._save_snapshot(bcf_zip, topic_dir, vpt.snapshot)
self._save_bitmaps(bcf_zip, topic_dir)
def _save_snapshot(self, bcf_zip: ZipFileInterface, topic_dir: str, filename: Optional[str]) -> None:
if self.snapshot and filename:
bcf_zip.writestr(f"{topic_dir}/{filename}", self.snapshot)
def _save_visinfo(self, bcf_zip: ZipFileInterface, topic_dir: str, vp_name: str) -> None:
bcf_zip.writestr(
f"{topic_dir}/{vp_name}",
self._xml_handler.serialize(self.visualization_info),
)
def _save_bitmaps(self, bcf_zip: ZipFileInterface, topic_dir: str) -> None:
if not self.bitmaps:
return
if not (bitmaps_defs := self.visualization_info.bitmaps):
return
for bitmap_def in bitmaps_defs.bitmap:
if not (bitmap_name := bitmap_def.reference):
continue
if bitmap_name in self.bitmaps:
bcf_zip.writestr(f"{topic_dir}/{bitmap_name}", self.bitmaps[bitmap_name])
@classmethod
def create_new(
cls,
element: entity_instance,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> "VisualizationInfoHandler":
"""
Create a new VisualizationInfoHandler object from an IFC element.
Args:
element: The IFC element to point at.
xml_handler: The XML handler to use.
Returns:
The VisualizationInfoHandler object.
"""
xml_handler = xml_handler or XmlParserSerializer()
return cls(visualization_info=build_viewpoint(element), xml_handler=xml_handler)
@lru_cache(maxsize=None)
def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
"""
Return a BCF viewpoint of an IFC element.
This function is cached to speedudp the creation of multiple BCF topics regarding the same element.
Args:
element: The IFC element to point at.
Returns:
The BCF viewpoint definition.
"""
elem_placement = placement.get_local_placement(element.ObjectPlacement)
return mdl.VisualizationInfo(
guid=str(uuid.uuid4()),
components=build_components(element.GlobalId),
perspective_camera=build_camera(elem_placement),
)
def build_components(guid: str) -> mdl.Components:
"""
Return the BCF components from an IFC element GUID.
Args:
guid: The IFC element GUID.
Returns:
The BCF components definition.
"""
return mdl.Components(
selection=mdl.ComponentSelection(component=[mdl.Component(ifc_guid=guid)]),
visibility=mdl.ComponentVisibility(default_visibility=True),
)
def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera:
"""
Return a BCF camera for an IFC element placement matrix.
Args:
elem_placement: The IFC element placement as a rototranslation matrix.
Returns:
The BCF camera definition.
"""
return build_camera_from_vectors(*calc_camera_vectors(elem_placement))
def build_camera_from_vectors(
camera_position: NDArray[np.float_], camera_dir: NDArray[np.float_], camera_up: NDArray[np.float_]
) -> mdl.PerspectiveCamera:
"""
Return a BCF camera for an IFC element placement matrix.
Args:
camera_position: camera position array
camera_dir: camera direction versor
camera_up_vector: camera up versor
Returns:
The BCF camera definition.
"""
camera_viewpoint = mdl.Point(x=camera_position[0], y=camera_position[1], z=camera_position[2])
camera_direction = mdl.Direction(x=camera_dir[0], y=camera_dir[1], z=camera_dir[2])
camera_up_vector = mdl.Direction(x=camera_up[0], y=camera_up[1], z=camera_up[2])
return mdl.PerspectiveCamera(
camera_view_point=camera_viewpoint,
camera_direction=camera_direction,
camera_up_vector=camera_up_vector,
aspect_ratio=1.0,
field_of_view=60.0,
)
+83
View File
@@ -0,0 +1,83 @@
"""XML Parser and Serializer factories."""
from typing import Optional, Protocol, Type, TypeVar
from xsdata.formats.dataclass.context import XmlContext
from xsdata.formats.dataclass.parsers import XmlParser
from xsdata.formats.dataclass.serializers import XmlSerializer
from xsdata.formats.dataclass.serializers.config import SerializerConfig
def build_xml_parser(context: Optional[XmlContext] = None) -> XmlParser:
"""Return a parser for an XML file."""
parser = XmlParser(context=context or XmlContext())
parser.register_namespace("xs", "http://www.w3.org/2001/XMLSchema")
return parser
def build_serializer(context: Optional[XmlContext] = None) -> XmlSerializer:
"""Return a serializer for an XML file."""
return XmlSerializer(
config=SerializerConfig(pretty_print=True),
context=context or XmlContext(),
)
T = TypeVar("T")
class AbstractXmlParserSerializer(Protocol):
"""XML Parser and serializer wrapper."""
def parse(self, xml: bytes, clazz: Type[T]) -> T:
"""
Parse an XML file to an object.
Args:
xml: The XML file as bytes.
clazz: The class to parse to.
"""
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
"""
Serialize an object to XML.
Args:
obj: The object to serialize.
ns_map: The namespace map to use.
Returns:
The XML as string.
"""
class XmlParserSerializer:
"""XML Parser and serializer wrapper."""
def __init__(self) -> None:
self.context = XmlContext()
self.parser = build_xml_parser(self.context)
self.serializer = build_serializer(self.context)
def parse(self, xml: bytes, clazz: Type[T]) -> T:
"""
Parse an XML file to an object.
Args:
xml: The XML file as bytes.
clazz: The class to parse to.
"""
return self.parser.from_bytes(xml, clazz)
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
"""
Serialize an object to XML.
Args:
obj: The object to serialize.
ns_map: The namespace map to use.
Returns:
The XML as string.
"""
ns_map = ns_map or {"xs": "http://www.w3.org/2001/XMLSchema"}
return self.serializer.render(obj, ns_map)
+1
View File
@@ -0,0 +1 @@
"""BCF tests."""
+8
View File
@@ -0,0 +1,8 @@
import pytest
from bcf.xml_parser import XmlParserSerializer
@pytest.fixture(scope="session")
def xml_handler() -> XmlParserSerializer:
return XmlParserSerializer()
Binary file not shown.
View File
+122
View File
@@ -0,0 +1,122 @@
"""BCF XML tests."""
import uuid
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
import bcf.v2.model as mdl
from bcf.v2.bcfxml import BcfXml
from bcf.v2.topic import TopicHandler
from bcf.v2.visinfo import (
VisualizationInfoHandler,
build_camera_from_vectors,
build_components,
)
from bcf.xml_parser import XmlParserSerializer
@pytest.fixture()
def build_sample(xml_handler: XmlParserSerializer) -> tuple[BcfXml, TopicHandler]:
bcf = BcfXml.create_new("Test project", xml_handler=xml_handler)
orig_th = bcf.add_topic("Test topic", "Test message", "Test author", "Test type")
return bcf, orig_th
def test_bcf_roundtrip(xml_handler, build_sample) -> None:
"""Saving and loading a bcf xml project returns the same objects."""
bcf, orig_th = build_sample
with TemporaryDirectory() as tmp_dir:
file_path = Path(tmp_dir) / "test.bcf"
bcf.save(file_path)
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
assert parsed == bcf
parsed_th = parsed.topics[orig_th.guid]
assert parsed_th == orig_th
def test_bcf_edit_saveas(xml_handler, build_sample) -> None:
"""Saving and loading a bcf xml project returns the same objects."""
bcf, orig_th = build_sample
with TemporaryDirectory() as tmp_dir:
file_path = Path(tmp_dir) / "test.bcf"
bcf.save(file_path)
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
for th in parsed.topics.values():
th.topic.title = "New Topic Title"
modified_path = Path(tmp_dir) / "edited.bcf"
parsed.save(modified_path)
with BcfXml.load(modified_path, xml_handler=xml_handler) as modified_parsed:
assert modified_parsed == bcf
parsed_th = modified_parsed.topics[orig_th.guid]
assert parsed_th.markup != orig_th.markup
assert parsed_th.markup == parsed.topics[orig_th.guid].markup
assert parsed_th.viewpoints == orig_th.viewpoints
assert parsed_th.bim_snippet == orig_th.bim_snippet
def test_bcf_edit(xml_handler, build_sample) -> None:
"""Saving and loading a bcf xml project returns the same objects."""
bcf, orig_th = build_sample
with TemporaryDirectory() as tmp_dir:
file_path = Path(tmp_dir) / "test.bcf"
bcf.save(file_path)
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
for th in parsed.topics.values():
th.topic.title = "New Topic Title"
parsed.save()
with BcfXml.load(file_path, xml_handler=xml_handler) as modified_parsed:
assert modified_parsed == bcf
assert len(modified_parsed.topics) == 1
parsed_th = modified_parsed.topics[orig_th.guid]
assert parsed_th.markup != orig_th.markup
assert parsed_th.markup == parsed.topics[orig_th.guid].markup
assert parsed_th.viewpoints == orig_th.viewpoints
assert parsed_th.bim_snippet == orig_th.bim_snippet
def test_save_no_filename(build_sample) -> None:
bcf, _ = build_sample
with pytest.raises(ValueError):
bcf.save()
def test_load_no_filename() -> None:
with pytest.raises(ValueError):
BcfXml.load("")
def test_save_keep_open(build_sample) -> None:
bcf, _ = build_sample
with TemporaryDirectory() as tmp_dir:
file_path = Path(tmp_dir) / "test.bcf"
bcf.save(file_path, keep_open=True)
assert bcf._zip_file is not None
bcf._zip_file.close()
def test_massive_bcf(xml_handler) -> None:
bcf = BcfXml.create_new("Test project", xml_handler=xml_handler)
for i in range(100):
th = bcf.add_topic(f"Topic {i:04}", f"Message {i:04}", "Test author", "Test type")
vi = mdl.VisualizationInfo(
guid=str(uuid.uuid4()),
components=build_components(str(uuid.uuid4())),
perspective_camera=build_camera_from_vectors([i, 0, 0], [0, 1, 0], [0, 0, 1]),
)
vh = VisualizationInfoHandler(visualization_info=vi, xml_handler=xml_handler)
th.add_visinfo_handler(vh)
assert len(bcf.topics) == 100
with TemporaryDirectory() as tmp_dir:
file_path = Path(tmp_dir) / "test.bcf"
bcf.save(file_path)
def test_equality_with_wrong_object() -> None:
with pytest.raises(TypeError):
build_sample[0] == "Wrong object"
def test_topic_equality_with_wrong_object() -> None:
with pytest.raises(TypeError):
build_sample[1] == "Wrong object"
+365
View File
@@ -0,0 +1,365 @@
import json
import os
import zipfile
from pathlib import Path
from xsdata.models.datatype import XmlDateTime
import bcf.v2.model as mdl
from bcf.v2.bcfxml import BcfXml
from bcf.v2.topic import TopicHandler
def test_maximum_information() -> None:
"""
All the info in a BCF is parsed correctly.
Uses sample file from https://github.com/buildingSMART/BCF-XML/
"""
bcf_path = Path(__file__).parent / "MaximumInformation.bcf"
with BcfXml.load(bcf_path) as bcf:
assert_everything_in_place(bcf)
def test_save_maximum_information() -> None:
base_dir = Path(__file__).parent
bcf_path = base_dir / "MaximumInformation.bcf"
target_path = base_dir / "MaximumInformationSaved.bcf"
with BcfXml.load(bcf_path) as bcf:
bcf.save(target_path)
assert target_path.exists()
with BcfXml.load(target_path) as bcf2:
assert_everything_in_place(bcf2)
expected_files = [
"bcf.version",
"project.bcfp",
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/markup.bcf",
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/bitmap.png",
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/tux.png",
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/JsonElement.json",
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Viewpoint_4ab7514b-b216-4d56-98d2-45cf8500ff5a.bcfv",
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Viewpoint_9a4a1878-ecbd-4916-83a8-dad82e560231.bcfv",
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Viewpoint_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.bcfv",
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Snapshot_4ab7514b-b216-4d56-98d2-45cf8500ff5a.png",
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Snapshot_9a4a1878-ecbd-4916-83a8-dad82e560231.png",
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Snapshot_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.png",
"d1068c81-af04-4546-b63c-348810f6c716/markup.bcf",
"extensions.xsd",
"IfcPile_01.ifc",
"markup.xsd",
]
assert_files_present(target_path, expected_files)
os.unlink(target_path)
def assert_everything_in_place(bcf: BcfXml):
assert bcf.version.version_id == "2.1"
assert bcf.project.name == "BCF API Implementation"
assert bcf.project_info.extension_schema == "extensions.xsd"
assert len(bcf.topics) == 2
assert_first_topic_handler(bcf.topics["7ddc3ef0-0ab7-43f1-918a-45e38b42369c"])
second_th = bcf.topics["d1068c81-af04-4546-b63c-348810f6c716"]
assert second_th.topic == mdl.Topic(
title="Referenced topic",
creation_date=XmlDateTime(2017, 5, 22, 7, 51, 0, 42987900),
creation_author="dangl@iabi.eu",
description="This is just an empty topic that acts as a referenced topic.",
guid="d1068c81-af04-4546-b63c-348810f6c716",
)
def assert_first_topic_handler(topic_handler: TopicHandler):
assert topic_handler.guid == "7ddc3ef0-0ab7-43f1-918a-45e38b42369c"
expected_bs1 = mdl.BimSnippet(
reference="JsonElement.json", reference_schema="http://json-schema.org", snippet_type="JSON"
)
expected_t1 = mdl.Topic(
reference_link=["https://bim--it.net"],
title="Maximum Content",
priority="High",
index=0,
labels=["Structural", "IT Development"],
creation_date=XmlDateTime(2015, 6, 21, 12, 0, 0),
creation_author="dangl@iabi.eu",
modified_date=XmlDateTime(2015, 6, 21, 14, 22, 47),
modified_author="dangl@iabi.eu",
due_date=XmlDateTime(2016, 10, 2, 14, 22, 47),
assigned_to="linhard@iabi.eu",
stage="Construction Start",
description="This is a topic with all informations present.",
bim_snippet=expected_bs1,
document_reference=[
mdl.TopicDocumentReference(
referenced_document="https://github.com/BuildingSMART/BCF-XML",
description="GitHub BCF Specification",
is_external=True,
),
mdl.TopicDocumentReference(
referenced_document="../markup.xsd",
description="Markup.xsd Schema",
is_external=False,
),
],
related_topic=[mdl.TopicRelatedTopic(guid="d1068c81-af04-4546-b63c-348810f6c716")],
guid="7ddc3ef0-0ab7-43f1-918a-45e38b42369c",
topic_type="Structural",
topic_status="Open",
)
assert topic_handler.topic == expected_t1
expected_h1 = mdl.Header(
file=[
mdl.HeaderFile(
filename="IfcPile_01.ifc",
date=XmlDateTime(2014, 10, 27, 16, 27, 27),
reference="../IfcPile_01.ifc",
ifc_project="0M6o7Znnv7hxsbWgeu7oQq",
ifc_spatial_structure_element="23B$bNeGHFQuMYJzvUX0FD",
is_external=False,
)
]
)
assert topic_handler.header == expected_h1
expected_th1_comments = [
mdl.Comment(
date=XmlDateTime(2015, 8, 31, 12, 40, 17),
author="dangl@iabi.eu",
comment="This is an unmodified topic at the uppermost hierarchical level.\nAll times in the XML are marked as UTC times.",
guid="07ccdba0-1736-47e1-807d-67dc6f3addaa",
),
mdl.Comment(
date=XmlDateTime(2015, 8, 31, 14, 0, 1),
author="dangl@iabi.eu",
comment="This comment was a reply to the first comment in BCF v2.0. This is a no longer supported functionality and therefore is to be treated as a regular comment in v2.1.",
guid="a12766c2-61bc-40b4-ab19-e9f45fd0b0bf",
),
mdl.Comment(
date=XmlDateTime(2015, 8, 31, 13, 7, 11),
author="dangl@iabi.eu",
comment="This comment again is in the highest hierarchy level.\nIt references a viewpoint.",
viewpoint=mdl.CommentViewpoint(guid="4ab7514b-b216-4d56-98d2-45cf8500ff5a"),
guid="c2bb5bb0-773d-45dd-bdaa-19a216439ed3",
),
mdl.Comment(
date=XmlDateTime(2015, 8, 31, 15, 42, 58),
author="dangl@iabi.eu",
comment="This comment contained some spllng errs.\nHopefully, the modifier did catch them all.",
modified_date=XmlDateTime(2015, 8, 31, 16, 7, 11),
modified_author="dangl@iabi.eu",
guid="0b843a5c-c3bf-41ef-be98-52a9f7bd9790",
),
]
assert topic_handler.comments == expected_th1_comments
expected_th1_viewpoints = [
mdl.ViewPoint(
viewpoint="Viewpoint_4ab7514b-b216-4d56-98d2-45cf8500ff5a.bcfv",
snapshot="Snapshot_4ab7514b-b216-4d56-98d2-45cf8500ff5a.png",
index=2,
guid="4ab7514b-b216-4d56-98d2-45cf8500ff5a",
),
mdl.ViewPoint(
viewpoint="Viewpoint_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.bcfv",
snapshot="Snapshot_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.png",
index=0,
guid="fc4019d7-365e-47f3-b6d0-b39fc48f15fc",
),
mdl.ViewPoint(
viewpoint="Viewpoint_9a4a1878-ecbd-4916-83a8-dad82e560231.bcfv",
snapshot="Snapshot_9a4a1878-ecbd-4916-83a8-dad82e560231.png",
index=1,
guid="9a4a1878-ecbd-4916-83a8-dad82e560231",
),
]
expected_m1 = mdl.Markup(
header=expected_h1,
topic=expected_t1,
comment=expected_th1_comments,
viewpoints=expected_th1_viewpoints,
)
assert topic_handler.markup == expected_m1
assert json.loads(topic_handler.bim_snippet) == {"Material": "Concrete", "Temperatures": ["Cold", "Hot", "Hotter"]}
assert topic_handler.reference_files["../IfcPile_01.ifc"] is not None
assert_viewpoints(topic_handler.viewpoints)
def assert_viewpoints(viewpoints):
assert len(viewpoints) == 3
expected_selection = mdl.ComponentSelection(
component=[
mdl.Component(ifc_guid="0cSRUx$EX1NRjqiKcYQ$a0"),
mdl.Component(ifc_guid="1jQQiGIAnFzxOUzrdmJYDS"),
mdl.Component(ifc_guid="0fdpeZZEX3FwJ7x0ox5kzF"),
mdl.Component(ifc_guid="23Zwlpd71EyvHlH6OZ77nK"),
mdl.Component(ifc_guid="1OpjQ1Nlv4sQuTxfUC_8zS"),
]
)
expected_exception = mdl.ComponentVisibilityExceptions(
component=[
mdl.Component(ifc_guid="0Gl71cVurFn8bxAOox6M4X"),
mdl.Component(ifc_guid="23Zwlpd71EyvHlH6OZ77nK"),
mdl.Component(ifc_guid="3DvyPxGIn8qR0KDwbL_9r1"),
mdl.Component(ifc_guid="0fdpeZZEX3FwJ7x0ox5kzF"),
mdl.Component(ifc_guid="1OpjQ1Nlv4sQuTxfUC_8zS"),
]
)
expected_coloring = mdl.ComponentColoring(
color=[
mdl.ComponentColoringColor(
component=[
mdl.Component(ifc_guid="0fdpeZZEX3FwJ7x0ox5kzF"),
mdl.Component(ifc_guid="23Zwlpd71EyvHlH6OZ77nK"),
mdl.Component(ifc_guid="1OpjQ1Nlv4sQuTxfUC_8zS"),
mdl.Component(ifc_guid="0cSRUx$EX1NRjqiKcYQ$a0"),
],
color="3498DB",
)
]
)
assert_first_viewpoint(
viewpoints["Viewpoint_4ab7514b-b216-4d56-98d2-45cf8500ff5a.bcfv"],
expected_selection,
expected_exception,
expected_coloring,
)
assert_second_viewpoint(
viewpoints["Viewpoint_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.bcfv"],
expected_selection,
expected_exception,
expected_coloring,
)
assert_third_viewpoint(
viewpoints["Viewpoint_9a4a1878-ecbd-4916-83a8-dad82e560231.bcfv"],
expected_selection,
expected_exception,
expected_coloring,
)
def assert_first_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo(
components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=True,
space_boundaries_visible=True,
openings_visible=True,
),
selection=expected_selection,
visibility=mdl.ComponentVisibility(
exceptions=expected_exception,
default_visibility=True,
),
coloring=expected_coloring,
),
perspective_camera=mdl.PerspectiveCamera(
camera_view_point=mdl.Point(x=0.43079984188079834, y=69.52057647705078, z=10.666350364685059),
camera_direction=mdl.Direction(x=0.09159398823976517, y=-0.9375035166740417, z=-0.3357048034667969),
camera_up_vector=mdl.Direction(x=0.01938679628074169, y=-0.3353792130947113, z=0.9418837428092957),
field_of_view=60,
),
lines=mdl.VisualizationInfoLines(
line=[
mdl.Line(start_point=mdl.Point(x=0, y=0, z=0), end_point=mdl.Point(x=0, y=0, z=1)),
mdl.Line(start_point=mdl.Point(x=0, y=0, z=1), end_point=mdl.Point(x=0, y=1, z=1)),
mdl.Line(start_point=mdl.Point(x=0, y=1, z=1), end_point=mdl.Point(x=1, y=1, z=1)),
]
),
clipping_planes=mdl.VisualizationInfoClippingPlanes(
clipping_plane=[
mdl.ClippingPlane(location=mdl.Point(x=0, y=0, z=0), direction=mdl.Direction(x=0, y=0, z=1)),
mdl.ClippingPlane(location=mdl.Point(x=0, y=0, z=0), direction=mdl.Direction(x=0, y=1, z=0)),
]
),
bitmap=[
mdl.VisualizationInfoBitmap(
bitmap=mdl.BitmapFormat.PNG,
reference="bitmap.png",
location=mdl.Point(x=10, y=-10, z=7),
normal=mdl.Direction(x=0, y=1, z=0),
up=mdl.Direction(x=0, y=0, z=1),
height=5.0,
),
mdl.VisualizationInfoBitmap(
bitmap=mdl.BitmapFormat.PNG,
reference="tux.png",
location=mdl.Point(x=20, y=-10, z=7),
normal=mdl.Direction(x=0, y=1, z=0),
up=mdl.Direction(x=0, y=0, z=1),
height=5.0,
),
],
guid="8dc86298-9737-40b4-a448-98a9e953293a",
)
assert viewpoint.visualization_info == expected_vp
assert viewpoint.snapshot is not None
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo(
components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=False,
),
selection=expected_selection,
visibility=mdl.ComponentVisibility(
exceptions=expected_exception,
default_visibility=False,
),
coloring=expected_coloring,
),
perspective_camera=mdl.PerspectiveCamera(
camera_view_point=mdl.Point(x=-47.18794250488281, y=43.829200744628906, z=10.666350364685059),
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
field_of_view=60,
),
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
)
assert viewpoint.visualization_info == expected_vp
assert viewpoint.snapshot is not None
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo(
components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=True,
),
selection=expected_selection,
visibility=mdl.ComponentVisibility(
exceptions=expected_exception,
default_visibility=True,
),
coloring=expected_coloring,
),
perspective_camera=mdl.PerspectiveCamera(
camera_view_point=mdl.Point(x=-48.974571228027344, y=-64.20051574707031, z=10.666350364685059),
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
field_of_view=60,
),
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
)
assert viewpoint.visualization_info == expected_vp
assert viewpoint.snapshot is not None
def assert_files_present(saved_bcf_path, expected_files):
with zipfile.ZipFile(saved_bcf_path) as bcf_zip:
for file_path in expected_files:
assert zipfile.Path(bcf_zip, file_path).exists()
Binary file not shown.
View File
+129
View File
@@ -0,0 +1,129 @@
"""BCF XML tests."""
import uuid
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
import bcf.v3.model as mdl
from bcf.v3.bcfxml import BcfXml
from bcf.v3.topic import TopicHandler
from bcf.v3.visinfo import (
VisualizationInfoHandler,
build_camera_from_vectors,
build_components,
)
from bcf.xml_parser import XmlParserSerializer
@pytest.fixture()
def build_sample(xml_handler: XmlParserSerializer) -> tuple[BcfXml, TopicHandler]:
ext = mdl.Extensions(topic_types=mdl.ExtensionsTopicTypes(topic_type=["Test type"]))
bcf = BcfXml.create_new("Test project", extensions=ext, xml_handler=xml_handler)
orig_th = bcf.add_topic("Test topic", "Test message", "Test author", "Test type")
return bcf, orig_th
def test_bcf_roundtrip(xml_handler, build_sample) -> None:
"""Saving and loading a bcf xml project returns the same objects."""
bcf, orig_th = build_sample
with TemporaryDirectory() as tmp_dir:
file_path = Path(tmp_dir) / "test.bcf"
bcf.save(file_path)
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
assert parsed == bcf
parsed_th = parsed.topics[orig_th.guid]
assert parsed_th == orig_th
def test_bcf_edit_saveas(xml_handler, build_sample) -> None:
"""Saving and loading a bcf xml project returns the same objects."""
bcf, orig_th = build_sample
with TemporaryDirectory() as tmp_dir:
file_path = Path(tmp_dir) / "test.bcf"
bcf.save(file_path)
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
for th in parsed.topics.values():
th.topic.title = "New Topic Title"
modified_path = Path(tmp_dir) / "edited.bcf"
parsed.save(modified_path)
with BcfXml.load(modified_path, xml_handler=xml_handler) as modified_parsed:
assert modified_parsed == bcf
parsed_th = modified_parsed.topics[orig_th.guid]
assert parsed_th.markup != orig_th.markup
assert parsed_th.markup == parsed.topics[orig_th.guid].markup
assert parsed_th.viewpoints == orig_th.viewpoints
assert parsed_th.bim_snippet == orig_th.bim_snippet
def test_bcf_edit(xml_handler, build_sample) -> None:
"""Saving and loading a bcf xml project returns the same objects."""
bcf, orig_th = build_sample
with TemporaryDirectory() as tmp_dir:
file_path = Path(tmp_dir) / "test.bcf"
bcf.save(file_path)
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
for th in parsed.topics.values():
th.topic.title = "New Topic Title"
parsed.save()
with BcfXml.load(file_path, xml_handler=xml_handler) as modified_parsed:
assert modified_parsed == bcf
assert len(modified_parsed.topics) == 1
parsed_th = modified_parsed.topics[orig_th.guid]
assert parsed_th.markup != orig_th.markup
assert parsed_th.markup == parsed.topics[orig_th.guid].markup
assert parsed_th.viewpoints == orig_th.viewpoints
assert parsed_th.bim_snippet == orig_th.bim_snippet
def test_save_no_filename(build_sample) -> None:
bcf, _ = build_sample
with pytest.raises(ValueError):
bcf.save()
def test_load_no_filename() -> None:
with pytest.raises(ValueError):
BcfXml.load("")
def test_save_keep_open(build_sample) -> None:
bcf, orig_th = build_sample
with TemporaryDirectory() as tmp_dir:
file_path = Path(tmp_dir) / "test.bcf"
bcf.save(file_path, keep_open=True)
assert bcf._zip_file is not None
bcf._zip_file.close()
# image = PIL.Image.new('RGB', size=(100, 100))
# file = BinaryIO()
# image.save(file)
def test_massive_bcf(xml_handler) -> None:
ext = mdl.Extensions(topic_types=mdl.ExtensionsTopicTypes(topic_type=["Test type"]))
bcf = BcfXml.create_new("Test project", extensions=ext, xml_handler=xml_handler)
for i in range(100):
th = bcf.add_topic(f"Topic {i:04}", f"Message {i:04}", "Test author", "Test type")
vi = mdl.VisualizationInfo(
guid=str(uuid.uuid4()),
components=build_components(str(uuid.uuid4())),
perspective_camera=build_camera_from_vectors([i, 0, 0], [0, 1, 0], [0, 0, 1]),
)
vh = VisualizationInfoHandler(visualization_info=vi, xml_handler=xml_handler)
th.add_visinfo_handler(vh)
assert len(bcf.topics) == 100
with TemporaryDirectory() as tmp_dir:
file_path = Path(tmp_dir) / "test.bcf"
bcf.save(file_path)
def test_equality_with_wrong_object() -> None:
with pytest.raises(TypeError):
build_sample[0] == "Wrong object"
def test_topic_equality_with_wrong_object() -> None:
with pytest.raises(TypeError):
build_sample[1] == "Wrong object"
+230
View File
@@ -0,0 +1,230 @@
import json
import os
import zipfile
from pathlib import Path
from xsdata.models.datatype import XmlDateTime
import bcf.v3.model as mdl
from bcf.v3.bcfxml import BcfXml
from bcf.v3.topic import TopicHandler
def test_doc_ref_internal() -> None:
"""
All the info in a BCF is parsed correctly.
Uses sample file from https://github.com/buildingSMART/BCF-XML/
"""
bcf_path = Path(__file__).parent / "Document reference internal.bcf"
with BcfXml.load(bcf_path) as bcf:
assert_everything_in_place(bcf)
def test_doc_ref_internal_save() -> None:
base_dir = Path(__file__).parent
bcf_path = base_dir / "Document reference internal.bcf"
target_path = base_dir / "Document reference internal Saved.bcf"
with BcfXml.load(bcf_path) as bcf:
bcf.save(target_path)
assert target_path.exists()
with BcfXml.load(target_path) as bcf2:
assert_everything_in_place(bcf2)
assert_files_present(target_path)
os.unlink(target_path)
def assert_everything_in_place(bcf: BcfXml):
assert bcf.version.version_id == "3.0"
assert bcf.project.name == "BCF 3.0 test cases"
assert bcf.project.project_id == "de894a86-3a08-4ea0-b2d1-6c222b5602d1"
assert len(bcf.topics) == 1
topic_handler = bcf.topics["8ac9822a-761a-4deb-9f39-f61286acbf6a"]
expected_h1 = mdl.Header(
files=mdl.HeaderFiles(
file=[
mdl.File(
filename="BCF-ARK",
date=XmlDateTime(2021, 1, 4, 9, 37, 45),
reference="https://bimsync.com/project/f5fbb3c695274d1890036bf64f77eb71/revisions/007afab57f264d2296aae0a452486ae1",
is_external=True,
),
mdl.File(
filename="BCF-MEP",
date=XmlDateTime(2017, 8, 7, 9, 51, 34),
reference="https://bimsync.com/project/f5fbb3c695274d1890036bf64f77eb71/revisions/a21ed391f9e046a2bb2bc879c48f1d48",
is_external=True,
),
]
)
)
assert topic_handler.header == expected_h1
expected_comments = [
mdl.Comment(
date=XmlDateTime(2021, 2, 17, 9, 16, 4, 160_000_000),
author="Architect@example.com",
comment="A comment",
viewpoint=mdl.CommentViewpoint(guid="20ad2ff7-ceac-4d4b-b288-ad92a0f65182"),
guid="c045ef04-324d-4c36-9ac8-831f2a15c6d6",
),
]
assert topic_handler.comments == expected_comments
expected_viewpoints = [
mdl.ViewPoint(
viewpoint="Viewpoint_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.bcfv",
snapshot="Snapshot_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.png",
guid="20ad2ff7-ceac-4d4b-b288-ad92a0f65182",
),
]
expected_topic = mdl.Topic(
reference_links=mdl.TopicReferenceLinks(),
title="Document Reference Internal",
labels=mdl.TopicLabels(),
creation_date=XmlDateTime(2021, 2, 17, 9, 16, 4, 160_000_000),
creation_author="Architect@example.com",
modified_date=XmlDateTime(2021, 2, 17, 9, 16, 4, 160_000_000),
assigned_to="OtherUser@doe.com",
document_references=mdl.TopicDocumentReferences(
document_reference=[
mdl.DocumentReference(
document_guid="b1d1b7f0-60b9-457d-ad12-16e0fb997bc5",
description="ThisIsADocument.txt",
guid="048e898f-555f-47c3-a273-6c664fb2ef69",
),
],
),
related_topics=mdl.TopicRelatedTopics(),
comments=mdl.TopicComments(comment=expected_comments),
viewpoints=mdl.TopicViewpoints(view_point=expected_viewpoints),
guid="8ac9822a-761a-4deb-9f39-f61286acbf6a",
server_assigned_id="6",
topic_type="ERROR",
topic_status="OPEN",
)
assert topic_handler.topic == expected_topic
expected_m1 = mdl.Markup(
header=expected_h1,
topic=expected_topic,
)
assert topic_handler.markup == expected_m1
assert topic_handler.bim_snippet is None
assert_viewpoints(topic_handler.viewpoints)
def assert_viewpoints(viewpoints):
assert len(viewpoints) == 1
expected_vp = mdl.VisualizationInfo(
components=mdl.Components(
selection=mdl.ComponentSelection(),
visibility=mdl.ComponentVisibility(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=False,
),
exceptions=mdl.ComponentVisibilityExceptions(
component=[
mdl.Component(ifc_guid="1m5wAJelDFdhn6qBdOGjos", originating_system="https://bimsync.com"),
mdl.Component(ifc_guid="1bbI761TbBCOoIa5Kt6PXt", originating_system="https://bimsync.com"),
]
),
default_visibility=True,
),
coloring=mdl.ComponentColoring(),
),
perspective_camera=mdl.PerspectiveCamera(
camera_view_point=mdl.Point(x=23.112083480037754, y=-25.45574897560043, z=18.52519828443092),
camera_direction=mdl.Direction(x=-0.6289237666876837, y=0.5933487270610425, z=-0.5023864884632315),
camera_up_vector=mdl.Direction(x=-0.36542566067664123, y=0.3447553774917179, z=0.8646431727652648),
field_of_view=60,
aspect_ratio=1,
),
lines=mdl.VisualizationInfoLines(),
clipping_planes=mdl.VisualizationInfoClippingPlanes(),
bitmaps=mdl.VisualizationInfoBitmaps(),
guid="20ad2ff7-ceac-4d4b-b288-ad92a0f65182",
)
viewpoint = viewpoints["Viewpoint_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.bcfv"]
assert viewpoint.visualization_info == expected_vp
assert viewpoint.snapshot is not None
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo(
components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=False,
),
selection=expected_selection,
visibility=mdl.ComponentVisibility(
exceptions=expected_exception,
default_visibility=False,
),
coloring=expected_coloring,
),
perspective_camera=mdl.PerspectiveCamera(
camera_view_point=mdl.Point(x=-47.18794250488281, y=43.829200744628906, z=10.666350364685059),
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
field_of_view=60,
),
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
)
assert viewpoint.visualization_info == expected_vp
assert viewpoint.snapshot is not None
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo(
components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=True,
),
selection=expected_selection,
visibility=mdl.ComponentVisibility(
exceptions=expected_exception,
default_visibility=True,
),
coloring=expected_coloring,
),
perspective_camera=mdl.PerspectiveCamera(
camera_view_point=mdl.Point(x=-48.974571228027344, y=-64.20051574707031, z=10.666350364685059),
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
field_of_view=60,
),
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
)
assert viewpoint.visualization_info == expected_vp
assert viewpoint.snapshot is not None
def assert_files_present(saved_bcf_path):
expected_files = [
"bcf.version",
"project.bcfp",
"documents.xml",
"extensions.xml",
"documents/b1d1b7f0-60b9-457d-ad12-16e0fb997bc5",
"8ac9822a-761a-4deb-9f39-f61286acbf6a/markup.bcf",
"8ac9822a-761a-4deb-9f39-f61286acbf6a/Viewpoint_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.bcfv",
"8ac9822a-761a-4deb-9f39-f61286acbf6a/Snapshot_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.png",
]
with zipfile.ZipFile(saved_bcf_path) as bcf_zip:
for file_path in expected_files:
assert zipfile.Path(bcf_zip, file_path).exists()
+8 -1
View File
@@ -182,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-fa6bbf2-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-cdde536-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -312,6 +312,13 @@ endif
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/ad/c7/17c9d16320d8e2cfdb27d2fd298b5e8f7a3f211025b0c1bc7a39a85bd690/xsdata-22.11.tar.gz
cd dist/working && tar -xzvf xsdata*
cp -r dist/working/xsdata-22.11/xsdata dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/11/bc/5afb61dd5d863e5cf77cd952445c50c17e65953405986f19e97e4389692a/elementpath-3.0.2.tar.gz
@@ -91,10 +91,12 @@ classes = [
operator.RemoveIfcFile,
operator.SelectDataDir,
operator.SelectIfcFile,
operator.ReloadSelectedIfcFile,
operator.SelectSchemaDir,
operator.SelectURIAttribute,
operator.EditBlenderCollection,
operator.BIM_OT_open_webbrowser,
operator.BIM_OT_show_description,
prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
prop.ObjProperty,
@@ -159,6 +161,8 @@ def register():
bpy.types.Camera.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
bpy.types.SCENE_PT_unit.append(ui.ifc_units)
if hasattr(bpy.types, "UI_MT_button_context_menu"):
bpy.types.UI_MT_button_context_menu.append(ui.draw_custom_context_menu)
for mod in modules.values():
mod.register()
@@ -179,6 +183,8 @@ def unregister():
del bpy.types.Camera.BIMMeshProperties
del bpy.types.PointLight.BIMMeshProperties
bpy.types.SCENE_PT_unit.remove(ui.ifc_units)
if hasattr(bpy.types, "UI_MT_button_context_menu"):
bpy.types.UI_MT_button_context_menu.remove(ui.draw_custom_context_menu)
for mod in reversed(list(modules.values())):
mod.unregister()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -26,6 +26,7 @@ from blenderbim.bim.module.drawing.prop import RasterStyleProperty
from bpy.app.handlers import persistent
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.owner.prop import get_user_person, get_user_organisation
from blenderbim.bim.module.model.data import AuthoringData
from ifcopenshell.api.material.data import Data as MaterialData
from ifcopenshell.api.type.data import Data as TypeData
@@ -301,3 +302,4 @@ def setDefaultProperties(scene):
drawing_style.name = "Blender Default"
drawing_style.render_type = "DEFAULT"
bpy.ops.bim.save_drawing_style(index="2")
AuthoringData.type_thumbnails = {}
+67 -30
View File
@@ -23,11 +23,11 @@ import math
import zipfile
import ifcopenshell
import ifcopenshell.util.attribute
from blenderbim.bim.prop import get_ifc_entity_doc_url
from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, get_property_doc
from mathutils import geometry
from mathutils import Vector
from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
def draw_attributes(props, layout, copy_operator=None):
@@ -85,6 +85,7 @@ def import_attribute(attribute, props, data, callback=None):
new.is_null = data[attribute.name()] is None
new.is_optional = attribute.optional()
new.data_type = data_type if isinstance(data_type, str) else ""
new.ifc_class = data["type"]
is_handled_by_callback = callback(attribute.name(), new, data) if callback else None
if is_handled_by_callback:
@@ -92,19 +93,66 @@ def import_attribute(attribute, props, data, callback=None):
elif is_handled_by_callback is False:
props.remove(len(props) - 1)
elif data_type == "string":
new.string_value = "" if new.is_null else data[attribute.name()]
new.string_value = "" if new.is_null else str(data[attribute.name()])
if attribute.type_of_attribute().declared_type().name() == "IfcURIReference":
new.is_uri = True
elif data_type == "boolean":
new.bool_value = False if new.is_null else data[attribute.name()]
new.bool_value = False if new.is_null else bool(data[attribute.name()])
elif data_type == "integer":
new.int_value = 0 if new.is_null else data[attribute.name()]
new.int_value = 0 if new.is_null else int(data[attribute.name()])
elif data_type == "float":
new.float_value = 0.0 if new.is_null else data[attribute.name()]
new.float_value = 0.0 if new.is_null else float(data[attribute.name()])
elif data_type == "enum":
new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute))
if data[attribute.name()]:
new.enum_value = data[attribute.name()]
enum_items = ifcopenshell.util.attribute.get_enum_items(attribute)
new.enum_items = json.dumps(enum_items)
add_attribute_enum_items_descriptions(new, enum_items)
if data[new.name]:
new.enum_value = data[new.name]
add_attribute_description(new, data)
add_attribute_min_max(new)
ATTRIBUTE_MIN_MAX_CONSTRAINTS = {"IfcMaterialLayer": {"Priority": {"value_min": 0, "value_max": 100}}}
def add_attribute_min_max(attribute_blender):
if attribute_blender.ifc_class in ATTRIBUTE_MIN_MAX_CONSTRAINTS:
constraints = ATTRIBUTE_MIN_MAX_CONSTRAINTS[attribute_blender.ifc_class].get(attribute_blender.name, {})
for constraint, value in constraints.items():
setattr(attribute_blender, constraint, value)
setattr(attribute_blender, constraint + "_constraint", True)
def add_attribute_enum_items_descriptions(attribute_blender, enum_items):
attribute_blender.enum_descriptions.clear()
if isinstance(enum_items, dict):
enum_items = enum_items.keys()
version = tool.Ifc.get_schema()
for enum_item in enum_items:
new_enum_description = attribute_blender.enum_descriptions.add()
try:
description = get_predefined_type_doc(version, attribute_blender.ifc_class, enum_item) or ""
except KeyError: # TODO this only supports predefined type enums. Add support for other types of enums ?
description = ""
new_enum_description.name = description
def add_attribute_description(attribute_blender, attribute_ifc=None):
if not attribute_blender.name:
return
version = tool.Ifc.get_schema()
description = ""
try:
description = get_attribute_doc(version, attribute_blender.ifc_class, attribute_blender.name)
except RuntimeError: # It's not an Entity Attribute. Let's try a Property Set attribute.
doc = get_property_doc(version, attribute_blender.ifc_class, attribute_blender.name)
if doc:
description = doc.get("description", "")
else: # It's a custom property set. Check if this attribute has a description
if attribute_ifc is not None:
description = getattr(attribute_ifc, "Description", "")
if description:
attribute_blender.description = description
def export_attributes(props, callback=None):
@@ -120,29 +168,18 @@ def export_attributes(props, callback=None):
def prop_with_search(layout, data, prop_name, **kwargs):
# kwargs are layout.prop arguments (text, icon, etc.)
row = layout.row(align=True)
# Magick courtesy of https://blender.stackexchange.com/a/203443/86891
row.context_pointer_set(name="data", data=data)
row.prop(data, prop_name, **kwargs)
op = row.operator("bim.enum_property_search", text="", icon="VIEWZOOM")
op.prop_name = prop_name
add_entity_url_button(row, getattr(data, prop_name))
try:
if len(get_enum_items(data, prop_name)) > 10:
# Magick courtesy of https://blender.stackexchange.com/a/203443/86891
row.context_pointer_set(name="data", data=data)
op = row.operator("bim.enum_property_search", text="", icon="VIEWZOOM")
op.prop_name = prop_name
except TypeError: # Prop is not iterable
pass
def add_entity_url_button(layout, entity):
if entity:
try:
url = get_ifc_entity_doc_url(entity)
except KeyError:
# TODO : support attributes, pset, etc.
pass
else:
if url:
url_op = layout.operator("bim.open_webbrowser", icon="INFO", text="")
url_op.url = url
def get_enum_items(data, prop_name, context):
def get_enum_items(data, prop_name, context=None):
# 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
@@ -152,7 +189,7 @@ def get_enum_items(data, prop_name, context):
return
if not isinstance(items, (list, tuple)):
# items are retrieved through a callback, not a static list :
items = items(data, context)
items = items(data, context or bpy.context)
return items
+32 -14
View File
@@ -112,7 +112,7 @@ class MaterialCreator:
def parse_representation(self, representation):
has_parsed = False
representation_items = self.resolve_all_representation_items(representation)
representation_items = self.resolve_all_stylable_representation_items(representation)
for item in representation_items:
if self.parse_representation_item(item):
has_parsed = True
@@ -153,11 +153,19 @@ class MaterialCreator:
]
self.mesh.polygons.foreach_set("material_index", material_index)
def resolve_all_representation_items(self, representation):
def resolve_all_stylable_representation_items(self, representation):
items = []
for item in representation.Items:
if item.is_a("IfcMappedItem"):
items.extend(item.MappingSource.MappedRepresentation.Items)
if item.is_a("IfcBooleanResult"):
operand = item.FirstOperand
while True:
items.append(operand)
if operand.is_a("IfcBooleanResult"):
operand = operand.FirstOperand
else:
break
items.append(item)
return items
@@ -329,11 +337,14 @@ class IfcImporter:
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.file.schema in ("IFC2X3", "IFC4"):
self.element_types = set(
self.file.by_type("IfcElementType")
+ self.file.by_type("IfcDoorStyle")
+ self.file.by_type("IfcWindowStyle")
)
else:
self.element_types = set(self.file.by_type("IfcElementType"))
if self.ifc_import_settings.has_filter and self.ifc_import_settings.should_filter_spatial_elements:
self.spatial_elements = self.get_spatial_elements_filtered_by_elements(self.elements)
@@ -517,14 +528,21 @@ class IfcImporter:
def does_element_likely_have_geometry_far_away(self, element):
for representation in element.Representation.Representations:
for subelement in self.file.traverse(representation):
if subelement.is_a("IfcCartesianPointList3D"):
for point in subelement.CoordList:
if len(point) == 3 and self.is_point_far_away(point, is_meters=False):
items = []
for item in representation.Items:
if item.is_a("IfcMappedItem"):
items.extend(item.MappingSource.MappedRepresentation.Items)
else:
items.append(item)
for item in items:
for subelement in self.file.traverse(item):
if subelement.is_a("IfcCartesianPointList3D"):
for point in subelement.CoordList:
if len(point) == 3 and self.is_point_far_away(point, is_meters=False):
return True
if subelement.is_a("IfcCartesianPoint"):
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
return True
if subelement.is_a("IfcCartesianPoint"):
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
return True
def apply_blender_offset_to_matrix_world(self, obj, matrix):
props = bpy.context.scene.BIMGeoreferenceProperties
@@ -28,3 +28,7 @@ class BcfStore:
if not BcfStore.bcfxml:
BcfStore.bcfxml = bcf.v2.bcfxml.BcfXml()
return BcfStore.bcfxml
@classmethod
def set(cls, bcfxml):
cls.bcfxml = bcfxml
@@ -20,7 +20,6 @@ import os
import bpy
import bcf
import bcf.bcfxml
import bcf.v2.data
import numpy as np
import ifcopenshell
import ifcopenshell.util.unit
@@ -38,8 +37,8 @@ class NewBcfProject(bpy.types.Operator):
def execute(self, context):
context.scene.BCFProperties.is_loaded = False
bcfxml = bcfstore.BcfStore.get_bcfxml()
bcfxml.new_project()
bcfxml = bcf.v2.bcfxml.BcfXml.create_new("New Project")
bcfstore.BcfStore.set(bcfxml)
bpy.ops.bim.load_bcf_project()
context.scene.BCFProperties.is_loaded = True
return {"FINISHED"}
@@ -97,34 +96,34 @@ class LoadBcfTopic(bpy.types.Operator):
new = context.scene.BCFProperties.topics[self.topic_index]
data_map = {
"name": topic.guid,
"title": topic.title,
"type": topic.topic_type,
"status": topic.topic_status,
"priority": topic.priority,
"stage": topic.stage,
"creation_date": topic.creation_date,
"creation_author": topic.creation_author,
"modified_date": topic.modified_date,
"modified_author": topic.modified_author,
"assigned_to": topic.assigned_to,
"due_date": topic.due_date,
"description": topic.description,
"title": topic.topic.title,
"type": topic.topic.topic_type,
"status": topic.topic.topic_status,
"priority": topic.topic.priority,
"stage": topic.topic.stage,
"creation_date": topic.topic.creation_date,
"creation_author": topic.topic.creation_author,
"modified_date": topic.topic.modified_date,
"modified_author": topic.topic.modified_author,
"assigned_to": topic.topic.assigned_to,
"due_date": topic.topic.due_date,
"description": topic.topic.description,
}
for key, value in data_map.items():
if value is not None:
setattr(new, key, str(value))
new.reference_links.clear()
for reference_link in topic.reference_links:
for reference_link in topic.topic.reference_link:
new_reference_link = new.reference_links.add()
new_reference_link.name = reference_link
new.labels.clear()
for label in topic.labels:
for label in topic.topic.labels:
new_label = new.labels.add()
new_label.name = label
if topic.bim_snippet:
if topic.topic.bim_snippet:
data_map = {
"type": topic.bim_snippet.snippet_type,
"is_external": topic.bim_snippet.is_external,
@@ -136,7 +135,7 @@ class LoadBcfTopic(bpy.types.Operator):
setattr(new.bim_snippet, key, value)
new.document_references.clear()
for doc in topic.document_references:
for doc in topic.topic.document_reference:
new_document_references = new.document_references.add()
data_map = {
"reference": doc.referenced_document,
@@ -149,7 +148,7 @@ class LoadBcfTopic(bpy.types.Operator):
setattr(new_document_references, key, value)
new.related_topics.clear()
for related_topic in topic.related_topics:
for related_topic in topic.topic.related_topic:
new_related_topic = new.related_topics.add()
new_related_topic.name = related_topic.guid
@@ -276,7 +275,7 @@ class AddBcfTopic(bpy.types.Operator):
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
bcfxml.add_topic()
bcfxml.add_topic("New Topic", "", context.scene.BCFProperties.author)
bpy.ops.bim.load_bcf_topics()
return {"FINISHED"}
@@ -285,6 +285,7 @@ class UpdateBoundaryGeometry(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
tool.Boundary.move_origin_to_space_origin(context.active_object)
settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object)
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
return {"FINISHED"}
@@ -67,7 +67,6 @@ class BIM_PT_Boundary(Panel):
def draw(self, context):
props = context.active_object.BIMObjectProperties
ifc_file = tool.Ifc.get()
element = ifc_file.by_id(props.ifc_definition_id)
boundary = ifc_file.by_id(props.ifc_definition_id)
self.bprops = context.active_object.bim_boundary_properties
if self.bprops.is_editing:
@@ -51,7 +51,6 @@ class BIM_PT_brickschema(Panel):
row = self.layout.row(align=True)
prop_with_search(row, self.props, "namespace", text="")
prop_with_search(row, self.props, "brick_equipment_class", text="")
row.prop(self.props, "brick_equipment_class", text="")
row.operator("bim.add_brick", text="", icon="ADD")
row = self.layout.row(align=True)
@@ -18,6 +18,7 @@
import os
import bpy
import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.model.data import AuthoringData
@@ -51,9 +52,14 @@ class CadTool(WorkSpaceTool):
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")
if obj.BIMObjectProperties.ifc_definition_id:
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")
else:
row.operator("bim.edit_arbitrary_profile", text="Save Profile")
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -180,7 +186,10 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_offset(distance=self.props.distance)
def hotkey_S_Q(self):
bpy.ops.bim.edit_extrusion_profile()
if tool.Ifc.get_entity(bpy.context.active_object):
bpy.ops.bim.edit_extrusion_profile()
else:
bpy.ops.bim.edit_arbitrary_profile()
def hotkey_S_R(self):
if self.is_profile():
@@ -17,7 +17,9 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.prop import StrProperty, Attribute
from ifcopenshell.util.doc import get_entity_doc
import blenderbim.tool as tool
from blenderbim.bim.prop import Attribute
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -31,6 +33,11 @@ from bpy.props import (
)
def get_available_constraint_types(self, context):
version = tool.Ifc.get_schema()
return [(c, c, get_entity_doc(version, c).get("description", "")) for c in ["IfcObjective"]]
class Constraint(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
@@ -46,6 +53,4 @@ class BIMConstraintProperties(PropertyGroup):
class BIMObjectConstraintProperties(PropertyGroup):
is_adding: StringProperty(name="Is Adding")
available_constraint_types: EnumProperty(
items=[(c, c, "") for c in ["IfcObjective"]], name="Available Constraint Types"
)
available_constraint_types: EnumProperty(items=get_available_constraint_types, name="Available Constraint Types")
@@ -37,6 +37,7 @@ class BIM_PT_debug(Panel):
row = self.layout.row(align=True)
row.prop(context.scene.BIMProperties, "ifc_file", text="")
row.operator("bim.validate_ifc_file", icon="CHECKMARK", text="")
row.operator("bim.reload_selected_ifc_file", icon="FILE_REFRESH", text="")
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="")
row = self.layout.row(align=True)
@@ -32,6 +32,12 @@ from mathutils import Vector, Matrix
from bpy_extras.view3d_utils import location_3d_to_region_2d
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
from math import acos, pi
def ccw(A, B, C):
"""whether a-b-c located in counter-clockwise order in 2d space"""
return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x)
class BaseDecorator:
@@ -66,6 +72,42 @@ class BaseDecorator:
}
}
bool check_counterclockwise(in vec4 A, in vec4 B, in vec4 C) {
return (C.y-A.y) * (B.x-A.x) > (B.y-A.y) * (C.x-A.x);
}
void angle_circle_head(
in vec4 circle_start, in float circle_angle,
in bool counterclockwise,
out vec4 head[CIRCLE_SEGS+1], out float angle_segs) {
// 1 added to CIRCLE_SEGS because we're number of vertices
// for n segments is n+1
float angle_d;
angle_d = PI * 2 / CIRCLE_SEGS; // 30d
// need to bottom clamp it to 1, otherwise it causes Blender crash at extruding the curve
angle_segs = max(1, ceil(circle_angle / angle_d));
angle_d = circle_angle / angle_segs;
for(int i = 0; i < (angle_segs + 1); i++) {
float angle = angle_d * i;
if (counterclockwise) {
head[i] = vec4(
circle_start.x * cos(-angle) + circle_start.y * sin(-angle),
circle_start.x * -sin(-angle) + circle_start.y * cos(-angle),
0, 0
);
} else {
head[i] = vec4(
circle_start.x * cos(angle) + circle_start.y * sin(angle),
circle_start.x * -sin(angle) + circle_start.y * cos(angle),
0, 0
);
}
}
}
void cross_head(in vec4 dir, in float size, out vec4 head[3]) {
vec4 nose = dir * size;
float c = cos(PI/2), s = sin(PI/2);
@@ -79,12 +121,15 @@ class BaseDecorator:
uniform mat4 viewMatrix;
in vec3 pos;
in uint topo;
in vec3 next_vert;
out vec4 gl_Position;
out uint type;
out vec4 v_next_vert;
void main() {
gl_Position = viewMatrix * vec4(pos, 1.0);
type = topo;
v_next_vert = viewMatrix * vec4(next_vert, 1.0);
}
"""
@@ -267,7 +312,13 @@ class BaseDecorator:
"""perform actual drawing stuff"""
raise NotImplementedError()
def draw_lines(self, context, obj, vertices, indices, topology=None, is_scale_dependant=True):
def draw_lines(
self, context, obj, vertices, indices, topology=None, is_scale_dependant=True, fill_next_vertices=False
):
# use is_scale_dependant = False if shader is not using uniform viewportDrawingScale
# otherwise uniform will be discarded during the optimization process
# and you will get "ValueError: GPUShader.uniform_float: uniform viewportDrawingScale not found"
region = context.region
region3d = context.region_data
color = context.scene.DocProperties.decorations_colour
@@ -276,12 +327,18 @@ class BaseDecorator:
fmt.attr_add(id="pos", comp_type="F32", len=3, fetch_mode="FLOAT")
if topology:
fmt.attr_add(id="topo", comp_type="U8", len=1, fetch_mode="INT")
if fill_next_vertices:
fmt.attr_add(id="next_vert", comp_type="F32", len=3, fetch_mode="FLOAT")
vbo = GPUVertBuf(len=len(vertices), format=fmt)
vbo.attr_fill(id="pos", data=vertices)
if topology:
vbo.attr_fill(id="topo", data=topology)
if fill_next_vertices:
shifted_vertices = vertices[1:] + [vertices[0]]
vbo.attr_fill(id="next_vert", data=shifted_vertices)
ibo = GPUIndexBuf(type="LINES", seq=indices)
batch = GPUBatch(type="LINES", buf=vbo, elem=ibo)
@@ -480,6 +537,132 @@ class DimensionDecorator(BaseDecorator):
self.draw_label(context, text, p0 + (dir) * 0.5, dir)
class AngleDecorator(BaseDecorator):
"""Decorator for angle objects
- each edge of a segment with arrow
- every non-last edge has angle circle
- every circle is labeled with angle in degrees
"""
objecttype = "ANGLE"
DEF_GLSL = (
BaseDecorator.DEF_GLSL
+ """
#define ARROW_ANGLE PI / 12.0
#define ARROW_SIZE 8.0
#define CIRCLE_SIZE 6.0
"""
)
GEOM_GLSL = """
uniform vec2 winsize;
uniform float viewportDrawingScale;
layout(lines) in;
layout(line_strip, max_vertices=MAX_POINTS) out;
in uint type[];
in vec4 v_next_vert[];
// per edge shader
void main() {
vec4 clip2win = matCLIP2WIN();
vec4 win2clip = matWIN2CLIP();
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
vec4 p2 = v_next_vert[1];
uint t0 = type[0], t1 = type[1];
vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1);
vec4 p2w = CLIP2WIN(p2);
vec4 edge0 = p1w - p0w, dir = normalize(edge0);
vec4 p;
// draw a segment line
p = p0w;
gl_Position = WIN2CLIP(p);
EmitVertex();
p = p1w;
gl_Position = WIN2CLIP(p);
EmitVertex();
EndPrimitive();
// end edge with angle circle for the non-last segment
if (t1 == 0u) { // draws only on internal vertex
edge0 = p0w - p1w;
vec4 dir0 = normalize(edge0);
vec4 edge1 = p2w - p1w;
vec4 dir1 = normalize(edge1);
float angle_circle_size = min( length(edge0), length(edge1) );
vec4 circle_start = dir0 * angle_circle_size;
vec4 circle_end = dir1 * angle_circle_size;
float cos_a = dot( edge0, edge1 ) / ( length(edge0) * length(edge1) );
float circle_angle = acos(cos_a);
vec4 circle_head_data[CIRCLE_SEGS+1];
float angle_segs;
bool counterclockwise = check_counterclockwise(p2w, p1w, p0w);
angle_circle_head(
circle_start,
circle_angle,
counterclockwise,
circle_head_data,
angle_segs);
for(int i=0; i<angle_segs+1; i++) {
p = p1w + circle_head_data[i];
gl_Position = WIN2CLIP(p);
EmitVertex();
}
EndPrimitive();
}
}
"""
def decorate(self, context, obj):
verts, idxs, topo = self.get_path_geom(obj)
self.draw_lines(context, obj, verts, idxs, topo, fill_next_vertices=True, is_scale_dependant=False)
self.draw_labels(context, obj, verts, idxs)
def draw_labels(self, context, obj, vertices, indices):
region = context.region
region3d = context.region_data
last_segment_i = len(indices) - 1
for edge_i, edge_vertices in enumerate(indices):
if edge_i == last_segment_i:
continue
# draw angle label
i0, i1 = edge_vertices
v0 = Vector(vertices[i0])
v1 = Vector(vertices[i1])
v2 = Vector(vertices[i1 + 1])
p0 = location_3d_to_region_2d(region, region3d, v0)
p1 = location_3d_to_region_2d(region, region3d, v1)
p2 = location_3d_to_region_2d(region, region3d, v2)
edge0 = p0 - p1
edge1 = p2 - p1
cos_a = edge0.dot(edge1) / (edge0.length * edge1.length)
circle_angle = acos(cos_a) / pi * 180
text = f"{int(circle_angle)}d"
# TODO: set label position pased on p1
# + y relative to p0p1 if p0p1p2 is clockwise
# - y relative to p0p1 if p0p1p2 is counter-clockwise
# counter_clockwise = ccw(p0, p1, p2)
# label_position = (p1 + Vector( (0, 10) ) * (1 if counter_clockwise else -1)) + edge1 * 0.1
label_position = p1 + edge1 * 0.1
# TODO: set label direction based on the first edge (p0, p1)
label_dir = Vector((1, 0))
self.draw_label(context, text, label_position, label_dir)
class DiameterDecorator(DimensionDecorator):
objecttype = "DIAMETER"
@@ -895,7 +1078,6 @@ class LevelDecorator(BaseDecorator):
def decorate(self, context, obj):
verts, idxs, topo = self.get_path_geom(obj)
self.draw_lines(context, obj, verts, idxs, topo)
splines = self.get_splines(obj)
self.draw_labels(context, obj, splines)
@@ -1585,6 +1767,7 @@ class TextDecorator(BaseDecorator):
class DecorationsHandler:
decorators_classes = [
DimensionDecorator,
AngleDecorator,
GridDecorator,
HiddenDecorator,
LeaderDecorator,
@@ -34,6 +34,7 @@ import blenderbim.bim.module.drawing.annotation as annotation
from mathutils import Vector
from mathutils import geometry
from blenderbim.bim.ifc import IfcStore
from bpy_extras import view3d_utils
class External(svgwrite.container.Group):
@@ -140,6 +141,8 @@ class SvgWriter:
self.draw_dimension_annotations(obj)
elif element.ObjectType == "ANGLE":
self.draw_angle_annotations(obj)
elif element.ObjectType == "ANGLE_OLD":
self.draw_angle_old_annotations(obj)
elif element.ObjectType == "RADIUS":
self.draw_radius_annotations(obj)
elif element.ObjectType == "DIAMETER":
@@ -619,19 +622,52 @@ class SvgWriter:
)
def draw_angle_annotations(self, obj):
# calculate p3 which is the center of the arc
# to use draw_svg_3point_arc()
points = obj.data.splines[0].points
region = bpy.context.region
region_3d = bpy.context.area.spaces.active.region_3d
points_2d = [view3d_utils.location_3d_to_region_2d(region, region_3d, p.co.xyz) for p in points]
edge0 = points_2d[0] - points_2d[1]
edge1 = points_2d[2] - points_2d[1]
angle_radius = min(edge0.length, edge1.length)
dir0 = edge0.normalized()
dir1 = edge1.normalized()
dir2 = ((dir0 + dir1) / 2).normalized()
p3 = points_2d[1] + dir2 * angle_radius
# make all edges the same radius
p0 = points_2d[1] + dir0 * angle_radius
p2 = points_2d[1] + dir1 * angle_radius
points = [view3d_utils.region_2d_to_origin_3d(region, region_3d, p) for p in [p0, p3, p2]]
# points = [p.co.xyz for p in bpy.context.object.data.splines[0].points[:3]]
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
new_verts = [bm.verts.new(p) for p in points]
new_edges = [bm.edges.new( (new_verts[e[0]], new_verts[e[1]]) ) for e in ((0, 1), (1, 2))]
self.draw_svg_3point_arc(obj, bm)
def draw_angle_old_annotations(self, obj):
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.verts.ensure_lookup_table()
self.draw_svg_3point_arc(obj, bm)
def draw_svg_3point_arc(self, obj, bm):
# This implementation uses an SVG arc, which means that it can only draw
# arcs that are orthogonal to the view (e.g. not arcs in 3D).
# Gosh this is bad code :(
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
points = [v.co for v in bm.verts][:3]
center = tool.Cad.get_center_of_arc(points, obj)
classes = self.get_attribute_classes(obj)
matrix_world = obj.matrix_world
points = [v.co for v in obj.data.vertices][:3]
center = tool.Cad.get_center_of_arc(points, obj)
bm = bmesh.new()
bm.from_mesh(obj.data)
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
bm.verts.ensure_lookup_table()
arc_end_verts = [v for v in bm.verts if len(v.link_edges) == 1]
arc_end_pts = [matrix_world @ v.co for v in arc_end_verts]
@@ -680,11 +716,11 @@ class SvgWriter:
# Center of gravity of all vertices, used to help position the text
cog = Vector((0, 0, 0))
for vert in obj.data.vertices:
cog += vert.co
cog = matrix_world @ (cog / len(obj.data.vertices))
for point in points:
cog += point
cog = matrix_world @ (cog / len(points))
radius = ((matrix_world @ obj.data.vertices[0].co) - center).length
radius = ((matrix_world @ points[0]) - center).length
arc_midpoint = center + ((cog - center).normalized() * radius)
@@ -171,10 +171,10 @@ class BIM_PT_drawings(Panel):
active_drawing = self.props.drawings[self.props.active_drawing_index]
row = self.layout.row(align=True)
col = row.column()
col.alignment = 'LEFT'
col.alignment = "LEFT"
col.operator("bim.remove_drawing", icon="X", text="").drawing = active_drawing.ifc_definition_id
col = row.column()
col.alignment = 'RIGHT'
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="")
@@ -376,6 +376,9 @@ class BIM_PT_annotation_utilities(Panel):
op.data_type = "curve"
op = row.operator("bim.add_annotation", text="Angle", icon="DRIVER_ROTATIONAL_DIFFERENCE")
op.object_type = "ANGLE"
op.data_type = "curve"
op = row.operator("bim.add_annotation", text="Angle (old)", icon="DRIVER_ROTATIONAL_DIFFERENCE")
op.object_type = "ANGLE_OLD"
op.data_type = "mesh"
row = layout.row(align=True)
@@ -481,7 +481,7 @@ class Helper:
unshared_vert.co, profile_face.verts[0].co, profile_face.normal
)
)
> 0.001
> 1e-6
):
if unshared_vert == edge.verts[1]:
return [edge.verts[0].index, edge.verts[1].index]
@@ -367,10 +367,12 @@ class OverrideDeleteTrait:
if getattr(element, "FillsVoids", None):
self.remove_filling(element)
if element.is_a("IfcOpeningElement"):
for rel in element.HasFillings:
self.remove_filling(rel.RelatedBuildingElement)
if element.VoidsElements:
self.delete_opening_element(element)
if element.HasFillings:
for rel in element.HasFillings:
self.remove_filling(rel.RelatedBuildingElement)
else:
if element.VoidsElements:
self.delete_opening_element(element)
else:
if getattr(element, "HasOpenings", None):
for rel in element.HasOpenings:
@@ -545,7 +547,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
# 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
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")
@@ -32,11 +32,14 @@ classes = (
operator.CopyMaterial,
operator.DisableEditingAssignedMaterial,
operator.DisableEditingMaterialSetItem,
operator.DisableEditingMaterialSetItemProfile,
operator.DisableEditingMaterials,
operator.EditAssignedMaterial,
operator.EditMaterialSetItem,
operator.EditMaterialSetItemProfile,
operator.EnableEditingAssignedMaterial,
operator.EnableEditingMaterialSetItem,
operator.EnableEditingMaterialSetItemProfile,
operator.ExpandMaterialCategory,
operator.LoadMaterials,
operator.RemoveConstituent,
@@ -37,6 +37,7 @@ class MaterialsData:
cls.data = {
"total_materials": cls.total_materials(),
"material_types": cls.material_types(),
"profiles": cls.profiles(),
}
cls.is_loaded = True
@@ -65,9 +66,14 @@ class MaterialsData:
"IfcMaterialProfileSet",
"IfcMaterialList",
]
if tool.Ifc.get_schema() == "IFC2X3":
version = tool.Ifc.get_schema()
if version == "IFC2X3":
material_types = ["IfcMaterial", "IfcMaterialLayerSet", "IfcMaterialList"]
return [(m, m, "") for m in material_types]
return [(m, m, ifcopenshell.util.doc.get_entity_doc(version, m).get("description", "")) for m in material_types]
@classmethod
def profiles(cls):
return [(str(p.id()), p.ProfileName or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcProfileDef")]
class ObjectMaterialData:
@@ -76,12 +82,142 @@ class ObjectMaterialData:
@classmethod
def load(cls):
cls.data = {
"materials": cls.materials(),
"type_material": cls.type_material(),
}
cls.data["material_class"] = cls.material_class()
cls.data["material_id"] = cls.material_id()
cls.data["material_name"] = cls.material_name()
cls.data["set"] = cls.set()
cls.data["set_usage"] = cls.set_usage()
cls.data["set_items"] = cls.set_items()
cls.data["set_item_name"] = cls.set_item_name()
cls.data["total_thickness"] = cls.total_thickness()
cls.data["materials"] = cls.materials()
cls.data["type_material"] = cls.type_material()
cls.is_loaded = True
@classmethod
def material_class(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
cls.material = ifcopenshell.util.element.get_material(element)
if cls.material:
return cls.material.is_a()
@classmethod
def material_id(cls):
if cls.material:
return cls.material.id()
return 0
@classmethod
def set(cls):
mset = None
if cls.material:
mset = cls.material
if cls.material.is_a("IfcMaterialLayerSetUsage"):
mset = cls.material.ForLayerSet
elif cls.material.is_a("IfcMaterialProfileSetUsage"):
mset = cls.material.ForProfileSet
return {
"id": mset.id(),
"name": getattr(mset, "LayerSetName", getattr(mset, "Name", None)) or "Unnamed",
"description": getattr(mset, "Description", None),
}
@classmethod
def set_usage(cls):
# TODO: complain to buildingSMART
cardinal_point_map = {
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",
}
results = {}
if cls.material:
if cls.material.is_a("IfcMaterialProfileSetUsage"):
if cls.material.CardinalPoint:
results["cardinal_point"] = cardinal_point_map[cls.material.CardinalPoint]
return results
@classmethod
def set_items(cls):
results = []
if cls.material:
items = []
if cls.material.is_a("IfcMaterialLayerSetUsage"):
items = cls.material.ForLayerSet.MaterialLayers
elif cls.material.is_a("IfcMaterialProfileSetUsage"):
items = cls.material.ForProfileSet.MaterialProfiles
elif cls.material.is_a("IfcMaterialLayerSet"):
items = cls.material.MaterialLayers
elif cls.material.is_a("IfcMaterialProfileSet"):
items = cls.material.MaterialProfiles
elif cls.material.is_a("IfcMaterialConstituentSet"):
items = cls.material.MaterialConstituents
icon = "LAYER_ACTIVE"
if "Layer" in cls.material.is_a():
icon = "ALIGN_CENTER"
elif "Profile" in cls.material.is_a():
icon = "ITALIC"
elif "Constituent" in cls.material.is_a():
icon = "POINTCLOUD_DATA"
for item in items or []:
data = {"id": item.id(), "name": item.Name or "Unnamed", "icon": icon}
if item.is_a("IfcMaterialProfile") and not item.Name:
data["name"] = item.Profile.ProfileName or "Unnamed"
if item.is_a("IfcMaterialLayer"):
data["name"] += f" ({item.LayerThickness})"
if not item.is_a("IfcMaterialList"):
data["material"] = item.Material.Name or "Unnamed"
results.append(data)
return results
@classmethod
def total_thickness(cls):
if cls.material:
layers = []
if cls.material.is_a("IfcMaterialLayerSetUsage"):
layers = cls.material.ForLayerSet.MaterialLayers
elif cls.material.is_a("IfcMaterialLayerSet"):
layers = cls.material.MaterialLayers
return sum([l.LayerThickness for l in layers or []])
@classmethod
def set_item_name(cls):
results = []
if cls.material:
if "Constituent" in cls.material.is_a():
return "constituent"
elif "Layer" in cls.material.is_a():
return "layer"
elif "Profile" in cls.material.is_a():
return "profile"
elif "List" in cls.material.is_a():
return "list_item"
@classmethod
def material_name(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
material = ifcopenshell.util.element.get_material(element)
if material:
return getattr(material, "Name", None) or "Unnamed"
@classmethod
def materials(cls):
return sorted(
@@ -25,10 +25,10 @@ import ifcopenshell.util.representation
import blenderbim.bim.helper
import blenderbim.tool as tool
import blenderbim.core.material as core
import blenderbim.bim.module.model.profile as model_profile
from blenderbim.bim.module.material.prop import purge as material_prop_purge
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.material.data import Data
from ifcopenshell.api.profile.data import Data as ProfileData
class LoadMaterials(bpy.types.Operator, tool.Ifc.Operator):
@@ -59,7 +59,7 @@ class SelectByMaterial(bpy.types.Operator, tool.Ifc.Operator):
core.select_by_material(tool.Material, material=tool.Ifc.get().by_id(self.material))
class AssignParameterizedProfile(bpy.types.Operator):
class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_parameterized_profile"
bl_label = "Assign Parameterized Profile"
bl_options = {"REGISTER", "UNDO"}
@@ -67,9 +67,6 @@ class AssignParameterizedProfile(bpy.types.Operator):
material_profile: bpy.props.IntProperty()
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
self.file = IfcStore.get_file()
@@ -84,9 +81,7 @@ class AssignParameterizedProfile(bpy.types.Operator):
**{"material_profile": self.file.by_id(self.material_profile), "profile": profile},
)
Data.load_profiles()
ProfileData.load(self.file)
bpy.ops.bim.enable_editing_material_set_item(obj=obj.name, material_set_item=self.material_profile)
return {"FINISHED"}
class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
@@ -145,16 +140,13 @@ class UnlinkMaterial(bpy.types.Operator, tool.Ifc.Operator):
core.unlink_material(tool.Ifc, obj=context.active_object.active_material)
class AssignMaterial(bpy.types.Operator):
class AssignMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_material"
bl_label = "Assign Material"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
material_type: 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
material_type = self.material_type or obj.BIMObjectMaterialProperties.material_type
@@ -171,18 +163,14 @@ class AssignMaterial(bpy.types.Operator):
)
Data.load(IfcStore.get_file())
Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
return {"FINISHED"}
class UnassignMaterial(bpy.types.Operator):
class UnassignMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_material"
bl_label = "Unassign Material"
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
self.file = IfcStore.get_file()
@@ -192,19 +180,15 @@ class UnassignMaterial(bpy.types.Operator):
**{"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)},
)
Data.purge()
return {"FINISHED"}
class AddConstituent(bpy.types.Operator):
class AddConstituent(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_constituent"
bl_label = "Add Constituent"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
constituent_set: bpy.props.IntProperty()
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
self.file = IfcStore.get_file()
@@ -217,19 +201,15 @@ class AddConstituent(bpy.types.Operator):
},
)
Data.load_constituents()
return {"FINISHED"}
class RemoveConstituent(bpy.types.Operator):
class RemoveConstituent(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_constituent"
bl_label = "Remove Constituent"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
constituent: bpy.props.IntProperty()
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
self.file = IfcStore.get_file()
@@ -237,64 +217,49 @@ class RemoveConstituent(bpy.types.Operator):
"material.remove_constituent", self.file, **{"constituent": self.file.by_id(self.constituent)}
)
Data.load_constituents()
return {"FINISHED"}
class AddProfile(bpy.types.Operator):
class AddProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_profile"
bl_label = "Add Profile"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
profile_set: bpy.props.IntProperty()
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
self.file = IfcStore.get_file()
ifcopenshell.api.run(
"material.add_profile",
self.file,
**{
"profile_set": self.file.by_id(self.profile_set),
"material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material)),
},
profile_set=self.file.by_id(self.profile_set),
material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material)),
profile=self.file.by_id(int(context.scene.BIMMaterialProperties.profiles)),
)
Data.load_profiles()
ProfileData.load(self.file)
return {"FINISHED"}
class RemoveProfile(bpy.types.Operator):
class RemoveProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_profile"
bl_label = "Remove Profile"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
profile: bpy.props.IntProperty()
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
self.file = IfcStore.get_file()
ifcopenshell.api.run("material.remove_profile", self.file, **{"profile": self.file.by_id(self.profile)})
Data.load_profiles()
ProfileData.load(self.file)
return {"FINISHED"}
class AddLayer(bpy.types.Operator):
class AddLayer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_layer"
bl_label = "Add Layer"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
layer_set: bpy.props.IntProperty()
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
self.file = IfcStore.get_file()
@@ -307,10 +272,9 @@ class AddLayer(bpy.types.Operator):
},
)
Data.load_layers()
return {"FINISHED"}
class ReorderMaterialSetItem(bpy.types.Operator):
class ReorderMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.reorder_material_set_item"
bl_label = "Reorder Material Set Item"
bl_options = {"REGISTER", "UNDO"}
@@ -319,9 +283,6 @@ class ReorderMaterialSetItem(bpy.types.Operator):
new_index: bpy.props.IntProperty()
material_set: bpy.props.IntProperty()
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
self.file = IfcStore.get_file()
@@ -341,40 +302,31 @@ class ReorderMaterialSetItem(bpy.types.Operator):
Data.load_layers()
elif material_set.is_a("IfcMaterialProfileSet"):
Data.load_profiles()
ProfileData.load(self.file)
elif material_set.is_a("IfcMaterialList"):
Data.load_lists()
return {"FINISHED"}
class RemoveLayer(bpy.types.Operator):
class RemoveLayer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_layer"
bl_label = "Remove Layer"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
layer: bpy.props.IntProperty()
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
self.file = IfcStore.get_file()
ifcopenshell.api.run("material.remove_layer", self.file, **{"layer": self.file.by_id(self.layer)})
Data.load_layers()
return {"FINISHED"}
class AddListItem(bpy.types.Operator):
class AddListItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_list_item"
bl_label = "Add List Item"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
list_item_set: bpy.props.IntProperty()
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
self.file = IfcStore.get_file()
@@ -387,10 +339,9 @@ class AddListItem(bpy.types.Operator):
},
)
Data.load_lists()
return {"FINISHED"}
class RemoveListItem(bpy.types.Operator):
class RemoveListItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_list_item"
bl_label = "Remove List Item"
bl_options = {"REGISTER", "UNDO"}
@@ -399,9 +350,6 @@ class RemoveListItem(bpy.types.Operator):
list_item: bpy.props.IntProperty()
list_item_index: bpy.props.IntProperty()
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
self.file = IfcStore.get_file()
@@ -414,7 +362,6 @@ class RemoveListItem(bpy.types.Operator):
},
)
Data.load_lists()
return {"FINISHED"}
class EnableEditingAssignedMaterial(bpy.types.Operator):
@@ -503,13 +450,14 @@ class DisableEditingAssignedMaterial(bpy.types.Operator):
obj: bpy.props.StringProperty()
def execute(self, context):
bpy.ops.bim.disable_editing_material_set_item()
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMObjectMaterialProperties
props.is_editing = False
return {"FINISHED"}
class EditAssignedMaterial(bpy.types.Operator):
class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_assigned_material"
bl_label = "Edit Assigned Material"
bl_options = {"REGISTER", "UNDO"}
@@ -517,15 +465,15 @@ class EditAssignedMaterial(bpy.types.Operator):
material_set: bpy.props.IntProperty()
material_set_usage: bpy.props.IntProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.file = IfcStore.get_file()
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMObjectMaterialProperties
product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id]
if props.active_material_set_item_id != 0: # We were editing a material layer set item
bpy.ops.bim.edit_material_set_item(material_set_item=props.active_material_set_item_id)
if product_data["type"] == "IfcMaterial":
bpy.ops.bim.unassign_material(obj=obj.name)
bpy.ops.bim.assign_material(obj=obj.name, material_type="IfcMaterial")
@@ -569,9 +517,54 @@ class EditAssignedMaterial(bpy.types.Operator):
elif material_set.is_a("IfcMaterialProfileSet"):
Data.load_profiles()
bpy.ops.bim.disable_editing_assigned_material(obj=obj.name)
class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
bl_idname = "bim.enable_editing_material_set_item_profile"
bl_label = "Enable Editing Material Set Item"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
material_set_item: bpy.props.IntProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.props = obj.BIMObjectMaterialProperties
self.props.material_set_item_profile_attributes.clear()
profile = tool.Ifc.get().by_id(self.material_set_item).Profile
blenderbim.bim.helper.import_attributes2(profile, self.props.material_set_item_profile_attributes)
return {"FINISHED"}
class DisableEditingMaterialSetItemProfile(bpy.types.Operator):
bl_idname = "bim.disable_editing_material_set_item_profile"
bl_label = "Disable Editing Material Set Item"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.props = obj.BIMObjectMaterialProperties
self.props.material_set_item_profile_attributes.clear()
return {"FINISHED"}
class EditMaterialSetItemProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_material_set_item_profile"
bl_label = "Edit Material Set Item Profile"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
material_set_item: bpy.props.IntProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.props = obj.BIMObjectMaterialProperties
attributes = blenderbim.bim.helper.export_attributes(self.props.material_set_item_profile_attributes)
profile = tool.Ifc.get().by_id(self.material_set_item).Profile
ifcopenshell.api.run("profile.edit_profile", tool.Ifc.get(), profile=profile, attributes=attributes)
self.props.material_set_item_profile_attributes.clear()
model_profile.DumbProfileRegenerator().regenerate_from_profile_def(profile)
class EnableEditingMaterialSetItem(bpy.types.Operator):
bl_idname = "bim.enable_editing_material_set_item"
bl_label = "Enable Editing Material Set Item"
@@ -582,6 +575,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
def execute(self, context):
self.file = IfcStore.get_file()
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.mprops = context.scene.BIMMaterialProperties
self.props = obj.BIMObjectMaterialProperties
self.props.active_material_set_item_id = self.material_set_item
product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id]
@@ -600,7 +594,8 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
self.load_set_item_attributes(material_set_item, material_set_item_data)
if material_set_item.is_a("IfcMaterialProfile"):
self.load_profile_attributes(material_set_item, material_set_item_data)
if material_set_item.Profile:
self.mprops.profiles = str(material_set_item.Profile.id())
return {"FINISHED"}
@@ -613,55 +608,20 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
continue
if attribute.name() in material_set_item_data:
new = self.props.material_set_item_attributes.add()
new.ifc_class = material_set_item.is_a()
new.name = attribute.name()
new.is_null = material_set_item_data[attribute.name()] is None
new.data_type = data_type
if data_type == "string":
new.string_value = "" if new.is_null else material_set_item_data[attribute.name()]
new.string_value = "" if new.is_null else str(material_set_item_data[attribute.name()])
elif data_type == "float":
new.float_value = 0.0 if new.is_null else material_set_item_data[attribute.name()]
new.float_value = 0.0 if new.is_null else float(material_set_item_data[attribute.name()])
elif data_type == "integer":
new.int_value = 0 if new.is_null else material_set_item_data[attribute.name()]
new.int_value = 0 if new.is_null else int(material_set_item_data[attribute.name()])
elif data_type == "boolean":
new.bool_value = False if new.is_null else material_set_item_data[attribute.name()]
def load_profile_attributes(self, material_set_item, material_set_item_data):
self.props.material_set_item_profile_attributes.clear()
if not material_set_item_data["Profile"]:
return
profile = self.file.by_id(material_set_item_data["Profile"])
profile_data = ProfileData.profiles[material_set_item_data["Profile"]]
for attribute in IfcStore.get_schema().declaration_by_name(profile.is_a()).all_attributes():
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
if data_type == "entity":
continue
if attribute.name() in profile_data:
new = self.props.material_set_item_profile_attributes.add()
new.name = attribute.name()
new.is_null = profile_data[attribute.name()] is None
new.is_optional = attribute.optional()
new.data_type = data_type
if data_type == "string":
new.string_value = "" if new.is_null else profile_data[attribute.name()]
elif data_type == "float":
new.float_value = 0.0 if new.is_null else profile_data[attribute.name()]
elif data_type == "integer":
new.int_value = 0 if new.is_null else profile_data[attribute.name()]
elif data_type == "boolean":
new.bool_value = False if new.is_null else profile_data[attribute.name()]
elif data_type == "enum":
new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute))
if profile_data[attribute.name()]:
new.enum_value = profile_data[attribute.name()]
# Force null to be false if the attribute is mandatory because when we first assign a profile, all of
# its fields are null (which is illegal).
# TODO: find a better solution.
if not new.is_optional:
new.is_null = False
new.bool_value = False if new.is_null else bool(material_set_item_data[attribute.name()])
blenderbim.bim.helper.add_attribute_description(new, material_set_item_data)
blenderbim.bim.helper.add_attribute_min_max(new)
class DisableEditingMaterialSetItem(bpy.types.Operator):
@@ -677,20 +637,18 @@ class DisableEditingMaterialSetItem(bpy.types.Operator):
return {"FINISHED"}
class EditMaterialSetItem(bpy.types.Operator):
class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_material_set_item"
bl_label = "Edit Material Set Item"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
material_set_item: bpy.props.IntProperty()
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
self.file = IfcStore.get_file()
props = obj.BIMObjectMaterialProperties
mprops = context.scene.BIMMaterialProperties
product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id]
attributes = blenderbim.bim.helper.export_attributes(props.material_set_item_attributes)
@@ -718,34 +676,30 @@ class EditMaterialSetItem(bpy.types.Operator):
)
Data.load_layers()
elif product_data["type"] == "IfcMaterialProfileSet" or product_data["type"] == "IfcMaterialProfileSetUsage":
profile_attributes = blenderbim.bim.helper.export_attributes(props.material_set_item_profile_attributes)
profile_def = None
if mprops.profiles:
profile_def = tool.Ifc.get().by_id(int(mprops.profiles))
ifcopenshell.api.run(
"material.edit_profile",
self.file,
**{
"profile": self.file.by_id(self.material_set_item),
"attributes": attributes,
"profile_attributes": profile_attributes,
"material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material_set_item_material)),
},
profile=self.file.by_id(self.material_set_item),
attributes=attributes,
profile_def=profile_def,
material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material_set_item_material)),
)
Data.load_profiles()
ProfileData.load(self.file)
else:
pass
bpy.ops.bim.disable_editing_material_set_item(obj=obj.name)
return {"FINISHED"}
class CopyMaterial(bpy.types.Operator):
class CopyMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.copy_material"
bl_label = "Copy Material"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.file = IfcStore.get_file()
material = ifcopenshell.util.element.get_material(
@@ -766,7 +720,6 @@ class CopyMaterial(bpy.types.Operator):
)
Data.load(self.file, obj.BIMObjectProperties.ifc_definition_id)
self.set_default_material(obj, material)
return {"FINISHED"}
def set_default_material(self, obj, material):
object_material_ids = [
@@ -17,7 +17,9 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from ifcopenshell.util.doc import get_entity_doc
from ifcopenshell.api.material.data import Data
import blenderbim.tool as tool
from blenderbim.bim.module.material.data import MaterialsData, ObjectMaterialData
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.prop import StrProperty, Attribute
@@ -47,28 +49,31 @@ def purge():
parameterizedprofileclasses_enum = []
def getProfileClasses(self, context):
def get_profile_classes(self, context):
global profileclasses_enum
if len(profileclasses_enum) == 0 and IfcStore.get_schema():
version = tool.Ifc.get_schema()
profileclasses_enum.clear()
profileclasses_enum = [
(t.name(), t.name(), "") for t in IfcStore.get_schema().declaration_by_name("IfcProfileDef").subtypes()
(t.name(), t.name(), get_entity_doc(version, t.name()).get("description", ""))
for t in IfcStore.get_schema().declaration_by_name("IfcProfileDef").subtypes()
]
return profileclasses_enum
def getParameterizedProfileClasses(self, context):
def get_parameterized_profile_classes(self, context):
global parameterizedprofileclasses_enum
if len(parameterizedprofileclasses_enum) == 0 and IfcStore.get_schema():
version = tool.Ifc.get_schema()
parameterizedprofileclasses_enum.clear()
parameterizedprofileclasses_enum = [
(t.name(), t.name(), "")
(t.name(), t.name(), get_entity_doc(version, t.name()).get("description", ""))
for t in IfcStore.get_schema().declaration_by_name("IfcParameterizedProfileDef").subtypes()
]
for ifc_class in parameterizedprofileclasses_enum:
parameterizedprofileclasses_enum.extend(
[
(t.name(), t.name(), "")
(t.name(), t.name(), get_entity_doc(version, t.name()).get("description", ""))
for t in IfcStore.get_schema().declaration_by_name(ifc_class[0]).subtypes() or []
]
)
@@ -81,7 +86,7 @@ def get_materials(self, context):
return ObjectMaterialData.data["materials"]
def getMaterialTypes(self, context):
def get_object_material_types(self, context):
global materialtypes_enum
if len(materialtypes_enum) == 0 and IfcStore.get_file():
material_types = [
@@ -93,10 +98,11 @@ def getMaterialTypes(self, context):
"IfcMaterialProfileSetUsage",
"IfcMaterialList",
]
if IfcStore.get_file().schema == "IFC2X3":
version = tool.Ifc.get_schema()
if version == "IFC2X3":
material_types = ["IfcMaterial", "IfcMaterialLayerSet", "IfcMaterialLayerSetUsage", "IfcMaterialList"]
materialtypes_enum.clear()
materialtypes_enum = [(m, m, "") for m in material_types]
materialtypes_enum = [(m, m, get_entity_doc(version, m).get("description", "")) for m in material_types]
return materialtypes_enum
@@ -106,6 +112,12 @@ def get_material_types(self, context):
return MaterialsData.data["material_types"]
def get_profiles(self, context):
if not MaterialsData.is_loaded:
MaterialsData.load()
return MaterialsData.data["profiles"]
class Material(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
@@ -119,10 +131,11 @@ class BIMMaterialProperties(PropertyGroup):
material_type: EnumProperty(items=get_material_types, name="Material Type")
materials: CollectionProperty(name="Materials", type=Material)
active_material_index: IntProperty(name="Active Material Index")
profiles: EnumProperty(items=get_profiles, name="Profiles")
class BIMObjectMaterialProperties(PropertyGroup):
material_type: EnumProperty(items=getMaterialTypes, name="Material Type")
material_type: EnumProperty(items=get_object_material_types, name="Material Type")
material: EnumProperty(items=get_materials, name="Material")
is_editing: BoolProperty(name="Is Editing", default=False)
material_set_usage_attributes: CollectionProperty(name="Material Set Usage Attributes", type=Attribute)
@@ -133,7 +146,7 @@ class BIMObjectMaterialProperties(PropertyGroup):
name="Material Set Item Profile Attributes", type=Attribute
)
material_set_item_material: EnumProperty(items=get_materials, name="Material")
profile_classes: EnumProperty(items=getProfileClasses, name="Profile Classes")
profile_classes: EnumProperty(items=get_profile_classes, name="Profile Classes")
parameterized_profile_classes: EnumProperty(
items=getParameterizedProfileClasses, name="Parameterized Profile Classes"
items=get_parameterized_profile_classes, name="Parameterized Profile Classes"
)
@@ -19,7 +19,6 @@
import blenderbim.bim.helper
from bpy.types import Panel, UIList
from ifcopenshell.api.material.data import Data
from ifcopenshell.api.profile.data import Data as ProfileData
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes
from blenderbim.bim.helper import prop_with_search
@@ -128,13 +127,11 @@ class BIM_PT_object_material(Panel):
self.file = IfcStore.get_file()
self.oprops = context.active_object.BIMObjectProperties
self.props = context.active_object.BIMObjectMaterialProperties
self.mprops = context.scene.BIMMaterialProperties
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 ProfileData.is_loaded:
ProfileData.load(self.file)
self.product_data = Data.products[self.oprops.ifc_definition_id]
if not ObjectMaterialData.data["materials"]:
row = self.layout.row(align=True)
@@ -146,46 +143,7 @@ class BIM_PT_object_material(Panel):
row = self.layout.row(align=True)
row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="FILE_PARENT")
if self.product_data:
if self.product_data["type"] == "IfcMaterialConstituentSet":
self.material_set_id = self.product_data["id"]
self.material_set_data = Data.constituent_sets[self.material_set_id]
self.set_items = self.material_set_data["MaterialConstituents"] or []
self.set_data = Data.constituents
self.set_item_name = "constituent"
elif self.product_data["type"] == "IfcMaterialLayerSet":
self.material_set_id = self.product_data["id"]
self.material_set_data = Data.layer_sets[self.material_set_id]
self.set_items = self.material_set_data["MaterialLayers"] or []
self.set_data = Data.layers
self.set_item_name = "layer"
elif self.product_data["type"] == "IfcMaterialLayerSetUsage":
self.material_set_usage = Data.layer_set_usages[self.product_data["id"]]
self.material_set_id = self.material_set_usage["ForLayerSet"]
self.material_set_data = Data.layer_sets[self.material_set_id]
self.set_items = self.material_set_data["MaterialLayers"] or []
self.set_data = Data.layers
self.set_item_name = "layer"
elif self.product_data["type"] == "IfcMaterialProfileSet":
self.material_set_id = self.product_data["id"]
self.material_set_data = Data.profile_sets[self.material_set_id]
self.set_items = self.material_set_data["MaterialProfiles"] or []
self.set_data = Data.profiles
self.set_item_name = "profile"
elif self.product_data["type"] == "IfcMaterialProfileSetUsage":
self.material_set_usage = Data.profile_set_usages[self.product_data["id"]]
self.material_set_id = self.material_set_usage["ForProfileSet"]
self.material_set_data = Data.profile_sets[self.material_set_id]
self.set_items = self.material_set_data["MaterialProfiles"] or []
self.set_data = Data.profiles
self.set_item_name = "profile"
elif self.product_data["type"] == "IfcMaterialList":
self.material_set_id = self.product_data["id"]
self.material_set_data = Data.lists[self.material_set_id]
self.set_items = self.material_set_data["Materials"] or []
self.set_item_name = "list_item"
else:
self.material_set_id = 0
if ObjectMaterialData.data["material_class"]:
return self.draw_material_ui()
row = self.layout.row(align=True)
@@ -196,21 +154,21 @@ class BIM_PT_object_material(Panel):
def draw_material_ui(self):
row = self.layout.row(align=True)
row.label(text=self.product_data["type"])
row.label(text=ObjectMaterialData.data["material_class"])
if self.props.is_editing:
op = row.operator("bim.edit_assigned_material", icon="CHECKMARK", text="")
op.material_set = self.material_set_id
if "Usage" in self.product_data["type"]:
op.material_set_usage = self.product_data["id"]
op.material_set = ObjectMaterialData.data["set"]["id"]
if "Usage" in ObjectMaterialData.data["material_class"]:
op.material_set_usage = ObjectMaterialData.data["material_id"]
row.operator("bim.disable_editing_assigned_material", icon="CANCEL", text="")
else:
if self.product_data["type"] == "IfcMaterial":
if ObjectMaterialData.data["material_class"] == "IfcMaterial":
row.operator("bim.copy_material", icon="COPYDOWN", text="")
row.operator("bim.enable_editing_assigned_material", icon="GREASEPENCIL", text="")
row.operator("bim.unassign_material", icon="X", text="")
if self.product_data["type"] == "IfcMaterial":
if ObjectMaterialData.data["material_class"] == "IfcMaterial":
self.draw_single_ui()
else:
self.draw_set_ui()
@@ -224,10 +182,9 @@ class BIM_PT_object_material(Panel):
prop_with_search(self.layout, self.props, "material", text="")
def draw_read_only_single_ui(self):
material = Data.materials[self.product_data["id"]]
row = self.layout.row(align=True)
row.label(text="Name")
row.label(text=material["Name"])
row.label(text=ObjectMaterialData.data["material_name"])
def draw_set_ui(self):
if self.props.is_editing:
@@ -235,166 +192,117 @@ class BIM_PT_object_material(Panel):
self.draw_read_only_set_ui()
def draw_editable_set_ui(self):
blenderbim.bim.helper.draw_attributes(self.props.material_set_attributes, self.layout)
blenderbim.bim.helper.draw_attributes(self.props.material_set_usage_attributes, self.layout)
for attribute in self.props.material_set_attributes:
if ObjectMaterialData.data["set_item_name"] == "profile" and not self.mprops.profiles:
row = self.layout.row(align=True)
row.prop(attribute, "string_value", text=attribute.name)
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
row = self.layout.row(align=True)
prop_with_search(row, self.props, "material", text="")
row.label(text="No Profiles Available")
row.operator("bim.add_profile_def", icon="ADD", text="")
else:
row = self.layout.row(align=True)
if ObjectMaterialData.data["set_item_name"] == "profile":
row.prop(self.mprops, "profiles", icon="ITALIC", text="")
prop_with_search(row, self.props, "material", icon="MATERIAL", text="")
op = row.operator(f"bim.add_{ObjectMaterialData.data['set_item_name']}", icon="ADD", text="")
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_set", ObjectMaterialData.data["set"]["id"])
op = row.operator(f"bim.add_{self.set_item_name}", icon="ADD", text="")
setattr(op, f"{self.set_item_name}_set", self.material_set_id)
total_items = len(self.set_items)
for index, set_item_id in enumerate(self.set_items):
if self.props.active_material_set_item_id == set_item_id:
self.draw_editable_set_item_ui(set_item_id)
total_items = len(ObjectMaterialData.data["set_items"])
for index, set_item in enumerate(ObjectMaterialData.data["set_items"]):
if len(self.props.material_set_item_profile_attributes):
self.draw_editable_set_item_profile_ui(set_item)
elif self.props.active_material_set_item_id == set_item["id"]:
self.draw_editable_set_item_ui(set_item)
else:
self.draw_read_only_set_item_ui(
set_item_id, index, is_first=index == 0, is_last=index == total_items - 1
set_item, index, is_first=index == 0, is_last=index == total_items - 1
)
def draw_editable_set_item_ui(self, set_item_id):
item = self.set_data[set_item_id]
material = Data.materials[item["Material"]]
def draw_editable_set_item_profile_ui(self, set_item):
box = self.layout.box()
row = box.row(align=True)
row.prop(self.props, "material_set_item_material", icon="MATERIAL")
op = row.operator("bim.edit_material_set_item", icon="CHECKMARK", text="")
op.material_set_item = set_item_id
op = row.operator("bim.edit_material_set_item_profile", icon="CHECKMARK", text="Save Changes")
op.material_set_item = set_item["id"]
row.operator("bim.disable_editing_material_set_item_profile", icon="CANCEL", text="")
draw_attributes(self.props.material_set_item_profile_attributes, box)
def draw_editable_set_item_ui(self, set_item):
box = self.layout.box()
row = box.row(align=True)
op = row.operator("bim.edit_material_set_item", icon="CHECKMARK", text="Save Changes")
op.material_set_item = set_item["id"]
row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="")
draw_attributes(self.props.material_set_item_attributes, self.layout)
draw_attributes(self.props.material_set_item_attributes, box)
if self.set_item_name == "profile":
self.draw_assign_profile_ui(box, item)
self.draw_editable_profile_ui(box, item)
row = box.row()
row.prop(self.props, "material_set_item_material", icon="MATERIAL", text="Material")
def draw_assign_profile_ui(self, layout, item):
row = layout.row(align=True)
row.prop(self.props, "profile_classes", text="")
if self.props.profile_classes == "IfcParameterizedProfileDef":
row.prop(self.props, "parameterized_profile_classes", text="")
op = row.operator(
"bim.assign_parameterized_profile", icon="GREASEPENCIL" if item["Profile"] else "ADD", text=""
)
op.ifc_class = self.props.parameterized_profile_classes
op.material_profile = item["id"]
else:
# TODO: support non parametric profiles by showing a list of named profiles to select from, or an
# eyedropper to pick profile geometry from the scene
row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="")
if ObjectMaterialData.data["set_item_name"] == "profile":
row = box.row()
row.prop(self.mprops, "profiles", icon="ITALIC", text="Profile")
def draw_editable_profile_ui(self, layout, item):
draw_attributes(self.props.material_set_item_profile_attributes, self.layout)
def draw_read_only_set_item_ui(self, set_item_id, index, is_first=False, is_last=False):
if self.product_data["type"] == "IfcMaterialList":
item = Data.materials[set_item_id]
def draw_read_only_set_item_ui(self, set_item, index, is_first=False, is_last=False):
if ObjectMaterialData.data["material_class"] == "IfcMaterialList":
row = self.layout.row(align=True)
row.label(text="IfcMaterial", icon="ALIGN_CENTER")
row.label(text=item["Name"], icon="MATERIAL")
row.label(text="IfcMaterial", icon="LAYER_ACTIVE")
row.label(text=set_item["name"], icon="MATERIAL")
else:
item = self.set_data[set_item_id]
row = self.layout.row(align=True)
item_name = item.get("Name", "Unnamed") or "Unnamed"
thickness = item.get("LayerThickness")
if thickness:
item_name += f" ({thickness})"
row.label(text=item_name, icon="ALIGN_CENTER")
row.label(text=Data.materials[item["Material"]]["Name"], icon="MATERIAL")
row.label(text=set_item["name"], icon=set_item["icon"])
row.label(text=set_item["material"], icon="MATERIAL")
if not is_first:
op = row.operator(f"bim.reorder_material_set_item", icon="TRIA_UP", text="")
op.old_index = index
op.new_index = index - 1
setattr(op, "material_set", self.material_set_id)
setattr(op, "material_set", ObjectMaterialData.data["set"]["id"])
if not is_last:
op = row.operator(f"bim.reorder_material_set_item", icon="TRIA_DOWN", text="")
op.old_index = index
op.new_index = index + 1
setattr(op, "material_set", self.material_set_id)
if not self.props.active_material_set_item_id and self.product_data["type"] != "IfcMaterialList":
setattr(op, "material_set", ObjectMaterialData.data["set"]["id"])
if not self.props.active_material_set_item_id and ObjectMaterialData.data["material_class"] != "IfcMaterialList":
if "Profile" in ObjectMaterialData.data["material_class"]:
op = row.operator("bim.enable_editing_material_set_item_profile", icon="ITALIC", text="")
op.material_set_item = set_item["id"]
op = row.operator("bim.enable_editing_material_set_item", icon="GREASEPENCIL", text="")
op.material_set_item = set_item_id
op = row.operator(f"bim.remove_{self.set_item_name}", icon="X", text="")
if self.product_data["type"] == "IfcMaterialList":
setattr(op, "list_item_set", self.material_set_id)
setattr(op, self.set_item_name, item["id"])
if hasattr(op, f"{self.set_item_name}_index"):
setattr(op, f"{self.set_item_name}_index", index)
op.material_set_item = set_item["id"]
op = row.operator(f"bim.remove_{ObjectMaterialData.data['set_item_name']}", icon="X", text="")
if ObjectMaterialData.data["material_class"] == "IfcMaterialList":
setattr(op, "list_item_set", ObjectMaterialData.data["set"]["id"])
setattr(op, ObjectMaterialData.data["set_item_name"], set_item["id"])
if hasattr(op, f"{ObjectMaterialData.data['set_item_name']}_index"):
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_index", index)
def draw_read_only_set_ui(self):
if (
self.product_data["type"] == "IfcMaterialLayerSetUsage"
or self.product_data["type"] == "IfcMaterialLayerSet"
):
name_attr = "LayerSetName"
else:
name_attr = "Name"
row = self.layout.row(align=True)
row.label(text=name_attr)
row.label(text=self.material_set_data.get(name_attr, "Unnamed") or "Unnamed")
if hasattr(self.material_set_data, "Description") and self.material_set_data["Description"]:
row.label(text="Name")
row.label(text=ObjectMaterialData.data["set"]["name"])
if ObjectMaterialData.data["set"]["description"]:
row = self.layout.row(align=True)
row.label(text="Description")
row.label(text=str(self.material_set_data["Description"]))
row.label(text=ObjectMaterialData.data["set"]["description"])
if self.product_data["type"] == "IfcMaterialProfileSetUsage":
# TODO: complain to buildingSMART
cardinal_point_map = {
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",
}
if self.material_set_usage["CardinalPoint"]:
if ObjectMaterialData.data["material_class"] == "IfcMaterialProfileSetUsage":
if ObjectMaterialData.data["set_usage"].get("cardinal_point"):
row = self.layout.row(align=True)
row.label(text="CardinalPoint")
row.label(text=cardinal_point_map[self.material_set_usage["CardinalPoint"]])
if self.material_set_usage["ReferenceExtent"]:
row = self.layout.row(align=True)
row.label(text="ReferenceExtent")
row.label(text=str(self.material_set_usage["ReferenceExtent"]))
row.label(text=ObjectMaterialData.data["set_usage"]["cardinal_point"])
total_thickness = 0
for item_id in self.set_items:
if self.product_data["type"] == "IfcMaterialList":
for set_item in ObjectMaterialData.data["set_items"]:
if ObjectMaterialData.data["material_class"] == "IfcMaterialList":
row = self.layout.row(align=True)
row.label(text="IfcMaterial", icon="ALIGN_CENTER")
row.label(text=Data.materials[item_id]["Name"], icon="MATERIAL")
row.label(text="IfcMaterial", icon="LAYER_ACTIVE")
row.label(text=set_item["name"], icon="MATERIAL")
else:
item = self.set_data[item_id]
row = self.layout.row(align=True)
item_name = item.get("Name", "Unnamed") or "Unnamed"
thickness = item.get("LayerThickness")
if thickness:
item_name += f" ({thickness:.3f})"
total_thickness += thickness
row.label(text=item_name, icon="ALIGN_CENTER")
row.label(text=Data.materials[item["Material"]]["Name"], icon="MATERIAL")
row.label(text=set_item["name"], icon=set_item["icon"])
row.label(text=set_item["material"], icon="MATERIAL")
if total_thickness:
if ObjectMaterialData.data["total_thickness"]:
row = self.layout.row(align=True)
row.label(text=f"Total Thickness: {total_thickness:.3f}")
row.label(text=f"Total Thickness: {ObjectMaterialData.data['total_thickness']:.3f}")
class BIM_UL_materials(UIList):
@@ -17,9 +17,15 @@
# 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, profile
from . import handler, prop, ui, grid, array, product, wall, slab, stair, opening, pie, workspace, profile
classes = (
array.AddArray,
array.DisableEditingArray,
array.EditArray,
array.EnableEditingArray,
array.RemoveArray,
array.SelectArrayParent,
product.AddConstrTypeInstance,
product.AddEmptyType,
product.AlignProduct,
@@ -66,13 +72,23 @@ classes = (
prop.ConstrClassInfo,
prop.ConstrBrowserState,
prop.BIMModelProperties,
prop.BIMArrayProperties,
prop.BIMStairProperties,
ui.BIM_PT_authoring,
ui.BIM_PT_array,
ui.BIM_PT_stair,
ui.DisplayConstrTypesUI,
ui.LaunchTypeManager,
ui.HelpConstrTypes,
ui.BIM_MT_model,
grid.BIM_OT_add_object,
stair.BIM_OT_add_object,
stair.BIM_OT_add_clever_stair,
stair.AddStair,
stair.CancelEditingStair,
stair.FinishEditingStair,
stair.EnableEditingStair,
stair.RemoveStair,
pie.OpenPieClass,
pie.PieUpdateContainer,
pie.PieAddOpening,
@@ -87,6 +103,8 @@ def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.BimTool, after={"builtin.scale_cage"}, separator=True, group=True)
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties)
bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties)
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_add.append(ui.add_menu)
@@ -103,6 +121,8 @@ def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.BimTool)
del bpy.types.Scene.BIMModelProperties
del bpy.types.Object.BIMArrayProperties
del bpy.types.Object.BIMStairProperties
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)
@@ -0,0 +1,176 @@
# 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 ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import blenderbim.tool as tool
from mathutils import Vector
class AddArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_array"
bl_label = "Add Array"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
array = {"children": [], "count": 1, "x": 0.0, "y": 0.0, "z": 0.0}
psets = ifcopenshell.util.element.get_psets(element)
pset = psets.get("BBIM_Array", None)
if pset:
data = json.loads(pset["Data"])
data.append(array)
pset = tool.Ifc.get().by_id(pset["id"])
else:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Array")
data = [array]
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Parent": element.GlobalId, "Data": json.dumps(data)},
)
return {"FINISHED"}
class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_array"
bl_label = "Disable Editing Array"
bl_options = {"REGISTER"}
def _execute(self, context):
props = context.active_object.BIMArrayProperties.is_editing = -1
return {"FINISHED"}
class EditArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_array"
bl_label = "Edit Array"
bl_options = {"REGISTER"}
item: bpy.props.IntProperty()
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMArrayProperties
psets = ifcopenshell.util.element.get_psets(element)
pset = psets["BBIM_Array"]
data = json.loads(pset["Data"])
data[self.item] = {
"children": data[self.item]["children"],
"count": props.count,
"x": props.x,
"y": props.y,
"z": props.z,
}
props.is_editing = -1
try:
parent = tool.Ifc.get_object(tool.Ifc.get().by_guid(pset["Parent"]))
except:
return {"FINISHED"}
tool.Model.regenerate_array(parent, data)
pset = tool.Ifc.get().by_id(pset["id"])
data = json.dumps(data)
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
return {"FINISHED"}
class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_array"
bl_label = "Enable Editing Array"
bl_options = {"REGISTER"}
item: bpy.props.IntProperty()
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
psets = ifcopenshell.util.element.get_psets(element)
data = json.loads(psets["BBIM_Array"]["Data"])[self.item]
props = obj.BIMArrayProperties
props.count = data["count"]
props.x = data["x"]
props.y = data["y"]
props.z = data["z"]
props.is_editing = self.item
class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_array"
bl_label = "Remove Array"
bl_options = {"REGISTER"}
item: bpy.props.IntProperty()
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMArrayProperties
psets = ifcopenshell.util.element.get_psets(element)
pset = psets["BBIM_Array"]
data = json.loads(pset["Data"])
data[self.item]["count"] = 1
props.is_editing = -1
try:
parent = tool.Ifc.get_object(tool.Ifc.get().by_guid(pset["Parent"]))
except:
return {"FINISHED"}
tool.Model.regenerate_array(parent, data)
pset = tool.Ifc.get().by_id(pset["id"])
if len(data) == 1:
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
else:
del data[self.item]
data = json.dumps(data)
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
return {"FINISHED"}
class SelectArrayParent(bpy.types.Operator):
bl_idname = "bim.select_array_parent"
bl_label = "Select Array Parent"
bl_options = {"REGISTER", "UNDO"}
parent: bpy.props.StringProperty()
def execute(self, context):
try:
element = tool.Ifc.get().by_guid(self.parent)
except:
return {"FINISHED"}
obj = tool.Ifc.get_object(element)
if obj:
context.view_layer.objects.active = obj
obj.select_set(True)
return {"FINISHED"}
@@ -18,8 +18,11 @@
import bpy
import math
import json
import functools
import ifcopenshell
import ifcopenshell.util.element
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
@@ -27,6 +30,8 @@ from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
def refresh():
AuthoringData.is_loaded = False
ArrayData.is_loaded = False
StairData.is_loaded = False
class AuthoringData:
@@ -60,15 +65,22 @@ class AuthoringData:
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)]
version = tool.Ifc.get_schema()
return [(c, c, get_entity_doc(version, c).get("description", "")) for c in sorted(names)]
@classmethod
def type_predefined_type(cls):
results = []
declaration = tool.Ifc().schema().declaration_by_name(cls.props.type_class)
version = tool.Ifc.get_schema()
for attribute in declaration.attributes():
if attribute.name() == "PredefinedType":
results.extend([(e, e, "") for e in attribute.type_of_attribute().declared_type().enumeration_items()])
results.extend(
[
(e, e, get_predefined_type_doc(version, cls.props.type_class, e))
for e in attribute.type_of_attribute().declared_type().enumeration_items()
]
)
break
return results
@@ -179,14 +191,17 @@ class AuthoringData:
if not ifc_class and ifc_classes:
ifc_class = ifc_classes[0][0]
if ifc_class:
elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name)
elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name or "Unnamed")
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)]
return [
(str(e.id()), e.Name or "Unnamed", e.Description or "")
for e in cls.constr_class_entities(ifc_class=ifc_class)
]
@classmethod
def relating_types_browser(cls):
@@ -313,3 +328,49 @@ class AuthoringData:
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]
class ArrayData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {"parameters": cls.parameters()}
@classmethod
def parameters(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
psets = ifcopenshell.util.element.get_psets(element)
parameters = psets.get("BBIM_Array", None)
if parameters:
try:
parent = tool.Ifc.get().by_guid(parameters["Parent"])
parameters["has_parent"] = True
parameters["parent_name"] = parent.Name or "Unnamed"
parameters["data"] = json.loads(parameters.get("Data", "[]") or "[]")
except:
parameters["has_parent"] = False
return parameters
class StairData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {"parameters": cls.parameters()}
@classmethod
def parameters(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
psets = ifcopenshell.util.element.get_psets(element)
parameters = psets.get("BBIM_Stair", None)
if parameters:
parameters["data"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
@@ -71,12 +71,12 @@ class AddFilledOpening(bpy.types.Operator, tool.Ifc.Operator):
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.rl2 * unit_scale)
if filling.is_a("IfcDoor"):
new_matrix[2][3] = voided_obj.matrix_world[2][3]
else:
new_matrix[2][3] = voided_obj.matrix_world[2][3] + (props.rl2 * unit_scale)
filling_obj.matrix_world = new_matrix
bpy.context.view_layer.update()
@@ -1,5 +1,5 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
@@ -201,7 +201,9 @@ class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator):
page: bpy.props.IntProperty()
def _execute(self, context):
context.scene.BIMModelProperties.type_page = self.page
props = context.scene.BIMModelProperties
bpy.ops.bim.load_type_thumbnails(ifc_class=props.type_class, offset=9 * (self.page - 1), limit=9)
props.type_page = self.page
return {"FINISHED"}
@@ -316,14 +318,27 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Load Type Thumbnails"
bl_options = {"REGISTER", "UNDO"}
ifc_class: bpy.props.StringProperty()
limit: bpy.props.IntProperty()
offset: bpy.props.IntProperty()
def _execute(self, context):
from PIL import Image, ImageDraw
if bpy.app.background:
return
props = bpy.context.scene.BIMModelProperties
processing = set()
# Only process at most one class at a time.
# Only process at most one paginated 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)
queue = sorted(tool.Ifc.get().by_type(self.ifc_class), key=lambda e: e.Name or "Unnamed")
if self.limit:
queue = queue[self.offset:self.offset + self.limit]
else:
offset = 9 * (props.type_page - 1)
if offset < 0:
offset = 0
queue = queue[offset:offset + 9]
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -199,6 +199,17 @@ class DumbProfileGenerator:
class DumbProfileRegenerator:
def regenerate_from_profile_def(self, profile):
self.file = tool.Ifc.get()
objs = []
if not profile:
return
for element in self.get_elements_using_profile(profile):
obj = tool.Ifc.get_object(element)
if obj:
objs.append(obj)
DumbProfileRecalculator().recalculate(objs)
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
self.file = ifc_file
objs = []
@@ -833,7 +844,7 @@ class DumbProfileRecalculator:
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:
if profile:
joiner.recreate_profile(element, profile)
@@ -228,8 +228,8 @@ class BIMModelProperties(PropertyGroup):
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)
rl1: bpy.props.FloatProperty(name="RL", default=1) # Used for things like walls, doors, flooring, skirting, etc
rl2: bpy.props.FloatProperty(name="RL", default=1) # Used for things like windows, other hosted furniture
rl1: bpy.props.FloatProperty(name="RL", default=1) # Used for things like walls, doors, flooring, skirting, etc
rl2: bpy.props.FloatProperty(name="RL", default=1) # Used for things like windows, other hosted furniture
x_angle: bpy.props.FloatProperty(name="X Angle", default=0)
type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
type_template: bpy.props.EnumProperty(
@@ -245,3 +245,52 @@ class BIMModelProperties(PropertyGroup):
)
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)
class BIMArrayProperties(PropertyGroup):
is_editing: bpy.props.IntProperty(default=-1)
count: bpy.props.IntProperty(name="Count", default=0)
x: bpy.props.FloatProperty(name="X", default=0)
y: bpy.props.FloatProperty(name="Y", default=0)
z: bpy.props.FloatProperty(name="Z", default=0)
class BIMStairProperties(PropertyGroup):
stair_types = (
("CONCRETE", "Concrete", ""),
("WOOD/STEEL", "Wood / Steel", ""),
)
is_editing: bpy.props.IntProperty(default=-1)
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01)
height: bpy.props.FloatProperty(name="Height", default=1.0, soft_min=0.01)
number_of_treads: bpy.props.IntProperty(name="Number of treads", default=6, soft_min=1)
tread_depth: bpy.props.FloatProperty(name="Tread Depth", default=0.25, soft_min=0.01)
tread_run: bpy.props.FloatProperty(name="Tread Run", default=0.3, soft_min=0.01)
base_slab_depth: bpy.props.FloatProperty(name="Base slab depth", default=0.25, soft_min=0)
top_slab_depth: bpy.props.FloatProperty(name="Top slab depth", default=0.25, soft_min=0)
has_top_nib: bpy.props.BoolProperty(name="Has top nib", default=True)
stair_type: bpy.props.EnumProperty(name="Stair type", items=stair_types, default="CONCRETE")
def get_props_kwargs(self):
if self.stair_type == "CONCRETE":
return {
"stair_type": self.stair_type,
"width": self.width,
"height": self.height,
"number_of_treads": self.number_of_treads,
"tread_depth": self.tread_depth,
"tread_run": self.tread_run,
"base_slab_depth": self.base_slab_depth,
"top_slab_depth": self.top_slab_depth,
"has_top_nib": self.has_top_nib,
}
elif self.stair_type == "WOOD/STEEL":
return {
"stair_type": self.stair_type,
"width": self.width,
"height": self.height,
"number_of_treads": self.number_of_treads,
"tread_depth": self.tread_depth,
"tread_run": self.tread_run,
}
@@ -173,11 +173,9 @@ class DumbSlabGenerator:
matrix_world[2][3] = self.collection_obj.location[2] - self.depth
else:
matrix_world[2][3] -= self.depth
obj.matrix_world = matrix_world
obj.matrix_world = Matrix.Rotation(self.x_angle, 4, "X") @ matrix_world
bpy.context.view_layer.update()
self.collection.objects.link(obj)
rotation = mathutils.Matrix.Rotation(self.x_angle, 4, 'X')
obj.matrix_world = rotation
element = blenderbim.core.root.assign_class(
tool.Ifc,
@@ -619,122 +617,13 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
else:
position = Matrix()
self.vertices = []
self.edges = []
self.arcs = []
self.circles = []
profile = extrusion.SweptArea
if profile.is_a("IfcArbitraryClosedProfileDef"):
self.process_curve(obj, position, profile.OuterCurve)
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
for inner_curve in profile.InnerCurves:
self.process_curve(obj, position, inner_curve)
elif profile.is_a() == "IfcRectangleProfileDef":
self.process_rectangle(obj, position, profile)
mesh = bpy.data.meshes.new("Profile")
mesh.from_pydata(self.vertices, self.edges, [])
mesh.BIMMeshProperties.is_profile = True
obj.data = mesh
for arc in self.arcs:
group = obj.vertex_groups.new(name="IFCARCINDEX")
group.add(arc, 1, "REPLACE")
for circle in self.circles:
group = obj.vertex_groups.new(name="IFCCIRCLE")
group.add(circle, 1, "REPLACE")
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position)
bpy.ops.object.mode_set(mode="EDIT")
DecorationsHandler.install(context)
bpy.ops.wm.tool_set_by_id(name="bim.cad_tool")
return {"FINISHED"}
def process_curve(self, obj, position, curve):
offset = len(self.vertices)
if curve.is_a("IfcPolyline"):
total_points = len(curve.Points)
last_index = len(curve.Points) - 1
for i, point in enumerate(curve.Points):
if i == last_index:
continue
global_point = position @ Vector(self.convert_unit_to_si(point.Coordinates)).to_3d()
self.vertices.append(global_point)
self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))])
self.edges[-1] = (len(self.vertices) - 1, offset) # Close the loop
elif curve.is_a("IfcIndexedPolyCurve"):
is_arc = False
if curve.Segments:
for segment in curve.Segments:
if len(segment[0]) == 3: # IfcArcIndex
is_arc = True
local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1])
global_point = position @ Vector(local_point).to_3d()
self.vertices.append(global_point)
local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][1] - 1])
global_point = position @ Vector(local_point).to_3d()
self.vertices.append(global_point)
self.arcs.append([len(self.vertices) - 2, len(self.vertices) - 1])
else:
local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1])
global_point = position @ Vector(local_point).to_3d()
self.vertices.append(global_point)
if is_arc:
self.arcs[-1].append(len(self.vertices) - 1)
is_arc = False
else:
for local_point in curve.Points.CoordList:
global_point = position @ Vector(self.convert_unit_to_si(local_point)).to_3d()
self.vertices.append(global_point)
# Curves without segments are self closing
del self.vertices[-1]
self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))])
self.edges[-1] = (len(self.vertices) - 1, offset) # Close the loop
elif curve.is_a("IfcCircle"):
center = self.convert_unit_to_si(
Matrix(ifcopenshell.util.placement.get_axis2placement(curve.Position).tolist()).col[3].to_3d()
)
radius = self.convert_unit_to_si(curve.Radius)
self.vertices.extend(
[
position @ Vector((center[0], center[1] - curve.Radius, 0.0)),
position @ Vector((center[0], center[1] + curve.Radius, 0.0)),
]
)
self.circles.append([offset, offset + 1])
self.edges.append((offset, offset + 1))
def process_rectangle(self, obj, position, profile):
if profile.Position:
p_position = Matrix(ifcopenshell.util.placement.get_axis2placement(profile.Position).tolist())
p_position[0][3] *= self.unit_scale
p_position[1][3] *= self.unit_scale
p_position[2][3] *= self.unit_scale
else:
p_position = Matrix()
x = self.convert_unit_to_si(profile.XDim)
y = self.convert_unit_to_si(profile.YDim)
self.vertices.extend(
[
position @ p_position @ Vector((-x / 2, -y / 2, 0.0)),
position @ p_position @ Vector((x / 2, -y / 2, 0.0)),
position @ p_position @ Vector((x / 2, y / 2, 0.0)),
position @ p_position @ Vector((-x / 2, y / 2, 0.0)),
]
)
self.edges.extend([(i, i + 1) for i in range(0, len(self.vertices))])
self.edges[-1] = (len(self.vertices) - 1, 0) # Close the loop
def convert_unit_to_si(self, value):
if isinstance(value, (tuple, list)):
return [v * self.unit_scale for v in value]
return value * self.unit_scale
class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_extrusion_profile"
@@ -759,10 +648,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
else:
position = Matrix()
helper = Helper(tool.Ifc.get())
indices = helper.auto_detect_arbitrary_profile_with_voids(obj, obj.data)
profile = tool.Model.export_profile(obj, position=position)
if isinstance(indices, tuple) and indices[0] is False: # Ugly
if not profile:
def msg(self, context):
self.layout.label(text="INVALID PROFILE: " + indices[1])
@@ -770,32 +658,11 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
DecorationsHandler.install(context)
bpy.ops.object.mode_set(mode="EDIT")
return {"FINISHED"}
self.bm = bmesh.new()
self.bm.from_mesh(obj.data)
self.bm.verts.ensure_lookup_table()
self.bm.edges.ensure_lookup_table()
if indices["inner_curves"]:
profile = tool.Ifc.get().createIfcArbitraryProfileDefWithVoids("AREA")
else:
profile = tool.Ifc.get().createIfcArbitraryClosedProfileDef("AREA")
if tool.Ifc.get().schema != "IFC2X3":
self.points = self.create_points(position, indices["points"])
profile.OuterCurve = self.create_curve(position, indices["profile"])
if indices["inner_curves"]:
results = []
for inner_curve in indices["inner_curves"]:
results.append(self.create_curve(position, inner_curve))
profile.InnerCurves = results
self.bm.free()
return
old_profile = extrusion.SweptArea
extrusion.SweptArea = profile
for inverse in tool.Ifc.get().get_inverse(old_profile):
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile)
blenderbim.core.geometry.switch_representation(
@@ -832,45 +699,6 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
)
return {"FINISHED"}
def create_points(self, position, indices):
position_i = position.inverted()
points = []
for point in indices:
local_point = (position_i @ point).to_2d()
points.append(self.convert_si_to_unit(list(local_point)))
return tool.Ifc.get().createIfcCartesianPointList2D(points)
def create_curve(self, position, edge_indices, points=None):
position_i = position.inverted()
if len(edge_indices) == 2:
diameter = edge_indices[0]
p1 = self.bm.verts[diameter[0]].co
p2 = self.bm.verts[diameter[1]].co
center = self.convert_si_to_unit(list(position_i @ p1.lerp(p2, 0.5)))
radius = self.convert_si_to_unit((p1 - p2).length / 2)
return tool.Ifc.get().createIfcCircle(
tool.Ifc.get().createIfcAxis2Placement2D(tool.Ifc.get().createIfcCartesianPoint(center[0:2])), radius
)
if tool.Ifc.get().schema == "IFC2X3":
points = []
for edge in edge_indices:
local_point = (position_i @ Vector(self.bm.verts[edge[0]].co)).to_2d()
points.append(tool.Ifc.get().createIfcCartesianPoint(self.convert_si_to_unit(local_point)))
points.append(points[0])
return tool.Ifc.get().createIfcPolyline(points)
segments = []
for segment in edge_indices:
if len(segment) == 2:
segments.append(tool.Ifc.get().createIfcLineIndex([i + 1 for i in segment]))
elif len(segment) == 3:
segments.append(tool.Ifc.get().createIfcArcIndex([i + 1 for i in segment]))
return tool.Ifc.get().createIfcIndexedPolyCurve(self.points, segments, False)
def convert_si_to_unit(self, value):
if isinstance(value, (tuple, list)):
return [v / self.unit_scale for v in value]
return value / self.unit_scale
class ResetVertex(bpy.types.Operator):
bl_idname = "bim.reset_vertex"
@@ -957,7 +785,7 @@ class DecorationsHandler:
special_vertex_indices = {}
selected_edges = []
unselected_edges = []
special_edges = []
special_edges = [] # edges that have a circle or an arc associated with them
arc_groups = []
circle_groups = []
@@ -1,5 +1,5 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>, @Andrej730
#
# This file is part of BlenderBIM Add-on.
#
@@ -18,14 +18,24 @@
import bpy
from bpy.types import Operator
from bpy.props import FloatProperty, IntProperty
from bpy.props import FloatProperty, IntProperty, BoolProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
import bmesh
from bmesh.types import BMVert
import ifcopenshell
import blenderbim
import blenderbim.tool as tool
from blenderbim.bim.module.model.prop import BIMStairProperties
from mathutils import Vector
from pprint import pprint
import json
def add_object(self, context):
if self.number_of_treads <= 0:
self.number_of_treads = 1
def add_dumb_stair_object(self, context):
verts = [
Vector((0, 0, 0)),
Vector((0, self.tread_length, 0)),
@@ -63,15 +73,313 @@ class BIM_OT_add_object(Operator, AddObjectHelper):
width: FloatProperty(name="Width", default=1.1)
height: FloatProperty(name="Height", default=1)
tread_depth: FloatProperty(name="Tread Depth", default=0.2)
number_of_treads: IntProperty(name="Number of Treads (Goings)", default=6)
number_of_treads: IntProperty(name="Number of Treads (Goings)", default=6, soft_min=1)
tread_length: FloatProperty(name="Tread Length (Going)", default=0.25)
riser_height: FloatProperty(name="*Calculated* Riser Height")
length: FloatProperty(name="*Calculated* Length")
def execute(self, context):
add_object(self, context)
add_dumb_stair_object(self, context)
return {"FINISHED"}
def generate_stair_2d_profile(
number_of_treads,
height,
width,
tread_run,
tread_depth,
stair_type,
# CONCRETE STAIR ARGUMENTS
has_top_nib=None,
top_slab_depth=None,
base_slab_depth=None,
):
vertices = []
edges = []
faces = []
number_of_risers = number_of_treads + 1
tread_rise = height / number_of_risers
length = tread_run * number_of_risers
if stair_type == "WOOD/STEEL":
for i in range(number_of_risers):
v1 = Vector(((i + 1) * tread_run, 0, (i + 1) * tread_rise))
v0 = v1 - Vector((tread_run, 0, 0))
v2 = v1 - Vector((0, 0, tread_depth))
v3 = v0 - Vector((0, 0, tread_depth))
vertices.extend([v0, v1, v2, v3])
cur_vertex = i * 4
edges.extend([
(cur_vertex, cur_vertex+1),
(cur_vertex+1, cur_vertex+2),
(cur_vertex+2, cur_vertex+3),
(cur_vertex+3, cur_vertex),
])
faces.append(list(range(4 * i, 4 * i + 1)))
return (vertices, edges, faces)
elif stair_type == "CONCRETE":
for i in range(number_of_risers):
vertices.extend([
Vector((tread_run*i, 0, tread_rise*i)),
Vector((tread_run*i, 0, tread_rise*(i+1)))
])
cur_vertex = i * 2
if i != 0:
edges.append((cur_vertex - 1, cur_vertex))
edges.append((cur_vertex, cur_vertex + 1))
vertices.append(Vector((tread_run * number_of_risers, 0, tread_rise * number_of_risers)))
edges.append((number_of_risers * 2, number_of_risers * 2 - 1))
td_vector = Vector((vertices[2][2], 0, -vertices[2][0])).normalized() * tread_depth
k = tread_rise / tread_run
s0 = vertices[0] + td_vector
b = s0.z - k * s0.x # comes from y = kx + b
# you could use td_vector as depth_vector
# but then stair won't be perpendicular to the X+
# b is kind of vertical tread_depth (along Z+)
depth_vector = Vector((0, 0, b))
# top nib
if has_top_nib:
vertices.append( vertices[number_of_risers * 2] + Vector((0, 0, -top_slab_depth)) )
vertices.append( vertices[number_of_risers * 2] + Vector(((-top_slab_depth - b) / k, 0, -top_slab_depth)) )
last_vertex_i = len(vertices) - 1
edges.append( (number_of_risers * 2, last_vertex_i - 1) )
edges.append( (last_vertex_i - 1, last_vertex_i) )
else:
vertices.append(vertices[number_of_risers * 2] + depth_vector)
last_vertex_i = len(vertices) - 1
edges.append((number_of_risers * 2, last_vertex_i))
top_nib_end = len(vertices) - 1
# bottom nib
if abs(b) <= base_slab_depth:
vertices.append(vertices[0] + depth_vector)
edges.append((0, len(vertices) - 1))
bottom_nib_end = len(vertices) - 1
else:
vertices.append(vertices[0] + Vector(((-base_slab_depth - b) / k, 0, -base_slab_depth)))
vertices.append(vertices[0] + Vector((0, 0, -base_slab_depth)))
last_vertex_i = len(vertices) - 1
edges.append( (0, last_vertex_i) )
edges.append( (last_vertex_i - 1, last_vertex_i) )
bottom_nib_end = len(vertices) - 2
edges.append( (bottom_nib_end, top_nib_end) )
faces = [list(range(len(vertices)))]
return (vertices, edges, faces)
def update_stair_modifier(context):
obj = context.active_object
props = obj.BIMStairProperties
props_kwargs = props.get_props_kwargs()
vertices, edges, faces = generate_stair_2d_profile(**props_kwargs)
obj = context.object
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
new_verts = [bm.verts.new(v) for v in vertices]
new_edges = [bm.edges.new( (new_verts[e[0]], new_verts[e[1]]) ) for e in edges]
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.contextual_create(bm, geom=new_edges)
bm.faces.ensure_lookup_table()
faces = bm.faces
extruded = bmesh.ops.extrude_face_region(bm, geom=faces)
extrusion_vector = Vector((0, 1, 0)) * props.width
translate_verts = [v for v in extruded["geom"] if isinstance(v, BMVert)]
bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts)
if context.object.mode == "EDIT":
bmesh.update_edit_mesh(obj.data)
else:
bm.to_mesh(obj.data)
bm.free()
obj.data.update()
class BIM_OT_add_clever_stair(Operator):
bl_idname = "mesh.add_clever_stair"
bl_label = "Clever Stair"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
ifcfile = tool.Ifc.get()
if not ifcfile:
self.report({"ERROR"}, "You need to start IFC project first to create a stair.")
return {"CANCELLED"}
if context.object is not None:
spawn_location = context.object.location.copy()
context.object.select_set(False)
else:
spawn_location = bpy.context.scene.cursor.location.copy()
mesh = bpy.data.meshes.new("StairFlight")
obj = bpy.data.objects.new("StairFlight", mesh)
bpy.context.scene.collection.objects.link(obj)
obj.location = spawn_location
body_context = ifcopenshell.util.representation.get_context(ifcfile, "Model", "Body", "MODEL_VIEW")
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class="IfcStairFlight",
should_add_representation=True,
context=body_context,
)
bpy.context.view_layer.objects.active = None
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.bim.add_stair()
return {"FINISHED"}
# UI operators
class AddStair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_stair"
bl_label = "Add Stair"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcStairFlight"):
self.report({"ERROR"}, "Object has to be IfcStairFlight type to add a stair.")
return {"CANCELLED"}
props = obj.BIMStairProperties
stair_data = props.get_props_kwargs()
psets = ifcopenshell.util.element.get_psets(element)
pset = psets.get("BBIM_Stair", None)
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Stair")
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Data": json.dumps(stair_data)},
)
update_stair_modifier(context)
# update IfcStairFlight properties
element.NumberOfRisers = props.number_of_treads + 1
element.NumberOfTreads = props.number_of_treads
element.RiserHeight = props.height / element.NumberOfRisers
element.TreadLength = props.tread_depth
element.PredefinedType = "STRAIGHT"
return {"FINISHED"}
class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_stair"
bl_label = "Cancel editing Stair"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
psets = ifcopenshell.util.element.get_psets(element)
data = json.loads(psets["BBIM_Stair"]["Data"])
props = obj.BIMStairProperties
# restore previous settings since editing was canceled
for prop_name in data:
setattr(props, prop_name, data[prop_name])
update_stair_modifier(context)
props.is_editing = -1
return {"FINISHED"}
class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_stair"
bl_label = "Finish editing stair"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMStairProperties
psets = ifcopenshell.util.element.get_psets(element)
pset = psets["BBIM_Stair"]
data = props.get_props_kwargs()
props.is_editing = -1
update_stair_modifier(context)
pset = tool.Ifc.get().by_id(pset["id"])
data = json.dumps(data)
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
# update IfcStairFlight properties
if element.is_a("IfcStairFlight"):
element.NumberOfRisers = props.number_of_treads + 1
element.NumberOfTreads = props.number_of_treads
element.RiserHeight = props.height / element.NumberOfRisers
element.TreadLength = props.tread_depth
element.PredefinedType = "STRAIGHT"
return {"FINISHED"}
class EnableEditingStair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_stair"
bl_label = "Enable Editing Stair"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMStairProperties
element = tool.Ifc.get_entity(obj)
pset = ifcopenshell.util.element.get_psets(element)
data = json.loads(pset["BBIM_Stair"]["Data"])
# required since we could load pset from .ifc and BIMStairProperties won't be set
for prop_name in data:
setattr(props, prop_name, data[prop_name])
props.is_editing = 1
class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_stair"
bl_label = "Remove Stair"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
obj.BIMStairProperties.is_editing = -1
pset = ifcopenshell.util.element.get_psets(element)
pset = tool.Ifc.get().by_id(pset["BBIM_Stair"]["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
return {"FINISHED"}
def add_object_button(self, context):
self.layout.operator(BIM_OT_add_object.bl_idname, icon="PLUGIN")
self.layout.operator(BIM_OT_add_clever_stair.bl_idname, icon="PLUGIN")
@@ -1,5 +1,5 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
@@ -17,10 +17,13 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.tool as tool
from bpy.types import Panel, Operator, Menu
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData
from blenderbim.bim.module.model.prop import store_cursor_position
from blenderbim.bim.helper import prop_with_search, close_operator_panel
from blenderbim.bim.module.model.stair import update_stair_modifier
from blenderbim.bim.helper import prop_with_search
class LaunchTypeManager(bpy.types.Operator):
@@ -37,7 +40,7 @@ class LaunchTypeManager(bpy.types.Operator):
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)
bpy.ops.bim.load_type_thumbnails(ifc_class=props.ifc_class, offset=0, limit=9)
if not AuthoringData.is_loaded:
AuthoringData.load()
return context.window_manager.invoke_popup(self, width=550)
@@ -46,7 +49,7 @@ class LaunchTypeManager(bpy.types.Operator):
props = context.scene.BIMModelProperties
row = self.layout.row()
row.prop(props, "type_class", text="")
prop_with_search(row, props, "type_class", text="")
columns = self.layout.column_flow(columns=3)
row = columns.row()
@@ -254,6 +257,117 @@ class HelpConstrTypes(Operator):
]
class BIM_PT_array(bpy.types.Panel):
bl_label = "IFC Array"
bl_idname = "BIM_PT_array"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "modifier"
@classmethod
def poll(cls, context):
return tool.Ifc.get() and tool.Ifc.get_entity(context.active_object)
def draw(self, context):
if not ArrayData.is_loaded:
ArrayData.load()
props = context.active_object.BIMArrayProperties
if ArrayData.data["parameters"]:
row = self.layout.row(align=True)
row.label(text=ArrayData.data["parameters"]["parent_name"], icon="CON_CHILDOF")
op = row.operator("bim.select_array_parent", icon="OBJECT_DATA", text="")
op.parent = ArrayData.data["parameters"]["Parent"]
if ArrayData.data["parameters"]["data"]:
row.operator("bim.add_array", icon="ADD", text="")
for i, array in enumerate(ArrayData.data["parameters"]["data"]):
box = self.layout.box()
if props.is_editing == i:
row = box.row(align=True)
row.prop(props, "count", icon="MOD_ARRAY")
row.operator("bim.edit_array", icon="CHECKMARK", text="").item = i
row.operator("bim.disable_editing_array", icon="CANCEL", text="")
row = box.row(align=True)
row.prop(props, "x")
row.prop(props, "y")
row.prop(props, "z")
else:
row = box.row(align=True)
row.label(text=f"{array['count']} Items", icon="MOD_ARRAY")
row.operator("bim.enable_editing_array", icon="GREASEPENCIL", text="").item = i
row.operator("bim.remove_array", icon="X", text="").item = i
row = box.row(align=True)
row.label(text=f"X: {array['x']}")
row.label(text=f"Y: {array['y']}")
row.label(text=f"Z: {array['z']}")
else:
row = self.layout.row()
row.label(text="No Array Found")
row.operator("bim.add_array", icon="ADD", text="")
class BIM_PT_stair(bpy.types.Panel):
bl_label = "IFC Stair"
bl_idname = "BIM_PT_stair"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "modifier"
@classmethod
def poll(cls, context):
# always display modifier if it's IFC object
return tool.Ifc.get() and tool.Ifc.get_entity(context.active_object)
def draw(self, context):
if not StairData.is_loaded:
StairData.load()
props = context.active_object.BIMStairProperties
if StairData.data["parameters"]:
row = self.layout.row(align=True)
row.label(text="Stair parameters", icon="IPO_CONSTANT")
stair_data = StairData.data["parameters"]["data"]
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish editing")
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
row = self.layout.row(align=True)
for prop_name in props.get_props_kwargs():
self.layout.prop(props, prop_name)
update_stair_modifier(context)
else:
row.operator("bim.enable_editing_stair", icon="GREASEPENCIL", text="")
row.operator("bim.remove_stair", icon="X", text="")
row = self.layout.row(align=True)
for prop in props.get_props_kwargs():
prop_value = stair_data[prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
row = self.layout.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
row.label(text=str(prop_value))
# calculated properties
number_of_rises = props.number_of_treads + 1
row = self.layout.row(align=True)
row.label(text="Number of risers")
row.label(text=str(number_of_rises))
row = self.layout.row(align=True)
row.label(text="Tread rise")
row.label(text=str(round(props.height / number_of_rises, 5)))
row = self.layout.row(align=True)
row.label(text="Length")
row.label(text=str(round(props.tread_run * number_of_rises, 5)))
else:
row = self.layout.row()
row.label(text="No Stair Found")
row.operator("bim.add_stair", icon="ADD", text="")
class BIM_MT_model(Menu):
bl_idname = "BIM_MT_model"
bl_label = "IFC Objects"
@@ -76,7 +76,7 @@ class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Join Wall"
bl_options = {"REGISTER", "UNDO"}
bl_description = """ Trim/Extend the selected walls to the last selected wall:
'T' mode: Trim/Extend to a selected wall or 3D target
'T' mode: Trim/Extend to a selected wall, slab, or 3D target
'L' mode: Butt join two selected walls
'V' mode: Mitre join two selected wall
'' (empty) mode: Unjoin selected walls
@@ -107,10 +107,18 @@ class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
if len(selected_objs) < 2:
return {"FINISHED"}
if self.join_type == "T":
for obj in selected_objs:
if obj == context.active_object:
continue
joiner.join_T(obj, context.active_object)
elements = [tool.Ifc.get_entity(o) for o in context.selected_objects]
targets = [e for e in elements if e and e.is_a() in ("IfcSlab", "IfcSlabStandardCase", "IfcRoof")]
if targets:
target = tool.Ifc.get_object(targets[0])
for element in elements:
if element.is_a("IfcWall"):
joiner.join_Z(tool.Ifc.get_object(element), target)
else:
for obj in selected_objs:
if obj == context.active_object:
continue
joiner.join_T(obj, context.active_object)
return {"FINISHED"}
@@ -234,8 +242,18 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
return
extrusion.Depth = self.depth
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
extrusion.Depth = self.depth * (1 / cos(x_angle))
if element.is_a("IfcWall"):
for rel in element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements":
ifcopenshell.api.run(
"geometry.disconnect_element",
tool.Ifc.get(),
relating_element=rel.RelatingElement,
related_element=element,
)
wall_objs.append(obj)
if wall_objs:
DumbWallRecalculator().recalculate(wall_objs)
@@ -256,6 +274,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
wall_objs = []
other_objs = []
x_angle = radians(self.x_angle)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
@@ -266,6 +285,10 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
return
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
existing_x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
perpendicular_depth = extrusion.Depth / (1 / cos(existing_x_angle))
extrusion.Depth = perpendicular_depth * (1 / cos(x_angle))
extrusion.ExtrudedDirection.DirectionRatios = (0.0, sin(x_angle), cos(x_angle))
if element.is_a("IfcWall"):
wall_objs.append(obj)
@@ -278,8 +301,12 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
is_global=True,
should_sync_changes_first=False,
)
rotation = mathutils.Matrix.Rotation(x_angle, 4, 'X')
obj.matrix_world = rotation
euler = obj.matrix_world.to_euler()
euler.x = x_angle
new_matrix = euler.to_matrix().to_4x4()
new_matrix.col[3] = obj.matrix_world.col[3]
obj.matrix_world = new_matrix
if wall_objs:
DumbWallRecalculator().recalculate(wall_objs)
return {"FINISHED"}
@@ -611,7 +638,7 @@ class DumbWallGenerator:
"geometry.add_wall_representation",
tool.Ifc.get(),
context=self.body_context,
thickness=self.layers["thickness"] * 1/cos(self.x_angle), #1/cos(self.x_angle) maintains the correct MaterialLayer thickness upon rotation
thickness=self.layers["thickness"],
offset=self.layers["offset"],
length=self.length,
height=self.height,
@@ -920,6 +947,20 @@ class DumbWallJoiner:
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
return wall2
def join_Z(self, wall1, slab2):
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(slab2)
ifcopenshell.api.run(
"geometry.connect_element",
tool.Ifc.get(),
relating_element=element2,
related_element=element1,
description="TOP",
)
self.recreate_wall(element1, wall1)
def join_L(self, wall1, wall2):
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(wall2)
@@ -1043,22 +1084,37 @@ class DumbWallJoiner:
layers = tool.Model.get_material_layer_parameters(element)
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
)
if rel.is_a("IfcRelConnectsPathElements"):
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
)
if rel.is_a("IfcRelConnectsPathElements"):
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,
)
previous_matrix = obj.matrix_world.copy()
previous_origin = previous_matrix.col[3].to_2d()
obj.matrix_world[0][3], obj.matrix_world[1][3] = self.body[0]
bpy.context.view_layer.update()
for rel in element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements":
height = self.clip(obj, tool.Ifc.get_object(rel.RelatingElement))
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:
@@ -1092,7 +1148,7 @@ class DumbWallJoiner:
height=height,
x_angle=x_angle,
offset=layers["offset"],
thickness=layers["thickness"] * 1/cos(x_angle), #1/cos(x_angle) maintains the correct MaterialLayer thickness upon rotation
thickness=layers["thickness"],
clippings=self.clippings,
booleans=tool.Model.get_manual_booleans(element),
)
@@ -1109,10 +1165,6 @@ class DumbWallJoiner:
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_body
)
previous_matrix = obj.matrix_world.copy()
previous_origin = previous_matrix.col[3].to_2d()
obj.matrix_world[0][3], obj.matrix_world[1][3] = 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 X axis of the wall
@@ -1152,16 +1204,17 @@ class DumbWallJoiner:
)
def get_extrusion_data(self, representation):
results = {"height": 3.0, "x_angle": 0, "is_sloped": False, "direction": Vector((0, 0, 1))}
results = {"item": None, "height": 3.0, "x_angle": 0, "is_sloped": False, "direction": Vector((0, 0, 1))}
item = representation.Items[0]
while True:
if item.is_a("IfcExtrudedAreaSolid"):
results["height"] = item.Depth * self.unit_scale
results["item"] = item
x, y, z = item.ExtrudedDirection.DirectionRatios
if not tool.Cad.is_x(x, 0) or not tool.Cad.is_x(y, 0) or not tool.Cad.is_x(z, 1):
results["direction"] = Vector(item.ExtrudedDirection.DirectionRatios)
results["x_angle"] = Vector((0, 1)).angle_signed(Vector((y, z)))
results["is_sloped"] = True
results["height"] = (item.Depth * self.unit_scale) / (1 / cos(results["x_angle"]))
break
elif item.is_a("IfcBooleanClippingResult"):
item = item.FirstOperand
@@ -1215,7 +1268,8 @@ class DumbWallJoiner:
bp2 = wall2.matrix_world @ Vector(wall2.bound_box[0])
tp1 = wall1.matrix_world @ Vector(wall1.bound_box[1])
# Axis lines on bottom, for base and side axes
# Axis lines on bottom, for reference, base, and side axes
bra1 = (Vector((*axis1["reference"][0], bp1[2])), Vector((*axis1["reference"][1], bp1[2])))
bba1 = (Vector((*axis1["base"][0], bp1[2])), Vector((*axis1["base"][1], bp1[2])))
bsa1 = (Vector((*axis1["side"][0], bp1[2])), Vector((*axis1["side"][1], bp1[2])))
bba2 = (Vector((*axis2["base"][0], bp2[2])), Vector((*axis2["base"][1], bp2[2])))
@@ -1260,7 +1314,7 @@ class DumbWallJoiner:
new_body = tool.Cad.furthest_vector(bba1[i], (bbf, bsf_)).copy()
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tbf_)).copy()
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tsf_)).copy()
self.body[j] = new_body.to_2d()
self.body[j] = tool.Cad.point_on_edge(new_body, bra1).to_2d()
if connection1 == connection2:
if (connection1 == "ATEND" and angle > 0) or (connection1 != "ATEND" and angle < 0):
@@ -1301,9 +1355,9 @@ class DumbWallJoiner:
and not extrusion2["is_sloped"]
):
if is_relating:
self.body[j] = bbf.to_2d()
self.body[j] = tool.Cad.point_on_edge(bbf, bra1).to_2d()
else:
self.body[j] = bbn.to_2d()
self.body[j] = tool.Cad.point_on_edge(bbn, bra1).to_2d()
return True
bsf_ = tool.Cad.point_on_edge(bsf, bba1)
@@ -1312,7 +1366,7 @@ class DumbWallJoiner:
new_body = tool.Cad.furthest_vector(bba1[i], (bbf, bsf_)).copy()
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tbf_)).copy()
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tsf_)).copy()
self.body[j] = new_body.to_2d()
self.body[j] = tool.Cad.point_on_edge(new_body, bra1).to_2d()
if is_relating:
pt = bbf.to_2d().to_3d()
@@ -1336,3 +1390,41 @@ class DumbWallJoiner:
)
return True
def clip(self, wall1, slab2):
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(slab2)
layers1 = tool.Model.get_material_layer_parameters(element1)
axis1 = tool.Model.get_wall_axis(wall1, layers1)
bases = [axis1["base"][0].to_3d(), axis1["base"][1].to_3d(), axis1["side"][0].to_3d(), axis1["side"][1].to_3d()]
extrusion = self.get_extrusion_data(tool.Ifc.get().by_id(wall1.data.BIMMeshProperties.ifc_definition_id))
d = wall1.matrix_world.to_quaternion() @ extrusion["direction"]
slab_pt = slab2.matrix_world @ Vector((0, 0, 0))
slab_dir = slab2.matrix_world.to_quaternion() @ Vector((0, 0, -1))
tops = [mathutils.geometry.intersect_line_plane(b, b + d, slab_pt, slab_dir) for b in bases]
i_bottom = None
i_top = None
for i, co in enumerate(tops):
if i_top is None or co[2] > i_top[2]:
i_top = co
i_bottom = bases[i]
quaternion = slab2.matrix_world.to_quaternion()
x_axis = quaternion @ Vector((1, 0, 0))
y_axis = quaternion @ Vector((0, 1, 0))
z_axis = quaternion @ Vector((0, 0, 1))
self.clippings.append(
{
"type": "IfcBooleanClippingResult",
"operand_type": "IfcHalfSpaceSolid",
"matrix": self.create_matrix(i_top, x_axis, y_axis, z_axis),
}
)
return (i_top - i_bottom).length
@@ -413,7 +413,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.align_product(align_type="CENTERLINE")
def hotkey_S_E(self):
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
if not bpy.context.selected_objects:
return
elif self.active_class in ("IfcWall", "IfcWallStandardCase"):
bpy.ops.bim.join_wall(join_type="T")
elif self.active_class in ("IfcSlab", "IfcSlabStandardCase", "IfcRamp", "IfcRoof"):
if not bpy.context.active_object:
@@ -17,6 +17,8 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from ifcopenshell.util.doc import get_entity_doc
import blenderbim.tool as tool
from blenderbim.bim.prop import StrProperty, Attribute
from blenderbim.bim.module.owner.data import OwnerData, ActorData, ObjectActorData
from bpy.types import PropertyGroup
@@ -64,6 +66,23 @@ def update_actor_class(self, context):
ActorData.data["actors"] = ActorData.actors()
def get_actor_class_enum(self, context):
version = tool.Ifc.get_schema()
return [
("IfcActor", "Actor", get_entity_doc(version, "IfcActor").get("description", "")),
("IfcOccupant", "Occupant", get_entity_doc(version, "IfcOccupant").get("description", "")),
]
def get_actor_type_enum(self, context):
version = tool.Ifc.get_schema()
return [
("IfcPerson", "Person", get_entity_doc(version, "IfcPerson").get("description", "")),
("IfcOrganization", "Organisation", get_entity_doc(version, "IfcOrganization").get("description", "")),
("IfcPersonAndOrganization", "User", get_entity_doc(version, "IfcPersonAndOrganization").get("description", "")),
]
class BIMOwnerProperties(PropertyGroup):
active_person_id: IntProperty(name="Active Person Id")
person_attributes: CollectionProperty(name="Person Attributes", type=Attribute)
@@ -81,24 +100,28 @@ class BIMOwnerProperties(PropertyGroup):
facsimile_numbers: CollectionProperty(type=StrProperty, name="Facsimile Numbers")
electronic_mail_addresses: CollectionProperty(type=StrProperty, name="Emails")
messaging_ids: CollectionProperty(type=StrProperty, name="IMs")
user_person: EnumProperty(items=get_user_person, name="Person")
user_organisation: EnumProperty(items=get_user_organisation, name="Organisation")
user_person: EnumProperty(
items=get_user_person, name="Person", description="This entity represents an individual human being."
)
user_organisation: EnumProperty(
items=get_user_organisation,
name="Organisation",
description="A named and structured grouping with a corporate identity.",
)
active_user_id: IntProperty(name="Active User Id")
active_actor_id: IntProperty(name="Active Actor Id")
actor_attributes: CollectionProperty(name="Actor Attributes", type=Attribute)
actor_class: EnumProperty(
items=(("IfcActor", "Actor", ""), ("IfcOccupant", "Occupant", "")),
items=get_actor_class_enum,
name="Actor Type",
update=update_actor_class,
)
actor_type: EnumProperty(
items=(
("IfcPerson", "Person", ""),
("IfcOrganization", "Organisation", ""),
("IfcPersonAndOrganization", "User", ""),
),
items=get_actor_type_enum,
name="Actor Type",
update=update_actor_type,
)
the_actor: EnumProperty(items=get_the_actor, name="Actor")
the_actor: EnumProperty(
items=get_the_actor, name="Actor", description="This entity represents an individual human being."
)
actor: EnumProperty(items=get_actor, name="Actor")

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