Compare commits

..

217 Commits

Author SHA1 Message Date
Ryan Schultz 6247fb3cdf keep font scaled correctly when changing drawing width and height 2024-06-01 15:41:46 -05:00
Ryan Schultz e2dc5f4a48 small tweak 2024-06-01 08:19:41 -05:00
Manu Varkey d28f62d327 Bug fix in editing material set item profile (#4784)
Fixes bug: Editing a profile under MaterialProfileSetUsage opened dialog boxes under all profiles.
2024-06-01 20:58:07 +10:00
Dion Moult 70064359c3 Whoops x2 2024-06-01 20:52:05 +10:00
Dion Moult d8b74cf125 Whoops 2024-06-01 20:30:32 +10:00
Dion Moult 00c13c5ea4 Fix #4756. You can now use parent as a key in a selector key. 2024-06-01 17:30:21 +10:00
Dion Moult e26dae67aa Convenience function for calculators to get maximum XY length
This is useful for 2D profiles
2024-06-01 15:11:29 +10:00
Dion Moult 6e30cd2ba7 Fix #4638. You can now filter by parent (in the spatial hierarchy) 2024-06-01 15:10:45 +10:00
Dion Moult 0f9ea1e1e7 New utility functions to get parent in spatial hierarchy, filled void, and voided element 2024-06-01 15:09:05 +10:00
Dion Moult 128ffc8fd7 Fix bug where selector didn't handle "not equals" correctly for locations or groups 2024-06-01 14:53:55 +10:00
Dion Moult f67911902f Remove obsolete config option for openlca port 2024-06-01 12:29:17 +10:00
Dion Moult 64a1a3eb29 Migrate PyP6Xer dep to install using pip 2024-06-01 12:03:41 +10:00
Dion Moult fcf70ad160 Attempt to bump support to intel mac 10.10 2024-06-01 10:48:36 +10:00
Dion Moult 116c9a5fd9 Continue migrating build deps to pip: pystache, lark, svgwrite, pillow, dateutil, isodate, networkx, deepdiff 2024-06-01 10:38:04 +10:00
Dion Moult 61b10a19c7 Attempt to make shapely be installed using pip 2024-06-01 10:21:40 +10:00
Andrej730 666f8e2459 Bump BBIM ifcopenshell build #4580 2024-05-31 18:28:52 +05:00
Andrej730 c303f216ad Migrate recipe - do not call .migrate twice for each element 2024-05-31 18:20:47 +05:00
Andrej730 162e66b775 typing 2024-05-31 18:20:46 +05:00
Andrej730 c05abc4d5b ifc4x3 support for adding IfcQuantityCount #4776
In IFC4X3 IfcQuantityCount is now more strict and requires only interger values.
Error for a reference:
TypeError: attribute 'CountValue' for entity 'IFC4X3.IfcQuantityCount' is expecting value of type 'INT', got 'float'.
2024-05-31 18:20:46 +05:00
Andrej730 dacb3b101c small docs fix 2024-05-31 18:20:46 +05:00
Andrej730 696c6b09b0 materials ui - preserve selected category on expanding/contracting all categories 2024-05-31 18:20:46 +05:00
Andrej730 6e738a0e3a materials ui - fix bug when it was impossible to contract category
E.g. it was impossible to contract category "TEST" if there was also a material with name "TEST" (and it's category was expanded). It occurred due to a mixup of materials and categories in expanded_categories.

Also changed is_expanded default value to False as it makes more sense (previous value didn't worked for materials and for categories you probably would want it to be False by default.
2024-05-31 18:20:46 +05:00
Andrej730 ba6cdc967c materials ui - shift+click to expand/contract all categories #4771 2024-05-31 18:20:46 +05:00
Andrej730 468baef5c8 materials ui - make expanding/contracting categories more natural
Previously if you had some item active before the category you're trying to expand/cotract it would jump back to that item which was confusing. Now it sets contracted/expanded category as active to prevent that jump.

Before - https://imgur.com/a/87ZDnVq
After - https://imgur.com/a/B9WOn8I

In Blender when we reload materials in collection property it's resetting scroll position in the template_list and setting it based template_list index, so the only way to preserve the scroll position is to manipulate template_list index.
2024-05-31 18:20:46 +05:00
Andrej730 bda9dfd4cc materials ui - avoid having multiple "Uncategorised" categories
Previously it would create multiple same named "Uncategorised" categories for each case when Category was "", None, "Uncategorised", which was confusing and some of them ("" and None) would expand simultaneously when you would try to expand one.

Removed `or "Uncategorised"` from ui.py as there shouldn't be a need for this since all cases are handled when items are added.
2024-05-31 18:20:46 +05:00
Dion Moult 930feb1d52 See #4768. Attempt to use pip for lxml packaging. 2024-05-31 23:06:12 +10:00
Dion Moult e0b11aa66e Fix #4434. User can now explicitly select whether they are choosing a whole or part when changing the aggregate. 2024-05-31 21:38:51 +10:00
Dion Moult b2e7044373 Fix bug where you couldn't change the aggregate of a spatial element (which has been locked during import) 2024-05-31 21:17:51 +10:00
Dion Moult 77abe9a0a5 [BBIM docs] change color of ui breadcrumb on dark background #4777 by @dirkolbrich
Sorry I messed up this merge and did a force push somehow
2024-05-31 21:15:33 +10:00
Dion Moult e2c67c2f37 Revert "fixes #4434"
This reverts commit cdd332d1ee.
2024-05-31 21:11:37 +10:00
Dion Moult a5d123fe83 Map a bunch more qto functions for the IfcOpenShell calculator 2024-05-31 13:09:05 +10:00
Dion Moult b7b99b6db7 Fix #4773. Bug where two materials that shared a style would cause the Blender <-> IFC link to break 2024-05-31 12:54:19 +10:00
Dion Moult 2be0cc697a Fix #4768. Support Python 3.12 version of BBIM. 2024-05-31 11:30:26 +10:00
Dion Moult dc92b973a0 Minor fix 2024-05-31 10:15:03 +10:00
Dion Moult 5466be9b7b Fix #4765. Only run Blender calculator on meshes. 2024-05-31 10:09:56 +10:00
Andrej730 b86dab2a76 fix removing aggregate's collections (and possible orphaned ifc data)
Probably have met this issue before - https://i.imgur.com/EOCSGg2.png

It occurred when you would remove e.g. IfcBuildingStorey's (or other aggregate's) collection from outliner. Deletion operator removes objects first and removing aggregate's main object is automatically removing it's collection. Then it would start removing collections and will break meeting an invalid collection.

Most of the times it was scary but harmless since all collections are probably removed either way but it's critical for batch removal as it would be never finished, possibly leaving unlinked ifc data (e.g. representations and it's items) that's not removed completely until batch removal is finalized.

Traceback:
Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\ifc.py", line 360, in execute_ifc_operator
    result = getattr(operator, "_execute")(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\blenderbim\bim\module\geometry\operator.py", line 720, in _execute
    bpy.data.collections.remove(collection)
ReferenceError: StructRNA of type Collection has been removed
2024-05-30 17:40:13 +05:00
Andrej730 a57e063c14 convert_to_blender to also unlink blender material from ifc material 2024-05-30 17:38:14 +05:00
Gorgious c7830e9e5b Using the "Select IFC class" button doesn't throw an error anymore if an object candidate is in an excluded collection
+ use walrus operator in a few instances
2024-05-29 16:23:05 +02:00
Gorgious d533ba1e6b Remove dead code going back to a time immemorial hack 2024-05-29 16:11:35 +02:00
Andrej730 45a1655485 typing 2024-05-29 18:00:44 +05:00
Andrej730 3679fccfbd reload_representation to support multple objects and optimize the process 2024-05-29 17:20:34 +05:00
Andrej730 cf9f792a3b fix issue unlinking objects after d219856
`unlink_style` doesn't need `tool.Style` anymore and `obj` argument was replaced with `style`
2024-05-29 17:20:34 +05:00
Dion Moult d87665de7e Minor fix 2024-05-29 22:03:22 +10:00
Dion Moult 95daf7efa3 Minor fix to expose both net and gross variants of IfcOpenShell Qto calculator 2024-05-29 21:58:02 +10:00
Dion Moult 435ce8d58a New qto tool is now unit aware 2024-05-29 21:46:23 +10:00
Dion Moult f92a2b3c8e Shift-Q QTO shortcut now works again 2024-05-29 19:37:35 +10:00
Dion Moult 5ea1c94822 Expose all previous calculation functions in new Qto UI 2024-05-29 19:29:48 +10:00
Andrej730 ab1bf7592e fix error exporting schedule of rates #4723
Issue occurred when cost item would have no quantities but had controlled objects with quantities.

1) just added a check to ensure that cost_item.CostQuantities are present
2) removed possible None return value in get_cost_item_quantity as it actually will break process_cost_data and it's never used anywhere else
3) has_changed_name was unused
4) fix possible similar issue in cost.data (though _get_object_quantities method is unused)
2024-05-29 11:49:27 +05:00
Dion Moult f4247c3527 Continue purging tools that were specific to base qtos
Base qtos shouldn't be too special, it should be based off a calculation mapping. This means we can also support IFC2X3 better (which has no base qtos)
2024-05-29 15:56:50 +10:00
Dion Moult 8c3d10a2bd Reimplement manual qty calculator using autodetected calculator functions and redo qto UI 2024-05-29 15:14:11 +10:00
Dion Moult 1bda47f76e Migrate calculator to qto module and mapper to Ifc5D. 2024-05-29 12:45:09 +10:00
Dion Moult 9001aa6383 Purge calculate quantities operator - to be replaced with new ifc5d qto tool
The calculate quantities operator assumes a single mapper, not a configurable calculator.
2024-05-29 12:39:05 +10:00
Ryan Schultz 097c5cee3c fix #3802 - Copy array attributes from another array. Big thank you to @brunoperdigao, for the help! 2024-05-28 21:17:23 -05:00
Dion Moult 9d789425ce Purge guess quantity feature. It will be reintroduced as a separate tool. 2024-05-29 11:19:33 +10:00
Dion Moult c0b86ea016 Fix incorrect colours in Basic FM template 2024-05-29 08:51:27 +10:00
Dion Moult 6806eb0a05 Create blank ifc4 qto base quantities ruleset 2024-05-29 08:41:43 +10:00
Andrej730 ce26d931c1 regenerate ifc4x3 pset templates after the release
Change log

Different props in pset 'Pset_BuildingCommon':
- new props: ElevationOfTerrain, ElevationOfRefHeight
New template 'Pset_DoorLiningProperties' with props: LiningDepth, LiningThickness, ThresholdDepth, ThresholdThickness, TransomThickness, TransomOffset, LiningOffset, ThresholdOffset, CasingThickness, CasingDepth, LiningToPanelOffsetX, LiningToPanelOffsetY
New template 'Pset_DoorPanelProperties' with props: PanelDepth, PanelOperation, PanelWidth, PanelPosition
New template 'Pset_PermeableCoveringProperties' with props: OperationType, PanelPosition, FrameDepth, FrameThickness
Different props in pset 'Pset_Stationing':
- new props: HasIncreasingStation
New template 'Pset_WindowLiningProperties' with props: LiningDepth, LiningThickness, TransomThickness, MullionThickness, FirstTransomOffset, SecondTransomOffset, FirstMullionOffset, SecondMullionOffset, LiningOffset, LiningToPanelOffsetX, LiningToPanelOffsetY
New template 'Pset_WindowPanelProperties' with props: OperationType, PanelPosition, FrameDepth, FrameThickness
2024-05-28 18:01:33 +05:00
Andrej730 eeaa0a5530 Fill IfcPropertySetTemplate.TemplateType in ifc2x3 pset templates 2024-05-28 18:00:22 +05:00
Andrej730 a09cb5dee4 typing 2024-05-28 18:00:21 +05:00
Andrej730 6035683492 sync get_base_qto after 70a4af2 2024-05-28 12:59:34 +05:00
Andrej730 f27dbacd28 prioritize qto base psets over other qto_ qsets after 70a4af2 #4740
1) e.g. in ifc4x3 previously for IfcWall it would prioritize Qto_BodyGeometryValidation over Qto_WallBaseQuantities

2) It would also prioritize general Qto_BodyGeometryValidation over Qto_BuildingElementProxyQuantities.

3) removed "Qto_" check as we do qto_only in get_applicable_names
2024-05-28 12:59:34 +05:00
Andrej730 8817a7e6d5 qtocalculator to support backported EQto_BodyGeometryValidation 2024-05-28 12:59:34 +05:00
Dion Moult 80c4c13998 Basic prototype of new Ifc5D QTO tool in Blender. 2024-05-28 17:33:58 +10:00
Dion Moult 5159e2daee Initial attempt at a qto module in Ifc5D
This is a more flexible, non-Blender approach to doing QTO which allows:

 - Doing QTO without Blender
 - Doing QTO using query filters
 - Using config files for QTOs
 - Choosing your calculation engine, either built-in or creating your own
2024-05-28 17:32:51 +10:00
Dion Moult 54951d2102 Add convenience max/min functions for xyz and side_area shape calculations 2024-05-28 17:30:01 +10:00
Dion Moult 8403c32e76 Adjust selector's behaviour to use the elements argument as a filtering subset
This was always the original intention, but never worked as designed. This is so that you don't need to filter the entire file (which can be huge).
2024-05-28 17:30:01 +10:00
ppaawweeuu 316934de97 Update selector_syntax.rst - small formating change (#4752)
typing
2024-05-28 09:43:51 +10:00
Andrej730 dd48fbcb0f remove dead code core.sequence.highlight_product_related_task
It's deprecated after UI changes in de3dfcd
2024-05-27 17:58:45 +05:00
Andrej730 54978a9868 bim.load_product_related_tasks - show info message when finished
to make ui a bit more responsive
2024-05-27 17:58:45 +05:00
Andrej730 02fd78df14 datetime2ifc to support None values as it was before ebd03e9 #4734 2024-05-27 17:34:28 +05:00
Andrej730 ed1202e1f2 limit quantity types for each resource based on IFC documentation
documentation - https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcConstructionResource.htm#Table-7.3.3.7.1.3.H

e.g. - https://i.imgur.com/PklMyfq.png (previously all quantity types were available for all resources)
2024-05-27 17:30:51 +05:00
Andrej730 d9de967197 specify editing_resource_type values for clarity 2024-05-27 17:30:51 +05:00
Dirk Olbrich e57076c84f [BBIM] Add preference option for default startup workspace, remove ambiguous wording (#4749)
* [BlenderBIM] change ambiguous wording from preferences

* [BlenderBIM] add option for BIM as default workspace

* [BlenderBIM] revert variable name change
2024-05-27 22:19:02 +10:00
Andrej730 75e6495999 load and link ifc files by drag'n'drop 🥳
Example - https://imgur.com/a/kIiqk3R

Now you can drag'n'drop files to Blender and they will get loaded as projects (currently there is no safe check whether current project is saved) or you can hold ALT while drag'n'dropping and link single or multiple IFC files.

Actually, it's possible to set it up that way so multiple files won't need ALT modifier (as we can't load multiple projects, only link them) but I've kept ALT modifier for that case too to avoid confusing situations.

Blender 4.1+ only as there wasn't bpy.types.FileHandler before.
2024-05-27 16:16:57 +05:00
Dion Moult fcf07bebfb Minor doc fix for IfcPatch ConvertLengthUnit
Helps indicate how to get the patched model - it's not an inplace patch
2024-05-27 17:30:17 +10:00
Dion Moult f190ddcd92 Move all N panels back into the scene tabs. No more panels spread around!
Qto, 4D tools, 5D tools now in the Costing/Scheduling tab. The misc panel is now in the sandbox. Git history panel is now with the main Git panel.
2024-05-27 17:29:51 +10:00
Dion Moult 39a841fb4e Fix #3174. Toggling edit mode no longer pops up annoying "do you want to save" dialog.
The default is now everything is saved which should be 90% of the usecase. If you want to discard changes, there is now a button in the top right next to the IFC mode.
2024-05-27 13:09:47 +10:00
Dion Moult bdfd0c4aa7 Minor fix to IfcCityJSON and migrate IfcCityJSON docs to sphinx 2024-05-27 12:24:55 +10:00
Dion Moult 37b14f2a3d Document difference between COBie 2.4 and COBie 2.4 Legacy in IfcFM 2024-05-27 11:59:25 +10:00
Dion Moult 70a4af2605 Fix #4740. Bug where building element proxies didn't work for the auto calculate all button. 2024-05-27 11:02:23 +10:00
Dion Moult 7142f3856c Add documentation for some unit utilities 2024-05-27 09:42:28 +10:00
Ryan Schultz f2ca030fab fix #4311: turn off local view when activating drawing 2024-05-26 18:27:44 -05:00
Dion Moult 37cacfcd06 Fix #4728. Bug where changing class doesn't work with IfcCSV if it didn't need to change anything. 2024-05-26 21:57:23 +10:00
ppaawweeuu 3e31309384 Update selector_syntax.rst - links to regex resources (#4741)
* Update selector_syntax.rst

add links to regex resources

* Update selector_syntax.rst

links misspelling change

* Update selector_syntax.rst

one misspealling more

* Update selector_syntax.rst

small misspelling in thousands separator location
2024-05-26 20:14:41 +10:00
Dion Moult a19b7614fa Fix #4737. Bug where loading profiles with a non-mesh selected would lead to trouble. 2024-05-26 20:12:12 +10:00
Dion Moult 763ee9198d See #3847. Preliminary support for including linked models when generating drawings. 2024-05-26 18:29:26 +10:00
Dion Moult 9ec8035a8d Add support for manual classification references for materials 2024-05-26 17:10:27 +10:00
Dion Moult 687bb32e37 Move material classification into scene tab. No more panels in the material tab! 2024-05-26 16:33:53 +10:00
Sigma Dimensions (Yass) 4279b769aa fix bcf viewpoint setup 2024-05-26 03:44:35 +01:00
Dion Moult 9a59b8cdae Material psets now include category-specific psets and docstrings
Previously it was not possible to get concrete / steel material psets. Yikes!
2024-05-26 11:23:08 +10:00
Dion Moult b9b0e81d25 Move object materials panels further up and profile panel down
Object materials panel is much more important and shouldn't be below the material manager.
2024-05-26 10:04:17 +10:00
Dion Moult 80d31b4e1a Move material psets to material manager. 2024-05-26 09:59:01 +10:00
Ryan Schultz dac9bfe3d4 Add backticks to copied error message 2024-05-25 10:37:52 -05:00
Ryan Schultz 58a7ef62d7 fix #4729: add a description and category when creating a new material 2024-05-25 09:49:15 -05:00
Dion Moult d2198568b2 Deprecate old style panel in the material tab and port over "unlink" operator.
All the functionality is now possible with the style manager and prevents the user needing to know how Blender materials work or wrangle nodes themselves and magically have to comply with the glTF spec.
2024-05-25 23:30:18 +10:00
Dion Moult b087aa09d9 Deprecate old material panel in the material tab and reimplement material duplication in the material manager
The material duplication now 1) actually creates a new blender material 2) uses the API to duplicate. One less panel outside our scene tab.
2024-05-25 15:15:12 +10:00
Dion Moult fd62ba63a5 Do not copy presentation styles when copying a material
Ideally, presentation styles are reused. For example, 5 types of concrete can all use the same presentation
2024-05-25 15:14:07 +10:00
Dion Moult 16c5fd56ca You can now copy a material list 2024-05-25 15:13:36 +10:00
Dion Moult d85f9af790 The API can now copy material sets too 2024-05-25 14:42:45 +10:00
Dion Moult 2f071bd809 Material total now shows the total of the select material class, not all material classes
This is more meaningful. Often the presence of sets will inflate the number artificially.
2024-05-25 14:11:48 +10:00
Andrej730 7a44f9e8a7 different columns for different cost schedules #4724
though columns are still stored in .blend file and not in .ifc

Example - https://imgur.com/a/N6epGZ4
2024-05-24 19:15:51 +05:00
Andrej730 59a19a1a70 disable adding cost schedule columns with an empty name
example - https://i.imgur.com/5lGdI2X.png
2024-05-24 19:15:51 +05:00
Andrej730 6a8b59f2cf remove unused code
1) core.enable_editing_task was outdated, tool.enable_editing_task doesn't exist
2) tool.get_task_outputs was duplicated
2024-05-24 19:15:51 +05:00
Andrej730 ec28d3d0af black format 2024-05-24 19:15:51 +05:00
Andrej730 f576aadee1 typing 2024-05-24 19:15:51 +05:00
Andrej730 fffd7360cb fix for d4ccadb
forgot to add it to global namespace
2024-05-24 19:15:43 +05:00
Andrej730 64ea455c01 move update_simple_openings to tool.Model 2024-05-24 16:52:33 +05:00
Andrej730 cf635986a6 same openings generator for non-/parametric doors/windows #4710 2024-05-24 16:51:37 +05:00
Andrej730 ac8040008a small refactor 2024-05-24 16:51:37 +05:00
Andrej730 df15a37127 fix core.root.assign_class error after 3884403 #4719 2024-05-24 15:22:55 +05:00
Andrej730 6d887d593e fix issues running core tests after 37c008487 2024-05-24 15:20:12 +05:00
Andrej730 fa87092fe0 Keep 1.2m minimum opening thickness instead of using wall thickness
That way there will be a workaround if the wall is 0.6m+ meters thick but won't have an issue when you first added a window to a thin wall and it's opening later won't work for the thicker walls.
Mentioned in #4710
2024-05-24 12:10:40 +05:00
Andrej730 1a4e4678d0 assign/unassign resource operators tooltips
also removed couple unused properties
2024-05-23 18:45:31 +05:00
Andrej730 7f2c7eb467 python 3.9 compatibility 2024-05-23 18:45:31 +05:00
Andrej730 c70745826a rename bim.export_ifc to bim.save_project (similar to bim.load_project)
note that old name "export_ifc.bim" is still available (cacb38a25) though deprecated
2024-05-23 18:45:30 +05:00
Andrej730 291bffca95 add tooltip for cost schedule columns
example - https://i.imgur.com/Uh8D7Vg.png
2024-05-23 18:45:30 +05:00
Andrej730 e5a316e509 fix api calls error handling
by accident replaced in f2696d5 TypeError with NotImplementedError
2024-05-23 18:45:30 +05:00
Andrej730 e4aa97091e fix bug removing cost schedule columns
it was using _execute though it's not an ifc operator
2024-05-23 18:45:30 +05:00
Andrej730 b4f0a346c1 fix couple broken properties definitions
There was no errors but Blender seems to ignore the provided name if it was provided as a positional argument instead of a keyword argument.
2024-05-23 18:45:30 +05:00
Andrej730 073e471305 typing 2024-05-23 18:45:30 +05:00
Andrej730 4d9f47b3a7 make it more clear that bim.export_cost_schedules is expecting a folder
1) add folder filter
2) remove filepath property - that way only directories will be selectable in the file dialog.
2024-05-23 18:31:59 +05:00
Andrej730 96088c1d08 fix ui error in 40e4949
bim.add_manual_classification_reference operator doesn't have a "type" property, only "obj_type"
2024-05-23 18:31:59 +05:00
Dion Moult c405ff1adf Fix unassign_representation to not incorrectly use remove_deep without first removing known inverses.
In theory, it would be possible for the representation map to be used elsewhere, then file.remove(representation_map) would cause problems.
2024-05-23 23:08:53 +10:00
Dion Moult 02cae9c7c2 Continue to revise optimisation of remove_deep to fix failing tests after a8729933 2024-05-23 22:54:19 +10:00
Dion Moult 3c42962635 Fix #4716. Fix bug where IfcTester fails to verify negative numbers. 2024-05-23 21:40:19 +10:00
Dion Moult 0336dbabf7 Fix #4722. Things like type elements are not allowed to have openings applied to them. 2024-05-23 20:41:15 +10:00
Dion Moult a8729933af Optimise remove_deep, which can make removing lots of elements 100x faster in scripts. 2024-05-23 20:15:09 +10:00
Andrej730 993ce46fcc improve ux for assigning cost item to product types
1) also consider active object as selected when assigning / unassigning cost item to product types. Typically types are hidden and if you select some type in outliner it will become active but still not selected. Now it will be possible to add this active object without unhiding the entire Types collection.

2) info messages to make UI more responsive
2024-05-23 12:13:19 +05:00
Dion Moult edfd446833 Allow custom qtos without a template 2024-05-23 15:17:08 +10:00
Dion Moult 87c184ef8a Easily add custom psets and props without needing to create a template first
This is not best practice but is useful for quick and dirty properties.
2024-05-23 13:28:59 +10:00
Andrej730 e9b9113df8 small fix for 1aaccba80 2024-05-22 21:41:07 +05:00
Thomas Krijnen f772a53dd5 Update ci.yml 2024-05-22 17:29:15 +02:00
Thomas Krijnen 85c6b6c4da Update ci.yml 2024-05-22 15:58:34 +02:00
Andrej730 1aaccba803 simpler way to check if enum property is valid
Well, it's simpler to use but not really simpler in implementation 😅
Thanks to @Gorgious56 for helping out
Hopefully, it's a farewell to those annoying warnings

see https://blenderartists.org/t/best-way-to-handle-dynamic-enum-items-without-pyrna-enum-to-py-current-value-matches-no-enum
2024-05-22 18:44:12 +05:00
Andrej730 f2eb08a066 typing 2024-05-22 17:43:05 +05:00
Andrej730 f460676e54 fix calculating cost summary #4704 (was confusing 0 qty and no qty)
similar to 295d0a677
2024-05-22 17:37:32 +05:00
Andrej730 382e028b90 csv to cost schedule - support more cases of 0 quantity #4704
Before - https://i.imgur.com/CYwSGxY.png
After - https://i.imgur.com/Wbe6Ij6.png
I'll attach example .csv and .ifc in #4704

What changed:

1) If query was provided but it didn't found any elements, then it will still autoassign quantity = 0 instead of cost item end up without quantities at all (which has a different meaning in ifc). Works both with Property provided and without it.

2) If provided quantity = 0, it will now load as quantity = 0 instead of not creating any quantities at all.

3) You can provide both Query and Quantity and they all will be added to the cost item. E.g. if Quantity = 15, Query = "IfcWall", Property="Prop" and there are 3 walls in the model each having Prop = 25 then final quantity will be 15+25*3=90. Previously Quantity would take the priority and the result would be just = 15.

Though this is still doesn't work with counting quantities due behaviour in cost.assign_cost_item_quantity.
E.g. if Quantity = 7, Query = "IfcWall", Property="" (to make sure it will just count them) and there are 3 walls in the model then final quantity will be not 7+3=10 but just = 3, as query will take the priority here.
2024-05-22 17:15:42 +05:00
Thomas Krijnen 53d02446bf Update ci.yml 2024-05-22 13:13:18 +02:00
Dion Moult 5a82d7a9d4 Refactor enabling pset editing operator into core/tool 2024-05-22 20:52:43 +10:00
Dion Moult 69c197c1a4 get_psets utility now defaults to merging duplicate psets instead of overriding
This behaviour is more comprehensive for broken data and now consistent for types and materials.
2024-05-22 08:12:38 +10:00
Ryan Schultz 4027f28c85 Can bim.select_similar from the pset or attribute panels (#4611) 2024-05-21 10:30:38 -05:00
Bruno Perdigão 3a1154c4ba Merge pull request #4597 from IfcOpenShell/linked_aggregate_operator_from_panel
when bpy.ops.bim.object_duplicate_move_linked_aggregate is called fro…
2024-05-21 10:45:49 -03:00
Dion Moult aeb1e4bf49 See #4696. Allow editing of properties that do not comply with the pset template definition.
Strictly speaking, the only properties that should exist in a pset should be those in a pset template. However, there are situations where this is not the case, such as when the pset template has since been modified, or if there is invalid data coming from other software, or migrating between schemas. In this case, we should still load the property, and give the user the option to edit it (or null it).
2024-05-21 23:06:15 +10:00
Andrej730 4fb34c0816 base autogenerated opening thickness on wall thickness #4710 2024-05-21 17:54:51 +05:00
Andrej730 9b562669d2 fix root.create_entity skipping setting up defaults on ifc4x3 2024-05-21 17:54:08 +05:00
Andrej730 295d0a677e cost schedule - prohibit adding multiple quantity types on 1 cost item
All IfcCostItem.CostQuantities supposed to use only 1 type of quantity, so they can be added up.

Changed UI to reflect that requirement - dropdown for selecting quantity type is now only visible if cost item has no quantities, otherwise it just reuses already used quantity type.

See https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcCostItem.htm

Before - https://i.imgur.com/gFlwHEk.png
After - https://i.imgur.com/pk3wXfe.png
2024-05-21 17:54:07 +05:00
Andrej730 9aacafd575 fix bug with cost schedules confusing 0 quantity with no quantities
a bit related to #4704

1) fixed util.cost.get_total_quantity
2) fixed similar issue in cost.data that calculates the final value that user will see in UI
3) changed UI, "-" is shown when there are no quantities and "0" is when quantities are there but they just equal to zero.

Before - https://i.imgur.com/EO53DhM.png
After - https://i.imgur.com/H6rK4sP.png

fyi @myoualid
2024-05-21 17:54:07 +05:00
Andrej730 ce6ce1b975 bim.assign_cost_item_type name change 2024-05-21 17:54:07 +05:00
Andrej730 d064ef1237 black format 2024-05-21 17:53:46 +05:00
Andrej730 2db9bd9c39 fix errors adding occurrences from type manager #4702
Errors occurred if occurrence type wasn't matching current tool's type. E.g. if you would add an IfcRoofType being in a Slab Tool.
2024-05-21 17:53:42 +05:00
Andrej730 7622504d68 avoid even more invalid enum warnings...
warnings like `pyrna_enum_to_py: current value '17' matches no enum in 'BIMModelProperties', '', 'relating_type_id'`
2024-05-21 17:53:42 +05:00
Andrej730 221dd9a7a2 typing 2024-05-21 17:53:42 +05:00
Andrej730 1b74a99667 use delete_ifc_object removing a type
It was acting a bit different - e.g. leaving mesh representation from type on occurrences leaving to lots of errors. Though there is still #3931
2024-05-21 17:07:32 +05:00
Dion Moult 9122f006c7 Deprecate material attributes panel in favour of new material manager. 2024-05-20 23:21:13 +10:00
Andrej730 678dbaa66a nest api to maintain order of related objects #4698
order of objects in .RelatedObjects is important (e.g. for cost items, it's the order of their appearance), so we should maintain it and cannot use sets for .RelatedObjects
2024-05-20 17:19:00 +05:00
Andrej730 fb88499197 ifc5d - check for mandatory fields in csv header 2024-05-20 17:19:00 +05:00
Andrej730 f84adf67cd typing 2024-05-20 17:19:00 +05:00
Andrej730 cacb38a251 maintain "bim." as operators bl_idname prefix for BlenderBIM
1) removed unused operator import_ifc.bim
2) marked export_ifc.bim as deprecated
2024-05-20 17:19:00 +05:00
Andrej730 7c6c01e6b9 fix python 3.10 loading issue after bb8e84e
The error is below.

It was failing because Callable was checking if `ifcopenshell.entity_instance` is a callable and at runtime actually it's not, it's a module.
In python 3.11 they've removed that check and therefore it's not throwing an error. Enabling forward annotations fixes it for python 3.10.
Not sure if there is a need to ensure `ifcopenshell.entity_instance` should be recgonized by python as a class at runtime rather than a module since we need it just for type checking and type checking seems to be clever enough to prioritize module classes over submodules.

  File "C:\Users\user_name\AppData\Roaming\Blender Foundation\Blender\4.0\scripts\addons\blenderbim\bim\__init__.py", line 25, in <module>
    from . import handler, ui, prop, operator, helper
  File "C:\Users\user_name\AppData\Roaming\Blender Foundation\Blender\4.0\scripts\addons\blenderbim\bim\handler.py", line 23, in <module>
    import ifcopenshell.api.owner.settings
  File "C:\Users\user_name\AppData\Roaming\Blender Foundation\Blender\4.0\scripts\addons\blenderbim\libs\site\packages\ifcopenshell\__init__.py", line 85, in <module>
    from .file import file
  File "C:\Users\user_name\AppData\Roaming\Blender Foundation\Blender\4.0\scripts\addons\blenderbim\libs\site\packages\ifcopenshell\file.py", line 179, in <module>
    class file:
  File "C:\Users\user_name\AppData\Roaming\Blender Foundation\Blender\4.0\scripts\addons\blenderbim\libs\site\packages\ifcopenshell\file.py", line 382, in file
    def __getattr__(self, attr) -> Union[Any, Callable[..., ifcopenshell.entity_instance]]:
  File "C:\Program Files\Blender Foundation\Blender 4.0\4.0\python\lib\typing.py", line 1206, in __getitem__
    return self.__getitem_inner__(params)
  File "C:\Program Files\Blender Foundation\Blender 4.0\4.0\python\lib\typing.py", line 312, in inner
    return func(*args, **kwds)
  File "C:\Program Files\Blender Foundation\Blender 4.0\4.0\python\lib\typing.py", line 1212, in __getitem_inner__
    result = _type_check(result, msg)
  File "C:\Program Files\Blender Foundation\Blender 4.0\4.0\python\lib\typing.py", line 176, in _type_check
    raise TypeError(f"{msg} Got {arg!r:.100}.")
TypeError: Callable[args, result]: result must be a type. Got <module 'ifcopenshell.entity_instance' from 'C:\\Users\\user_name\\AppData\\Roaming\\Blender F.
2024-05-20 13:17:17 +05:00
Andrej730 4e462a3cf7 use .alert in fatal loading error UI to draw more attention (3320acc)
https://i.imgur.com/5ZukO0b.png
we probably will need to use 1 method for both error UIs at some point 😅
2024-05-20 12:47:09 +05:00
Andrej730 e7f4c6c9cc update error UI similar to 7bf8c0bbf 2024-05-20 12:34:56 +05:00
Dion Moult 7bf8c0bbfb Fix #4686. Automatically derive correct download URI if add-on fails to install. 2024-05-20 16:32:42 +10:00
Dion Moult c4157673f5 Fix #4696. Null properties are now purged by default to prevent cruft build up. Add support for purging empty enum props. 2024-05-20 16:04:27 +10:00
E Shattow 16285e0625 Update installation.rst refering to System not Import-Export
Blender BIM addon is listed by Blender 4.1 as `System: BlenderBIM`
2024-05-20 10:57:38 +05:00
Dion Moult a07d3b9669 Fix #4633. Only update the text value of the object, not try to be clever with types and occurrences.
Previously, we were clever in detecting types and occurrences because if you edited a type, you wanted all occurrences to be updated. Now, we no longer need to be clever because like any other UI element, the annotations shown in the active view are updated after every IFC operation.
2024-05-20 14:12:03 +10:00
Dion Moult 2d0b356203 See #4690. User can now configure font and font-scaling in schedules. 2024-05-20 12:00:59 +10:00
Dion Moult f666053b63 Use pip instead of hardcoding bcf packages during build. See #4640. 2024-05-19 23:50:57 +10:00
Dion Moult 3bdc969efb Fix #4690. User can now configure the font used to render viewport text. 2024-05-19 23:36:38 +10:00
Bruno Postle 932817877c Fix CPack source tarball generation
This was broken, now a IfcOpenShell-0.7.0.tar.gz file can be created
like so:

  mkdir build
  cd build
  cmake ../cmake/ -DEXTRA_VERSION=
  make package_source
2024-05-19 13:29:26 +01:00
Andrej730 cb255a18b9 fix error editing pset templates when you just opened ifc file
It was failing with an error below if you'd try to edit any property since `primary_measure_type()` returned `[]` because it was executed before `pset_templates()` and `IfcStore.pset_template_file` wasn't yet set.

```python
Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\module\pset_template\operator.py", line 146, in execute
    props.active_prop_template.primary_measure_type = template.PrimaryMeasureType
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: bpy_struct: item.attr = val: enum "IfcLabel" not found in ()
```
2024-05-18 11:24:10 +05:00
Andrej730 183028570e bump xsdata in blenderbim after 87c9bbe2a #4640 2024-05-18 01:49:56 +05:00
Andrej730 341cdd4ef7 fix error updating curve representations after 3fa573e #4685 #4682 2024-05-17 17:40:13 +05:00
Andrej730 fe68e16ca3 bim.update_representation - error if add_representation didn't worked 2024-05-17 17:31:01 +05:00
Andrej730 a8f0c90d47 set_element_value - fix issues with classification, similar to 52ae759 2024-05-17 17:31:01 +05:00
Andrej730 39f550529b experimental - set_element_value to throw exception for invalid queries 2024-05-17 17:31:01 +05:00
Andrej730 25c1a14e64 set_element_value - pass attr value to the next part of the query #4495
E.g. previously set_element_value with query "material.item.Material.Name" would fail with error "Material property is expecting an IFC entity and not a string".

But now it will detect that "Material" property is not a last key in the query and will pass `.Material` value forward and try to set it's `.Name` attribute with value.

The main goal is to make sure get_element_value and set_element_value would have a same result for same queries.
2024-05-17 17:31:01 +05:00
Andrej730 9f7d710119 fix set_element_value bug #4495 2024-05-17 17:31:01 +05:00
Andrej730 ea592775e4 fix set_element_value setting bool attributes with "False" str value 2024-05-17 17:31:01 +05:00
Andrej730 42be4eb064 typing 2024-05-17 17:31:01 +05:00
Hilko 8662e20c58 remove unused include #4620 2024-05-17 10:26:04 +02:00
Andrej730 da4ce3773f bcf - fix xsdata warning about deprecated argument in SerializerConfig
`pretty_print` was deprecated in 24.2 (https://github.com/tefra/xsdata/commit/2b01dbf6cf586e7d64e93ec7045dc17d2585e0de) and replaced with `indent`
2024-05-17 11:57:46 +05:00
Andrej730 87c9bbe2a7 Fix bcf due xsdata update #4680
After https://github.com/tefra/xsdata/commit/93a8ca0548d1f7badb628ad9fd0327672abe140b

`parser.register_namespace("xs", "http://www.w3.org/2001/XMLSchema")` started failing with `TypeError: PushParser.register_namespace() missing 1 required positional argument: 'uri'` since xsdata added a new argument `ns_map` (previously it was always using `parse.ns_map` under the hood, now it allows to provide some external dictionary). We just restore the original behaviour with internal `ns_map` by providing it explicitly.

Specified xsdata version after that change in pyproject.toml.

Also `serialize()` is now using `parser.ns_map` as a fallback value (otherwise why we do `parser.register_namespace` if we never used the `parser.ns_map`?)
2024-05-17 11:46:53 +05:00
Andrej730 a66ee06e3d dev environment - add bcf 2024-05-17 11:42:06 +05:00
Andrej730 9969de58cb add a console toggle to error message section 2024-05-16 18:14:07 +05:00
Andrej730 6995cafe63 bim.split_along_edge fixes #4675
1) description to emphasize that active object will be the one that's cutting
2) skip non-ifc elements, previously they failed to process with error `AttributeError: 'NoneType' object has no attribute 'RepresentationType'`. Also skip objects without representations.
3) add an info message at the end

Here's a short gif in case if anyone doesn't about that feature: https://imgur.com/a/338w7jx
2024-05-16 18:14:06 +05:00
Andrej730 eaa80ad08a fix issue getting styles if object had empty mat slots
Occurred only with only_assigned_to_faces = True
A bit related to #4675

Error was
    styles = [style for style, usage in zip(styles, usage_count, strict=True) if usage > 0]

ValueError: zip() argument 2 is longer than argument 1
2024-05-16 18:14:06 +05:00
Andrej730 d001180082 remove unused code 2024-05-16 18:14:06 +05:00
Andrej730 3884403231 typing 2024-05-16 18:14:06 +05:00
Andrej730 8d4b5d83e1 same as e1250f217 2024-05-16 16:13:22 +05:00
Andrej730 e1250f2177 fix saving debug info to clipboard #4675
now we just rely on blender to make it crossplatform
2024-05-16 16:00:38 +05:00
Andrej730 a2ee920a5f fix group.update_group_products to work with multiple rels 2024-05-15 17:18:34 +05:00
Andrej730 fdbe74a432 maintain snake in api calls
there were only two methods using camel case for arguments
2024-05-15 17:18:34 +05:00
Andrej730 4e39fb3edd fix issue adding layers (name is not optional in ifc) 2024-05-15 17:18:34 +05:00
Andrej730 c2049246cd fix issue adding classification failing to use a None value for the date 2024-05-15 17:18:34 +05:00
Andrej730 335e2b7a78 ifc2x3 tests 2024-05-15 17:18:34 +05:00
Andrej730 bb8e84e5ec typing 2024-05-15 17:18:33 +05:00
Andrej730 2fa30be0d0 small optimization for da1bdc802
On large projects predict dense mesh stage can take 20s+ and reusing attribute value can save up to half of this time.
Using indices also helps but it's not that significant and sometimes it's the same time as using attribute names.
2024-05-14 18:19:13 +05:00
Bruno Perdigão c50149ad87 added 'product' parameter to a few usages of 'pset.remove_pset' after ebd03e9 2024-05-14 17:48:05 +05:00
Dion Moult da1bdc802d Accommodate invalid models coming from Cadwork 2024-05-14 16:16:18 +10:00
Andrej730 a9cba30da6 ifc2x3 tests
removed part of test_append_two_type_products_sharing_the_same_material_indirectly_via_a_material_set for ifc2x3 compatibility and removed part is already tested in test_append_two_type_products_sharing_the_same_material_with_properties
2024-05-13 18:01:18 +05:00
Andrej730 3665dda87c library.remove_library, remove_reference to support ifc2x3 2024-05-13 18:01:18 +05:00
Andrej730 f2696d5352 avoid confusing TypeErrors from api calls
After ab5ea4c85 it was always throwing wrong singature errors like below even if TypeError was caused by some internal issues inside API - it was adding couple extra steps to traceback making errors more noisy.

TypeError: Incorrect function arguments provided for library.edit_library
attribute 'VersionDate' for entity 'IFC2X3.IfcLibraryInformation' is expecting value of type 'ENTITY INSTANCE', got 'str'.. You specified args (<ifcopenshell.file.file object at 0x0000027EB8E6BCD0>,) and settings {'library': #1=IfcLibraryInformation('Name','Version',$,$,$), 'attributes': {'Name': 'Name', 'Version': 'Version', 'VersionDate': 'VersionDate', 'Location': 'Location', 'Description': 'Description'}}
E
Correct signature is (file: ifcopenshell.file.file, library: ifcopenshell.entity_instance.entity_instance, attributes: dict[str, typing.Any]) -> None
See help(ifcopenshell.api.library.edit_library) for documentation.
2024-05-13 18:01:18 +05:00
Andrej730 5e394e1576 library.edit_library - support datetime for VersionDate attribute 2024-05-13 18:01:18 +05:00
Andrej730 ebd03e9290 typing 2024-05-13 18:01:17 +05:00
Andrej730 c3bfd7354f pset.add_pset - throw an error if entity doesn't support adding a pset 2024-05-13 18:01:17 +05:00
Andrej730 1d046eaadf material.remove_material and copy_material to handle ifc2x3 props 2024-05-13 18:01:17 +05:00
Andrej730 2f554db54b owner.remove_person to remove IfcInventory in ifc2x3 2024-05-13 18:01:17 +05:00
Andrej730 54b4cef25e fix errors appending related materials in ifc2x3
it wasn't processing materials properties correctly because it was expecting material to have class IfcMaterialDefinition
2024-05-13 18:01:17 +05:00
Andrej730 c6106e6636 fix edit_pset error for editing material properties in ifc2x3 2024-05-13 18:01:17 +05:00
Andrej730 865dbab3ec fix error removing array pset after c50d1ea2f 2024-05-13 18:01:17 +05:00
Andrej730 76e6c63a01 ifc2x3 tests - prevent using removed user/application
When some test was creating an element and then removing it, it would also remove user and application as they wasn't used anywhere else.
`ifcopenshell.util.element.remove_deep2(file, history)` we use in every api for element deletion can possibly remove user and application which can be unsafe if `get_user` is returning some specific entity that then will become invalid.

Also fixed tests breaking due ifcownerhistory and user/application appearing in ifc2x3.
2024-05-13 18:01:16 +05:00
Gorgious56 3320accc7a The error box is now drawn in red to draw attention to it 2024-05-13 12:00:01 +02:00
Gorgious56 ac686db298 Prevent error when user deletes all spatial structure elements and the spatial manager panel is expanded 2024-05-13 11:57:48 +02:00
Dion Moult aca26c7597 Fix #4659. 2024-05-12 17:16:50 +10:00
Bruno Perdigão 1110f0cffb Merge branch 'v0.7.0' into linked_aggregate_operator_from_panel 2024-05-07 21:00:03 -03:00
Bruno Perdigão 66e829a8aa - Fix the basepoint to be the parent aggregate.
- Change the way that "location_to_cursor_3d" is called.
- Create a new operator to use in the ui.
2024-05-07 20:56:12 -03:00
Bruno Perdigão 04172a87cb when bpy.ops.bim.object_duplicate_move_linked_aggregate is called from the panel, the new objects are placed based on the 3d cursor position 2024-04-30 17:50:31 -03:00
360 changed files with 14293 additions and 10756 deletions
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311]
pyver: [py39, py310, py311, py312]
config:
- {
name: "Windows Build",
+2 -2
View File
@@ -46,7 +46,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install src/bcf --no-deps
pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
@@ -90,7 +90,7 @@ jobs:
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
-DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \
-DCOLLADA_SUPPORT=Off \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add1" \
"-DSCHEMA_VERSIONS=2x3;4;4x3;4x3_add1;4x3_add2" \
-DGLTF_SUPPORT=On \
-DJSON_INCLUDE_DIR=/usr/include \
-DCGAL_INCLUDE_DIR=/usr/include \
+6 -3
View File
@@ -36,7 +36,9 @@ if(NOT CMAKE_BUILD_TYPE)
endif()
# use extra version to make pre-release using eg semver
set(EXTRA_VERSION "-alpha.3")
if(NOT DEFINED EXTRA_VERSION)
set(EXTRA_VERSION "-alpha.3")
endif()
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
option(WASM_BUILD "Build a WebAssembly binary." OFF)
@@ -1147,9 +1149,10 @@ endif()
# Packaging
list(APPEND CPACK_SOURCE_IGNORE_FILES
.git
.gitignore
"/\\\\.git"
"/build/"
)
set(CPACK_SOURCE_INSTALLED_DIRECTORIES "${CMAKE_SOURCE_DIR}/..;/")
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}")
set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}${EXTRA_VERSION}")
SET(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}-${CMAKE_SYSTEM_NAME}")
+1 -1
View File
@@ -13,7 +13,7 @@ readme = "README.md"
requires-python = ">=3.8"
keywords = ["IFC", "BCF", "BIM"]
dependencies = [
"xsdata",
"xsdata>=24.4",
"numpy",
"ifcopenshell",
]
+7 -5
View File
@@ -10,14 +10,14 @@ 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")
parser.register_namespace(ns_map=parser.ns_map, prefix="xs", uri="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),
config=SerializerConfig(indent=" "),
context=context or XmlContext(),
)
@@ -36,8 +36,9 @@ class AbstractXmlParserSerializer(Protocol):
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:
def serialize(self, obj: object, ns_map: Optional[dict[str, str]] = None) -> str:
"""
Serialize an object to XML.
@@ -48,6 +49,7 @@ class AbstractXmlParserSerializer(Protocol):
Returns:
The XML as string.
"""
...
class XmlParserSerializer:
@@ -68,7 +70,7 @@ class XmlParserSerializer:
"""
return self.parser.from_bytes(xml, clazz)
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
def serialize(self, obj: object, ns_map: Optional[dict[Optional[str], str]] = None) -> str:
"""
Serialize an object to XML.
@@ -79,5 +81,5 @@ class XmlParserSerializer:
Returns:
The XML as string.
"""
ns_map = ns_map or {"xs": "http://www.w3.org/2001/XMLSchema"}
ns_map = ns_map or self.parser.ns_map or {"xs": "http://www.w3.org/2001/XMLSchema"}
return self.serializer.render(obj, ns_map)
+35 -212
View File
@@ -55,70 +55,28 @@ PYLIBDIR:=python3.11
PYNUMBER:=311
PYPI_VERSION:=3.11
endif
ifeq ($(PYVERSION), py312)
PYLIBDIR:=python3.12
PYNUMBER:=312
PYPI_VERSION:=3.12
endif
ifeq ($(PLATFORM), linux)
PYPI_PLATFORM:=--platform manylinux_2_17_x86_64
LXML_ARCH:=manylinux_2_28_x86_64
SHAPELY_ARCH:=manylinux_2_17_x86_64.manylinux2014_x86_64
PILLOW_ARCH:=manylinux_2_28_x86_64
endif
ifeq ($(PLATFORM), macos)
PYPI_PLATFORM:=--platform macosx_10_9_x86_64
SHAPELY_ARCH:=macosx_10_9_x86_64
PILLOW_ARCH:=macosx_10_10_x86_64
ifeq ($(PYVERSION), py39)
LXML_ARCH:=macosx_11_0_x86_64
endif
ifeq ($(PYVERSION), py310)
LXML_ARCH:=macosx_11_0_x86_64
endif
ifeq ($(PYVERSION), py311)
LXML_ARCH:=macosx_11_0_universal2
endif
PYPI_PLATFORM:=--platform macosx_10_10_x86_64
endif
ifeq ($(PLATFORM), macosm1)
PYPI_PLATFORM:=--platform macosx_11_0_arm64
LXML_ARCH:=macosx_11_0_universal2
SHAPELY_ARCH:=macosx_11_0_arm64
PILLOW_ARCH:=macosx_11_0_arm64
endif
ifeq ($(PLATFORM), win)
PYPI_PLATFORM:=--platform win_amd64
LXML_ARCH:=win_amd64
SHAPELY_ARCH:=win_amd64
PILLOW_ARCH:=win_amd64
endif
LXML_NAME:=lxml
LXML_VER:=4.9.4
SHAPELY_NAME:=shapely
SHAPELY_VER:=2.0.2
PILLOW_NAME:=pillow
PILLOW_PKG_NAME:=Pillow
PILLOW_VER:=9.5.0
SHAPELY_URL:=https://files.pythonhosted.org/packages/cp$(PYNUMBER)/s/$(SHAPELY_NAME)/$(SHAPELY_NAME)-$(SHAPELY_VER)-cp$(PYNUMBER)-cp$(PYNUMBER)-$(SHAPELY_ARCH).whl
PILLOW_URL:=https://files.pythonhosted.org/packages/cp$(PYNUMBER)/p/$(PILLOW_NAME)/$(PILLOW_PKG_NAME)-$(PILLOW_VER)-cp$(PYNUMBER)-cp$(PYNUMBER)-$(PILLOW_ARCH).whl
ifeq ($(PLATFORM), macosm1)
ifeq ($(PYVERSION), py39)
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py39h0520ce3_1.tar.bz2
endif
ifeq ($(PYVERSION), py310)
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py310h02f21da_0.tar.bz2
endif
ifeq ($(PYVERSION), py311)
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py311h246f609_1.tar.bz2
endif
else
LXML_URL:=https://files.pythonhosted.org/packages/cp$(PYNUMBER)/l/$(LXML_NAME)/$(LXML_NAME)-$(LXML_VER)-cp$(PYNUMBER)-cp$(PYNUMBER)-$(LXML_ARCH).whl
endif
$(info $$LXML_URL is [${LXML_URL}])
$(info $$SHAPELY_URL is [${SHAPELY_URL}])
$(info $$PILLOW_URL is [${PILLOW_URL}])
.PHONY: bump
bump:
cd . && $(SED) "s/$(OLD)/$(NEW)/" Makefile
@@ -137,7 +95,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-f7c03db-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-c7830e9-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -192,49 +150,6 @@ endif
cp -r dist/working/site-packages/git dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides Mustache templating in construction documentation
# TODO: remove this dependency, it seems overkill and we seem to get by with str replaces
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/3f/e7/8750ba6c6101d6aa5ceeb20c013adf2c6f3554a12c71d75654b468404bfa/pystache-0.6.0.tar.gz
cd dist/working && tar -xzvf pystache*
cd dist/working/pystache-0.6.0/ && $(PYTHON) setup.py build && cp -r build/lib/pystache ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Provides SVG export in construction documentation
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/79/e8/7eb2ba188eda14a4b47e33b51f3b4978985f4116655c699bcd18c79279b5/svgwrite-1.3.1.zip
cd dist/working && unzip svgwrite*
cp -r dist/working/svgwrite-1.3.1/svgwrite dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides fuzzy date parsing for construction sequencing
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/be/ed/5bbc91f03fa4c839c4c7360375da77f9659af5f7086b7a7bdda65771c8e0/python-dateutil-2.8.1.tar.gz
cd dist/working && tar -xzvf python-dateutil*
cp -r dist/working/python-dateutil-2.8.1/dateutil dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides duration parsing for construction sequencing
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/b1/80/fb8c13a4cd38eb5021dc3741a9e588e4d1de88d895c1910c6fc8a08b7a70/isodate-0.6.0.tar.gz
cd dist/working && tar -xzvf isodate*
cp -r dist/working/isodate-0.6.0/src/isodate dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides networkx graph analysis for project dependency calculations
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/b0/21/adfbf6168631e28577e4af9eb9f26d75fe72b2bb1d33762a5f2c425e6c2a/networkx-2.5.1.tar.gz
cd dist/working && tar -xzvf networkx*
cp -r dist/working/networkx-2.5.1/networkx dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by networkx
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/4f/51/15a4f6b8154d292e130e5e566c730d8ec6c9802563d58760666f1818ba58/decorator-5.0.9.tar.gz
cd dist/working && tar -xzvf decorator*
cp -r dist/working/decorator-5.0.9/src/decorator.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides audio playback for costing
mkdir -p dist/working
git clone https://github.com/Andrej730/aud.git --branch master-reduced-size --depth 1 dist/working/aud
@@ -249,107 +164,6 @@ endif
cp dist/working/jsgantt* dist/blenderbim/bim/data/gantt/
rm -rf dist/working
# Required by IFCDiff
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/0f/ca/caead2949fbb824c7142e3774fa841aa853bb4d4331b440da8c8514dfc6f/deepdiff-5.8.1.tar.gz
cd dist/working && tar -xzvf deepdiff*
cp -r dist/working/deepdiff-5.8.1/deepdiff dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by deepdiff
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/00/55/ce2cbc6d64034b30cad81a29ba61bdba456f190f5e83c09831304bf68d6b/jsonpickle-1.2.tar.gz
cd dist/working && tar -xzvf jsonpickle*
cp -r dist/working/jsonpickle-1.2/jsonpickle dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by deepdiff
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/a3/b7/d4d69641cbe707a45c23b190f2d717466ba5accc4c70b5f7a8a450387895/ordered-set-3.1.1.tar.gz
cd dist/working && tar -xzvf ordered-set*
cp -r dist/working/ordered-set-3.1.1/ordered_set.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by lark
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/00/32/8076fa13e832bb4dcff379f18f228e5a53412be0631808b9ca2610c0f566/pyparsing-2.4.5.tar.gz
cd dist/working && tar -xzvf pyparsing*
cp -r dist/working/pyparsing-2.4.5/pyparsing.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/5b/ef/f97c3e1a7efa00e989a793fe15297214fc95ad7d9e3810586bd08ce9f0f3/xmlschema-2.0.2.tar.gz
cd dist/working && tar -xzvf xmlschema*
cp -r dist/working/xmlschema-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
cd dist/working && tar -xzvf elementpath*
cp -r dist/working/elementpath-3.0.2/elementpath dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/21/9f/b251f7f8a76dec1d6651be194dfba8fb8d7781d10ab3987190de8391d08e/six-1.14.0.tar.gz
cd dist/working && tar -xzvf six*
cp -r dist/working/six-1.14.0/six.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by IFCCSV and ifcopenshell.util.selector
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/18/4d/8d522136c37d9e1ea74062b41b8d5e1318ebf45063ae46ce72ed60af223b/lark-parser-0.8.5.tar.gz
cd dist/working && tar -xzvf lark-parser*
cp -r dist/working/lark-parser-0.8.5/lark dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by IFC4D
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/b0/bb/9c4dddd3ca173cb56241cfb2eddfae24690dc676d357ac4cab17d0a36d9d/PyP6Xer-1.13.0.tar.gz
cd dist/working && tar -xzvf PyP6Xer*
cp -r dist/working/PyP6Xer-1.13.0/xerparser dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by QTOCalculator
mkdir dist/working
cd dist/working && wget $(SHAPELY_URL)
cd dist/working && cp *.whl shapely.zip && unzip shapely.zip
cp -r dist/working/shapely dist/blenderbim/libs/site/packages/
ifeq ($(PLATFORM), win)
cp -r dist/working/shapely.libs dist/blenderbim/libs/site/packages/
endif
ifeq ($(PLATFORM), linux)
cp -r dist/working/shapely.libs dist/blenderbim/libs/site/packages/
endif
rm -rf dist/working
# Required by the BIM tool thumbnail generator
mkdir dist/working
cd dist/working && wget $(PILLOW_URL)
cd dist/working && cp *.whl pillow.zip && unzip pillow.zip
cp -r dist/working/PIL dist/blenderbim/libs/site/packages/
ifeq ($(PLATFORM), linux)
cp -r dist/working/Pillow.libs dist/blenderbim/libs/site/packages/
endif
rm -rf dist/working
# Required by xerparser and IFC4D
# TODO: remove this dependency. It's only used to show a progress bar.
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/9f/7b/76c4e5ef1a1b528fcaada4133f972e77d33c252831676cf414119ca6093d/tqdm-4.50.0.tar.gz
cd dist/working && tar -xzvf tqdm*
cp -r dist/working/tqdm-4.50.0/tqdm dist/blenderbim/libs/site/packages/
rm -rf dist/working
mkdir dist/working
cd dist/working && $(PYTHON) -m venv env
cd dist/working && mkdir site-packages
@@ -366,7 +180,34 @@ endif
# Provides Brickschema functionality
cd dist/working && . env/bin/activate && $(PIP) install "brickschema[persistence]==0.7.6a2" --target=./site-packages
# Required for SVG to DXF conversion
cd dist/working && . env/bin/activate && $(PIP) install "ezdxf" --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install ezdxf --target=./site-packages
# Required by bcf
cd dist/working && . env/bin/activate && $(PIP) install xsdata --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install xmlschema --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install elementpath --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install six --target=./site-packages
# Required by drawing module
cd dist/working && . env/bin/activate && $(PIP) install lxml $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --target=./site-packages
# Required by qto and drawing module
cd dist/working && . env/bin/activate && $(PIP) install shapely $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --target=./site-packages
# Required by the BIM type manager thumbnail generator
cd dist/working && . env/bin/activate && $(PIP) install pillow $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --target=./site-packages
# Provides mustache templating in construction docs and web UI data
cd dist/working && . env/bin/activate && $(PIP) install pystache --target=./site-packages
# Provides SVG export in construction documentation
cd dist/working && . env/bin/activate && $(PIP) install svgwrite --target=./site-packages
# Provides fuzzy date parsing for construction sequencing
cd dist/working && . env/bin/activate && $(PIP) install python-dateutil --target=./site-packages
# Provides duration parsing for construction sequencing
cd dist/working && . env/bin/activate && $(PIP) install isodate --target=./site-packages
# Provides networkx graph analysis for project dependency calculations
cd dist/working && . env/bin/activate && $(PIP) install networkx --target=./site-packages
# Required by IFCDiff
cd dist/working && . env/bin/activate && $(PIP) install deepdiff --target=./site-packages
# Required by IFCCSV and ifcopenshell.util.selector
cd dist/working && . env/bin/activate && $(PIP) install lark --target=./site-packages
# Required by IFC4D
cd dist/working && . env/bin/activate && $(PIP) install PyP6Xer --target=./site-packages
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
rm -rf dist/working
@@ -385,24 +226,6 @@ endif
cd dist/working/ && $(PATCH) ../blenderbim/libs/site/packages/behave/reporter/junit.py < junit.patch
rm -rf dist/working
# Required by ids - uh, not any more, but I'm trying it out in the drawing module
ifeq ($(PLATFORM), macosm1)
# No wheels available for MacOS M1, but turns out this Anaconda build works
mkdir dist/working
cd dist/working && wget $(LXML_URL)
cd dist/working && tar -xf lxml*
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/lxml dist/blenderbim/libs/site/packages/
rm -rf dist/working
else
# Wheels are preferred for other OSes. A first attempt at using Anaconda
# builds for everything shows it breaks on windows. See #2422.
mkdir dist/working
cd dist/working && wget $(LXML_URL)
cd dist/working && cp *.whl lxml.zip && unzip lxml.zip
cp -r dist/working/lxml dist/blenderbim/libs/site/packages/
rm -rf dist/working
endif
# Required by behave
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/f4/65/220bb4075fddb09d5b3ea2c1c1fa66c1c72be9361ec187aab50fa161e576/parse-1.15.0.tar.gz
+35 -14
View File
@@ -18,7 +18,15 @@
import os
import sys
import bpy
# Ensure we don't try to import bpy or blenderbim.bim
# to support running core tests.
# We assume if bpy was never loaded in current python session
# then we're not in Blender. It's still possible to use
# bpy in core and core tests for annotations using TYPE_CHECKING.
IN_BLENDER = sys.modules.get("bpy", None)
if IN_BLENDER:
import bpy
import platform
import traceback
import subprocess
@@ -78,7 +86,7 @@ def format_debug_info(info: dict):
return text.strip()
if sys.modules.get("bpy", None):
if IN_BLENDER:
# Process *.pth in /libs/site/packages to setup globally importable modules
# This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda
# site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
@@ -120,11 +128,16 @@ if sys.modules.get("bpy", None):
bl_context = "scene"
def draw(self, context):
layout = self.layout
layout.label(text="BlenderBIM could not load.", icon="ERROR")
layout.label(text="View the console for full logs.", icon="CONSOLE")
box = layout.box()
info = get_debug_info()
layout = self.layout
layout.alert = True
layout.label(text="BlenderBIM could not load.", icon="ERROR")
if info["os"] == "Windows":
layout.operator("wm.console_toggle", text="View the console for full logs.", icon="CONSOLE")
else:
layout.label(text="View the console for full logs.", icon="CONSOLE")
box = layout.box()
py = ".".join(info["python_version"].split(".")[0:2])
b3d = ".".join(info["blender_version"].split(".")[0:2])
box.label(text=f"Blender {b3d} {info['os']} {info['machine']}", icon="BLENDER")
@@ -133,6 +146,21 @@ if sys.modules.get("bpy", None):
op = layout.operator("bim.open_uri", text="How Can I Fix This?")
op.uri = "https://docs.blenderbim.org/users/troubleshooting.html#installation-issues"
layout.label(text="Try Reinstalling:", icon="IMPORT")
op = layout.operator("bim.open_uri", text="Re-download Add-on")
bbim_date = info["blenderbim_version"].split(".")[-1]
py_tag = py.replace(".", "")
if "Linux" in info["os"]:
os = "linux"
elif "Darwin" in info["os"]:
if "arm64" in info["machine"]:
os = "macosm1"
else:
os = "macos"
else:
os = "win"
op.uri = f"https://github.com/IfcOpenShell/IfcOpenShell/releases/download/blenderbim-{bbim_date}/blenderbim-{bbim_date}-py{py_tag}-{os}.zip"
class OpenUri(bpy.types.Operator):
bl_idname = "bim.open_uri"
bl_label = "Open URI"
@@ -149,14 +177,7 @@ if sys.modules.get("bpy", None):
def execute(self, context):
info = format_debug_info(get_debug_info())
if platform.system() == "Windows":
command = "echo | set /p nul=" + info
elif platform.system() == "Darwin": # for MacOS
command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | pbcopy'
else: # Linux
command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
subprocess.run(command, shell=True, check=True)
context.window_manager.clipboard = info
return {"FINISHED"}
class HiddenPanel:
+3 -1
View File
@@ -154,9 +154,10 @@ classes = [
ui.BIM_PT_tab_representations,
ui.BIM_PT_tab_geometric_relationships,
ui.BIM_PT_tab_parametric_geometry,
ui.BIM_PT_tab_profiles,
ui.BIM_PT_tab_object_materials,
ui.BIM_PT_tab_materials,
ui.BIM_PT_tab_styles,
ui.BIM_PT_tab_profiles,
# Drawings and documents
ui.BIM_PT_tab_sheets,
ui.BIM_PT_tab_drawings,
@@ -169,6 +170,7 @@ classes = [
ui.BIM_PT_tab_structural,
# Construction scheduling
ui.BIM_PT_tab_status,
ui.BIM_PT_tab_qto,
ui.BIM_PT_tab_resources,
ui.BIM_PT_tab_cost,
ui.BIM_PT_tab_sequence,
@@ -0,0 +1,45 @@
/*
* BlenderBIM Add-on - OpenBIM Blender Add-on
* Copyright (C) 2020, 2021 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/>.
*/
/**
* You may copy this `schedule.css` template alongside your input schedule
* document with the same filename. For example if you have a schedule called
* `door_types.ods`, you can create a `door_types.css` in the same folder to
* style that schedule.
*/
/**
* If you specify a font size in CSS, such as text { font-size: 5; }, all fonts
* will be overriden to match that size.
*
* If your CSS, ODS, XLSX does not specify a font size, the variables below
* will specify the default font size.
*
* If your ODS, XLSX does specify a font size, they will scale linearly based
* on the variables below.
*/
:root {
--font-size-pt: 12;
--font-size-px: 4.13;
--font-width: 0.45;
}
text, tspan { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT', 'DejaVu Sans Condensed', 'Liberation Sans', 'Arial Narrow', 'Arial'; }
@@ -5,6 +5,7 @@ FILE_NAME('EQto_BodyGeometryValidation.ifc','2020-01-01T00:00:00',(),(),'EQto_Bo
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
/* This quantity set is Qto_BodyGeometryValidation backport from IFC4X3 to support IFC4. */
#1=IFCPROPERTYSETTEMPLATE('2KS9su6r517uLXTGKwwDdn',$,'EQto_BodyGeometryValidation','Quantities supplied for validating the correct interpretation of the body shape representation at import. In case of multiple representation items, the quantities are summed for each of the items (irrespective of any overlap). Choosing a suitable tolerance value for comparing the supplied numbers to the numbers calculated from the reconstructed geometry is at the discretion of the importing application.',.QTO_OCCURRENCEDRIVEN.,'IfcProduct',(#2,#3,#4,#5,#6,#7));
#2=IFCSIMPLEPROPERTYTEMPLATE('3iCdOOLRjCwQ_HRTB41Xh8',$,'GrossSurfaceArea','Total gross area of the object, normally generated as perimeter * length + 2 * cross section area. It is the sum of OuterSurfaceArea + (2 x CrossSectionArea) and shall only be given, if the OuterSurfaceArea and CrossSectionArea cannot be established separately.\X2\000A000A\X0\Total gross surface area of the element before applying product-level geometric features such as openings and projections.',.Q_AREA.,$,$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('0juemEqF5889vg7HwR87Fv',$,'NetSurfaceArea','Net surface area of the object, normally generated as perimeter * length + 2 * cross section area taking into account possible processing features (cut-out''s, etc.) or openings and recesses.\X2\000A000A\X0\Total net surface area of the element after applying product-level geometric features such as openings and projections.',.Q_AREA.,$,$,$,$,$,$,.READWRITE.);
+2 -1
View File
@@ -301,7 +301,8 @@ def load_post(scene):
if bpy.context.preferences.addons["blenderbim"].preferences.should_setup_workspace:
if "BIM" in bpy.data.workspaces:
bpy.context.window.workspace = bpy.data.workspaces["BIM"]
if bpy.context.preferences.addons["blenderbim"].preferences.activate_workspace:
bpy.context.window.workspace = bpy.data.workspaces["BIM"]
else:
bpy.ops.workspace.append_activate(idname="BIM", filepath=os.path.join(cwd, "data", "workspace.blend"))
+9 -11
View File
@@ -29,7 +29,7 @@ from mathutils import geometry
from mathutils import Vector
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from typing import Optional, Callable, Any
from typing import Optional, Callable, Any, Union
def draw_attributes(props, layout, copy_operator=None, popup_active_attribute=None):
@@ -91,7 +91,11 @@ def import_attributes(ifc_class, props, data, callback=None):
# A more elegant attribute importer signature, intended to supersede import_attributes
def import_attributes2(element, props, callback=None):
def import_attributes2(
element: Union[str, ifcopenshell.entity_instance],
props: bpy.types.PropertyGroup,
callback: Optional[Callable] = None,
) -> None:
if isinstance(element, str):
attributes = tool.Ifc.schema().declaration_by_name(element).as_entity().all_attributes()
info = {a.name(): None for a in attributes}
@@ -221,15 +225,6 @@ def get_enum_items(data, prop_name, context=None):
return items
# hack to close popup
# https://blender.stackexchange.com/a/202576/130742
def close_operator_panel(event):
x, y = event.mouse_x, event.mouse_y
bpy.context.window.cursor_warp(10, 10)
move_back = lambda: bpy.context.window.cursor_warp(x, y)
bpy.app.timers.register(move_back, first_interval=0.01)
def convert_property_group_from_si(property_group, skip_props=()):
"""Method converts property group values from si to current ifc project units
@@ -309,6 +304,9 @@ def draw_filter(layout, filter_groups, data, module):
elif ifc_filter.type == "group":
row = box.row(align=True)
row.prop(ifc_filter, "value", text="", icon="OUTLINER_COLLECTION")
elif ifc_filter.type == "parent":
row = box.row(align=True)
row.prop(ifc_filter, "value", text="", icon="FILE_PARENT")
elif ifc_filter.type == "query":
row = box.row(align=True)
row.prop(ifc_filter, "name", text="", icon="POINTCLOUD_DATA")
+1 -1
View File
@@ -222,7 +222,7 @@ class IfcStore:
elif existing_obj:
try:
existing_obj.name
IfcStore.unlink_element(obj=existing_obj)
IfcStore.unlink_element(element=element, obj=existing_obj)
except:
pass
IfcStore.id_map[element.id()] = obj
+24 -16
View File
@@ -26,6 +26,7 @@ import bmesh
import logging
import mathutils
import numpy as np
import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
@@ -303,22 +304,22 @@ class IfcImporter:
self.update_progress(100)
bpy.context.window_manager.progress_end()
def is_element_far_away(self, element):
def is_element_far_away(self, element: ifcopenshell.entity_instance) -> bool:
try:
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
point = placement[:, 3][0:3]
return self.is_point_far_away(point, is_meters=False)
except:
pass
return False
def is_point_far_away(self, point, is_meters=True):
def is_point_far_away(
self, point: Union[ifcopenshell.entity_instance, npt.NDArray[np.float64]], is_meters: bool = True
) -> bool:
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
# Users can configure this if they have to handle larger sites but beware of surveying precision
limit = self.ifc_import_settings.distance_limit
limit = limit if is_meters else (limit / self.unit_scale)
coords = point
if hasattr(point, "Coordinates"):
coords = point.Coordinates
coords = getattr(point, "Coordinates", point)
return abs(coords[0]) > limit or abs(coords[1]) > limit or abs(coords[2]) > limit
def process_context_filter(self):
@@ -608,7 +609,9 @@ class IfcImporter:
threshold = 10000 # Just from experience.
faces = [len(e.CfsFaces) for e in self.file.by_type("IfcClosedShell")]
# The check for CfsFaces/Faces/CoordIndex accommodates invalid data from Cadwork
# 0 IfcClosedShell.CfsFaces
faces = [len(faces) for e in self.file.by_type("IfcClosedShell") if (faces := e[0])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
return
@@ -616,12 +619,14 @@ class IfcImporter:
if self.file.schema == "IFC2X3":
return
faces = [len(e.Faces) for e in self.file.by_type("IfcPolygonalFaceSet")]
# 2 IfcPolygonalFaceSet.Faces
faces = [len(faces) for e in self.file.by_type("IfcPolygonalFaceSet") if (faces := e[2])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
return
faces = [len(e.CoordIndex) for e in self.file.by_type("IfcTriangulatedFaceSet")]
# 3 IfcTriangulatedFaceSet.CoordIndex
faces = [len(index) for e in self.file.by_type("IfcTriangulatedFaceSet") if (index := e[3])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
@@ -679,7 +684,7 @@ class IfcImporter:
props.blender_orthogonal_height = str(offset_point[2])
props.has_blender_offset = True
def get_offset_point(self):
def get_offset_point(self) -> Union[npt.NDArray[np.float64], None]:
elements_checked = 0
# If more than these elements aren't far away, the file probably isn't absolutely positioned
element_checking_threshold = 10
@@ -714,7 +719,7 @@ class IfcImporter:
if self.is_point_far_away(point, is_meters=False):
return point
def does_element_likely_have_geometry_far_away(self, element):
def does_element_likely_have_geometry_far_away(self, element: ifcopenshell.entity_instance) -> bool:
for representation in element.Representation.Representations:
items = []
for item in representation.Items:
@@ -731,13 +736,14 @@ class IfcImporter:
if subelement.is_a("IfcCartesianPoint"):
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
return True
return False
def apply_blender_offset_to_matrix_world(self, obj: bpy.types.Object, matrix: np.ndarray) -> mathutils.Matrix:
props = bpy.context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
if obj.data and obj.data.get("has_cartesian_point_offset", None):
obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT"
elif self.is_point_far_away((matrix[0, 3], matrix[1, 3], matrix[2, 3])):
elif self.is_point_far_away((matrix[:3, 3])):
obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT"
matrix = ifcopenshell.util.geolocation.global2local(
matrix,
@@ -750,7 +756,9 @@ class IfcImporter:
return mathutils.Matrix(matrix.tolist())
def find_decomposed_ifc_class(self, element, ifc_class):
def find_decomposed_ifc_class(
self, element: ifcopenshell.entity_instance, ifc_class: str
) -> Union[ifcopenshell.entity_instance, None]:
if element.is_a(ifc_class):
return element
rel_aggregates = element.IsDecomposedBy
@@ -1695,18 +1703,18 @@ class IfcImporter:
self.type_collection.children.link(aggregate["collection"])
continue
def create_materials(self):
def create_materials(self) -> None:
for material in self.file.by_type("IfcMaterial"):
self.create_material(material)
def create_material(self, material):
def create_material(self, material: ifcopenshell.entity_instance) -> bpy.types.Material:
blender_material = bpy.data.materials.new(material.Name)
self.link_element(material, blender_material)
self.material_creator.materials[material.id()] = blender_material
blender_material.use_fake_user = True
return blender_material
def create_styles(self):
def create_styles(self) -> None:
parsed_styles = set()
for material_definition_representation in self.file.by_type("IfcMaterialDefinitionRepresentation"):
@@ -40,22 +40,22 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, Operator):
bl_label = "Assign Object To Aggregation"
bl_options = {"REGISTER", "UNDO"}
relating_object: bpy.props.IntProperty()
related_object: bpy.props.IntProperty()
def _execute(self, context):
relating_obj = None
if self.relating_object:
element = tool.Ifc.get().by_id(self.relating_object)
if element.IsDecomposedBy:
relating_obj = tool.Ifc.get_object(element)
else:
assembly = element.Decomposes[0].RelatingObject
relating_obj = tool.Ifc.get_object(assembly)
relating_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object))
elif self.related_object:
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get().by_id(self.related_object))
if aggregate:
relating_obj = tool.Ifc.get_object(aggregate)
elif context.active_object:
relating_obj = context.active_object
if not relating_obj:
return
for obj in bpy.context.selected_objects:
for obj in bpy.context.selected_objects + [bpy.context.active_object]:
if obj == relating_obj:
continue
element = tool.Ifc.get_entity(obj)
@@ -100,7 +100,7 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
@@ -34,15 +34,27 @@ from bpy.props import (
def update_relating_object(self, context):
def message(self, context):
self.layout.label(text="Please select a valid Ifc Element")
self.layout.label(text="Please select a valid IFC Element")
if self.relating_object is None:
return
if not self.relating_object.BIMObjectProperties.ifc_definition_id:
context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO")
if self.relating_object:
self.related_object = None
if not self.relating_object.BIMObjectProperties.ifc_definition_id:
context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO")
self.relating_object = None
def update_related_object(self, context):
def message(self, context):
self.layout.label(text="Please select a valid IFC Element")
if self.related_object:
self.relating_object = None
if not self.related_object.BIMObjectProperties.ifc_definition_id:
context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO")
self.related_object = None
class BIMObjectAggregateProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
relating_object: PointerProperty(name="Aggregate", type=bpy.types.Object, update=update_relating_object)
relating_object: PointerProperty(name="Relating Whole", type=bpy.types.Object, update=update_relating_object)
related_object: PointerProperty(name="Related Part", type=bpy.types.Object, update=update_related_object)
@@ -53,12 +53,21 @@ class BIM_PT_aggregate(Panel):
props = context.active_object.BIMObjectAggregateProperties
if props.is_editing:
row = layout.row()
row.prop(props, "relating_object", text="Whole")
row = layout.row()
row.prop(props, "related_object", text="Or Part")
row = layout.row(align=True)
row.prop(props, "relating_object", text="")
col = row.column(align=True)
if not props.relating_object and not props.related_object:
col.enabled = False
op = col.operator("bim.aggregate_assign_object", icon="CHECKMARK")
if props.relating_object:
op = row.operator("bim.aggregate_assign_object", icon="CHECKMARK", text="")
op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id
elif props.related_object:
op.related_object = props.related_object.BIMObjectProperties.ifc_definition_id
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
return
else:
row = layout.row(align=True)
if AggregateData.data["has_relating_object"]:
@@ -73,7 +82,7 @@ class BIM_PT_aggregate(Panel):
row.operator("bim.add_aggregate", icon="ADD", text="")
op = row.operator("bim.aggregate_unassign_object", icon="X", text="")
else:
row.label(text="No Whole relation defined", icon="TRIA_UP")
row.label(text="No Whole Relationship Found", icon="TRIA_UP")
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
@@ -144,7 +153,7 @@ class BIM_PT_linked_aggregate(Panel):
row.label(text="Not a Linked Aggregate")
else:
row.label(text=f"{Number_Linked_Aggregates} Linked Aggregates")
op = row.operator("bim.object_duplicate_move_linked_aggregate", text="", icon="DUPLICATE")
op = row.operator("bim.duplicate_linked_aggregate_to_3d_cursor", text="", icon="DUPLICATE")
if type(Number_Linked_Aggregates) is int:
if Number_Linked_Aggregates > 0:
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_DATA_POINTCLOUD")
@@ -27,15 +27,12 @@ classes = (
operator.CopyAttributeToSelection,
prop.BIMAttributeProperties,
ui.BIM_PT_object_attributes,
ui.BIM_PT_material_attributes,
)
def register():
bpy.types.Object.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
bpy.types.Material.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
def unregister():
del bpy.types.Object.BIMAttributeProperties
del bpy.types.Material.BIMAttributeProperties
@@ -23,7 +23,6 @@ import blenderbim.tool as tool
def refresh():
AttributesData.is_loaded = False
MaterialAttributesData.is_loaded = False
class AttributesData:
@@ -51,28 +50,3 @@ class AttributesData:
key = "STEP ID"
results.append({"name": key, "value": str(value)})
return results
class MaterialAttributesData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"ifc_definition_id": cls.ifc_definition_id(), "attributes": cls.attributes()}
cls.is_loaded = True
@classmethod
def ifc_definition_id(cls):
return bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id
@classmethod
def attributes(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object.active_material)
data = element.get_info()
for key, value in data.items():
if value is None or isinstance(value, ifcopenshell.entity_instance) or key in ["id", "type"]:
continue
results.append({"name": key, "value": str(value)})
return results
@@ -40,14 +40,10 @@ class EnableEditingAttributes(bpy.types.Operator):
bl_label = "Enable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
self.file = IfcStore.get_file()
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
props.attributes.clear()
@@ -85,13 +81,9 @@ class DisableEditingAttributes(bpy.types.Operator):
bl_label = "Disable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
props.is_editing_attributes = False
return {"FINISHED"}
@@ -102,14 +94,10 @@ class EditAttributes(bpy.types.Operator, Operator):
bl_label = "Edit Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def _execute(self, context):
self.file = IfcStore.get_file()
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
product = tool.Ifc.get_entity(obj)
@@ -126,7 +114,7 @@ class EditAttributes(bpy.types.Operator, Operator):
attributes = blenderbim.bim.helper.export_attributes(props.attributes, callback=callback)
ifcopenshell.api.run("attribute.edit_attributes", self.file, product=product, attributes=attributes)
bpy.ops.bim.disable_editing_attributes(obj=obj.name, obj_type=self.obj_type)
bpy.ops.bim.disable_editing_attributes(obj=obj.name)
return {"FINISHED"}
@@ -19,34 +19,34 @@
import blenderbim.bim.helper
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.attribute.data import AttributesData, MaterialAttributesData
from blenderbim.bim.module.attribute.data import AttributesData
def draw_ui(context, layout, obj_type, attributes):
obj = context.active_object if obj_type == "Object" else context.active_object.active_material
def draw_ui(context, layout, attributes):
obj = context.active_object
oprops = obj.BIMObjectProperties
props = obj.BIMAttributeProperties
if props.is_editing_attributes:
row = layout.row(align=True)
op = row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
op.obj_type = obj_type
op.obj = obj.name
op = row.operator("bim.disable_editing_attributes", icon="CANCEL", text="")
op.obj_type = obj_type
op.obj = obj.name
blenderbim.bim.helper.draw_attributes(props.attributes, layout, copy_operator="bim.copy_attribute_to_selection")
else:
row = layout.row()
op = row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit")
op.obj_type = obj_type
op.obj = obj.name
for attribute in attributes:
row = layout.row(align=True)
row.label(text=attribute["name"])
row.label(text=attribute["value"])
# row.label(text=attribute["value"])
op = row.operator("bim.select_similar", text=attribute["value"], icon="NONE", emboss=False)
op.key = attribute['name']
# TODO: reimplement, see #1222
# if "IfcSite/" in context.active_object.name or "IfcBuilding/" in context.active_object.name:
@@ -72,32 +72,4 @@ class BIM_PT_object_attributes(Panel):
def draw(self, context):
if not AttributesData.is_loaded:
AttributesData.load()
draw_ui(context, self.layout, "Object", AttributesData.data["attributes"])
class BIM_PT_material_attributes(Panel):
bl_label = "Material Attributes"
bl_idname = "BIM_PT_material_attributes"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
@classmethod
def poll(cls, context):
if not IfcStore.get_file():
return False
try:
return bool(context.active_object.active_material.BIMObjectProperties.ifc_definition_id)
except:
return False
def draw(self, context):
if not MaterialAttributesData.is_loaded:
MaterialAttributesData.load()
elif (
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
!= MaterialAttributesData.data["ifc_definition_id"]
):
MaterialAttributesData.load()
draw_ui(context, self.layout, "Material", MaterialAttributesData.data["attributes"])
draw_ui(context, self.layout, AttributesData.data["attributes"])
@@ -938,7 +938,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
# Operators with context overrides are used because they are
# significantly faster than looping through all objects
exception_global_ids = {v.ifc_guid for v in viewpoint.visualization_info.components.visibility.exceptions or []}
exception_global_ids = {v.ifc_guid for v in viewpoint.visualization_info.components.visibility.exceptions.component or []}
if viewpoint.visualization_info.components.visibility.default_visibility:
old = context.area.type
@@ -958,11 +958,13 @@ class ActivateBcfViewpoint(bpy.types.Operator):
if objs:
old = context.area.type
context.area.type = "VIEW_3D"
bpy.ops.object.hide_view_clear()
context_override = {}
context_override["object"] = context_override["active_object"] = objs[0]
context_override["selected_objects"] = context_override["selected_editable_objects"] = objs
with context.temp_override(**context_override):
bpy.ops.object.hide_view_set(unselected=True)
bpy.data.objects["Viewpoint"].hide_set(False)
context.area.type = old
if viewpoint.visualization_info.components.view_setup_hints:
@@ -1000,6 +1002,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
obj = IfcStore.get_element(global_id)
if obj:
obj.select_set(True)
obj.hide_set(False)
def set_colours(self, viewpoint):
global_id_colours = {}
@@ -54,11 +54,11 @@ classes = (
def register():
bpy.types.Scene.BIMClassificationProperties = bpy.props.PointerProperty(type=prop.BIMClassificationProperties)
bpy.types.Object.BIMClassificationReferenceProperties = bpy.props.PointerProperty(
bpy.types.Scene.BIMClassificationReferenceProperties = bpy.props.PointerProperty(
type=prop.BIMClassificationReferenceProperties
)
def unregister():
del bpy.types.Scene.BIMClassificationProperties
del bpy.types.Object.BIMClassificationReferenceProperties
del bpy.types.Scene.BIMClassificationReferenceProperties
@@ -88,7 +88,7 @@ class ClassificationReferencesData(ReferencesData):
cls.data["references"] = cls.references()
cls.data["active_classification_library"] = cls.active_classification_library()
cls.data["classifications"] = cls.classifications()
cls.data["object_type"] = "OBJECT"
cls.data["object_type"] = "Object"
@classmethod
def references(cls):
@@ -112,17 +112,21 @@ class MaterialClassificationsData(ReferencesData):
cls.data["references"] = cls.references()
cls.data["active_classification_library"] = cls.active_classification_library()
cls.data["classifications"] = cls.classifications()
cls.data["object_type"] = "MATERIAL"
cls.data["object_type"] = "Material"
@classmethod
def references(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object.active_material)
if element:
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
del data["ReferencedSource"]
results.append(data)
props = bpy.context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
element = tool.Ifc.get().by_id(material.ifc_definition_id)
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
del data["ReferencedSource"]
results.append(data)
return results
@@ -136,7 +140,7 @@ class CostClassificationsData(ReferencesData):
cls.data["references"] = cls.references()
cls.data["active_classification_library"] = cls.active_classification_library()
cls.data["classifications"] = cls.classifications()
cls.data["object_type"] = "COST"
cls.data["object_type"] = "Cost"
@classmethod
def references(cls):
@@ -77,26 +77,37 @@ class AddManualClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_manual_classification_reference"
bl_label = "Add Manual Classification Reference"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def _execute(self, context):
obj = context.active_object
props = obj.BIMClassificationReferenceProperties
if self.obj_type == "Object":
if context.selected_objects:
objects = [o.name for o in context.selected_objects]
else:
objects = [context.active_object.name]
else:
objects = [self.obj]
props = context.scene.BIMClassificationReferenceProperties
attributes = blenderbim.bim.helper.export_attributes(props.reference_attributes)
product = tool.Ifc.get_entity(obj)
# TODO: refactor and support material and cost item classifications
classification = tool.Ifc.get().by_id(int(props.classifications))
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
products=[product],
classification=classification,
identification="X",
name="Unnamed",
)
ifcopenshell.api.run(
"classification.edit_reference", tool.Ifc.get(), reference=reference, attributes=attributes
)
products = [
tool.Ifc.get().by_id(ifc_definition_id)
for obj in objects
if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context))
]
if products:
classification = tool.Ifc.get().by_id(int(props.classifications))
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
products=products,
classification=classification,
identification="X",
name="Unnamed",
)
ifcopenshell.api.run(
"classification.edit_reference", tool.Ifc.get(), reference=reference, attributes=attributes
)
props.is_adding = False
@@ -150,8 +161,7 @@ class EnableAddingManualClassificationReference(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMClassificationReferenceProperties
props = context.scene.BIMClassificationReferenceProperties
props.is_adding = True
props.reference_attributes.clear()
blenderbim.bim.helper.import_attributes2("IfcClassificationReference", props.reference_attributes)
@@ -164,8 +174,7 @@ class DisableAddingManualClassificationReference(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMClassificationReferenceProperties
props = context.scene.BIMClassificationReferenceProperties
props.is_adding = False
return {"FINISHED"}
@@ -257,8 +266,7 @@ class EnableEditingClassificationReference(bpy.types.Operator):
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMClassificationReferenceProperties
props = context.scene.BIMClassificationReferenceProperties
props.reference_attributes.clear()
blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
props.active_reference_id = self.reference
@@ -272,8 +280,7 @@ class DisableEditingClassificationReference(bpy.types.Operator):
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
obj.BIMClassificationReferenceProperties.active_reference_id = 0
context.scene.BIMClassificationReferenceProperties.active_reference_id = 0
return {"FINISHED"}
@@ -326,8 +333,7 @@ class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
obj: bpy.props.StringProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMClassificationReferenceProperties
props = context.scene.BIMClassificationReferenceProperties
attributes = {}
for attribute in props.reference_attributes:
if attribute.is_null:
@@ -368,9 +374,8 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
classification = element
break
ifc_file = tool.Ifc.get()
products = [
ifc_file.by_id(ifc_definition_id)
tool.Ifc.get().by_id(ifc_definition_id)
for obj in objects
if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context))
]
@@ -118,10 +118,9 @@ class BIM_PT_classifications(Panel):
class ReferenceUI:
def draw_ui(self, context):
obj = context.active_object
self.oprops = obj.BIMObjectProperties
self.sprops = context.scene.BIMClassificationProperties
self.bprops = context.scene.BIMBSDDProperties
self.props = obj.BIMClassificationReferenceProperties
self.props = context.scene.BIMClassificationReferenceProperties
self.file = IfcStore.get_file()
self.draw_add_ui(context)
@@ -155,7 +154,7 @@ class ReferenceUI:
blenderbim.bim.helper.draw_attributes(self.props.reference_attributes, self.layout)
row = self.layout.row(align=True)
op = row.operator("bim.add_manual_classification_reference", text="Save", icon="CHECKMARK")
op.type = self.data.data["object_type"]
op.obj_type = self.data.data["object_type"]
row.operator("bim.disable_adding_manual_classification_reference", text="", icon="CANCEL")
else:
row = self.layout.row()
@@ -295,28 +294,31 @@ class BIM_PT_material_classifications(Panel, ReferenceUI):
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
bl_context = "scene"
bl_parent_id = "BIM_PT_materials"
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
return False
try:
return bool(context.active_object.active_material.BIMObjectProperties.ifc_definition_id)
except:
return False
props = context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
return True
return False
def draw(self, context):
if not MaterialClassificationsData.is_loaded:
MaterialClassificationsData.load()
self.data = MaterialClassificationsData
self.obj = context.active_object.active_material.name
self.obj = ""
self.obj_type = "Material"
self.draw_ui(context)
class BIM_PT_cost_classifications(Panel, ReferenceUI):
bl_label = "Cost Classifications"
bl_label = "Cost Item Classifications"
bl_idname = "BIM_PT_cost_classifications"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
@@ -82,6 +82,7 @@ classes = (
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
prop.ScheduleColumn,
prop.BIMCostProperties,
ui.BIM_PT_cost_schedules,
ui.BIM_PT_cost_item_quantities,
@@ -19,10 +19,13 @@
import bpy
import ifcopenshell
import ifcopenshell.util.cost
import ifcopenshell.util.date
import ifcopenshell.util.element
import ifcopenshell.util.unit
import blenderbim.tool as tool
import blenderbim.bim.schema
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
from typing import Any
def refresh():
@@ -34,6 +37,7 @@ def refresh():
class CostSchedulesData:
data = {}
is_loaded = False
_cost_values: dict[int, dict[str, Any]]
@classmethod
def load(cls):
@@ -136,7 +140,7 @@ class CostSchedulesData:
data["UnitBasisUnitSymbol"] = "U"
if cost_value.Category == "*":
is_sum = True
cost_quantity = data["TotalCostQuantity"] or 1
cost_quantity = 1 if data["TotalCostQuantity"] is None else data["TotalCostQuantity"]
if has_unit_basis:
data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity / data["UnitBasisValueComponent"]
else:
@@ -155,6 +159,7 @@ class CostSchedulesData:
data["UnitSymbol"] = "-"
if cost_item.CostQuantities:
quantity = cost_item.CostQuantities[0]
data["QuantityType"] = quantity.is_a()
unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get())
if unit:
data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit)
@@ -193,10 +198,16 @@ class CostSchedulesData:
# data["DerivedUnitSymbol"] = "?"
# print("Total Cost", data["DerivedTotalCostQuantity"], cost_item.Name)
# TODO: dead code?
@classmethod
def _get_object_quantities(cls, cost_item, element):
def _get_object_quantities(
cls, cost_item: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance
) -> list[int]:
if not element.is_a("IfcObject"):
return []
cost_quantities = cost_item.CostQuantities
if not cost_quantities:
return []
results = []
for relationship in element.IsDefinedBy:
if not relationship.is_a("IfcRelDefinesByProperties"):
@@ -205,7 +216,7 @@ class CostSchedulesData:
if not qto.is_a("IfcElementQuantity"):
continue
for prop in qto.Quantities:
if prop in cost_item.CostQuantities or []:
if prop in cost_quantities:
results.append(prop.id())
return results
@@ -73,7 +73,7 @@ class RemoveCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
cost_schedule: bpy.props.IntProperty()
def _execute(self, context):
core.remove_cost_schedule(tool.Ifc, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
core.remove_cost_schedule(tool.Ifc, tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
class EnableEditingCostSchedule(bpy.types.Operator):
@@ -223,36 +223,40 @@ class EditCostItem(bpy.types.Operator, tool.Ifc.Operator):
class AssignCostItemType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_cost_item_type"
bl_label = "Assign Cost Item Type Product"
bl_label = "Assign Cost Item To Product Types"
bl_description = "Assign cost item to currently selected or active product types"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
prop_name: bpy.props.StringProperty()
def _execute(self, context):
core.assign_cost_item_type(
product_types = core.assign_cost_item_type(
tool.Ifc,
tool.Cost,
tool.Spatial,
cost_item=tool.Ifc.get().by_id(self.cost_item),
prop_name=self.prop_name, # TODO: REVIEW PROP_NAME USABILITY
)
self.report({"INFO"}, f"Cost item was assigned to {len(product_types)} product types.")
class UnassignCostItemType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_cost_item_type"
bl_label = "Unassign Cost Item Type"
bl_description = "Unassign cost item from currently selected or active product types"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
related_object: bpy.props.IntProperty()
def _execute(self, context):
core.unassign_cost_item_type(
product_types = core.unassign_cost_item_type(
tool.Ifc,
tool.Cost,
tool.Spatial,
cost_item=tool.Ifc.get().by_id(self.cost_item),
product_types=[tool.Ifc.get().by_id(self.related_object)] if self.related_object else [],
product_types=[tool.Ifc.get().by_id(self.related_object)] if self.related_object else None,
)
self.report({"INFO"}, f"Cost item was unassigned from {len(product_types)} product types.")
return {"FINISHED"}
@@ -287,9 +291,11 @@ class UnassignCostItemQuantity(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc,
tool.Cost,
cost_item=tool.Ifc.get().by_id(self.cost_item),
products=[tool.Ifc.get().by_id(self.related_object)]
if self.related_object
else tool.Spatial.get_selected_products(),
products=(
[tool.Ifc.get().by_id(self.related_object)]
if self.related_object
else tool.Spatial.get_selected_products()
),
)
@@ -509,7 +515,7 @@ class SelectCostScheduleProducts(bpy.types.Operator):
class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
bl_idname = "import_cost_schedule_csv.bim"
bl_idname = "bim.import_cost_schedule_csv"
bl_label = "Import Cost Schedule CSV"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
@@ -527,6 +533,13 @@ class AddCostColumn(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
if not context.scene.BIMCostProperties.cost_column:
cls.poll_message_set("Cost column name is empty")
return False
return True
def execute(self, context):
core.add_cost_column(tool.Cost, self.name)
return {"FINISHED"}
@@ -538,7 +551,7 @@ class RemoveCostColumn(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
def _execute(self, context):
def execute(self, context):
core.remove_cost_column(tool.Cost, self.name)
return {"FINISHED"}
@@ -656,13 +669,17 @@ class ExportCostSchedules(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file"
cost_schedule: bpy.props.IntProperty()
format: bpy.props.EnumProperty("Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
format: bpy.props.EnumProperty(name="Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
directory: bpy.props.StringProperty(subtype="FILE_PATH")
filter_folder: bpy.props.BoolProperty(
name="Filter Folders",
default=True,
)
def execute(self, context):
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
r = core.export_cost_schedules(
tool.Cost, filepath=self.filepath, format=self.format, cost_schedule=cost_schedule
tool.Cost, filepath=self.directory, format=self.format, cost_schedule=cost_schedule
)
if isinstance(r, str):
self.report({"ERROR"}, r)
@@ -676,6 +693,7 @@ class ExportCostSchedules(bpy.types.Operator):
def draw(self, context):
self.layout.label(text="Choose a format")
self.layout.prop(self, "format")
self.layout.label(text="Select a directory.")
class ClearCostItemAssignments(bpy.types.Operator, tool.Ifc.Operator):
@@ -175,6 +175,10 @@ def update_active_cost_item_resources(self, context):
bpy.ops.bim.load_cost_item_resource_quantities()
class ScheduleColumn(PropertyGroup):
schedule_id: IntProperty()
class BIMCostProperties(PropertyGroup):
cost_schedule_predefined_types: EnumProperty(
items=get_schedule_predefined_types, name="Predefined Type", default=None
@@ -210,9 +214,14 @@ class BIMCostProperties(PropertyGroup):
cost_value_attributes: CollectionProperty(name="Cost Value Attributes", type=Attribute)
cost_value_formula: StringProperty(name="Cost Value Formula")
cost_column: StringProperty(name="Cost Column")
should_show_column_ui: BoolProperty(name="Should Show Column UI", default=False)
should_show_column_ui: BoolProperty(
name="Should Show Column UI",
description="Display UI for adding cost schedule columns, column names represent a category for cost item values",
default=False,
)
should_show_currency_ui: BoolProperty(name="Should Show Currency UI", default=False)
columns: CollectionProperty(name="Columns", type=StrProperty)
columns: CollectionProperty(name="Active Schedule Columns", type=StrProperty)
columns_storage: CollectionProperty(name="Columns", type=ScheduleColumn)
active_column_index: IntProperty(name="Active Column Index")
cost_item_products: CollectionProperty(name="Cost Item Products", type=CostItemQuantity)
active_cost_item_product_index: IntProperty(name="Active Cost Item Product Index")
+22 -10
View File
@@ -21,6 +21,7 @@ import blenderbim.bim.module.cost.prop as CostProp
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.cost.data import CostSchedulesData
from typing import Any
class BIM_PT_cost_schedules(Panel):
@@ -53,7 +54,7 @@ class BIM_PT_cost_schedules(Panel):
row.label(text="No Cost Schedules found.", icon="TEXT")
row.operator("bim.add_cost_schedule", icon="ADD", text="")
row.operator("import_cost_schedule_csv.bim",icon="IMPORT",text="")
row.operator("bim.import_cost_schedule_csv",icon="IMPORT",text="")
for schedule in CostSchedulesData.data["schedules"]:
self.draw_cost_schedule_ui(schedule)
@@ -178,24 +179,33 @@ class BIM_PT_cost_schedules(Panel):
"active_cost_item_index",
)
if self.props.active_cost_item_id:
cost_item = CostSchedulesData.data["cost_items"][ifc_definition_id]
if self.props.cost_item_editing_type == "ATTRIBUTES":
self.draw_editable_cost_item_attributes_ui()
elif self.props.cost_item_editing_type == "QUANTITIES":
self.draw_editable_cost_item_quantities_ui()
self.draw_editable_cost_item_quantities_ui(cost_item)
elif self.props.cost_item_editing_type == "VALUES":
self.draw_editable_cost_item_values_ui()
def draw_editable_cost_item_attributes_ui(self):
blenderbim.bim.helper.draw_attributes(self.props.cost_item_attributes, self.layout)
def draw_editable_cost_item_quantities_ui(self):
def draw_editable_cost_item_quantities_ui(self, cost_item: dict[str, Any]):
quantities = CostSchedulesData.data["cost_quantities"]
row = self.layout.row(align=True)
row.prop(self.props, "quantity_types", text="")
# In IFC, all quantities of IfcCostTime should have 1 type.
if quantities:
quantity_class = cost_item["QuantityType"]
row.label(text=quantity_class)
else:
row.prop(self.props, "quantity_types", text="")
quantity_class = self.props.quantity_types
op = row.operator("bim.add_cost_item_quantity", text="", icon="ADD")
op.cost_item = self.props.active_cost_item_id
op.ifc_class = self.props.quantity_types
op.ifc_class = quantity_class
for quantity in CostSchedulesData.data["cost_quantities"]:
for quantity in quantities:
row = self.layout.row(align=True)
row.label(text=quantity["name"])
row.label(text=quantity["value"])
@@ -634,7 +644,7 @@ class BIM_UL_cost_items_trait:
layout.label(text=cost_item["UnitBasisUnitSymbol"])
def draw_total_quantity_column(self, layout, cost_item):
if cost_item["TotalCostQuantity"]:
if cost_item["TotalCostQuantity"] is not None:
label = "{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}"
layout.label(text=label)
else:
@@ -745,9 +755,11 @@ class BIM_UL_product_cost_items(UIList):
class BIM_PT_Costing_Tools(Panel):
bl_label = "5D Tools"
bl_idname = "BIM_PT_Costing_Tools"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "4D/5D Toolkit"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_cost"
def draw(self, context):
self.props = context.scene.BIMCostProperties
@@ -23,6 +23,7 @@ import ifccsv
import logging
import tempfile
import ifcopenshell
import ifcopenshell.util.selector
import blenderbim.tool as tool
import blenderbim.bim.module.drawing.scheduler as scheduler
from blenderbim.bim.ifc import IfcStore
@@ -40,7 +40,7 @@ from blenderbim.bim.ifc import IfcStore
class CopyDebugInformation(bpy.types.Operator):
bl_idname = "bim.copy_debug_information"
bl_label = "Copy Debug Information"
bl_description = "Copies debugging information to your clipboard for use in bugreports"
bl_description = "Copies debugging information to your clipboard for use in bug reports"
def execute(self, context):
info = get_debug_info()
@@ -56,17 +56,13 @@ class CopyDebugInformation(bpy.types.Operator):
text = format_debug_info(info)
text_with_backticks = f"```\n{text}\n```"
print("-" * 80)
print(text)
print(text_with_backticks)
print("-" * 80)
if platform.system() == "Windows":
command = "echo | set /p nul=" + text
elif platform.system() == "Darwin": # for MacOS
command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | pbcopy'
else: # Linux
command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
subprocess.run(command, shell=True, check=True)
context.window_manager.clipboard = text_with_backticks
return {"FINISHED"}
@@ -97,7 +93,8 @@ class ConvertToBlender(bpy.types.Operator):
if obj.data:
obj.data.BIMMeshProperties.ifc_definition_id = 0
for material in bpy.data.materials:
material.BIMMaterialProperties.ifc_style_id = False
material.BIMObjectProperties.ifc_definition_id = 0
material.BIMMaterialProperties.ifc_style_id = 0
context.scene.BIMProperties.ifc_file = ""
context.scene.BIMDebugProperties.attributes.clear()
IfcStore.purge()
@@ -159,9 +159,7 @@ class BaseDecorator:
objecttype = "NOTDEFINED"
def __init__(self):
self.font_id = blf.load(
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
)
self.font_id = 0 # 0 is the default font
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
@@ -170,15 +168,12 @@ class BaseDecorator:
def get_camera_width_mm(self):
# Horrific prototype code to ensure bgl draws at drawing scales
# https://blender.stackexchange.com/questions/16493/is-there-a-way-to-fit-the-viewport-to-the-current-field-of-view
def is_landscape(render):
return render.resolution_x > render.resolution_y
camera = bpy.context.scene.camera
render = bpy.context.scene.render
if is_landscape(render):
camera_width_model = camera.data.ortho_scale
else:
camera_width_model = camera.data.ortho_scale / render.resolution_y * render.resolution_x
camera_width_model = camera.data.ortho_scale
scale = tool.Drawing.get_scale_ratio(tool.Drawing.get_diagram_scale(camera)["Scale"])
camera_width_mm = scale * camera_width_model
@@ -404,7 +399,6 @@ class BaseDecorator:
line_no += 1 if multiline_to_bottom else -1
return
# 0 is the default font, but we're fancier than that
font_id = self.font_id
color = context.preferences.addons["blenderbim"].preferences.decorations_colour
@@ -2014,6 +2008,13 @@ class DecorationsHandler:
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
if drawing_font := bpy.context.scene.DocProperties.drawing_font:
drawing_font_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", drawing_font)
if os.path.exists(drawing_font_path):
font_id = blf.load(drawing_font_path)
for decorator in self.decorators.values():
decorator.font_id = font_id
def get_objects_and_decorators(self, collection):
# TODO: do it in data instead of the handler for performance?
@@ -529,28 +529,35 @@ class CreateDrawing(bpy.types.Operator):
files = {context.scene.BIMProperties.ifc_file: tool.Ifc.get()}
for link in context.scene.BIMProjectProperties.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
files[link.name] = IfcStore.session_files[link.name]
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
self.setup_serialiser(target_view)
tree = ifcopenshell.geom.tree()
tree.enable_face_styles(True)
for ifc_path, ifc in files.items():
# Don't use draw.main() just whilst we're prototyping and experimenting
# TODO: hash paths are never used
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
ifc_cache_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
self.serialiser.setFile(ifc)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
# Get all representation contexts to see what we're dealing with.
# Drawings only draw bodies and annotations (and facetation, due to a Revit bug).
# A drawing prioritises a target view context first, followed by a model view context as a fallback.
# Specifically for PLAN_VIEW and REFLECTED_PLAN_VIEW, any Plan context is also prioritised.
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
contexts = self.get_linework_contexts(ifc, target_view)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element)
self.setup_serialiser(ifc, target_view)
tree = ifcopenshell.geom.tree()
tree.enable_face_styles(True)
self.serialize_contexts_elements(ifc, tree, contexts, "body", drawing_elements, target_view)
self.serialize_contexts_elements(ifc, tree, contexts, "annotation", drawing_elements, target_view)
if self.camera_element not in drawing_elements:
if tool.Ifc.get() == ifc and self.camera_element not in drawing_elements:
with profile("Camera element"):
# The camera must always be included, regardless of any include/exclude filters.
geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True)
@@ -865,13 +872,12 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
def setup_serialiser(self, ifc, target_view):
def setup_serialiser(self, target_view):
self.svg_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
self.serialiser = ifcopenshell.geom.serializers.svg(self.svg_buffer, self.svg_settings)
self.serialiser.setFile(ifc)
self.serialiser.setWithoutStoreys(True)
self.serialiser.setPolygonal(True)
self.serialiser.setUseHlrPoly(True)
@@ -935,6 +941,18 @@ class CreateDrawing(bpy.types.Operator):
self.is_manifold_cache[obj.data.name] = True
return True
def get_element_by_guid(self, guid):
try:
return tool.Ifc.get().by_guid(guid)
except:
for link in bpy.context.scene.BIMProjectProperties.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
try:
return IfcStore.session_files[link.name].by_guid(guid)
except:
continue
def merge_linework_and_add_metadata(self, root):
join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria")
if join_criteria:
@@ -949,7 +967,7 @@ class CreateDrawing(bpy.types.Operator):
ifc = tool.Ifc.get()
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
element = ifc.by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
if "projection" in el.get("class", "").split():
classes = self.get_svg_classes(element)
@@ -962,6 +980,10 @@ class CreateDrawing(bpy.types.Operator):
el.set("class", " ".join(classes))
obj = tool.Ifc.get_object(element)
if not obj: # This is a linked model object. For now, do nothing.
continue
if not self.is_manifold(obj):
continue
@@ -315,9 +315,7 @@ class RasterStyleProperty(enum.Enum):
SPACE_SHADING = "space.shading"
RASTER_STYLE_PROPERTIES_EXCLUDE = (
"scene.render.filepath",
)
RASTER_STYLE_PROPERTIES_EXCLUDE = ("scene.render.filepath",)
class DocProperties(PropertyGroup):
@@ -371,6 +369,7 @@ class DocProperties(PropertyGroup):
default=os.path.join("drawings", "assets", "shading_styles.json"), name="Default Shading Styles"
)
shadingstyle_default: StringProperty(default="Blender Default", name="Default Shading Style")
drawing_font: StringProperty(default="OpenGost Type B TT.ttf", name="Drawing Font")
class BIMCameraProperties(PropertyGroup):
@@ -16,22 +16,21 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
import os
import re
import bpy
import string
import svgwrite
import openpyxl
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
from odf.opendocument import load as load_ods
from odf.table import Table, TableRow, TableColumn, TableCell
from odf.text import P
from odf.style import Style
from textwrap import wrap
from pathlib import Path
import string
FONT_SIZE = 4.13
FONT_WIDTH = lambda size: size * 0.45
FONT_SIZE_PT = 12
FONT_FAMILY = "OpenGost Type B TT"
DEBUG = False
@@ -63,11 +62,29 @@ class Scheduler:
)
self.padding = 1
self.margin = 1
self.parse_css(infile)
if infile.endswith("ods"):
self.schedule_ods(infile, outfile)
elif infile.endswith("xlsx"):
self.schedule_xlsx(infile, outfile)
def parse_css(self, infile):
stylesheet_path = os.path.splitext(infile)[0] + ".css"
if not os.path.exists(stylesheet_path):
stylesheet_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "schedule.css")
with open(stylesheet_path, "r") as stylesheet:
css = stylesheet.read()
matches = re.search("--font-size-pt:\s*([0-9.]+);", css)
self.font_size_pt = float(matches.groups()[0]) if matches else 12 # Default to 12pt
matches = re.search("--font-size-px:\s*([0-9.]+);", css)
self.font_size_px = float(matches.groups()[0]) if matches else 4.13 # Magic number 4.13px ~= 12pt
matches = re.search("--font-width:\s*([0-9.]+);", css)
self.font_width = float(matches.groups()[0]) if matches else 0.45 # A magic number for OpenGost
self.svg.defs.add(self.svg.style(css))
def schedule_xlsx(self, infile, outfile):
workbook = openpyxl.open(infile, data_only=True)
sheet = workbook.active
@@ -144,8 +161,8 @@ class Scheduler:
x += unmerged_width
continue
font_size = cell.font.size or 11 # 11pt default
font_size = font_size / FONT_SIZE_PT * FONT_SIZE # Magic?
font_size = cell.font.size or self.font_size_pt # 12pt default
font_size = font_size / self.font_size_pt * self.font_size_px # Magic?
text_position = [0.0, 0.0]
if cell.alignment.horizontal == "left":
@@ -424,9 +441,9 @@ class Scheduler:
italic_text = final_cell_style.get("font-style", None) == "italic"
# NOTE: very naive since we're scaling text proportionally
font_size = (
float(final_cell_style.get("font-size", f"{FONT_SIZE_PT}pt")[:-2])
/ FONT_SIZE_PT
* FONT_SIZE
float(final_cell_style.get("font-size", f"{self.font_size_pt}pt")[:-2])
/ self.font_size_pt
* self.font_size_px
)
if p_tags:
@@ -523,10 +540,7 @@ class Scheduler:
"""
text_lines = [str(p) for p in p_tags]
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
text_params = {
"font-size": font_size,
"font-family": FONT_FAMILY,
}
text_params = {"font-size": font_size}
if bold:
text_params["font-weight"] = "bold"
if italic:
@@ -546,13 +560,13 @@ class Scheduler:
text_tag = self.svg.text("", **(text_params | {"font-size": "0"} | box_alignment_params))
# TODO: should be done in less naive way
# without using magic number for FONT_WIDTH
# currently it might not work for all fonts and font sizes
# TODO: Should be done without using magic number for self.font_width
if wrap_text:
wrapped_lines = []
for line in text_lines:
wrapped_line = wrap(line, width=int(cell_width // FONT_WIDTH(font_size)), break_long_words=False)
wrapped_line = wrap(
line, width=int(cell_width // (font_size * self.font_width)), break_long_words=False
)
wrapped_lines.extend(wrapped_line)
else:
wrapped_lines = text_lines
@@ -22,7 +22,6 @@ import bpy
import math
import bmesh
import shutil
import pystache
import mathutils
import xml.etree.ElementTree as ET
import svgwrite
@@ -22,13 +22,20 @@ from . import ui, prop, operator
classes = (
operator.AddRepresentation,
operator.CopyRepresentation,
operator.DisableEditingRepresentationItemShapeAspect,
operator.DisableEditingRepresentationItemStyle,
operator.DisableEditingRepresentationItems,
operator.DuplicateLinkedAggregateTo3dCursor,
operator.DuplicateMoveLinkedAggregate,
operator.DuplicateMoveLinkedAggregateMacro,
operator.EditObjectPlacement,
operator.EditRepresentationItemShapeAspect,
operator.EditRepresentationItemStyle,
operator.EnableEditingRepresentationItemShapeAspect,
operator.EnableEditingRepresentationItemStyle,
operator.EnableEditingRepresentationItems,
operator.FlipObject,
operator.GetRepresentationIfcParameters,
operator.DuplicateMoveLinkedAggregate,
operator.DuplicateMoveLinkedAggregateMacro,
operator.OverrideDelete,
operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked,
@@ -45,19 +52,13 @@ classes = (
operator.RefreshLinkedAggregate,
operator.RemoveConnection,
operator.RemoveRepresentation,
operator.RemoveRepresentationItem,
operator.RemoveRepresentationItemFromShapeAspect,
operator.SelectConnection,
operator.SwitchRepresentation,
operator.UnassignRepresentationItemStyle,
operator.UpdateParametricRepresentation,
operator.UpdateRepresentation,
operator.RemoveRepresentationItem,
operator.EnableEditingRepresentationItemStyle,
operator.EditRepresentationItemStyle,
operator.DisableEditingRepresentationItemStyle,
operator.UnassignRepresentationItemStyle,
operator.EnableEditingRepresentationItemShapeAspect,
operator.EditRepresentationItemShapeAspect,
operator.DisableEditingRepresentationItemShapeAspect,
operator.RemoveRepresentationItemFromShapeAspect,
prop.RepresentationItem,
prop.ShapeAspect,
prop.BIMObjectGeometryProperties,
@@ -119,6 +120,7 @@ def register():
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
kmi.properties.should_save = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("wm.call_menu", "P", "PRESS")
kmi.properties.name = ui.BIM_MT_hotkey_separate.bl_idname
@@ -28,6 +28,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.placement
import ifcopenshell.api
import blenderbim.core.geometry
import blenderbim.core.geometry as core
import blenderbim.core.aggregate
import blenderbim.core.style
@@ -39,6 +40,7 @@ from mathutils import Vector, Matrix
from time import time
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.util.shape_builder import ShapeBuilder
from typing import Any
class Operator:
@@ -400,7 +402,11 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
representation_data["profile_set_usage"] = tool.Geometry.get_profile_set_usage(product)
representation_data["text_literal"] = tool.Geometry.get_text_literal(old_representation)
# TODO: replace with core.add_representation?
new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
if new_representation is None:
self.report({"ERROR"}, "Error creating representation for Blender object.")
return {"CANCELLED"}
if tool.Geometry.is_body_representation(new_representation):
[
@@ -709,6 +715,10 @@ class OverrideOutlinerDelete(bpy.types.Operator):
else:
bpy.data.objects.remove(obj)
for collection in collections_to_delete:
# Removing an aggregate object would also remove it's collection
# making the collection data-block invalid.
if not tool.Blender.is_valid_data_block(collection):
continue
bpy.data.collections.remove(collection)
if self.is_batch:
old_file = tool.Ifc.get()
@@ -720,7 +730,7 @@ class OverrideOutlinerDelete(bpy.types.Operator):
IfcStore.add_transaction_operation(self)
return {"FINISHED"}
def get_collection_objects_and_children(self, collection):
def get_collection_objects_and_children(self, collection: bpy.types.Collection) -> dict[str, Any]:
objects = set()
children = set()
queue = [collection]
@@ -938,7 +948,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new[0],pset=pset)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
@@ -973,15 +983,15 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
bl_description = "Create a new linked aggregate"
bl_description = "Create and move a new linked aggregate"
bl_idname = "bim.object_duplicate_move_linked_aggregate_macro"
bl_label = "IFC Duplicate Linked Aggregate"
bl_label = "IFC Duplicate and Move Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
class DuplicateMoveLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.object_duplicate_move_linked_aggregate"
bl_label = "IFC Duplicate Linked Aggregate"
bl_label = "IFC Duplicate and Move Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
@@ -996,7 +1006,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
return DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
@staticmethod
def execute_ifc_duplicate_linked_aggregate_operator(self, context):
def execute_ifc_duplicate_linked_aggregate_operator(self, context, location_from_3d_cursor=False):
self.new_active_obj = None
self.group_name = "BBIM_Linked_Aggregate"
self.pset_name = "BBIM_Linked_Aggregate"
@@ -1050,7 +1060,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if self.group_name in product_groups_name:
return
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.group_name)
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.group_name)
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group)
def custom_incremental_naming_for_element_assembly(old_to_new):
@@ -1109,6 +1119,16 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
]
tool.Ifc.run("group.assign_group", group=linked_aggregate_group[0], products=new)
def get_location_from_3d_cursor(old_to_new, aggregate):
base_obj = tool.Ifc.get_object(aggregate)
base_obj_location = base_obj.location.copy()
for new in old_to_new.values():
new_obj = tool.Ifc.get_object(new[0])
location_diff = new_obj.location - base_obj_location
new_obj.location = context.scene.cursor.location + location_diff
if len(context.selected_objects) != 1:
return {"FINISHED"}
@@ -1135,11 +1155,29 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
custom_incremental_naming_for_element_assembly(old_to_new)
if location_from_3d_cursor:
get_location_from_3d_cursor(old_to_new, selected_element)
blenderbim.bim.handler.refresh_ui_data()
return old_to_new
class DuplicateLinkedAggregateTo3dCursor(bpy.types.Operator):
bl_idname = "bim.duplicate_linked_aggregate_to_3d_cursor"
bl_label = "IFC Duplicate Linked Aggregate to 3d Cursor"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
def _execute(self, context):
return DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context, location_from_3d_cursor=True)
class RefreshLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.refresh_linked_aggregate"
bl_label = "IFC Refresh Linked Aggregate"
@@ -1651,20 +1689,11 @@ class OverrideModeSetObject(bpy.types.Operator):
apply_openings=True,
)
def draw(self, context):
if self.is_valid:
row = self.layout.row()
row.prop(self, "should_save")
else:
row = self.layout.row()
row.label(text="No Geometry Found: Object will revert to previous state.")
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, is_invoke=True)
def _invoke(self, context, event):
self.is_valid = True
self.should_save = True
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
@@ -1726,8 +1755,6 @@ class OverrideModeSetObject(bpy.types.Operator):
else:
tool.Ifc.finish_edit(obj)
if self.edited_objs:
return context.window_manager.invoke_props_dialog(self)
return self.execute(context)
@@ -34,6 +34,8 @@ from blenderbim.bim.module.layer.data import LayersData
def mode_menu(self, context):
row = self.layout.row(align=True)
if context.scene.BIMGeometryProperties.mode == "EDIT":
row.operator("bim.override_mode_set_object", icon="CANCEL", text="Discard Changes").should_save = False
row.prop(context.scene.BIMGeometryProperties, "mode", text="", icon_value=blenderbim.bim.icons["IFC"].icon_id)
@@ -247,9 +247,11 @@ class IFCGIT_PT_revision_inspector(bpy.types.Panel):
bl_idname = "IFCGIT_PT_revision_inspector"
bl_label = "Git History"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "IFCGIT_PT_panel"
def draw(self, context):
@@ -30,7 +30,6 @@ classes = (
operator.AssignMaterial,
operator.AssignParameterizedProfile,
operator.ContractMaterialCategory,
operator.CopyMaterial,
operator.DisableEditingAssignedMaterial,
operator.DisableEditingMaterial,
operator.DisableEditingMaterialSetItem,
@@ -64,7 +63,6 @@ classes = (
prop.BIMMaterialProperties,
prop.BIMObjectMaterialProperties,
ui.BIM_PT_materials,
ui.BIM_PT_material,
ui.BIM_PT_object_material,
ui.BIM_UL_materials,
)
@@ -36,31 +36,17 @@ class MaterialsData:
@classmethod
def load(cls):
cls.data = {
"total_materials": cls.total_materials(),
"material_types": cls.material_types(),
"profiles": cls.profiles(),
"styles": cls.styles(),
"contexts": cls.contexts(),
"active_styles": cls.active_styles(),
}
cls.is_loaded = True
cls.data["material_types"] = cls.material_types()
cls.data["total_materials"] = cls.total_materials()
cls.data["profiles"] = cls.profiles()
cls.data["styles"] = cls.styles()
cls.data["contexts"] = cls.contexts()
cls.data["active_styles"] = cls.active_styles()
@classmethod
def total_materials(cls):
if tool.Ifc.get_schema() == "IFC2X3":
return (
len(tool.Ifc.get().by_type("IfcMaterial"))
+ len(tool.Ifc.get().by_type("IfcMaterialLayerSet"))
+ len(tool.Ifc.get().by_type("IfcMaterialList"))
)
return (
len(tool.Ifc.get().by_type("IfcMaterial"))
+ len(tool.Ifc.get().by_type("IfcMaterialConstituentSet"))
+ len(tool.Ifc.get().by_type("IfcMaterialLayerSet"))
+ len(tool.Ifc.get().by_type("IfcMaterialProfileSet"))
+ len(tool.Ifc.get().by_type("IfcMaterialList"))
)
return len(tool.Ifc.get().by_type(bpy.context.scene.BIMMaterialProperties.material_type))
@classmethod
def material_types(cls):
@@ -120,6 +120,8 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty(name="Material Name")
name: bpy.props.StringProperty(default="Default")
category: bpy.props.StringProperty(default="")
description: bpy.props.StringProperty(default="")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -127,10 +129,14 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
def draw(self, context):
row = self.layout
row.prop(self, "name", text="Name")
row = self.layout
row.prop(self, "description", text="Description")
row = self.layout
row.prop(self, "category", text="Category")
def _execute(self, context):
obj = bpy.data.materials.get(self.obj) if self.obj else None
core.add_material(tool.Ifc, tool.Material, tool.Style, obj=obj, name=self.name)
core.add_material(tool.Ifc, tool.Material, tool.Style, obj=obj, name=self.name, category=self.category, description=self.description)
material_prop_purge()
@@ -141,8 +147,19 @@ class DuplicateMaterial(bpy.types.Operator, tool.Ifc.Operator):
material: bpy.props.IntProperty(name="Material ID")
def _execute(self, context):
ifc_file = tool.Ifc.get()
tool.Material.duplicate_material(ifc_file.by_id(self.material))
material = tool.Ifc.get().by_id(self.material)
new = tool.Ifc.run("material.copy_material", material=material)
blender_material = tool.Ifc.get_object(material)
new_blender = blender_material.copy()
new_blender.use_fake_user = True
tool.Ifc.link(new, new_blender)
if not new.is_a("IfcMaterialList"):
name = new[0] + " Copy"
new[0] = name
new_blender.name = name
material_prop_purge()
bpy.ops.bim.load_materials()
@@ -185,9 +202,10 @@ class UnlinkMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unlink_material"
bl_label = "Unlink Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty(name="Material ID")
def _execute(self, context):
core.unlink_material(tool.Ifc, obj=context.active_object.active_material)
core.unlink_material(tool.Ifc, obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.material)))
class AssignMaterial(bpy.types.Operator, tool.Ifc.Operator):
@@ -544,6 +562,7 @@ class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.props = obj.BIMObjectMaterialProperties
self.props.active_material_set_item_id = self.material_set_item
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)
@@ -559,6 +578,7 @@ class DisableEditingMaterialSetItemProfile(bpy.types.Operator):
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.props = obj.BIMObjectMaterialProperties
self.props.active_material_set_item_id = 0
self.props.material_set_item_profile_attributes.clear()
return {"FINISHED"}
@@ -679,45 +699,31 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.disable_editing_material_set_item(obj=obj.name)
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):
blender_material = context.active_object.active_material
material = tool.Ifc.get_entity(blender_material)
if tool.Ifc.has_changed_shading(blender_material):
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=blender_material)
copied_material = ifcopenshell.api.run("material.copy_material", tool.Ifc.get(), material=material)
copied_blender_material = blender_material.copy()
copied_style = self.get_style(copied_material)
tool.Ifc.link(copied_material, copied_blender_material)
if copied_style:
tool.Ifc.link(copied_style, copied_blender_material)
context.active_object.active_material = copied_blender_material
def get_style(self, material):
for material_representation in material.HasRepresentation:
for representation in material_representation.Representations:
for item in representation.Items:
for style in item.Styles:
if style.is_a("IfcSurfaceStyle"):
return style
class ExpandMaterialCategory(bpy.types.Operator):
bl_idname = "bim.expand_material_category"
bl_label = "Expand Material Category"
bl_description = "Expand material category.\n\nSHIFT+CLICK to expand all material categories"
bl_options = {"REGISTER", "UNDO"}
category: bpy.props.StringProperty()
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# Expanding all categories on shift+click.
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck.
if event.type == "LEFTMOUSE" and event.shift:
self.expand_all = True
return self.execute(context)
def execute(self, context):
props = context.scene.BIMMaterialProperties
for category in [c for c in props.materials if c.is_category and c.name == self.category]:
for index, category in [
(i, c)
for i, c in enumerate(props.materials)
if c.is_category and (self.expand_all or c.name == self.category)
]:
category.is_expanded = True
if category.name == self.category:
props.active_material_index = index
core.load_materials(tool.Material, props.material_type)
return {"FINISHED"}
@@ -725,13 +731,28 @@ class ExpandMaterialCategory(bpy.types.Operator):
class ContractMaterialCategory(bpy.types.Operator):
bl_idname = "bim.contract_material_category"
bl_label = "Contract Material Category"
bl_description = "Contract material category.\n\nSHIFT+CLICK to contract all material categories"
bl_options = {"REGISTER", "UNDO"}
category: bpy.props.StringProperty()
contract_all: bpy.props.BoolProperty(name="Contract All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# Contracting all categories on shift+click.
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck.
if event.type == "LEFTMOUSE" and event.shift:
self.contract_all = True
return self.execute(context)
def execute(self, context):
props = context.scene.BIMMaterialProperties
for category in [c for c in props.materials if c.is_category and c.name == self.category]:
for index, category in [
(i, c)
for i, c in enumerate(props.materials)
if c.is_category and (self.contract_all or c.name == self.category)
]:
category.is_expanded = False
if category.name == self.category:
props.active_material_index = index
core.load_materials(tool.Material, props.material_type)
return {"FINISHED"}
@@ -97,6 +97,10 @@ def get_material_types(self, context):
return MaterialsData.data["material_types"]
def update_material_type(self, context):
MaterialsData.data["total_materials"] = MaterialsData.total_materials()
def get_profiles(self, context):
if not MaterialsData.is_loaded:
MaterialsData.load()
@@ -119,14 +123,14 @@ class Material(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
is_category: BoolProperty(name="Is Category", default=False)
is_expanded: BoolProperty(name="Is Expanded", default=True)
is_expanded: BoolProperty(name="Is Expanded", default=False)
has_style: BoolProperty(name="Has Style", default=True)
total_elements: IntProperty(name="Total Elements")
class BIMMaterialProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
material_type: EnumProperty(items=get_material_types, name="Material Type")
material_type: EnumProperty(items=get_material_types, update=update_material_type, name="Material Type")
materials: CollectionProperty(name="Materials", type=Material)
active_material_index: IntProperty(name="Active Material Index")
profiles: EnumProperty(items=get_profiles, name="Profiles")
@@ -45,7 +45,7 @@ class BIM_PT_materials(Panel):
self.props = context.scene.BIMMaterialProperties
row = self.layout.row(align=True)
row.label(text="{} Materials".format(MaterialsData.data["total_materials"]), icon="NODE_MATERIAL")
row.label(text=f"{MaterialsData.data['total_materials']} Materials", icon="NODE_MATERIAL")
if self.props.is_editing:
row.operator("bim.disable_editing_materials", text="", icon="CANCEL")
else:
@@ -69,6 +69,8 @@ class BIM_PT_materials(Panel):
op.material = material.ifc_definition_id
op = row.operator("bim.enable_editing_material_style", text="", icon="SHADING_RENDERED")
op.material = material.ifc_definition_id
op = row.operator("bim.unlink_material", icon="UNLINKED", text="")
op.material = material.ifc_definition_id
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
self.draw_editing_ui()
@@ -114,36 +116,14 @@ class BIM_PT_materials(Panel):
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
class BIM_PT_material(Panel):
bl_label = "Material"
bl_idname = "BIM_PT_material"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
@classmethod
def poll(cls, context):
return IfcStore.get_file() and context.active_object and context.active_object.active_material
def draw(self, context):
row = self.layout.row(align=True)
material_id = context.active_object.active_material.BIMObjectProperties.ifc_definition_id
if bool(material_id):
row.operator("bim.remove_material", icon="X", text="Remove IFC Material").material = material_id
row.operator("bim.copy_material", icon="DUPLICATE", text="")
row.operator("bim.unlink_material", icon="UNLINKED", text="")
else:
op = row.operator("bim.add_material", icon="ADD", text="Create IFC Material")
op.obj = context.active_object.active_material.name
class BIM_PT_object_material(Panel):
bl_label = "Object Material"
bl_idname = "BIM_PT_object_material"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_materials"
bl_parent_id = "BIM_PT_tab_object_materials"
bl_options = {"HIDE_HEADER"}
@classmethod
def poll(cls, context):
@@ -248,7 +228,7 @@ class BIM_PT_object_material(Panel):
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):
if len(self.props.material_set_item_profile_attributes) and self.props.active_material_set_item_id == set_item["id"]:
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)
@@ -363,7 +343,7 @@ class BIM_UL_materials(UIList):
row.operator(
"bim.expand_material_category", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).category = item.name
row.label(text=item.name or "Uncategorised")
row.label(text=item.name)
else:
row.label(text="", icon="BLANK1")
row.label(text=item.name, icon="MATERIAL")
@@ -23,6 +23,7 @@ import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.misc as core
import blenderbim.core.geometry as core_geometry
import blenderbim.core.root
from blenderbim.bim.ifc import IfcStore
from mathutils import Vector, Matrix, Euler
@@ -134,6 +135,10 @@ class ResizeToStorey(bpy.types.Operator, Operator):
class SplitAlongEdge(bpy.types.Operator, Operator):
bl_idname = "bim.split_along_edge"
bl_label = "Split Along Edge"
bl_description = (
"Active object is considered to be a cutting object."
"Will unassign element from a type if type has a representation."
)
bl_options = {"REGISTER", "UNDO"}
@classmethod
@@ -144,16 +149,25 @@ class SplitAlongEdge(bpy.types.Operator, Operator):
cutter = context.active_object
objs = [o for o in context.selected_objects if o != cutter]
objs_to_cut = []
# Splitting only works on meshes
for obj in objs:
# You cannot split meshes if the representation is mapped.
element = tool.Ifc.get_entity(obj)
if element:
relating_type = tool.Root.get_element_type(element)
if relating_type and tool.Root.does_type_have_representations(relating_type):
bpy.ops.bim.unassign_type(related_object=obj.name)
if not element:
continue
relating_type = tool.Root.get_element_type(element)
if relating_type and tool.Root.does_type_have_representations(relating_type):
bpy.ops.bim.unassign_type(related_object=obj.name)
# refresh representation
representation = tool.Geometry.get_active_representation(obj)
# skip empty objects that might get in the way
if not representation:
continue
core_geometry.switch_representation(
tool.Ifc,
tool.Geometry,
@@ -168,11 +182,13 @@ class SplitAlongEdge(bpy.types.Operator, Operator):
if not tool.Geometry.is_meshlike(representation):
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="IfcTessellatedFaceSet")
new_objs = tool.Misc.split_objects_with_cutter(objs, cutter)
objs_to_cut.append(obj)
new_objs = tool.Misc.split_objects_with_cutter(objs_to_cut, cutter)
for obj in new_objs:
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
bpy.ops.bim.update_representation(obj=obj.name)
for obj in objs:
for obj in objs_to_cut:
bpy.ops.bim.update_representation(obj=obj.name)
representation = tool.Geometry.get_active_representation(obj)
@@ -187,6 +203,8 @@ class SplitAlongEdge(bpy.types.Operator, Operator):
apply_openings=True,
)
self.report({"INFO"}, f"Splitting finished, {len(new_objs)} new objects created.")
class GetConnectedSystemElements(bpy.types.Operator, Operator):
bl_idname = "bim.get_connected_system_elements"
@@ -22,10 +22,11 @@ import bpy
class BIM_PT_misc_utilities(bpy.types.Panel):
bl_idname = "BIM_PT_misc_utilities"
bl_label = "Miscellaneous"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "output"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
bl_parent_id = "BIM_PT_tab_sandbox"
def draw(self, context):
layout = self.layout
@@ -208,7 +208,7 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
pset = tool.Ifc.get().by_id(pset["id"])
if len(data) == 1:
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
else:
del data[self.item]
data = tool.Ifc.get().createIfcText(json.dumps(data))
@@ -53,7 +53,7 @@ class AuthoringData:
cls.data["ifc_element_type"] = cls.ifc_element_type
cls.data["ifc_classes"] = cls.ifc_classes()
cls.data["relating_type_id"] = cls.relating_type_id() # only after .ifc_classes()
cls.data["predefined_type"] = cls.predefined_type()
cls.data["predefined_type"] = cls.predefined_type() # only after .relating_type_id()
cls.data["type_class"] = cls.type_class()
# only after .type_class()
@@ -121,9 +121,10 @@ class AuthoringData:
def type_thumbnail(cls):
if not cls.data["relating_type_id"]:
return 0
if not tool.Blender.enum_property_has_valid_index(cls.props, "relating_type_id", cls.data["relating_type_id"]):
relating_type_id = tool.Blender.get_enum_safe(cls.props, "relating_type_id")
if relating_type_id is None:
return 0
element = tool.Ifc.get().by_id(int(cls.props.relating_type_id))
element = tool.Ifc.get().by_id(int(relating_type_id))
return cls.type_thumbnails.get(element.id(), None) or 0
@classmethod
@@ -251,8 +252,8 @@ class AuthoringData:
@classmethod
def predefined_type(cls):
relating_type_id = cls.props.relating_type_id
if not relating_type_id:
relating_type_id = tool.Blender.get_enum_safe(cls.props, "relating_type_id")
if relating_type_id is None:
return
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
if not hasattr(relating_type, "PredefinedType"):
@@ -22,20 +22,24 @@ import bmesh
from bmesh.types import BMVert, BMFace
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.representation
from ifcopenshell.util.shape_builder import V
import blenderbim
import blenderbim.tool as tool
import blenderbim.core.geometry
import blenderbim.core.geometry as core
from blenderbim.bim.module.model.window import create_bm_window, create_bm_box, update_simple_openings
import blenderbim.core.root
from blenderbim.bim.module.model.window import create_bm_window, create_bm_box
from mathutils import Vector, Matrix
from pprint import pprint
import json
import collections
def update_door_modifier_representation(context, obj):
def update_door_modifier_representation(context: bpy.types.Context, obj: bpy.types.Object) -> None:
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
@@ -159,7 +163,7 @@ def update_door_modifier_representation(context, obj):
occurrence.OverallWidth = props.overall_width / si_conversion
occurrence.OverallHeight = props.overall_height / si_conversion
update_simple_openings(element, props.overall_width / si_conversion, props.overall_height / si_conversion)
tool.Model.update_simple_openings(element)
# TODO: move it out to tools
@@ -644,6 +648,6 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMDoorProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -44,6 +44,7 @@ from bpy.types import SpaceView3D
from bpy.props import FloatProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
from gpu_extras.batch import batch_for_shader
from typing import Union, Optional, Any
class AddFilledOpening(bpy.types.Operator, tool.Ifc.Operator):
@@ -59,9 +60,15 @@ class AddFilledOpening(bpy.types.Operator, tool.Ifc.Operator):
class FilledOpeningGenerator:
def generate(self, filling_obj, voided_obj, target=None):
def generate(
self,
filling_obj: Union[bpy.types.Object, None],
voided_obj: Union[bpy.types.Object, None],
target: Optional[Vector] = None,
) -> None:
props = bpy.context.scene.BIMModelProperties
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
opening_thickness_si = 0.0
filling = tool.Ifc.get_entity(filling_obj)
element = tool.Ifc.get_entity(voided_obj)
@@ -92,6 +99,7 @@ class FilledOpeningGenerator:
# In this prototype, we assume openings are only added to axis-based elements
layers = tool.Model.get_material_layer_parameters(element)
if layers["layer_set_direction"] == "AXIS2":
opening_thickness_si = layers["thickness"] * 2
axis = tool.Model.get_wall_axis(voided_obj, layers=layers)["base"]
new_matrix = voided_obj.matrix_world.copy()
point_on_axis = tool.Cad.point_on_edge(target, axis)
@@ -122,44 +130,37 @@ class FilledOpeningGenerator:
existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling)
if existing_opening_occurrence:
opening = ifcopenshell.api.run(
"root.create_entity",
tool.Ifc.get(),
ifc_class="IfcOpeningElement",
predefined_type="OPENING",
name="Opening",
)
ifcopenshell.api.run(
"geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=filling_obj.matrix_world
)
opening = ifcopenshell.api.run(
"root.create_entity",
tool.Ifc.get(),
ifc_class="IfcOpeningElement",
predefined_type="OPENING",
name="Opening",
)
ifcopenshell.api.run(
"geometry.edit_object_placement",
tool.Ifc.get(),
product=opening,
matrix=np.array(filling_obj.matrix_world),
is_si=True,
)
if existing_opening_occurrence:
representation = ifcopenshell.util.representation.get_representation(
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
)
representation = ifcopenshell.util.representation.resolve_representation(representation)
mapped_representation = ifcopenshell.api.run(
"geometry.map_representation", tool.Ifc.get(), representation=representation
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=opening, representation=mapped_representation
)
else:
representation = self.generate_opening_from_filling(filling, filling_obj)
opening = ifcopenshell.api.run(
"root.create_entity", tool.Ifc.get(), ifc_class="IfcOpeningElement", predefined_type="OPENING"
representation = self.generate_opening_from_filling(
filling, filling_obj, opening_thickness_si=opening_thickness_si
)
matrix = np.array(filling_obj.matrix_world)
ifcopenshell.api.run(
"geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=matrix, is_si=True
)
mapped_representation = ifcopenshell.api.run(
"geometry.map_representation", tool.Ifc.get(), representation=representation
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=opening, representation=mapped_representation
)
mapped_representation = ifcopenshell.api.run(
"geometry.map_representation", tool.Ifc.get(), representation=representation
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=opening, representation=mapped_representation
)
ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=opening, element=element)
ifcopenshell.api.run("void.add_filling", tool.Ifc.get(), opening=opening, element=filling)
@@ -187,7 +188,7 @@ class FilledOpeningGenerator:
should_sync_changes_first=False,
)
def regenerate_from_type(self, usecase_path, ifc_file, settings):
def regenerate_from_type(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
relating_type = settings["relating_type"]
for related_object in settings["related_objects"]:
@@ -252,10 +253,15 @@ class FilledOpeningGenerator:
should_sync_changes_first=False,
)
def generate_opening_from_filling(self, filling, filling_obj):
def generate_opening_from_filling(
self,
filling: ifcopenshell.entity_instance,
filling_obj: bpy.types.Object,
opening_thickness_si: float = 0.0,
) -> ifcopenshell.entity_instance:
# Since openings are reused later, we give a default thickness of 1.2m
# which should cover the majority of curved, or super thick walls.
thickness = 1.2
thickness = max(1.2, opening_thickness_si)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -346,7 +352,9 @@ class FilledOpeningGenerator:
return True
return False
def get_existing_opening_occurrence_if_any(self, filling):
def get_existing_opening_occurrence_if_any(
self, filling: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
filling_type = ifcopenshell.util.element.get_type(filling)
if filling_type:
filling_occurrences = ifcopenshell.util.element.get_types(filling_type)
@@ -138,7 +138,10 @@ class AddConstrTypeInstance(bpy.types.Operator):
if not relating_type_id:
return {"FINISHED"}
if self.from_invoke:
# Check relating_type_id enum_items since it's possible
# that we're adding e.g. IfcRoofType being in a Slab Tool
# and roof type id won't be present in the relating_type_id enum.
if self.from_invoke and str(self.relating_type_id) in AuthoringData.data["relating_type_id"]:
props.relating_type_id = str(self.relating_type_id)
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
@@ -94,7 +94,6 @@ class DumbProfileGenerator:
obj=obj,
ifc_class=ifc_class,
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
@@ -931,13 +930,11 @@ class PatchNonParametricMepSegment(bpy.types.Operator, tool.Ifc.Operator):
return context.active_object
def _execute(self, context):
styles = tool.Geometry.get_styles(context.active_object)
blenderbim.core.material.patch_non_parametric_mep_segment(
tool.Ifc, tool.Material, tool.Profile, obj=context.active_object
)
bpy.ops.bim.enable_editing_extrusion_axis()
bpy.ops.bim.edit_extrusion_axis()
styles = tool.Geometry.get_styles(context.active_object)
class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
@@ -60,7 +60,7 @@ def update_ifc_class(self, context):
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
if not tool.Blender.enum_property_has_valid_index(self, "relating_type_id", AuthoringData.data["relating_type_id"]):
if tool.Blender.get_enum_safe(self, "relating_type_id") is None:
self["relating_type_id"] = 0
@@ -20,8 +20,12 @@
import bpy
import bmesh
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.representation
import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import V
import blenderbim
import blenderbim.core.root
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.door import bm_sort_out_geom
@@ -535,5 +539,5 @@ class RemoveRailing(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMRailingProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -20,7 +20,11 @@ import bpy
import bmesh
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import blenderbim
import blenderbim.core.root
import blenderbim.tool as tool
from blenderbim.bim.helper import convert_property_group_from_si
from blenderbim.bim.module.model.door import bm_sort_out_geom
@@ -757,7 +761,7 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMRoofProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Roof")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -176,7 +176,6 @@ class DumbSlabGenerator:
obj=obj,
ifc_class=ifc_class,
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
@@ -20,7 +20,11 @@ import bpy
import json
import bmesh
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import blenderbim
import blenderbim.core.root
import blenderbim.tool as tool
from mathutils import Vector
from bmesh.types import BMVert
@@ -217,7 +221,6 @@ class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
@@ -225,7 +228,6 @@ class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
obj=obj,
ifc_class="IfcStairFlight",
should_add_representation=False,
context=body_context,
)
if tool.Ifc.get_schema() != "IFC2X3":
element.PredefinedType = "STRAIGHT"
@@ -337,6 +339,6 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMStairProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -607,7 +607,6 @@ class DumbWallGenerator:
obj=obj,
ifc_class=ifc_class,
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
if self.axis_context:
@@ -26,66 +26,16 @@ import blenderbim.tool as tool
import blenderbim.core.root
import blenderbim.core.geometry
from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import V
from bmesh.types import BMVert
from mathutils import Vector
# TODO: move to some utils helpers/tool module
def update_simple_openings(element, opening_width, opening_height):
ifc_file = tool.Ifc.get()
fillings = tool.Ifc.get_all_element_occurrences(element)
voided_objs = set()
has_replaced_opening_representation = False
for filling in fillings:
if not filling.FillsVoids:
continue
opening = filling.FillsVoids[0].RelatingOpeningElement
voided_obj = tool.Ifc.get_object(opening.VoidsElements[0].RelatingBuildingElement)
voided_objs.add(voided_obj)
# we assume that the same element type (e.g. window)
# will be used only for voiding objects of the same thickness (by y-dimension)
# (e.g. all walls window's attached to will share the same thickness)
# If that's not the case it will some linings will be too thick or too thin
if has_replaced_opening_representation:
continue
old_representation = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
old_representation = tool.Geometry.resolve_mapped_representation(old_representation)
ifcopenshell.api.run(
"geometry.unassign_representation", ifc_file, product=opening, representation=old_representation
)
thickness = voided_obj.dimensions[1] + 0.1 + 0.1
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
extrusion = shape_builder.extrude(
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height]).xz),
magnitude=thickness / unit_scale,
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
**shape_builder.extrude_kwargs("Y")
)
new_representation = shape_builder.get_representation(context, extrusion)
for inverse in ifc_file.get_inverse(old_representation):
ifcopenshell.util.element.replace_attribute(inverse, old_representation, new_representation)
ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=old_representation)
has_replaced_opening_representation = True
tool.Model.reload_body_representation(voided_objs)
if fillings:
with bpy.context.temp_override(selected_objects=[tool.Ifc.get_object(f) for f in fillings]):
bpy.ops.bim.recalculate_fill()
def update_window_modifier_representation(context, obj):
element = tool.Ifc.get_entity(obj)
props = obj.BIMWindowProperties
@@ -175,7 +125,7 @@ def update_window_modifier_representation(context, obj):
occurrence.OverallWidth = props.overall_width / si_conversion
occurrence.OverallHeight = props.overall_height / si_conversion
update_simple_openings(element, props.overall_width / si_conversion, props.overall_height / si_conversion)
tool.Model.update_simple_openings(element)
def create_bm_window_frame(bm, size: Vector, thickness: list, position: Vector = V(0, 0, 0).freeze()):
@@ -590,6 +540,6 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMWindowProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -22,7 +22,7 @@ import ifcopenshell
import ifcopenshell.util.unit
import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
from blenderbim.bim.helper import prop_with_search, close_operator_panel
from blenderbim.bim.helper import prop_with_search
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.drawing.data import DecoratorData
@@ -485,7 +485,7 @@ class BimToolUI:
cls.layout.separator()
add_layout_hotkey_operator(
cls.layout, "Calculate All Quantities", "S_Q", bpy.ops.bim.calculate_all_quantities.__doc__
cls.layout, "Perform Quantity Take-off", "S_Q", bpy.ops.bim.perform_quantity_take_off.__doc__
)
@classmethod
@@ -614,7 +614,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.selected_objects:
return
bpy.ops.bim.calculate_all_quantities()
bpy.ops.bim.perform_quantity_take_off()
def hotkey_C_P(self):
if not bpy.context.selected_objects:
@@ -175,7 +175,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
def disable_editing_arbitrary_profile(context):
obj = context.active_object
if obj and obj.data and obj.data.BIMMeshProperties.subshape_type == "PROFILE":
if obj and obj.type == "MESH" and obj.data and obj.data.BIMMeshProperties.subshape_type == "PROFILE":
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
profile_mesh = obj.data
@@ -36,8 +36,8 @@ classes = (
operator.EnableCulling,
operator.EnableEditingHeader,
operator.ExportIFC,
operator.ExportIFCDeprecated,
operator.FlipClippingPlane,
operator.ImportIFC,
operator.LinkIfc,
operator.LoadLink,
operator.LoadLinkedProject,
@@ -76,6 +76,11 @@ classes = (
gizmo.ClippingPlane,
)
if bpy.app.version >= (4, 1, 0):
classes += (
operator.IFCFileHandlerOperator,
operator.BIM_FH_import_ifc,
)
addon_keymaps = []
@@ -97,7 +102,7 @@ def register():
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
kmi = km.keymap_items.new("export_ifc.bim", "S", "PRESS", ctrl=True)
kmi = km.keymap_items.new("bim.save_project", "S", "PRESS", ctrl=True)
kmi.properties.should_save_as = False
addon_keymaps.append((km, kmi))
@@ -1106,8 +1106,8 @@ class ToggleLinkVisibility(bpy.types.Operator):
]
class ExportIFC(bpy.types.Operator):
bl_idname = "export_ifc.bim"
class ExportIFCBase:
bl_idname = "bim.save_project"
bl_label = "Save IFC"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc"
@@ -1224,14 +1224,20 @@ class ExportIFC(bpy.types.Operator):
return "Save the IFC file. Will save both .IFC/.BLEND files if synced together"
class ImportIFC(bpy.types.Operator):
bl_idname = "import_ifc.bim"
bl_label = "Import IFC"
bl_options = {"REGISTER", "UNDO"}
class ExportIFC(ExportIFCBase, bpy.types.Operator):
pass
# TODO: remove as deprecated, better wait couple releases since
# this operator is used for saving IFC files in user scripts.
class ExportIFCDeprecated(ExportIFCBase, bpy.types.Operator):
bl_idname = "export_ifc.bim"
def execute(self, context):
bpy.ops.bim.load_project("INVOKE_DEFAULT")
return {"FINISHED"}
msg = f"'{ExportIFCDeprecated.bl_idname}' operator name is deprecated, use '{ExportIFC.bl_idname}'."
self.report({"WARNING"}, msg)
print(msg)
return super().execute(context)
class LoadLinkedProject(bpy.types.Operator):
@@ -2079,3 +2085,46 @@ class BIM_OT_load_clipping_planes(bpy.types.Operator):
obj.location = values["location"]
obj.rotation_euler = values["rotation"]
return {"FINISHED"}
if bpy.app.version >= (4, 1, 0):
class IFCFileHandlerOperator(bpy.types.Operator):
bl_idname = "bim.load_project_file_handler"
bl_label = "Import .ifc file"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
directory: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"})
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"})
def invoke(self, context, event):
# Keeping code in .invoke() as we'll probably add some
# popup windows later.
# `files` contain only .ifc files.
filepath = Path(self.directory)
# If user is just drag'n'dropping a single file -> load it as a new project,
# if they're holding ALT -> link the file/files to the current project.
if event.alt:
# Passing self.files directly results in TypeError.
serialized_files = [{"name": f.name} for f in self.files]
return bpy.ops.bim.link_ifc(directory=self.directory, files=serialized_files)
else:
if len(self.files) == 1:
return bpy.ops.bim.load_project(filepath=(filepath / self.files[0].name).as_posix())
else:
self.report(
{"INFO"},
"To link multiple IFC files hold ALT while drag'n'dropping them.",
)
return {"FINISHED"}
class BIM_FH_import_ifc(bpy.types.FileHandler):
bl_label = "IFC File Handler"
bl_import_operator = IFCFileHandlerOperator.bl_idname
bl_file_extensions = ".ifc"
# FileHandler won't work without poll_drop defined.
@classmethod
def poll_drop(cls, context):
return True
@@ -68,9 +68,9 @@ def file_menu(self, context):
op = self.layout.operator("bim.load_project", text="Open IFC Project", icon="FILEBROWSER")
op.should_start_fresh_session = True
self.layout.separator()
op = self.layout.operator("export_ifc.bim", icon="FILE_TICK", text="Save IFC Project")
op = self.layout.operator("bim.save_project", icon="FILE_TICK", text="Save IFC Project")
op.should_save_as = False
op = self.layout.operator("export_ifc.bim", text="Save IFC Project As...")
op = self.layout.operator("bim.save_project", text="Save IFC Project As...")
op.should_save_as = True
self.layout.separator()
self.layout.operator("bim.revert_project")
@@ -20,14 +20,13 @@ import bpy
from . import ui, prop, operator
classes = (
operator.AddProposedProp,
operator.AddPset,
operator.AddQto,
operator.CopyPropertyToSelection,
operator.DisablePsetEditing,
operator.EditPset,
operator.EnablePsetEditing,
operator.GuessQuantity,
operator.CalculateQuantity,
operator.RemovePset,
operator.TogglePsetExpansion,
operator.BIM_OT_add_property_to_edit,
@@ -39,14 +38,6 @@ classes = (
prop.IfcPropertyEnumeratedValue,
prop.IfcProperty,
prop.PsetProperties,
prop.MaterialPsetProperties,
prop.MaterialSetPsetProperties,
prop.MaterialSetItemPsetProperties,
prop.TaskPsetProperties,
prop.ResourcePsetProperties,
prop.GroupPsetProperties,
prop.ProfilePsetProperties,
prop.WorkSchedulePsetProperties,
prop.RenameProperties,
prop.AddEditProperties,
prop.DeletePsets,
@@ -71,14 +62,14 @@ classes = (
def register():
bpy.types.Object.PsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Object.MaterialSetPsetProperties = bpy.props.PointerProperty(type=prop.MaterialSetPsetProperties)
bpy.types.Object.MaterialSetItemPsetProperties = bpy.props.PointerProperty(type=prop.MaterialSetItemPsetProperties)
bpy.types.Material.PsetProperties = bpy.props.PointerProperty(type=prop.MaterialPsetProperties)
bpy.types.Scene.TaskPsetProperties = bpy.props.PointerProperty(type=prop.TaskPsetProperties)
bpy.types.Scene.ResourcePsetProperties = bpy.props.PointerProperty(type=prop.ResourcePsetProperties)
bpy.types.Scene.GroupPsetProperties = bpy.props.PointerProperty(type=prop.GroupPsetProperties)
bpy.types.Scene.ProfilePsetProperties = bpy.props.PointerProperty(type=prop.ProfilePsetProperties)
bpy.types.Scene.WorkSchedulePsetProperties = bpy.props.PointerProperty(type=prop.WorkSchedulePsetProperties)
bpy.types.Scene.MaterialPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Object.MaterialSetPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Object.MaterialSetItemPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.TaskPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.ResourcePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.GroupPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.ProfilePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.WorkSchedulePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.RenameProperties = bpy.props.CollectionProperty(type=prop.RenameProperties)
bpy.types.Scene.AddEditProperties = bpy.props.CollectionProperty(type=prop.AddEditProperties)
bpy.types.Scene.DeletePsets = bpy.props.CollectionProperty(type=prop.DeletePsets)
@@ -86,7 +77,9 @@ def register():
def unregister():
del bpy.types.Object.PsetProperties
del bpy.types.Material.PsetProperties
del bpy.types.Scene.MaterialPsetProperties
del bpy.types.Object.MaterialSetPsetProperties
del bpy.types.Object.MaterialSetItemPsetProperties
del bpy.types.Scene.TaskPsetProperties
del bpy.types.Scene.ResourcePsetProperties
del bpy.types.Scene.GroupPsetProperties
@@ -1,577 +0,0 @@
mapper = {
'Qto_AudioVisualApplianceBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_PlateBaseQuantities' : {
'Width' : "get_height",
'Perimeter' : "get_gross_perimeter",
'GrossArea' : "get_gross_footprint_area",
'NetArea' : "get_net_footprint_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
'GrossWeight' : "get_gross_weight",
'NetWeight' : "get_net_weight",
},
'Qto_OpeningElementBaseQuantities' : {
'Width' : "get_length",
'Height' : "get_opening_height",
'Depth' : "get_opening_depth",
'Area' : "get_opening_mapping_area",
'Volume' : "get_net_volume",
},
'Qto_MarineFacilityBaseQuantities' : {
'Length' : "get_length",
'Width' : "get_width",
'Height' : "get_height",
'Area' : "get_net_footprint_area",
'Volume' : "get_net_volume",
},
'Qto_ChillerBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_PileBaseQuantities' : {
'Length' : "get_length",
'CrossSectionArea' : "get_cross_section_area",
'OuterSurfaceArea' : "get_outer_surface_area",
'GrossSurfaceArea' : "get_gross_surface_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
'GrossWeight' : "get_gross_weight",
'NetWeight' : "get_net_weight",
},
'Qto_VibrationIsolatorBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_LampBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_VehicleBaseQuantities' : {
'Length' : "get_length",
'Width' : "get_width",
'Height' : "get_height",
},
'Qto_PipeFittingBaseQuantities' : {
'Length' : None,
'GrossCrossSectionArea' : None,
'NetCrossSectionArea' : None,
'OuterSurfaceArea' : None,
'GrossWeight' : None,
'NetWeight' : None,
},
'Qto_CableCarrierFittingBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_HeatExchangerBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_DoorBaseQuantities' : {
'Width' : { "function_name" : "get_length", "args" : ", main_axis = 'x'"},
'Height' : "get_height",
'Perimeter' : "get_rectangular_perimeter",
'Area' : "get_net_side_area",
},
'Qto_DuctSegmentBaseQuantities' : {
'Length' : "get_length",
'GrossCrossSectionArea' : None,
'NetCrossSectionArea' : None,
'OuterSurfaceArea' : "get_outer_surface_area",
'GrossWeight' : None,
},
'Qto_TransformerBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_FacilityPartBaseQuantities' : {
'Length' : "get_length",
'Width' : "get_width",
'Height' : "get_height",
'Area' : "get_net_footprint_area",
'Volume' : "get_net_volume",
},
'Qto_ProjectionElementBaseQuantities' : {
'Area' : "get_net_side_area",
'Volume' : "get_net_volume",
},
'Qto_SignBaseQuantities' : {
'Height' : "get_height",
'Width' : { "function_name" : "get_length", "args" : ", main_axis = 'x'"},
'Thickness' : "get_width",
'Weight' : None,
},
'Qto_CableSegmentBaseQuantities' : {
'GrossWeight' : None,
'Length' : "get_length",
'CrossSectionArea' : None,
'OuterSurfaceArea' : "get_outer_surface_area",
},
'Qto_BuildingBaseQuantities' : {
'Height' : None,
'EavesHeight' : None,
'FootPrintArea' : None,
'GrossFloorArea' : None,
'NetFloorArea' : None,
'GrossVolume' : None,
'NetVolume' : None,
},
'Qto_ElectricFlowStorageDeviceBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_CommunicationsApplianceBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_RailBaseQuantities' : {
'Length' : "get_length",
'Volume' : "get_net_volume",
'Weight' : "get_net_weight",
},
'Qto_PictorialSignQuantities' : {
'Area' : "get_net_side_area",
'SignArea' : None,
},
'Qto_SpaceHeaterBaseQuantities' : {
'Length' : "get_length",
'GrossWeight' : None,
'NetWeight' : None,
},
'Qto_CoveringBaseQuantities' : {
'Width' : "get_covering_width",
'GrossArea' : "get_covering_gross_area",
'NetArea' : "get_covering_net_area",
},
'Qto_PipeSegmentBaseQuantities' : {
'Length' : "get_length",
'GrossCrossSectionArea' : None,
'NetCrossSectionArea' : "get_cross_section_area",
'OuterSurfaceArea' : "get_outer_surface_area",
'GrossWeight' : "get_gross_weight",
'NetWeight' : "get_net_weight",
},
'Qto_HumidifierBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_ConstructionEquipmentResourceBaseQuantities' : {
'UsageTime' : None,
'OperatingTime' : None,
},
'Qto_AlarmBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_JunctionBoxBaseQuantities' : {
'GrossWeight' : None,
'NumberOfGangs' : None,
'Length' : "get_length",
'Width' : "get_width",
'Height' : "get_height",
},
'Qto_ArealStratumBaseQuantities' : {
'Area' : "get_net_footprint_area",
'Length' : "get_length",
'PlanLength' : None,
},
'Qto_SiteBaseQuantities' : {
'GrossPerimeter' : "get_gross_perimeter",
'GrossArea' : "get_gross_footprint_area",
},
'Qto_MotorConnectionBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_RoofBaseQuantities' : {
'GrossArea' : "get_gross_top_area",
'NetArea' : "get_net_top_area",
'ProjectedArea' : None,
},
'Qto_ChimneyBaseQuantities' : {
'Length' : "get_height",
},
'Qto_ElectricTimeControlBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_ElectricMotorBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_EarthworksFillBaseQuantities' : {
'Length' : None,
'Width' : None,
'Depth' : None,
'CompactedVolume' : None,
'LooseVolume' : None,
},
'Qto_ConduitSegmentBaseQuantities' : {
'InnerDiameter' : None,
'OuterDiameter' : None,
},
'Qto_SignalBaseQuantities' : {
'Weight' : None,
},
'Qto_DuctFittingBaseQuantities' : {
'Length' : "get_length",
'GrossCrossSectionArea' : None,
'NetCrossSectionArea' : None,
'OuterSurfaceArea' : "get_outer_surface_area",
'GrossWeight' : None,
},
'Qto_UnitaryControlElementBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_ActuatorBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_CurtainWallQuantities' : {
'Length' : None,
'Height' : None,
'Width' : None,
'GrossSideArea' : None,
'NetSideArea' : None,
},
'Qto_BoilerBaseQuantities' : {
'GrossWeight' : None,
'NetWeight' : None,
'TotalSurfaceArea' : None,
},
'Qto_FlowMeterBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_AirTerminalBaseQuantities' : {
'GrossWeight' : None,
'Perimeter' : None,
'TotalSurfaceArea' : None,
},
'Qto_DuctSilencerBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_WasteTerminalBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_SlabBaseQuantities' : {
'Width' : "get_width",
'Length' : "get_length",
'Depth' : "get_height",
'Perimeter' : "get_gross_perimeter",
'GrossArea' : "get_gross_footprint_area",
'NetArea' : "get_net_footprint_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
'GrossWeight' : "get_gross_weight",
'NetWeight' : "get_net_weight",
},
'Qto_ImpactProtectionDeviceBaseQuantities' : {
'Weight' : None,
},
'Qto_LightFixtureBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_FlowInstrumentBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_BuildingStoreyBaseQuantities' : {
'GrossHeight' : None,
'NetHeight' : None,
'GrossPerimeter' : None,
'GrossFloorArea' : None,
'NetFloorArea' : None,
'GrossVolume' : None,
'NetVolume' : None,
},
'Qto_ReinforcedSoilBaseQuantities' : {
'Length' : None,
'Width' : None,
'Depth' : None,
'Area' : None,
'Volume' : None,
},
'Qto_DistributionBoardBaseQuantities' : {
'GrossWeight' : None,
'NumberOfCircuits' : None,
},
'Qto_FootingBaseQuantities' : {
'Length' : "get_length",
'Width' : "get_width",
'Height' : "get_height",
'CrossSectionArea' : "get_cross_section_area",
'OuterSurfaceArea' : "get_outer_surface_area",
'GrossSurfaceArea' : "get_gross_surface_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
'GrossWeight' : "get_gross_weight",
'NetWeight' : "get_net_weight",
},
'Qto_PumpBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_CableCarrierSegmentBaseQuantities' : {
'GrossWeight' : None,
'Length' : None,
'CrossSectionArea' : None,
'OuterSurfaceArea' : None,
},
'Qto_InterceptorBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_ColumnBaseQuantities' : {
'Length' : "get_length",
'CrossSectionArea' : "get_cross_section_area",
'OuterSurfaceArea' : "get_outer_surface_area",
'GrossSurfaceArea' : "get_gross_surface_area",
'NetSurfaceArea' : "get_net_surface_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
'GrossWeight' : "get_gross_weight",
'NetWeight' : "get_net_weight",
},
'Qto_EarthworksCutBaseQuantities' : {
'Length' : "get_length",
'Width' : "get_width",
'Depth' : "get_height",
'UndisturbedVolume' : "get_net_volume",
'LooseVolume' : None,
'Weight' : None,
},
'Qto_StackTerminalBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_CoilBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_KerbBaseQuantities' : {
'Length' : "get_length",
'Width' : "get_width",
'Height' : "get_height",
'Depth' : None,
'Volume' : "get_net_volume",
'Weight' : None,
},
'Qto_PavementBaseQuantities' : {
'Length' : "get_length",
'Width' : "get_width",
'Depth' : "get_height",
'GrossArea' : "get_gross_footprint_area",
'NetArea' : "get_net_footprint_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
},
'Qto_ControllerBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_SolarDeviceBaseQuantities' : {
'GrossWeight' : None,
'GrossArea' : None,
},
'Qto_RampFlightBaseQuantities' : {
'Length' : "get_stair_length",
'Width' : "get_width",
'GrossArea' : "get_gross_stair_area",
'NetArea' : "get_net_stair_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
},
'Qto_ElectricApplianceBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_ValveBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_DamperBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_SurfaceFeatureBaseQuantities' : {
'Area' : "get_net_footprint_area",
'Length' : "get_length",
},
'Qto_WallBaseQuantities' : {
'Length' : { "function_name" : "get_length", "args" : ", main_axis = 'x'"},
'Width' : "get_width",
'Height' : "get_height",
'GrossFootprintArea' : "get_gross_footprint_area",
'NetFootprintArea' : "get_net_footprint_area",
'GrossSideArea' : "get_gross_side_area",
'NetSideArea' : "get_net_side_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
'GrossWeight' : "get_gross_weight",
'NetWeight' : "get_net_weight",
},
'Qto_StairFlightBaseQuantities' : {
'Length' : "get_stair_length",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
},
'Qto_SwitchingDeviceBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_BurnerBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_ConstructionMaterialResourceBaseQuantities' : {
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
'GrossWeight' : None,
'NetWeight' : None,
},
'Qto_ElectricGeneratorBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_LinearStratumBaseQuantities' : {
'Diameter' : None,
'Length' : None,
},
'Qto_CableFittingBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_DistributionChamberElementBaseQuantities' : {
'GrossSurfaceArea' : "get_gross_surface_area",
'NetSurfaceArea' : "get_net_surface_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
'Depth' : "get_length",
},
'Qto_CompressorBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_ProtectiveDeviceTrippingUnitBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_EvaporatorBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_SpaceBaseQuantities' : {
'Height' : "get_height",
'FinishCeilingHeight' : "get_finish_ceiling_height",
'FinishFloorHeight' : "get_finish_floor_height",
'GrossPerimeter' : "get_gross_perimeter",
'NetPerimeter' : None,
'GrossFloorArea' : "get_gross_footprint_area",
'NetFloorArea' : "get_net_floor_area",
'GrossWallArea' : None,
'NetWallArea' : None,
'GrossCeilingArea' : "get_gross_ceiling_area",
'NetCeilingArea' : "get_net_ceiling_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_space_net_volume",
},
'Qto_CourseBaseQuantities' : {
'Length' : "get_length",
'Width' : "get_width",
'Thickness' : "get_height",
'Volume' : "get_net_volume",
'GrossVolume' : "get_gross_volume",
'Weight' : None,
},
'Qto_CondenserBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_FireSuppressionTerminalBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_RailingBaseQuantities' : {
'Length' : "get_length",
},
'Qto_TubeBundleBaseQuantities' : {
'GrossWeight' : None,
'NetWeight' : None,
},
'Qto_BeamBaseQuantities' : {
'Length' : "get_length",
'CrossSectionArea' : "get_cross_section_area",
'GrossSurfaceArea' : "get_gross_surface_area",
'OuterSurfaceArea' : "get_outer_surface_area",
'NetSurfaceArea' : "get_net_surface_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
'GrossWeight' : "get_gross_weight",
'NetWeight' : "get_net_weight",
},
'Qto_SleeperBaseQuantities' : {
'Length' : "get_length",
'Width' : "get_width",
'Height' : "get_height",
},
'Qto_ProtectiveDeviceBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_CooledBeamBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_TankBaseQuantities' : {
'GrossWeight' : None,
'NetWeight' : None,
'TotalSurfaceArea' : "get_outer_surface_area",
},
'Qto_AirToAirHeatRecoveryBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_CoolingTowerBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_SensorBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_WindowBaseQuantities' : {
'Width' : { "function_name" : "get_length", "args" : ", main_axis = 'x'"},
'Height' : "get_height",
'Perimeter' : "get_rectangular_perimeter",
'Area' : "get_net_side_area",
},
'Qto_SanitaryTerminalBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_BuildingElementProxyQuantities' : {
'NetSurfaceArea' : "get_net_surface_area",
'NetVolume' : "get_net_volume",
},
'Qto_FanBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_OutletBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_UnitaryEquipmentBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_FilterBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_MemberBaseQuantities' : {
'Length' : "get_length",
'CrossSectionArea' : "get_cross_section_area",
'OuterSurfaceArea' : "get_outer_surface_area",
'GrossSurfaceArea' : "get_gross_surface_area",
'NetSurfaceArea' : "get_net_surface_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
'GrossWeight' : "get_gross_weight",
'NetWeight' : "get_net_weight",
},
'Qto_BodyGeometryValidation' : {
'GrossSurfaceArea' : "get_gross_surface_area",
'NetSurfaceArea' : "get_net_surface_area",
'GrossVolume' : "get_gross_volume",
'NetVolume' : "get_net_volume",
'SurfaceGenusBeforeFeatures' : None,
'SurfaceGenusAfterFeatures' : None,
},
'Qto_VolumetricStratumBaseQuantities' : {
'Area' : "get_net_footprint_area",
'Mass' : None,
'PlanArea' : "get_net_footprint_area",
'Volume' : "get_net_volume",
},
'Qto_SpatialZoneBaseQuantities' : {
'Length' : "get_length",
'Width' : "get_width",
'Height' : "get_height",
},
'Qto_ReinforcingElementBaseQuantities' : {
'Count' : None,
'Length' : "get_length",
'Weight' : None,
},
'Qto_EvaporativeCoolerBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_AirTerminalBoxTypeBaseQuantities' : {
'GrossWeight' : None,
},
'Qto_LaborResourceBaseQuantities' : {
'StandardWork' : None,
'OvertimeWork' : None,
},
}
@@ -19,6 +19,7 @@
import bpy
import ifcopenshell
import ifcopenshell.util.doc
import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.bim.schema
@@ -61,6 +62,15 @@ class Data:
)
return sorted(results, key=lambda v: v["Name"])
@classmethod
def format_pset_enum(cls, psets):
enum_items = []
version = tool.Ifc.get_schema()
for pset in psets:
doc = ifcopenshell.util.doc.get_property_set_doc(version, pset.Name) or {}
enum_items.append((pset.Name, pset.Name, doc.get("description", "")))
return enum_items
class ObjectPsetsData(Data):
data = {}
@@ -114,15 +124,6 @@ class ObjectPsetsData(Data):
)
return cls.format_pset_enum(qtos)
@classmethod
def format_pset_enum(cls, psets):
enum_items = []
version = tool.Ifc.get_schema()
for pset in psets:
doc = ifcopenshell.util.doc.get_property_set_doc(version, pset.Name) or {}
enum_items.append((pset.Name, pset.Name, doc.get("description", "")))
return enum_items
class ObjectQtosData(Data):
data = {}
@@ -157,12 +158,34 @@ class MaterialPsetsData(Data):
@classmethod
def load(cls):
ifc_definition_id = None
props = bpy.context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
ifc_definition_id = props.materials[props.active_material_index].ifc_definition_id
cls.data = {
"ifc_definition_id": bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id,
"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object.active_material)),
"ifc_definition_id": ifc_definition_id,
"psets": cls.psetqtos(tool.Ifc.get().by_id(ifc_definition_id)),
"pset_name": cls.pset_name(),
}
cls.is_loaded = True
@classmethod
def pset_name(cls):
props = bpy.context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
material = tool.Ifc.get().by_id(material.ifc_definition_id)
category = getattr(material, "Category", None) or None
psets = blenderbim.bim.schema.ifc.psetqto.get_applicable("IfcMaterial", category, pset_only=True)
psetnames = cls.format_pset_enum(psets)
assigned_names = ifcopenshell.util.element.get_psets(
material, psets_only=True, should_inherit=False
).keys()
return [p for p in psetnames if p[0] not in assigned_names]
return []
class MaterialSetPsetsData(Data):
data = {}
@@ -275,7 +298,6 @@ class WorkSchedulePsetsData(Data):
@classmethod
def load(cls):
props = bpy.context.scene.WorkSchedulePsetProperties
ifc_definition_id = bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id
cls.data = {"psets": cls.psetqtos(tool.Ifc.get().by_id(ifc_definition_id), psets_only=True)}
cls.is_loaded = True
@@ -27,11 +27,8 @@ import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.pset as core
import blenderbim.core.qto as QtoCore
import blenderbim.bim.module.pset.data
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
class Operator:
@@ -41,35 +38,15 @@ class Operator:
return {"FINISHED"}
def get_pset_props(context, obj, obj_type):
if obj_type == "Object":
return bpy.data.objects.get(obj).PsetProperties
elif obj_type == "Material":
return bpy.data.materials.get(obj).PsetProperties
elif obj_type == "MaterialSet":
return bpy.data.objects.get(obj).MaterialSetPsetProperties
elif obj_type == "MaterialSetItem":
return bpy.data.objects.get(obj).MaterialSetItemPsetProperties
elif obj_type == "Task":
return context.scene.TaskPsetProperties
elif obj_type == "Resource":
return context.scene.ResourcePsetProperties
elif obj_type == "Profile":
return context.scene.ProfilePsetProperties
elif obj_type == "WorkSchedule":
return context.scene.WorkSchedulePsetProperties
elif obj_type == "Group":
return context.scene.GroupPsetProperties
class TogglePsetExpansion(bpy.types.Operator, Operator):
bl_idname = "bim.toggle_pset_expansion"
bl_label = "Toggle Pset Expansion"
pset_id: bpy.props.IntProperty()
def _execute(self, context):
blenderbim.bim.module.pset.data.is_expanded[
self.pset_id
] = not blenderbim.bim.module.pset.data.is_expanded.setdefault(self.pset_id, True)
blenderbim.bim.module.pset.data.is_expanded[self.pset_id] = (
not blenderbim.bim.module.pset.data.is_expanded.setdefault(self.pset_id, True)
)
class EnablePsetEditing(bpy.types.Operator):
@@ -83,160 +60,15 @@ class EnablePsetEditing(bpy.types.Operator):
obj_type: bpy.props.StringProperty()
def execute(self, context):
self.props = get_pset_props(context, self.obj, self.obj_type)
self.props.properties.clear()
if self.pset_id:
pset = tool.Ifc.get().by_id(self.pset_id)
self.props.active_pset_name = pset.Name
self.props.active_pset_type = ""
pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(pset.Name)
self.pset_name = pset.Name
else:
pset = None
self.props.active_pset_name = self.pset_name
self.props.active_pset_type = self.pset_type
pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(self.pset_name)
if pset_template:
self.load_from_pset_template(pset_template, pset)
else:
self.load_from_pset_data(pset)
self.props.active_pset_id = self.pset_id
core.enable_pset_editing(tool.Pset, pset, self.pset_name, self.pset_type, self.obj, self.obj_type)
return {"FINISHED"}
def load_from_pset_template(self, pset_template, pset):
if pset:
data = ifcopenshell.util.element.get_property_definition(pset)
del data["id"]
else:
data = {}
for prop_template in pset_template.HasPropertyTemplates:
if not prop_template.is_a("IfcSimplePropertyTemplate"):
continue # Other types not yet supported
if prop_template.TemplateType == "P_SINGLEVALUE":
self.load_single_value(pset_template, prop_template, data)
elif prop_template.TemplateType.startswith("Q_"):
self.load_single_value(pset_template, prop_template, data)
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
self.load_enumerated_value(prop_template, data)
else:
# NOTE: currently unsupported types:
# - P_BOUNDEDVALUE
# - P_LISTVALUE
# - P_REFERENCEVALUE
# - P_TABLEVALUE
pass
def load_single_value(self, pset_template, prop_template, data):
prop = self.props.properties.add()
prop.name = prop_template.Name
prop.value_type = "IfcPropertySingleValue"
metadata = prop.metadata
metadata.name = prop_template.Name
metadata.is_null = data.get(prop_template.Name, None) is None
metadata.is_optional = True
metadata.is_uri = prop_template.PrimaryMeasureType == "IfcURIReference"
metadata.has_calculator = bool(mapper.get(pset_template.Name, {}).get(prop_template.Name, None))
metadata.data_type = self.get_data_type(prop_template)
special_type = ""
if prop_template.PrimaryMeasureType in (
"IfcPositiveLengthMeasure",
"IfcLengthMeasure",
) or prop_template.TemplateType == "Q_LENGTH":
special_type = "LENGTH"
elif prop_template.PrimaryMeasureType == "IfcAreaMeasure" or prop_template.TemplateType == "Q_AREA":
special_type = "AREA"
elif prop_template.PrimaryMeasureType == "IfcVolumeMeasure" or prop_template.TemplateType == "Q_VOLUME":
special_type = "VOLUME"
metadata.special_type = special_type
if metadata.data_type == "string":
metadata.string_value = "" if metadata.is_null else str(data[prop_template.Name])
elif metadata.data_type == "integer":
metadata.int_value = 0 if metadata.is_null else int(data[prop_template.Name])
elif metadata.data_type == "float":
metadata.float_value = 0.0 if metadata.is_null else float(data[prop_template.Name])
elif metadata.data_type == "boolean":
metadata.bool_value = False if metadata.is_null else bool(data[prop_template.Name])
metadata.ifc_class = pset_template.Name
blenderbim.bim.helper.add_attribute_description(metadata, prop_template)
def get_data_type(self, prop_template):
if prop_template.TemplateType in ["Q_LENGTH", "Q_AREA", "Q_VOLUME", "Q_WEIGHT", "Q_TIME"]:
return "float"
elif prop_template.TemplateType == "Q_COUNT":
return "integer"
return ifcopenshell.util.attribute.get_primitive_type(
IfcStore.get_schema().declaration_by_name(prop_template.PrimaryMeasureType or "IfcLabel")
)
def load_enumerated_value(self, prop_template, data):
enum_items = [v.wrappedValue for v in prop_template.Enumerators.EnumerationValues]
selected_enum_items = data.get(prop_template.Name, []) or []
prop = self.props.properties.add()
prop.name = prop_template.Name
prop.value_type = "IfcPropertyEnumeratedValue"
metadata = prop.metadata
metadata.name = prop_template.Name
metadata.is_null = data.get(prop_template.Name, None) is None
metadata.is_optional = True
metadata.is_uri = prop_template.PrimaryMeasureType == "IfcURIReference"
# Cute hack to abuse the metadata to find the Blender data_type
metadata.set_value(enum_items[0])
data_type = metadata.get_value_name()
for enum in enum_items:
new = prop.enumerated_value.enumerated_values.add()
setattr(new, data_type, enum)
new.is_selected = enum in selected_enum_items
def load_from_pset_data(self, pset):
props = []
if pset.is_a("IfcElementQuantity"):
props = pset.Quantities
elif pset.is_a("IfcPropertySet"):
props = pset.HasProperties
elif pset.is_a("IfcMaterialProperties") or pset.is_a("IfcProfileProperties"):
props = pset.Properties
for prop in props:
if prop.is_a("IfcPropertyEnumeratedValue"):
simple_prop = self.props.properties.add()
simple_prop.name = prop.Name
simple_prop.value_type = "IfcPropertyEnumeratedValue"
metadata = simple_prop.metadata
metadata.name = prop.Name
metadata.is_null = len(simple_prop.enumerated_value.enumerated_values) == 0
metadata.is_optional = True
metadata.set_value(prop.EnumerationReference.EnumerationValues[0].wrappedValue)
enum_items = [v.wrappedValue for v in prop.EnumerationReference.EnumerationValues]
selected_enum_items = [v.wrappedValue for v in prop.EnumerationValues]
data_type = metadata.get_value_name(display_only=True)
for enum in enum_items:
new = simple_prop.enumerated_value.enumerated_values.add()
setattr(new, data_type, enum)
new.is_selected = enum in selected_enum_items
else:
if prop.is_a("IfcPropertySingleValue"):
value = prop.NominalValue.wrappedValue if prop.NominalValue else None
elif prop.is_a("IfcPhysicalSimpleQuantity"):
value = prop[3]
new_prop = self.props.properties.add()
new_prop.name = prop.Name
metadata = new_prop.metadata
metadata.set_value(value)
metadata.name = prop.Name
metadata.is_null = value is None
metadata.is_optional = True
metadata.set_value(metadata.get_value_default() if metadata.is_null else value)
class DisablePsetEditing(bpy.types.Operator, Operator):
bl_idname = "bim.disable_pset_editing"
@@ -246,7 +78,7 @@ class DisablePsetEditing(bpy.types.Operator, Operator):
obj_type: bpy.props.StringProperty()
def _execute(self, context):
props = get_pset_props(context, self.obj, self.obj_type)
props = tool.Pset.get_pset_props(self.obj, self.obj_type)
if props.active_pset_id:
pset = tool.Ifc.get().by_id(props.active_pset_id)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
@@ -270,7 +102,7 @@ class EditPset(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
props = tool.Pset.get_pset_props(self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
properties = {}
@@ -308,8 +140,12 @@ class EditPset(bpy.types.Operator, Operator):
)
else:
for key, value in properties.items():
if value is None:
continue
if isinstance(value, float):
properties[key] = round(value, 4)
elif not isinstance(value, int):
properties[key] = 0
ifcopenshell.api.run(
"pset.edit_qto",
self.file,
@@ -341,7 +177,7 @@ class RemovePset(bpy.types.Operator, Operator):
objects = [self.obj]
pset_name = tool.Ifc.get().by_id(self.pset_id).Name
for obj in objects:
props = get_pset_props(context, obj, self.obj_type)
props = tool.Pset.get_pset_props(obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
pset = ifcopenshell.util.element.get_psets(element, should_inherit=False).get(pset_name, None)
@@ -372,63 +208,12 @@ class AddQto(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
qto_name = tool.Pset.get_pset_name(self.obj, self.obj_type, pset_type="QTO")
bpy.ops.bim.enable_pset_editing(
pset_id=0, pset_name=props.qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type
pset_id=0, pset_name=qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type
)
class CalculateQuantity(bpy.types.Operator):
bl_idname = "bim.calculate_quantity"
bl_label = "Calculate Quantity"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Calculates the quantity with a defined formula for this exact entity and quantity"
prop: bpy.props.StringProperty()
def execute(self, context):
self.qto_calculator = QtoCalculator()
obj = context.active_object
prop = obj.PsetProperties.properties.get(self.prop)
quantity = self.calculate_quantity(obj, context)
if quantity is None:
self.report({"ERROR"}, "Could not calculate quantity")
return {"CANCELLED"}
prop.metadata.float_value = quantity
return {"FINISHED"}
def calculate_quantity(self, obj, context):
quantity = self.qto_calculator.calculate_quantity(obj.PsetProperties.active_pset_name, self.prop, obj)
if quantity is None:
return
return round(quantity, 3)
class GuessQuantity(bpy.types.Operator):
bl_idname = "bim.guess_quantity"
bl_label = "Guess Quantity"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Calculate the quantity by guessing the formula from the quantity name. "
"Less reliable than Calculate Quantity"
)
prop: bpy.props.StringProperty()
def execute(self, context):
self.qto_calculator = QtoCalculator()
obj = context.active_object
prop = obj.PsetProperties.properties.get(self.prop)
prop.metadata.float_value = self.guess_quantity(obj, context)
return {"FINISHED"}
def guess_quantity(self, obj, context):
quantity = self.qto_calculator.guess_quantity(self.prop, [p.name for p in obj.PsetProperties.properties], obj)
return round(quantity, 3) if quantity is not None else None
class CopyPropertyToSelection(bpy.types.Operator, Operator):
bl_idname = "bim.copy_property_to_selection"
bl_label = "Copy Property To Selection"
@@ -634,3 +419,17 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator):
self.report({"INFO"}, "Finished applying changes")
return {"FINISHED"}
class AddProposedProp(bpy.types.Operator):
bl_idname = "bim.add_proposed_prop"
bl_label = "Add Proposed Prop"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
prop_name: bpy.props.StringProperty()
prop_value: bpy.props.StringProperty()
def execute(self, context):
core.add_proposed_prop(tool.Pset, self.obj, self.obj_type, self.prop_name, self.prop_value)
return {"FINISHED"}
@@ -19,10 +19,12 @@
import bpy
import blenderbim.bim.schema
import ifcopenshell
import ifcopenshell.util.attribute
import ifcopenshell.util.doc
import ifcopenshell.util.element
import blenderbim.tool as tool
from blenderbim.bim.prop import Attribute, StrProperty
from blenderbim.bim.module.pset.data import AddEditCustomPropertiesData, ObjectPsetsData
from blenderbim.bim.module.pset.data import AddEditCustomPropertiesData, ObjectPsetsData, MaterialPsetsData
from blenderbim.bim.ifc import IfcStore
from bpy.types import PropertyGroup
from bpy.props import (
@@ -58,18 +60,39 @@ def blender_formatted_enum_from_psets(psets):
def get_pset_name(self, context):
pset_type = repr(self)
prop_type = pset_type.split(".")[-1]
results = []
if "bpy.data.objects" in pset_type:
if prop_type == "PsetProperties":
results = get_object_pset_name(self, context)
elif prop_type == "MaterialSetPsetProperties":
results = get_material_set_pset_names(self, context)
elif prop_type == "MaterialSetItemPsetProperties":
results = get_material_set_item_pset_names(self, context)
elif prop_type == "MaterialPsetProperties":
results = get_material_pset_names(self, context)
elif prop_type == "ResourcePsetProperties":
results = get_resource_pset_names(self, context)
elif prop_type == "GroupPsetProperties":
results = get_group_pset_names(self, context)
elif prop_type == "ProfilePsetProperties":
results = get_profile_pset_names(self, context)
elif prop_type == "WorkSchedulePsetProperties":
results = get_work_schedule_pset_names(self, context)
return [("BBIM_CUSTOM", "Custom Pset", "Create a property set without using a template."), None] + results
def get_object_pset_name(self, context):
if not ObjectPsetsData.is_loaded:
ObjectPsetsData.load()
return ObjectPsetsData.data["pset_name"]
def get_material_pset_names(self, context):
global psetnames
ifc_class = "IfcMaterial"
if ifc_class not in psetnames:
psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True)
psetnames[ifc_class] = blender_formatted_enum_from_psets(psets)
return psetnames[ifc_class]
if not MaterialPsetsData.is_loaded:
MaterialPsetsData.load()
return MaterialPsetsData.data["pset_name"]
def get_material_set_pset_names(self, context):
@@ -167,6 +190,21 @@ def get_work_schedule_pset_names(self, context):
def get_qto_name(self, context):
pset_type = repr(self)
prop_type = pset_type.split(".")[-1]
if "bpy.data.objects" in pset_type:
if prop_type == "PsetProperties":
results = get_object_qto_name(self, context)
elif prop_type == "TaskPsetProperties":
results = get_task_qto_names(self, context)
elif prop_type == "ResourcePsetProperties":
results = get_resource_qto_names(self, context)
elif prop_type == "GroupPsetProperties":
results = get_group_qto_names(self, context)
return [("BBIM_CUSTOM", "Custom Qto", "Create a quantity set without using a template."), None] + results
def get_object_qto_name(self, context):
if not ObjectPsetsData.is_loaded:
ObjectPsetsData.load()
return ObjectPsetsData.data["qto_name"]
@@ -198,76 +236,14 @@ class IfcProperty(PropertyGroup):
class PsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_has_template: BoolProperty(name="Active Pset Has Template")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_pset_name, name="Pset Name")
qto_name: EnumProperty(items=get_qto_name, name="Qto Name")
class MaterialPsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_material_pset_names, name="Pset Name")
class MaterialSetPsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_material_set_pset_names, name="Pset Name")
class MaterialSetItemPsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_material_set_item_pset_names, name="Pset Name")
class TaskPsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
qto_name: EnumProperty(items=get_task_qto_names, name="Qto Name")
class ResourcePsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_resource_pset_names, name="Pset Name")
qto_name: EnumProperty(items=get_resource_qto_names, name="Qto Name")
class GroupPsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_group_pset_names, name="Pset Name")
qto_name: EnumProperty(items=get_group_qto_names, name="Qto Name")
class ProfilePsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_profile_pset_names, name="Pset Name")
class WorkSchedulePsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_work_schedule_pset_names, name="Pset Name")
prop_name: StringProperty(name="Property Name", default="MyProperty")
prop_value: StringProperty(name="Property Value", default="Some Value")
class RenameProperties(PropertyGroup):
File diff suppressed because it is too large Load Diff
+32 -46
View File
@@ -79,13 +79,7 @@ def draw_enumerated_property(prop, layout, copy_operator=None):
def get_active_pset_obj_name(context, obj_type):
if obj_type == "Object":
return context.active_object.name
elif obj_type == "Material":
return context.active_object.active_material.name
elif obj_type == "MaterialSet":
return context.active_object.name
elif obj_type == "MaterialSetItem":
if obj_type in ("Object", "Material", "MaterialSet", "MaterialSetItem"):
return context.active_object.name
return ""
@@ -101,6 +95,7 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_remov
if props.active_pset_id == pset_id:
row.prop(props, "active_pset_name", icon="COPY_ID", text="")
op = row.operator("bim.edit_pset", icon="CHECKMARK", text="")
op.pset_id = pset_id
op.obj = obj_name
op.obj_type = obj_type
op = row.operator("bim.disable_pset_editing", icon="CANCEL", text="")
@@ -108,8 +103,6 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_remov
op.obj_type = obj_type
elif not props.active_pset_id:
row.label(text=pset["Name"], icon="COPY_ID")
if "Qto" in pset["Name"] and "Base" in pset["Name"]:
op = row.operator("bim.calculate_all_quantities", icon="MOD_EDGESPLIT", text="")
op = row.operator("bim.enable_pset_editing", icon="GREASEPENCIL", text="")
op.pset_id = pset_id
op.obj = obj_name
@@ -132,6 +125,16 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_remov
if props.active_pset_id == pset_id:
for prop in props.properties:
draw_psetqto_editable_ui(box, props, prop)
if not props.active_pset_has_template:
row = box.row(align=True)
row.prop(props, "prop_name", text="")
row.prop(props, "prop_value", text="")
op = row.operator("bim.add_proposed_prop", text="", icon="ADD")
op.obj = obj_name
op.obj_type = obj_type
op.prop_name = props.prop_name
op.prop_value = props.prop_value
else:
has_props_displayed = False
for prop in pset["Properties"]:
@@ -143,7 +146,9 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_remov
row = box.row(align=True)
row.scale_y = 0.8
row.label(text=prop["Name"])
row.label(text=str(prop["NominalValue"]))
op = row.operator("bim.select_similar", text=str(prop["NominalValue"]), icon="NONE", emboss=False)
op.key = pset['Name']
if not has_props_displayed:
row = box.row()
row.scale_y = 0.8
@@ -153,25 +158,6 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_remov
def draw_psetqto_editable_ui(box, props, prop):
row = box.row(align=True)
draw_property(prop, row, copy_operator="bim.copy_property_to_selection")
if prop.metadata.has_calculator:
op = row.operator("bim.calculate_quantity", icon="MOD_EDGESPLIT", text="")
op.prop = prop.name
# Old "guess quantity" feature to be removed once new calculator is comprehensive
if (
"length" in prop.name.lower()
or "width" in prop.name.lower()
or "height" in prop.name.lower()
or "depth" in prop.name.lower()
or "perimeter" in prop.name.lower()
):
op = row.operator("bim.guess_quantity", icon="IPO_EASE_IN_OUT", text="")
op.prop = prop.name
elif "area" in prop.name.lower():
op = row.operator("bim.guess_quantity", icon="MESH_CIRCLE", text="")
op.prop = prop.name
elif "volume" in prop.name.lower():
op = row.operator("bim.guess_quantity", icon="SPHERE", text="")
op.prop = prop.name
class BIM_PT_object_psets(Panel):
@@ -273,36 +259,36 @@ class BIM_PT_material_psets(Panel):
bl_idname = "BIM_PT_material_psets"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
bl_context = "scene"
bl_parent_id = "BIM_PT_materials"
@classmethod
def poll(cls, context):
if not context.active_object:
return False
if not context.active_object.active_material:
return False
props = context.active_object.active_material.BIMObjectProperties
if not props.ifc_definition_id:
return False
file = IfcStore.get_file()
if not file or file.schema == "IFC2X3":
ifc_file = tool.Ifc.get()
if not ifc_file or ifc_file.schema == "IFC2X3":
return False # We don't support material psets in IFC2X3 because they suck
return True
props = context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
return True
return False
def draw(self, context):
props = context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
ifc_definition_id = props.materials[props.active_material_index].ifc_definition_id
if not MaterialPsetsData.is_loaded:
MaterialPsetsData.load()
elif (
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
!= MaterialPsetsData.data["ifc_definition_id"]
):
elif ifc_definition_id != MaterialPsetsData.data["ifc_definition_id"]:
MaterialPsetsData.load()
props = context.active_object.active_material.PsetProperties
props = context.scene.MaterialPsetProperties
row = self.layout.row(align=True)
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
op.obj = context.active_object.active_material.name
op.obj = ""
op.obj_type = "Material"
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET":
@@ -21,6 +21,7 @@ import bpy
import pathlib
import ifcopenshell
import ifcopenshell.util.attribute
import ifcopenshell.util.doc
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
@@ -36,10 +37,14 @@ class PsetTemplatesData:
@classmethod
def load(cls):
cls.is_loaded = True
cls.data["pset_template_files"] = cls.pset_template_files()
# after pset_template_files
cls.data["pset_templates"] = cls.pset_templates()
# after pset_template_files because it loads IfcStore.pset_template_file
cls.data["primary_measure_type"] = cls.primary_measure_type()
cls.data["property_template_type"] = cls.property_template_type()
cls.data["pset_template_files"] = cls.pset_template_files()
cls.data["pset_templates"] = cls.pset_templates()
cls.data["pset_template"] = cls.pset_template()
cls.data["prop_templates"] = cls.prop_templates()
@@ -190,4 +190,4 @@ class BIMPsetTemplateProperties(PropertyGroup):
active_prop_template_id: IntProperty(name="Active Prop Template Id")
active_pset_template: PointerProperty(type=PsetTemplate)
active_prop_template: PointerProperty(type=PropTemplate)
new_template_filename: StringProperty("New TemplateFileName")
new_template_filename: StringProperty(name="New TemplateFileName")
@@ -24,12 +24,13 @@ classes = (
operator.CalculateEdgeLengths,
operator.CalculateFaceAreas,
operator.CalculateObjectVolumes,
operator.ExecuteQtoMethod,
operator.QuantifyObjects,
operator.AssignBaseQto,
operator.CalculateAllQuantities,
operator.CalculateSingleQuantity,
operator.PerformQuantityTakeOff,
prop.BIMQtoProperties,
ui.BIM_PT_qto_utilities,
ui.BIM_PT_qto,
ui.BIM_PT_qto_manual,
ui.BIM_PT_qto_simple,
ui.BIM_PT_qto_cost,
)
File diff suppressed because it is too large Load Diff
@@ -19,9 +19,11 @@
import bpy
import blenderbim.tool as tool
def refresh():
QtoData.is_loaded = False
class QtoData:
data = {}
is_loaded = False
@@ -29,9 +31,9 @@ class QtoData:
@classmethod
def load(cls):
cls.data = {
"has_cost_item" : cls.has_cost_item(),
"relating_cost_items" : cls.relating_cost_items(),
}
"has_cost_item": cls.has_cost_item(),
"relating_cost_items": cls.relating_cost_items(),
}
cls.is_loaded = True
@@ -53,23 +55,13 @@ class QtoData:
for relating_cost_item in relating_cost_items:
results.append(
{
'cost_item_id' : relating_cost_item['cost_item_id'],
'cost_item_name' : relating_cost_item['cost_item_name'],
'quantity_id' : relating_cost_item['quantity_id'],
'quantity_name' : relating_cost_item['quantity_name'],
'quantity_value' : relating_cost_item['quantity_value'],
'quantity_type' : relating_cost_item['quantity_type'],
"cost_item_id": relating_cost_item["cost_item_id"],
"cost_item_name": relating_cost_item["cost_item_name"],
"quantity_id": relating_cost_item["quantity_id"],
"quantity_name": relating_cost_item["quantity_name"],
"quantity_value": relating_cost_item["quantity_value"],
"quantity_type": relating_cost_item["quantity_type"],
}
)
return results
@@ -23,7 +23,6 @@ import blenderbim.tool as tool
import blenderbim.core.qto as core
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.qto import helper
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
class CalculateCircleRadius(bpy.types.Operator):
@@ -85,115 +84,67 @@ class CalculateObjectVolumes(bpy.types.Operator):
return {"FINISHED"}
class ExecuteQtoMethod(bpy.types.Operator):
bl_idname = "bim.execute_qto_method"
bl_label = "Execute Qto Method"
class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.calculate_single_quantity"
bl_label = "Calculate Single Quantity"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def execute(self, context):
selected_mesh_objects = [o for o in context.selected_objects if o.type == "MESH"]
props = context.scene.BIMQtoProperties
result = 0
if props.qto_methods == "HEIGHT":
for obj in selected_mesh_objects:
result += helper.calculate_height(obj)
elif props.qto_methods == "VOLUME":
result = helper.calculate_volumes(selected_mesh_objects, context)
elif props.qto_methods == "FORMWORK":
result = helper.calculate_formwork_area(selected_mesh_objects, context)
elif props.qto_methods == "SIDE_FORMWORK":
result = helper.calculate_side_formwork_area(selected_mesh_objects, context)
elif props.qto_methods == "NetFootprintArea":
result = QtoCalculator().get_net_footprint_area(selected_mesh_objects[0])
elif props.qto_methods == "NetRoofprintArea":
result = QtoCalculator().get_net_roofprint_area(selected_mesh_objects[0])
elif props.qto_methods == "LateralArea":
result = QtoCalculator().get_lateral_area(selected_mesh_objects[0])
elif props.qto_methods == "TotalSurfaceArea":
result = QtoCalculator().get_total_surface_area(selected_mesh_objects[0])
elif props.qto_methods == "OpeningArea":
result = QtoCalculator().get_opening_area(selected_mesh_objects[0])
elif props.qto_methods == "GrossTopArea":
result = QtoCalculator().get_gross_top_area(selected_mesh_objects[0])
elif props.qto_methods == "NetTopArea":
result = QtoCalculator().get_net_top_area(selected_mesh_objects[0])
elif props.qto_methods == "ProjectedArea":
result = QtoCalculator().get_projected_area(selected_mesh_objects[0])
elif props.qto_methods == "TotalContactArea":
result = QtoCalculator().get_total_contact_area(selected_mesh_objects[0])
elif props.qto_methods == "ContactArea":
result = QtoCalculator().get_contact_area(selected_mesh_objects[0], selected_mesh_objects[1])
props.qto_result = str(round(result, 3))
return {"FINISHED"}
class QuantifyObjects(bpy.types.Operator):
bl_idname = "bim.quantify_objects"
bl_label = "Quantify Objects"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return IfcStore.get_file() and context.selected_objects
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
props = context.scene.BIMQtoProperties
self.file = IfcStore.get_file()
for obj in (o for o in context.selected_objects if o.type == "MESH"):
if not obj.BIMObjectProperties.ifc_definition_id:
continue
result = 0
if props.qto_methods == "HEIGHT":
result = helper.calculate_height(obj)
elif props.qto_methods == "VOLUME":
result = helper.calculate_volumes([obj], context)
elif props.qto_methods == "FORMWORK":
result = helper.calculate_formwork_area([obj], context)
elif props.qto_methods == "SIDE_FORMWORK":
result = helper.calculate_side_formwork_area([obj], context)
if not result:
continue
result = round(result, 3)
qto = ifcopenshell.api.run(
"pset.add_qto",
self.file,
product=self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
name=props.qto_name,
)
ifcopenshell.api.run("pset.edit_qto", self.file, qto=qto, properties={props.prop_name: result})
return {"FINISHED"}
class AssignBaseQto(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_objects_base_qto"
bl_label = "Assign IFC Object Quantity Set"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Assign IFC quantity set to selected object"
bl_description = "Calculate a single quantity using a function on the selected objects"
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.selected_objects
def _execute(self, context):
core.assign_objects_base_qto(tool.Ifc, tool.Qto, selected_objects = context.selected_objects)
import ifc5d.qto
props = context.scene.BIMQtoProperties
elements = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element:
elements.add(element)
rules = {
"calculators": {
props.calculator: {
"IfcProduct": {props.qto_name: {props.prop_name: props.calculator_function}},
}
}
}
ifc_file = tool.Ifc.get()
results = ifc5d.qto.quantify(ifc_file, elements, rules)
ifc5d.qto.edit_qtos(ifc_file, results)
return {"FINISHED"}
class CalculateAllQuantities(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.calculate_all_quantities"
bl_label = "Calculate All Quantities"
class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.perform_quantity_take_off"
bl_label = "Perform Quantity Take-off"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Calculate all possible quantities and assign them to selected object"
bl_description = "Perform a quantity take off based of a QTO rule configuration"
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.selected_objects
return tool.Ifc.get()
def _execute(self, context):
core.calculate_objects_base_quantities(tool.Ifc, tool.Cost, tool.Qto, QtoCalculator(), selected_objects = context.selected_objects)
import ifc5d.qto
props = context.scene.BIMQtoProperties
if context.selected_objects:
elements = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element:
elements.add(element)
else:
elements = set(tool.Ifc.get().by_type("IfcElement"))
rules = ifc5d.qto.rules[props.qto_rule]
ifc_file = tool.Ifc.get()
results = ifc5d.qto.quantify(ifc_file, elements, rules)
ifc5d.qto.edit_qtos(ifc_file, results)
return {"FINISHED"}
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifc5d.qto
from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
@@ -31,34 +32,37 @@ from bpy.props import (
)
def get_qto_rule(self, context):
results = []
for rule_id, rule in ifc5d.qto.rules.items():
results.append((rule_id, rule["name"], rule["description"]))
return results
def get_calculator(self, context):
results = []
for name, calculator in ifc5d.qto.calculators.items():
results.append((name, name, calculator.__doc__))
return results
def get_calculator_function(self, context):
calculator = ifc5d.qto.calculators[self.calculator]
results = []
previous_measure = None
for function_id, function in calculator.functions.items():
measure = function.measure.split("Measure")[0][3:]
if previous_measure is not None and measure != previous_measure:
results.append(None)
results.append((function_id, f"{measure}: {function.name}", function.description))
previous_measure = measure
return results
class BIMQtoProperties(PropertyGroup):
qto_rule: EnumProperty(items=get_qto_rule, name="Qto Rule")
calculator: EnumProperty(items=get_calculator, name="Calculator")
calculator_function: EnumProperty(items=get_calculator_function, name="Calculator Function")
qto_result: StringProperty(default="", name="Qto Result")
qto_methods: EnumProperty(
items=[
("HEIGHT", "Height", "Calculate the Z height of an object"),
("VOLUME", "Volume", "Calculate the volume of an object"),
(
"FORMWORK",
"Formwork",
"Calculate the exposed formwork for all bottoms and sides (e.g. for beams and slabs) of one or more objects",
),
(
"SIDE_FORMWORK",
"Side Formwork",
"Calculate the exposed formwork for all sides only (e.g. for columns) of one or more objects",
),
("NetFootprintArea", "Net footprint area", "Calculate the net footprint area"),
("NetRoofprintArea", "Net roofprint area", "Calculate the net roofprint area"),
("LateralArea", "Lateral area", "Calculate the lateral area"),
("TotalSurfaceArea", "Total surface area", "Calculate the total surface area"),
("OpeningArea", "Opening area", "Calculate the opening area"),
("GrossTopArea", "Gross top area", "Calculate the gross top area"),
("NetTopArea", "Net top area", "Calculate the net top area"),
("ProjectedArea", "Projected area", "Calculate the projected area"),
("TotalContactArea", "Total contact area", "Get the total contact area"),
("ContactArea", "Contact area between two objects", "Get the contact area")
],
name="Qto Methods",
)
qto_name: StringProperty(name="Qto Name")
prop_name: StringProperty(name="Prop Name")
qto_name: StringProperty(name="Qto Name", default="My_Qto")
prop_name: StringProperty(name="Prop Name", default="MyDimension")
+92 -41
View File
@@ -20,18 +20,67 @@ import bpy
from blenderbim.bim.module.qto.data import QtoData
class BIM_PT_qto_utilities(bpy.types.Panel):
bl_idname = "BIM_PT_qto_utilities"
class BIM_PT_qto(bpy.types.Panel):
bl_idname = "BIM_PT_qto"
bl_label = "Quantity Take-off"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_qto"
bl_options = {"HIDE_HEADER"}
def draw(self, context):
if not QtoData.is_loaded:
QtoData.load()
layout = self.layout
props = context.scene.BIMQtoProperties
row = layout.row()
if context.selected_objects:
row.label(text=f"Quantifying {len(context.selected_objects)} Selected Objects", icon="MOD_EDGESPLIT")
else:
row.label(text="Quantifying All Objects", icon="MOD_EDGESPLIT")
row = layout.row()
row.prop(props, "qto_rule", text="")
row = layout.row()
row.operator("bim.perform_quantity_take_off")
class BIM_PT_qto_manual(bpy.types.Panel):
bl_idname = "BIM_PT_qto_manual"
bl_label = "Manual Quantification"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_qto"
def draw(self, context):
layout = self.layout
props = context.scene.BIMQtoProperties
row = layout.row()
row.prop(props, "calculator")
row = layout.row()
row.prop(props, "calculator_function", text="Function")
row = layout.row(align=True)
row.prop(props, "qto_name", text="")
row.prop(props, "prop_name", text="")
row = layout.row()
row.operator("bim.calculate_single_quantity")
class BIM_PT_qto_simple(bpy.types.Panel):
bl_idname = "BIM_PT_qto_simple"
bl_label = "Simple Quantity Calculator"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_qto"
def draw(self, context):
layout = self.layout
props = context.scene.BIMQtoProperties
@@ -47,42 +96,44 @@ class BIM_PT_qto_utilities(bpy.types.Panel):
row = layout.row(align=True)
row.operator("bim.calculate_object_volumes")
row = layout.row(align=True)
row.prop(props, "qto_methods", text="")
row.operator("bim.execute_qto_method", icon="PROPERTIES", text="")
row = layout.row(align=True)
row.prop(props, "qto_name", text="")
row.prop(props, "prop_name", text="")
row.operator("bim.quantify_objects", icon="COPYDOWN", text="")
class BIM_PT_qto_cost(bpy.types.Panel):
bl_idname = "BIM_PT_qto_cost"
bl_label = "Parametric Cost Relationships"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_qto"
row = layout.row(align=True)
row.operator("bim.assign_objects_base_qto")
def draw(self, context):
if not QtoData.is_loaded:
QtoData.load()
row = layout.row(align=True)
row.operator("bim.calculate_all_quantities", icon="MOD_EDGESPLIT")
if not context.selected_objects:
row = self.layout.row()
row.label(text="No Selected Object")
return
if context.selected_objects:
row = layout.row(align=True)
row.label(text=f"Relating Cost Item:")
if QtoData.data['has_cost_item']:
for relating_cost_item in QtoData.data['relating_cost_items']:
row.label(text=f"\n")
row = layout.row(align=True)
row.label(text=f"Cost item name:")
row.label(text=f"{relating_cost_item['cost_item_name']}")
row = layout.row(align=True)
row.label(text=f"Quantity name:")
row.label(text=f"{relating_cost_item['quantity_name']}")
row = layout.row(align=True)
row.label(text=f"Quantity value:")
row.label(text=f"{relating_cost_item['quantity_value']}")
row = layout.row(align=True)
row.label(text=f"Quantity type:")
row.label(text=f"{relating_cost_item['quantity_type']}")
row = layout.row(align=True)
else:
row = layout.row(align=True)
row.label(text = f"No cost item related")
if not QtoData.data["has_cost_item"]:
row = self.layout.row()
row.label(text="No Related Cost Item")
return
row = self.layout.row(align=True)
row.label(text="Relating Cost Item:")
for relating_cost_item in QtoData.data["relating_cost_items"]:
row.label(text="\n")
row = self.layout.row(align=True)
row.label(text="Cost item name:")
row.label(text=f"{relating_cost_item['cost_item_name']}")
row = self.layout.row(align=True)
row.label(text="Quantity name:")
row.label(text=f"{relating_cost_item['quantity_name']}")
row = self.layout.row(align=True)
row.label(text="Quantity value:")
row.label(text=f"{relating_cost_item['quantity_value']}")
row = self.layout.row(align=True)
row.label(text="Quantity type:")
row.label(text=f"{relating_cost_item['quantity_type']}")
row = self.layout.row(align=True)
@@ -19,6 +19,7 @@
import bpy
import blenderbim.tool as tool
import ifcopenshell
import ifcopenshell.util.resource
def refresh():
@@ -28,7 +29,6 @@ def refresh():
class ResourceData:
data = {}
is_loaded = False
cost_values = {}
@classmethod
def load(cls):
@@ -139,7 +139,7 @@ class ResourceData:
return results
@classmethod
def active_resource_ids(cls):
def active_resource_ids(cls) -> list[int]:
obj = bpy.context.active_object
element = tool.Ifc.get_entity(obj)
if not element:
@@ -128,9 +128,9 @@ class ContractResource(bpy.types.Operator):
class AssignResource(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_resource"
bl_label = "Assign Resource"
bl_description = "Assign resource to the selected objects"
bl_options = {"REGISTER", "UNDO"}
resource: bpy.props.IntProperty()
related_object: bpy.props.StringProperty()
def _execute(self, context):
core.assign_resource(tool.Ifc, tool.Spatial, resource=tool.Ifc.get().by_id(self.resource))
@@ -139,9 +139,9 @@ class AssignResource(bpy.types.Operator, tool.Ifc.Operator):
class UnassignResource(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_resource"
bl_label = "Unassign Resource"
bl_description = "Unassign resource from the selected objects"
bl_options = {"REGISTER", "UNDO"}
resource: bpy.props.IntProperty()
related_object: bpy.props.StringProperty()
def _execute(self, context):
core.unassign_resource(tool.Ifc, tool.Spatial, resource=tool.Ifc.get().by_id(self.resource))
@@ -328,7 +328,7 @@ class EditResourceQuantity(bpy.types.Operator, tool.Ifc.Operator):
class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "import_resources.bim"
bl_idname = "bim.import_resources"
bl_label = "Import Resources"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
@@ -37,13 +37,29 @@ from bpy.props import (
CollectionProperty,
)
quantitytypes_enum = []
quantitytypes_enum = {}
def purge():
global quantitytypes_enum
quantitytypes_enum = []
def setup_quantity_types_enum():
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcConstructionResource.htm#Table-7.3.3.7.1.3.H
resources = {
"IfcCrewResource": ("IfcQuantityTime",),
"IfcLaborResource": ("IfcQuantityTime",),
"IfcSubContractResource": ("IfcQuantityTime",),
"IfcConstructionEquipmentResource": ("IfcQuantityTime",),
"IfcConstructionMaterialResource": (
"IfcQuantityVolume",
"IfcQuantityArea",
"IfcQuantityLength",
"IfcQuantityWeight",
),
"IfcConstructionProductResource": ("IfcQuantityCount",),
}
for resource, quantities in resources.items():
quantitytypes_enum[resource] = [(q, q, "") for q in quantities]
setup_quantity_types_enum()
def updateResourceName(self, context):
@@ -63,15 +79,7 @@ def updateResourceName(self, context):
def get_quantity_types(self, context):
global quantitytypes_enum
if len(quantitytypes_enum) == 0 and IfcStore.get_schema():
quantitytypes_enum.extend(
[
(t.name(), t.name(), "")
for t in IfcStore.get_schema().declaration_by_name("IfcPhysicalSimpleQuantity").subtypes()
]
)
return quantitytypes_enum
return quantitytypes_enum[self.active_resource_class]
def update_active_resource_index(self, context):
@@ -88,9 +96,7 @@ def updateResourceUsage(self, context):
resource = tool.Ifc.get().by_id(self.ifc_definition_id)
if resource.Usage and resource.Usage.ScheduleUsage == self.schedule_usage:
return
tool.Resource.run_edit_resource_time(resource, attributes={
"ScheduleUsage": self.schedule_usage
})
tool.Resource.run_edit_resource_time(resource, attributes={"ScheduleUsage": self.schedule_usage})
tool.Sequence.load_task_properties()
tool.Resource.load_resource_properties()
tool.Sequence.refresh_task_resources()
@@ -127,12 +133,21 @@ class BIMResourceProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
active_resource_index: IntProperty(name="Active Resource Index", update=update_active_resource_index)
active_resource_id: IntProperty(name="Active Resource Id")
active_resource_class: StringProperty(name="Active Resource Type")
contracted_resources: StringProperty(name="Contracted Resources", default="[]")
is_resource_update_enabled: BoolProperty(name="Is Resource Update Enabled", default=True)
is_loaded: BoolProperty(name="Is Editing")
active_resource_time_id: IntProperty(name="Active Resource Usage Id")
resource_time_attributes: CollectionProperty(name="Resource Usage Attributes", type=Attribute)
editing_resource_type: StringProperty(name="Editing Resource Type")
editing_resource_type: EnumProperty(
name="Editing Resource Type",
items=(
("ATTRIBUTES", "", ""),
("USAGE", "", ""),
("COSTS", "", ""),
("QUANTITY", "", ""),
),
)
cost_types: EnumProperty(
items=[
("FIXED", "Fixed", "The cost value is a fixed number"),
@@ -51,7 +51,7 @@ class BIM_PT_resources(Panel):
row.operator("bim.disable_resource_editing_ui", text="", icon="CANCEL")
else:
row.operator("bim.load_resources", text="", icon="GREASEPENCIL")
row.operator("import_resources.bim", text="", icon="IMPORT")
row.operator("bim.import_resources", text="", icon="IMPORT")
if not self.props.is_editing:
return
@@ -20,13 +20,13 @@ import re
import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.selector
from ifcopenshell.util.selector import Selector
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import close_operator_panel
from blenderbim.bim.module.group import ui
import blenderbim.core.search as core
from itertools import cycle
@@ -121,14 +121,12 @@ class SelectFilterElements(bpy.types.Operator):
filter_groups = tool.Search.get_filter_groups(self.module)
global_ids = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element:
global_id = getattr(element, "GlobalId", None)
if global_id:
if element := tool.Ifc.get_entity(obj):
if global_id := getattr(element, "GlobalId", None):
global_ids.append(global_id)
if len(global_ids) > 50:
# Too much to store in a string property
name = "globalid-filter-" + ifcopenshell.guid.new()
name = f"globalid-filter-{ifcopenshell.guid.new()}"
text_data = bpy.data.texts.new(name)
text_data.from_string(",".join(global_ids))
filter_groups[self.group_index].filters[self.index].value = f"bpy.data.texts['{name}']"
@@ -181,8 +179,7 @@ class Search(Operator):
total_selected = 0
for element in results:
obj = tool.Ifc.get_object(element)
if obj:
if obj := tool.Ifc.get_object(element):
obj.select_set(True)
self.report({"INFO"}, f"{len(results)} Results")
return {"FINISHED"}
@@ -214,7 +211,7 @@ class SaveSearch(Operator, tool.Ifc.Operator):
group = group[0]
group.Description = description
else:
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.name, description=description)
if results:
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=list(results), group=group)
@@ -284,8 +281,7 @@ class ColourByProperty(Operator):
else:
colourscheme[value] = {"colour": next(colours)[0:3], "total": 1}
obj.color = (*colourscheme[value]["colour"], 1)
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
if areas:
if areas := [a for a in context.screen.areas if a.type == "VIEW_3D"]:
areas[0].spaces[0].shading.color_type = "OBJECT"
props.colourscheme.clear()
@@ -298,8 +294,7 @@ class ColourByProperty(Operator):
return {"FINISHED"}
def store_state(self, context):
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
if areas:
if areas := [a for a in context.screen.areas if a.type == "VIEW_3D"]:
self.transaction_data = {"area": areas[0], "color_type": areas[0].spaces[0].shading.color_type}
def rollback(self, data):
@@ -367,7 +362,7 @@ class SaveColourscheme(Operator, tool.Ifc.Operator):
description = json.dumps(
{"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query}
)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.name, description=description)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -449,8 +444,7 @@ class SelectIfcClass(Operator):
classes = set()
predefined_types = set()
for obj in objects:
element = tool.Ifc.get_entity(obj)
if element:
if element := tool.Ifc.get_entity(obj):
classes.add(element.is_a())
predefined_types.add(ifcopenshell.util.element.get_predefined_type(element))
for cls in classes:
@@ -460,9 +454,8 @@ class SelectIfcClass(Operator):
and ifcopenshell.util.element.get_predefined_type(element) not in predefined_types
):
continue
obj = tool.Ifc.get_object(element)
if obj:
obj.select_set(True)
if obj := tool.Ifc.get_object(element):
tool.Blender.select_object(obj)
return {"FINISHED"}
@@ -488,10 +481,7 @@ class ToggleFilterSelection(Operator):
def execute(self, context):
props = bpy.context.scene.BIMSearchProperties
if self.action == "SELECT":
self.selecting_actionbool = True
else:
self.selecting_actionbool = False
self.selecting_actionbool = self.action == "SELECT"
if props.filter_type == "CLASSES":
for ifc_class in props.filter_classes:
ifc_class.is_selected = self.selecting_actionbool
@@ -545,11 +535,7 @@ class ActivateIfcClassFilter(Operator):
"filter_classes",
context.scene.BIMSearchProperties,
"filter_classes_index",
rows=(
20
if len(bpy.context.scene.BIMSearchProperties.filter_classes) > 20
else len(bpy.context.scene.BIMSearchProperties.filter_classes)
),
rows=min(len(bpy.context.scene.BIMSearchProperties.filter_classes), 20),
)
row = self.layout.row(align=True)
row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT"
@@ -604,11 +590,7 @@ class ActivateContainerFilter(Operator):
"filter_container",
context.scene.BIMSearchProperties,
"filter_container_index",
rows=(
20
if len(bpy.context.scene.BIMSearchProperties.filter_container) > 20
else len(bpy.context.scene.BIMSearchProperties.filter_container)
),
rows=min(len(bpy.context.scene.BIMSearchProperties.filter_container), 20),
)
row = self.layout.row(align=True)
row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT"
@@ -634,12 +616,14 @@ class SelectSimilar(Operator, tool.Ifc.Operator):
bl_label = "Select Similar"
bl_options = {"REGISTER", "UNDO"}
key: bpy.props.StringProperty()
def _execute(self, context):
props = context.scene.BIMSearchProperties
obj = context.active_object
element = tool.Ifc.get_entity(obj)
key = props.element_key
if props.element_key == "PredefinedType":
key = self.key
if key == "PredefinedType":
key = "predefined_type"
value = ifcopenshell.util.selector.get_element_value(element, key)
for obj in context.visible_objects:
@@ -118,8 +118,9 @@ class BIMSearchProperties(PropertyGroup):
("location", "Location", "", "PACKAGE", 5),
("type", "Type", "", "FILE_VOLUME", 6),
("group", "Group", "", "OUTLINER_COLLECTION", 7),
("query", "Query", "", "POINTCLOUD_DATA", 8),
("instance", "GlobalId", "", "GRIP", 9),
("parent", "Parent", "", "FILE_PARENT", 8),
("query", "Query", "", "POINTCLOUD_DATA", 9),
("instance", "GlobalId", "", "GRIP", 10),
],
)
saved_searches: EnumProperty(items=get_saved_searches, name="Saved Searches")
@@ -113,7 +113,8 @@ class BIM_PT_select_similar(Panel):
if SelectSimilarData.data["element_key"]:
row = self.layout.row(align=True)
row.prop(props, "element_key", text="")
row.operator("bim.select_similar", text="", icon="RESTRICT_SELECT_OFF")
op = row.operator("bim.select_similar", text="", icon="RESTRICT_SELECT_OFF")
op.key = props.element_key
else:
row = self.layout.row()
row.label(text=f"No Active Element")
@@ -22,7 +22,6 @@ import json
import time
import calendar
import isodate
import pystache
import blenderbim.core.sequence as core
import blenderbim.tool as tool
import blenderbim.bim.module.sequence.helper as helper
@@ -628,7 +627,7 @@ class DisableEditingWorkCalendar(bpy.types.Operator):
class ImportCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "import_csv.bim"
bl_idname = "bim.import_csv"
bl_label = "Import CSV"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
@@ -653,7 +652,7 @@ class ImportCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
class ImportP6(bpy.types.Operator, ImportHelper):
bl_idname = "import_p6.bim"
bl_idname = "bim.import_p6"
bl_label = "Import P6"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xml"
@@ -679,7 +678,7 @@ class ImportP6(bpy.types.Operator, ImportHelper):
class ImportP6XER(bpy.types.Operator, ImportHelper):
bl_idname = "import_p6xer.bim"
bl_idname = "bim.import_p6xer"
bl_label = "Import P6 XER"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xer"
@@ -705,7 +704,7 @@ class ImportP6XER(bpy.types.Operator, ImportHelper):
class ImportPP(bpy.types.Operator, ImportHelper):
bl_idname = "import_pp.bim"
bl_idname = "bim.import_pp"
bl_label = "Import Powerproject .pp"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".pp"
@@ -731,7 +730,7 @@ class ImportPP(bpy.types.Operator, ImportHelper):
class ImportMSP(bpy.types.Operator, ImportHelper):
bl_idname = "import_msp.bim"
bl_idname = "bim.import_msp"
bl_label = "Import MSP"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xml"
@@ -757,7 +756,7 @@ class ImportMSP(bpy.types.Operator, ImportHelper):
class ExportMSP(bpy.types.Operator, ImportHelper):
bl_idname = "export_msp.bim"
bl_idname = "bim.export_msp"
bl_label = "Export MSP"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xml"
@@ -787,7 +786,7 @@ class ExportMSP(bpy.types.Operator, ImportHelper):
class ExportP6(bpy.types.Operator, ImportHelper):
bl_idname = "export_p6.bim"
bl_idname = "bim.export_p6"
bl_label = "Export P6"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xml"
@@ -1409,9 +1408,13 @@ class LoadProductTasks(bpy.types.Operator):
return True
def execute(self, context):
core.load_product_related_tasks(
result = core.load_product_related_tasks(
tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
)
if isinstance(result, str):
self.report({"INFO"}, result)
else:
self.report({"INFO"}, f"{len(result)} product tasks loaded.")
return {"FINISHED"}
@@ -1084,9 +1084,11 @@ class BIM_PT_work_calendars(Panel):
class BIM_PT_4D_Tools(Panel):
bl_label = "4D Tools"
bl_idname = "BIM_PT_4D_Tools"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "4D/5D Toolkit"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_sequence"
def draw(self, context):
self.props = context.scene.BIMWorkScheduleProperties
@@ -44,6 +44,8 @@ def update_elevation(self, context):
def update_active_container_index(self, context):
if self.active_container_index < 0:
return
self.active_container_id = self.containers[self.active_container_index].ifc_definition_id
self.container_name = self.containers[self.active_container_index].name
self.elevation = self.containers[self.active_container_index].elevation
@@ -116,7 +116,7 @@ class BIM_PT_SpatialManager(Panel):
self.props = context.scene.BIMSpatialManagerProperties
row = self.layout.row()
row.operator("bim.load_container_manager", icon="FILE_REFRESH", text="Load Spatial Structure")
if self.props.active_container_index < len(self.props.containers):
if 0 <= self.props.active_container_index < len(self.props.containers):
ifc_definition_id = self.props.containers[self.props.active_container_index].ifc_definition_id
row = self.layout.row()
row.alignment = "RIGHT"
@@ -134,7 +134,7 @@ class BIM_PT_SpatialManager(Panel):
"active_container_index",
)
row = self.layout.row()
if self.props.active_container_index < len(self.props.containers):
if 0 <= self.props.active_container_index < len(self.props.containers):
row.prop(self.props, "container_name", text="")
row.prop(self.props, "elevation", text="")
op = row.operator("bim.edit_container_attributes", icon="CHECKMARK", text="Apply")
@@ -20,6 +20,7 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.attribute
import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.core.structural as core
@@ -50,8 +50,6 @@ classes = (
prop.BIMStylesProperties,
prop.BIMStyleProperties,
ui.BIM_PT_styles,
ui.BIM_PT_style,
ui.BIM_PT_style_attributes,
ui.BIM_UL_styles,
)
@@ -24,7 +24,6 @@ from ifcopenshell.util.doc import get_entity_doc
def refresh():
StylesData.is_loaded = False
StyleAttributesData.is_loaded = False
class StylesData:
@@ -68,30 +67,3 @@ class StylesData:
@classmethod
def total_styles(cls):
return len(tool.Ifc.get().by_type("IfcPresentationStyle"))
class StyleAttributesData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {
"ifc_style_id": cls.ifc_style_id(),
"attributes": cls.attributes(),
}
cls.is_loaded = True
@classmethod
def ifc_style_id(cls):
return bpy.context.active_object.active_material.BIMMaterialProperties.ifc_style_id
@classmethod
def attributes(cls):
style = tool.Ifc.get().by_id(bpy.context.active_object.active_material.BIMMaterialProperties.ifc_style_id)
results = []
for name, value in style.get_info().items():
if name in ["id", "type", "Styles"]:
continue
results.append({"name": name, "value": str(value)})
return results
@@ -22,12 +22,14 @@ import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.style as core
import ifcopenshell.api
import ifcopenshell.util.representation
from blenderbim.bim.module.style.prop import switch_shading
from pathlib import Path
from mathutils import Vector
# TODO: is this still relevant or can it be deleted?
class UpdateStyleColours(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_style_colours"
bl_label = "Save Current Shading Style"
@@ -53,6 +55,7 @@ class UpdateStyleColours(bpy.types.Operator, tool.Ifc.Operator):
self.report({"INFO"}, "Check the system console to see saved style properties")
# TODO: is this still relevant or can it be deleted?
class UpdateStyleTextures(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_style_textures"
bl_label = "Update Style Textures"
@@ -93,9 +96,10 @@ class UnlinkStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unlink_style"
bl_label = "Unlink Style"
bl_options = {"REGISTER", "UNDO"}
style: bpy.props.IntProperty(default=0)
def _execute(self, context):
core.unlink_style(tool.Ifc, tool.Style, obj=context.active_object.active_material)
core.unlink_style(tool.Ifc, style=tool.Ifc.get().by_id(self.style))
class EnableEditingStyle(bpy.types.Operator, tool.Ifc.Operator):
@@ -122,7 +122,7 @@ class ColourRgb(PropertyGroup):
name: StringProperty()
color_value: FloatVectorProperty(size=3, subtype="COLOR", default=(1, 1, 1))
# not exposed in the UI, here just to preserve the data
color_name: StringProperty("Color Name")
color_name: StringProperty(name="Color Name")
# to fit blender.bim.helper.export_attributes
def get_value(self):
@@ -21,7 +21,7 @@ import blenderbim.bim.helper
import blenderbim.tool as tool
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.style.data import StylesData, StyleAttributesData
from blenderbim.bim.module.style.data import StylesData
from bl_ui.properties_material import MaterialButtonsPanel
@@ -62,11 +62,11 @@ class BIM_PT_styles(Panel):
row.operator("bim.enable_adding_presentation_style", text="", icon="ADD")
if active_style:
style = self.props.styles[self.props.active_style_index]
material_name = StylesData.data["styles_to_blender_material_names"][self.props.active_style_index]
material = bpy.data.materials[material_name]
row.operator("bim.duplicate_style", text="", icon="DUPLICATE").style = style.ifc_definition_id
row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF").style = style.ifc_definition_id
op = row.operator("bim.unlink_style", text="", icon="UNLINKED")
op.style = style.ifc_definition_id
op = row.operator("bim.enable_editing_style", text="", icon="GREASEPENCIL")
op.style = style.ifc_definition_id
row.operator("bim.remove_style", text="", icon="X").style = style.ifc_definition_id
@@ -93,9 +93,12 @@ class BIM_PT_styles(Panel):
# style ui tools
if active_style:
row = self.layout.row(align=True)
row.prop(material.BIMStyleProperties, "active_style_type", icon="SHADING_RENDERED", text="")
op = row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
op.style_id = style.ifc_definition_id
material_name = StylesData.data["styles_to_blender_material_names"][self.props.active_style_index]
if material_name: # The user may have unlinked the style, so the material may not exist
material = bpy.data.materials[material_name]
row.prop(material.BIMStyleProperties, "active_style_type", icon="SHADING_RENDERED", text="")
op = row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
op.style_id = style.ifc_definition_id
if self.props.style_type == "IfcSurfaceStyle":
self.layout.label(text="Surface Style Element:")
@@ -259,80 +262,6 @@ class BIM_PT_styles(Panel):
row.operator("bim.disable_editing_style", text="", icon="CANCEL")
class BIM_PT_style(MaterialButtonsPanel, Panel):
bl_label = "Style"
bl_idname = "BIM_PT_style"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
@classmethod
def poll(cls, context):
return (
IfcStore.get_file()
and context.active_object is not None
and context.active_object.active_material is not None
)
def draw(self, context):
mat = context.material
props = mat.BIMMaterialProperties
row = self.layout.row(align=True)
if not props.ifc_style_id:
row.operator("bim.add_style", icon="ADD")
return
row = self.layout.row(align=True)
row.operator("bim.update_style_colours", icon="GREASEPENCIL")
row.operator("bim.update_style_textures", icon="TEXTURE", text="")
row.operator("bim.unlink_style", icon="UNLINKED", text="")
row.operator("bim.remove_style", icon="X", text="").style = props.ifc_style_id
class BIM_PT_style_attributes(Panel):
bl_label = "Style Attributes"
bl_idname = "BIM_PT_style_attributes"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
bl_parent_id = "BIM_PT_style"
@classmethod
def poll(cls, context):
if not IfcStore.get_file():
return False
try:
return bool(context.active_object.active_material.BIMMaterialProperties.ifc_style_id)
except:
return False
def draw(self, context):
if not StyleAttributesData.is_loaded:
StyleAttributesData.load()
elif (
context.active_object.active_material.BIMMaterialProperties.ifc_style_id
!= StyleAttributesData.data["ifc_style_id"]
):
StyleAttributesData.load()
obj = context.active_object.active_material
mprops = obj.BIMMaterialProperties
props = obj.BIMStyleProperties
if props.is_editing:
row = self.layout.row(align=True)
row.operator("bim.edit_style", icon="CHECKMARK")
row.operator("bim.disable_editing_style", icon="CANCEL", text="")
blenderbim.bim.helper.draw_attributes(props.attributes, self.layout)
else:
row = self.layout.row(align=True)
row.label(text="STEP ID")
row.label(text=str(mprops.ifc_style_id))
for attribute in StyleAttributesData.data["attributes"]:
row = self.layout.row(align=True)
row.label(text=attribute["name"])
row.label(text=attribute["value"])
class BIM_UL_styles(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
@@ -22,6 +22,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.schema
import ifcopenshell.util.representation
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.api
import blenderbim.tool as tool
import blenderbim.core.geometry
@@ -454,7 +455,6 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
predefined_type=predefined_type,
ifc_class="IfcStairFlightType",
should_add_representation=False,
context=body,
)
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.bim.add_stair()
@@ -512,10 +512,7 @@ class RemoveType(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
element = tool.Ifc.get().by_id(self.element)
obj = tool.Ifc.get_object(element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
if obj:
tool.Ifc.unlink(obj=obj)
bpy.data.objects.remove(obj)
tool.Geometry.delete_ifc_object(obj)
class RenameType(bpy.types.Operator, tool.Ifc.Operator):

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