Compare commits

...

108 Commits

Author SHA1 Message Date
Dion Moult 60ae4e80ce New get_contained util.element function 2024-06-05 13:04:50 +10:00
Dion Moult 05ec36a37d Spatial manager now shows aggregated objects inside selected space 2024-06-05 13:04:35 +10:00
Andrej730 dc8ea31138 Bump ladybug tools build in docs 2024-06-04 13:42:33 +05:00
Dion Moult faaea7db49 Spatial manager now allows inline editing of names and elevations for a slicker UI 2024-06-04 15:10:28 +10:00
Dion Moult b14ff94303 Spatial manager renamed to project tree and added icons 2024-06-04 13:57:57 +10:00
Dion Moult b3dcfbf0dc See #4796. Fix issue where orphaned aggregate is never shown when loading IFC. 2024-06-04 12:31:07 +10:00
Ryan Schultz b68109d32b small fix for #4745 2024-06-03 12:31:03 -05:00
Dion Moult 2ea3fe4ab9 Fix qto bug where cubic meter units are incorrectly converted. 2024-06-03 22:49:40 +10:00
Andrej730 94c159ccd0 Consider elements with openings reloading representations
Before - https://imgur.com/a/MMnGMBo .
Occurrences with openings wasn't reloaded if you'd change their representation items (add/remove a style, remove a representation item). Also type/occurrences representations were not always reloading.

After - https://imgur.com/a/vBDejb8

Similar thing will be added to switch representation and to representation update to solve issues like this - https://imgur.com/a/0DFIBj2 (issue is still present now).
2024-06-03 17:47:37 +05:00
Andrej730 e4eead6662 styles ui - bim.assign_style_to_selected
Just a simple operator to explicitly assign selected style to the selected objects from Styles UI

https://imgur.com/a/JOLVPz7
2024-06-03 17:44:06 +05:00
Andrej730 ee4e92dfa3 remove bim.update_current_style poll
as this operator can be executed without any objects selected
2024-06-03 17:44:05 +05:00
Andrej730 f4ce2965f9 fix error saving ifc file / assigning classes 6eb3c4814 2024-06-03 17:44:05 +05:00
Andrej730 5fb7e6e375 resource.remove_resource - fix issue removing resources in ifc2x3 2024-06-03 17:44:05 +05:00
Andrej730 c399eb259a resource.add_resource_quantity to ensure quantity type is supported 2024-06-03 17:44:05 +05:00
Andrej730 5ccdf187d5 change type of exception when setting non-optional property to TypeError
TypeError is more correct since error occurs when user is trying to set some attribute value with None while atttribute doesn't support None type. ValueError is typically raised when provided value has a correct type but unsupported value.
2024-06-03 17:44:05 +05:00
Andrej730 d53351d218 Remove redundant description repeating the label #4771 2024-06-03 17:44:05 +05:00
Andrej730 08a82c7b57 remove dead code after #4745 2024-06-03 17:44:05 +05:00
Andrej730 499cdbb9b8 bump old api argument deprecation date 2024-06-03 17:44:05 +05:00
Andrej730 4a5d1eba0b specify exported api methods in __all__
It's py.types requirement, similar to 6e2edbf

Example issue without __all__:
import ifcopenshell
import ifcopenshell.api.project

# "create_file" is not exported from module "ifcopenshell.api.project"
ifcopenshell.api.project.create_file()
2024-06-03 17:44:05 +05:00
Andrej730 70091aa28b typing 2024-06-03 17:44:04 +05:00
Manu Varkey 3d566cc401 Fix missing code in pull request #4784 (#4794)
One line of code was inadvertently missed out in the pull request https://github.com/IfcOpenShell/IfcOpenShell/pull/4784
2024-06-03 21:30:58 +10:00
Dion Moult 6eb3c48144 Fix #3973. Fix #4787. Clarify that you cannot have aggregated physical elements where the whole has a body representation. 2024-06-03 15:37:24 +10:00
Dion Moult 44d180007e Migrate test qto tool to new qto calculator 2024-06-03 15:16:27 +10:00
Dion Moult 7e8060b224 Fix #4791. Hide irrelevant spatial containment panel for spatial elements. 2024-06-03 14:32:01 +10:00
Dion Moult d408568d01 Fix #4793. Bug where autodetected patch UI failed with new signature typing. 2024-06-03 14:08:54 +10:00
Ryan Schultz 47704b1625 Keeps selected objects selected when switching from one drawing to an… (#4745)
Keeps selected objects selected when switching from one drawing to another. As discussed: proposed: https://community.osarch.org/discussion/2179/not-unselecting-objects-when-switching-from-one-drawing-to-the-other#latest
2024-06-02 09:05:38 -05:00
Dion Moult 23fee9c8c8 Bring back formwork calculation feature that uses remesh 2024-06-02 17:43:25 +10:00
Dion Moult c5d1801e1e Fix #4786. Speed up parent filter. 2024-06-02 17:39:23 +10: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
162 changed files with 10600 additions and 8465 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",
+29 -183
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,79 +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 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
@@ -344,6 +186,28 @@ endif
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
@@ -362,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
@@ -142,6 +142,7 @@ classes = [
ui.BIM_PT_tabs,
# Project overview
ui.BIM_PT_tab_project_info,
ui.BIM_PT_tab_project_tree,
ui.BIM_PT_tab_project_setup,
ui.BIM_PT_tab_geometry,
ui.BIM_PT_tab_stakeholders,
@@ -170,6 +171,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,
@@ -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"))
+3 -9
View File
@@ -225,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
@@ -313,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
+4 -3
View File
@@ -1702,19 +1702,20 @@ class IfcImporter:
if aggregate["element"].is_a("IfcElementType"):
self.type_collection.children.link(aggregate["collection"])
continue
self.project["blender"].children.link(aggregate["collection"])
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,36 +40,39 @@ 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 tool.Blender.get_selected_objects():
if obj == relating_obj:
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
result = core.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=relating_obj,
related_obj=obj,
)
if not result:
self.report({"ERROR"}, f" Cannot aggregate {obj.name} to {relating_obj.name}")
try:
core.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=relating_obj,
related_obj=obj,
)
except core.IncompatibleAggregateError:
self.report({"ERROR"}, f"Cannot aggregate {obj.name} to {relating_obj.name}")
except core.AggregateRepresentationError:
self.report({"ERROR"}, f"Cannot aggregate to {relating_obj.name} with a body representation")
class BIM_OT_aggregate_unassign_object(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="")
@@ -198,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"):
@@ -210,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
@@ -755,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
@@ -29,13 +29,9 @@ def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.CoveringTool, after={"bim.structural_tool"}, separator=False, group=False)
bpy.types.Scene.BIMCoveringProperties = bpy.props.PointerProperty(type=prop.BIMCoveringProperties)
# bpy.types.Object.BIMObjectSpatialProperties = bpy.props.PointerProperty(type=prop.BIMObjectSpatialProperties)
# bpy.types.Scene.BIMSpatialManagerProperties = bpy.props.PointerProperty(type=prop.BIMSpatialManagerProperties)
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.CoveringTool)
del bpy.types.Scene.BIMCoveringProperties
# del bpy.types.Object.BIMObjectSpatialProperties
# del bpy.types.Scene.BIMSpatialManagerProperties
@@ -93,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()
@@ -29,6 +29,7 @@ import subprocess
import numpy as np
import multiprocessing
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.ifcopenshell_wrapper
import ifcopenshell.geom
import ifcopenshell.util.selector
@@ -1537,14 +1538,7 @@ class ActivateDrawing(bpy.types.Operator):
if not self.camera_view_point:
viewport_position = tool.Blender.get_viewport_position()
try:
core.activate_drawing_view(tool.Ifc, tool.Blender, tool.Drawing, drawing=drawing)
except core.CameraNotAvailableError:
self.report(
{"ERROR"},
"The drawing view is not available. Ensure you have not excluded it in the active view layer.",
)
return {"CANCELLED"}
core.activate_drawing_view(tool.Ifc, tool.Blender, tool.Drawing, drawing=drawing)
if not self.camera_view_point:
tool.Blender.set_viewport_position(viewport_position)
@@ -1638,7 +1632,7 @@ class ReloadDrawingStyles(bpy.types.Operator):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
camera_props = context.active_object.data.BIMCameraProperties
camera_props = context.scene.camera.data.BIMCameraProperties
# added this part as a temporary fallback
# TODO: should remove it a bit later when projects get more accommodated
@@ -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,14 +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.DuplicateLinkedAggregateTo3dCursor,
operator.OverrideDelete,
operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked,
@@ -46,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,
@@ -120,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:
@@ -713,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()
@@ -724,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]
@@ -1683,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)
@@ -1758,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):
@@ -562,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)
@@ -577,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"}
@@ -594,6 +596,7 @@ class EditMaterialSetItemProfile(bpy.types.Operator, tool.Ifc.Operator):
attributes = blenderbim.bim.helper.export_attributes(self.props.material_set_item_profile_attributes)
profile = tool.Ifc.get().by_id(self.material_set_item).Profile
ifcopenshell.api.run("profile.edit_profile", tool.Ifc.get(), profile=profile, attributes=attributes)
self.props.active_material_set_item_id = 0
self.props.material_set_item_profile_attributes.clear()
model_profile.DumbProfileRegenerator().regenerate_from_profile_def(profile)
@@ -700,13 +703,28 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
class ExpandMaterialCategory(bpy.types.Operator):
bl_idname = "bim.expand_material_category"
bl_label = "Expand Material Category"
bl_description = "SHIFT+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"}
@@ -714,13 +732,28 @@ class ExpandMaterialCategory(bpy.types.Operator):
class ContractMaterialCategory(bpy.types.Operator):
bl_idname = "bim.contract_material_category"
bl_label = "Contract Material Category"
bl_description = "SHIFT+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"}
@@ -123,7 +123,7 @@ 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")
@@ -228,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)
@@ -343,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")
@@ -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
@@ -83,8 +83,17 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Data"))[self.item]
props = obj.BIMArrayProperties
relating_obj = props.relating_array_object
if relating_obj:
element = tool.Ifc.get_entity(relating_obj)
parent_globalid = ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Parent")
parent_element = tool.Ifc.get().by_guid(parent_globalid)
data = json.loads(ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array", "Data"))[self.item]
else:
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Data"))[self.item]
props.count = data["count"]
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props.x = data["x"] * si_conversion
@@ -93,7 +102,9 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
props.use_local_space = data.get("use_local_space", False)
props.sync_children = data.get("sync_children", False)
props.method = data.get("method", "OFFSET")
props.is_editing = self.item
return {"FINISHED"}
@@ -137,6 +148,10 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, True)
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
#clears the relating_array_object so it doesn't show again next time
props.relating_array_object = None
return {"FINISHED"}
@@ -87,6 +87,21 @@ def update_type_page(self, context):
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
def update_relating_array_from_object(self, context):
bpy.ops.bim.enable_editing_array(item=self.is_editing)
return
def is_object_array_applicable(self, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return False
return ifcopenshell.util.element.get_pset(element, "BBIM_Array")
class BIMModelProperties(PropertyGroup):
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
relating_type_id: bpy.props.EnumProperty(
@@ -203,6 +218,14 @@ class BIMArrayProperties(PropertyGroup):
description="Regenerate all children based on the parent object",
default=False,
)
relating_array_object: bpy.props.PointerProperty(
type=bpy.types.Object,
name="Copy Array Properties",
update=update_relating_array_from_object,
poll=is_object_array_applicable,
)
class BIMStairProperties(PropertyGroup):
@@ -223,6 +223,8 @@ class BIM_PT_array(bpy.types.Panel):
row = col.row(align=True)
row.prop(props, "z")
row.operator("bim.input_cursor_z_array", icon="CURSOR", text="")
row = col.row(align=True)
row.prop(props, "relating_array_object", icon="COPYDOWN")
else:
row = box.row(align=True)
name = f"{array['count']} Items ({array.get('method', 'OFFSET').capitalize()})"
@@ -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:
@@ -121,12 +121,16 @@ class UpdateIfcPatchArguments(bpy.types.Operator):
for arg_name in inputs:
arg_info = inputs[arg_name]
new_attr = patch_args.add()
data_type = arg_info.get("type", "str")
if isinstance(data_type, list):
data_type = [dt for dt in data_type if dt != "NoneType"][0]
new_attr.data_type = {
"Literal": "string",
"str": "string",
"float": "float",
"int": "integer",
"bool": "boolean",
}[arg_info.get("type", "str")]
}[data_type]
new_attr.name = arg_name
new_attr.set_value(arg_info.get("default", new_attr.get_value_default()))
return {"FINISHED"}
@@ -76,6 +76,11 @@ classes = (
gizmo.ClippingPlane,
)
if bpy.app.version >= (4, 1, 0):
classes += (
operator.IFCFileHandlerOperator,
operator.BIM_FH_import_ifc,
)
addon_keymaps = []
@@ -2085,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
@@ -27,8 +27,6 @@ classes = (
operator.DisablePsetEditing,
operator.EditPset,
operator.EnablePsetEditing,
operator.GuessQuantity,
operator.CalculateQuantity,
operator.RemovePset,
operator.TogglePsetExpansion,
operator.BIM_OT_add_property_to_edit,
@@ -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
@@ -297,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,10 +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
class Operator:
@@ -142,6 +140,8 @@ 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):
@@ -214,55 +214,6 @@ class AddQto(bpy.types.Operator, Operator):
)
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"
@@ -20,6 +20,7 @@ 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
File diff suppressed because it is too large Load Diff
@@ -103,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
@@ -160,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):
@@ -23,13 +23,15 @@ classes = (
operator.CalculateCircleRadius,
operator.CalculateEdgeLengths,
operator.CalculateFaceAreas,
operator.CalculateFormworkArea,
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,82 @@ class CalculateObjectVolumes(bpy.types.Operator):
return {"FINISHED"}
class ExecuteQtoMethod(bpy.types.Operator):
bl_idname = "bim.execute_qto_method"
bl_label = "Execute Qto Method"
class CalculateFormworkArea(bpy.types.Operator):
bl_idname = "bim.calculate_formwork_area"
bl_label = "Calculate Formwork Area"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
return context.selected_objects and context.active_object
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))
result = helper.calculate_formwork_area([o for o in context.selected_objects if o.type == "MESH"], context)
context.scene.BIMQtoProperties.qto_result = str(round(result, 3))
return {"FINISHED"}
class QuantifyObjects(bpy.types.Operator):
bl_idname = "bim.quantify_objects"
bl_label = "Quantify Objects"
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 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")
+98 -45
View File
@@ -20,69 +20,122 @@ 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
row = layout.row()
row.prop(props, "qto_result", text="Results")
row = layout.row(align=True)
row = layout.row()
row.operator("bim.calculate_circle_radius")
row = layout.row(align=True)
row = layout.row()
row.operator("bim.calculate_edge_lengths")
row = layout.row(align=True)
row = layout.row()
row.operator("bim.calculate_face_areas")
row = layout.row(align=True)
row = layout.row()
row.operator("bim.calculate_object_volumes")
row = layout.row()
row.operator("bim.calculate_formwork_area")
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)
@@ -37,13 +37,16 @@ from bpy.props import (
CollectionProperty,
)
quantitytypes_enum = []
quantitytypes_enum = {}
def purge():
global quantitytypes_enum
quantitytypes_enum = []
def setup_quantity_types_enum():
resources = ifcopenshell.util.resource.RESOURCES_TO_QUANTITIES
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 +66,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 +83,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 +120,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"),
@@ -27,7 +27,6 @@ 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
@@ -122,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}']"
@@ -182,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"}
@@ -285,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()
@@ -299,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):
@@ -450,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:
@@ -461,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"}
@@ -489,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
@@ -546,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"
@@ -605,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"
@@ -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")
@@ -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
@@ -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
@@ -22,31 +22,32 @@ from . import ui, prop, operator, workspace
classes = (
operator.AssignContainer,
operator.ChangeSpatialLevel,
operator.ContractContainer,
operator.CopyToContainer,
operator.DeleteContainer,
operator.DereferenceStructure,
operator.DisableEditingContainer,
operator.EditContainerAttributes,
operator.EnableEditingContainer,
operator.ExpandContainer,
operator.LoadContainerManager,
operator.ReferenceStructure,
operator.RemoveContainer,
operator.SelectContainer,
operator.SelectSimilarContainer,
operator.SelectProduct,
operator.LoadContainerManager,
operator.EditContainerAttributes,
operator.AddBuildingStorey,
operator.ContractContainer,
operator.ExpandContainer,
operator.DeleteContainer,
operator.SelectDecomposedElements,
operator.SelectProduct,
operator.SelectSimilarContainer,
prop.SpatialElement,
prop.Element,
prop.BIMSpatialProperties,
prop.BIMObjectSpatialProperties,
prop.BIMContainer,
prop.BIMSpatialManagerProperties,
prop.BIMProjectTreeProperties,
ui.BIM_PT_spatial,
ui.BIM_UL_containers,
ui.BIM_UL_containers_manager,
ui.BIM_PT_SpatialManager,
ui.BIM_UL_elements,
ui.BIM_PT_project_tree,
workspace.Hotkey,
)
@@ -56,7 +57,7 @@ def register():
bpy.utils.register_tool(workspace.SpatialTool, after={"bim.annotation_tool"}, separator=False, group=False)
bpy.types.Scene.BIMSpatialProperties = bpy.props.PointerProperty(type=prop.BIMSpatialProperties)
bpy.types.Object.BIMObjectSpatialProperties = bpy.props.PointerProperty(type=prop.BIMObjectSpatialProperties)
bpy.types.Scene.BIMSpatialManagerProperties = bpy.props.PointerProperty(type=prop.BIMSpatialManagerProperties)
bpy.types.Scene.BIMProjectTreeProperties = bpy.props.PointerProperty(type=prop.BIMProjectTreeProperties)
def unregister():
@@ -64,4 +65,4 @@ def unregister():
bpy.utils.unregister_tool(workspace.SpatialTool)
del bpy.types.Scene.BIMSpatialProperties
del bpy.types.Object.BIMObjectSpatialProperties
del bpy.types.Scene.BIMSpatialManagerProperties
del bpy.types.Scene.BIMProjectTreeProperties
@@ -23,6 +23,7 @@ import ifcopenshell.util.element
def refresh():
SpatialData.is_loaded = False
ProjectTreeData.is_loaded = False
class SpatialData:
@@ -31,28 +32,28 @@ class SpatialData:
@classmethod
def load(cls):
cls.data = {
"parent_container_id": cls.parent_container_id(),
"is_directly_contained": cls.is_directly_contained(),
"label": cls.label(),
"references": cls.references(),
"containers": cls.containers(),
}
cls.is_loaded = True
cls.data["poll"] = cls.poll()
if cls.data["poll"]:
cls.data.update(
{
"parent_container_id": cls.parent_container_id(),
"is_directly_contained": cls.is_directly_contained(),
"label": cls.label(),
"references": cls.references(),
}
)
@classmethod
def containers(cls):
results = {}
if tool.Ifc.get_schema() == "IFC2X3":
spatial_elements = tool.Ifc.get().by_type("IfcSpatialStructureElement")
else:
spatial_elements = tool.Ifc.get().by_type("IfcSpatialElement")
for container in spatial_elements:
results[container.id()] = {
"type": container.is_a(),
"id": container.id(),
}
return results
def poll(cls):
if not bpy.context.active_object:
return False
element = tool.Ifc.get_entity(bpy.context.active_object)
if not element:
return False
if element.is_a("IfcElement") or element.is_a("IfcAnnotation") or element.is_a("IfcGrid"):
return True
return False
@classmethod
def parent_container_id(cls):
@@ -81,3 +82,29 @@ class SpatialData:
@classmethod
def is_directly_contained(cls):
return bool(getattr(tool.Ifc.get_entity(bpy.context.active_object), "ContainedInStructure", False))
class ProjectTreeData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {
"containers": cls.containers(),
}
@classmethod
def containers(cls):
results = {}
if tool.Ifc.get_schema() == "IFC2X3":
spatial_elements = tool.Ifc.get().by_type("IfcSpatialStructureElement")
else:
spatial_elements = tool.Ifc.get().by_type("IfcSpatialElement")
for container in spatial_elements:
results[container.id()] = {
"type": container.is_a(),
"id": container.id(),
}
return results
@@ -227,26 +227,6 @@ class DeleteContainer(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.delete_container(tool.Ifc, tool.Spatial, tool.Geometry, container=tool.Ifc.get().by_id(self.container))
class AddBuildingStorey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_building_storey"
bl_label = "Add Storey"
bl_options = {"REGISTER", "UNDO"}
part_class: bpy.props.StringProperty()
def _execute(self, context):
active_container = tool.Spatial.get_active_container()
obj = tool.Ifc.get_object(active_container)
blenderbim.core.aggregate.add_part_to_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
tool.Blender,
obj=obj,
part_class=self.part_class,
part_name="Unnamed",
)
core.load_container_manager(tool.Spatial)
class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.select_decomposed_elements"
@@ -36,27 +36,21 @@ import ifcopenshell
def update_elevation(self, context):
entity = tool.Ifc.get().by_id(self.active_container_id)
obj = tool.Ifc.get_object(entity)
if not obj:
return
obj.location.z = self.elevation
if ifc_definition_id := self.ifc_definition_id:
entity = tool.Ifc.get().by_id(ifc_definition_id)
obj = tool.Ifc.get_object(entity)
if not obj:
return
obj.location.z = self.elevation
def update_name(self, context):
if ifc_definition_id := self.ifc_definition_id:
tool.Spatial.edit_container_name(tool.Ifc.get().by_id(ifc_definition_id), self.name)
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
def updateContainerName(self, context):
props = context.scene.BIMSpatialManagerProperties
if not props.is_container_update_enabled or self.name == "Unnamed":
return
tool.Spatial.edit_container_name(tool.Ifc.get().by_id(props.active_container_id), self.name)
props.container_name = self.name
tool.Spatial.load_contained_elements()
def update_relating_container_from_object(self, context):
@@ -104,20 +98,34 @@ class BIMObjectSpatialProperties(PropertyGroup):
class BIMContainer(PropertyGroup):
name: StringProperty(name="Name", update=updateContainerName)
elevation: FloatProperty(name="Elevation", subtype="DISTANCE")
name: StringProperty(name="Name", update=update_name)
ifc_class: StringProperty(name="IFC Class")
description: StringProperty(name="Description")
long_name: StringProperty(name="Long Name")
elevation: FloatProperty(name="Elevation", subtype="DISTANCE", update=update_elevation)
level_index: IntProperty(name="Level Index")
has_children: BoolProperty(name="Has Children")
is_expanded: BoolProperty(name="Is Expanded")
ifc_definition_id: IntProperty(name="IFC Definition ID")
class BIMSpatialManagerProperties(PropertyGroup):
class Element(PropertyGroup):
name: StringProperty(name="Name")
is_class: BoolProperty(name="Is Class", default=False)
is_type: BoolProperty(name="Is Type", default=False)
total: IntProperty(name="Total")
class BIMProjectTreeProperties(PropertyGroup):
containers: CollectionProperty(name="Containers", type=BIMContainer)
contracted_containers: StringProperty(name="Contracted containers", default="[]")
expanded_containers: StringProperty(name="Expanded containers", default="[]")
active_container_index: IntProperty(name="Active Container Index", update=update_active_container_index)
active_container_id: IntProperty(name="Active Container Id")
container_name: StringProperty(name="Container Name")
elevation: FloatProperty(name="Elevation", update=update_elevation, subtype="DISTANCE")
is_container_update_enabled: BoolProperty(name="Is Container Update Enabled", default=True) # TODO:review
elements: CollectionProperty(name="Elements", type=Element)
active_element_index: IntProperty(name="Active Element Index")
total_elements: IntProperty(name="Total Elements")
@property
def active_container(self):
if self.containers and self.active_container_index < len(self.containers):
return self.containers[self.active_container_index]
@@ -17,8 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.spatial.data import SpatialData
from blenderbim.bim.module.spatial.data import SpatialData, ProjectTreeData
import blenderbim.tool as tool
@@ -32,14 +31,9 @@ class BIM_PT_spatial(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
return False
oprops = context.active_object.BIMObjectProperties
if not oprops.ifc_definition_id:
return False
if not IfcStore.get_element(oprops.ifc_definition_id):
return False
return True
if not SpatialData.is_loaded:
SpatialData.load()
return SpatialData.data["poll"]
def draw(self, context):
if not SpatialData.is_loaded:
@@ -97,34 +91,41 @@ class BIM_UL_containers(UIList):
)
class BIM_PT_SpatialManager(Panel):
bl_label = "Spatial Manager"
bl_idname = "BIM_PT_SpatialManager"
class BIM_PT_project_tree(Panel):
bl_label = "Project Tree"
bl_idname = "BIM_PT_project_tree"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "BIM_PT_tab_project_setup"
bl_parent_id = "BIM_PT_tab_project_tree"
bl_options = {"HIDE_HEADER"}
@classmethod
def poll(cls, context):
return tool.Ifc.get() and tool.Ifc.schema().name() != "IFC2X3"
return tool.Ifc.get()
def draw(self, context):
if not SpatialData.is_loaded:
SpatialData.load()
self.props = context.scene.BIMSpatialManagerProperties
row = self.layout.row()
row.operator("bim.load_container_manager", icon="FILE_REFRESH", text="Load Spatial Structure")
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"
row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="Select Children")
spatial_data = SpatialData.data["containers"].get(ifc_definition_id, None)
if spatial_data and spatial_data["type"] in ["IfcBuildingStorey", "IfcBuilding"]:
row.operator("bim.add_building_storey", icon="ADD", text="Add storey").part_class = "IfcBuildingStorey"
row.operator("bim.delete_container", icon="X", text="Delete").container = ifc_definition_id
if not ProjectTreeData.is_loaded:
ProjectTreeData.load()
self.props = context.scene.BIMProjectTreeProperties
if self.props.active_container:
row = self.layout.row(align=True)
row.label(
text=f"Active {self.props.active_container.ifc_class}: {self.props.active_container.name}",
icon="OUTLINER_COLLECTION",
)
row.operator("bim.select_decomposed_elements", icon="HIDE_OFF", text="")
row.operator("bim.delete_container", icon="X", text="").container = (
self.props.active_container.ifc_definition_id
)
row.operator("bim.load_container_manager", icon="FILE_REFRESH", text="")
else:
row = self.layout.row(align=True)
row.label(text="Warning: No Active Container", icon="ERROR")
row.operator("bim.load_container_manager", icon="FILE_REFRESH", text="")
self.layout.template_list(
"BIM_UL_containers_manager",
"",
@@ -132,13 +133,33 @@ class BIM_PT_SpatialManager(Panel):
"containers",
self.props,
"active_container_index",
rows=10,
)
if not self.props.active_container:
return
# spatial_data = ProjectTreeData.data["containers"].get(ifc_definition_id, None)
# if spatial_data and spatial_data["type"] in ["IfcBuildingStorey", "IfcBuilding"]:
# row.operator("bim.add_building_storey", icon="ADD", text="Add storey").part_class = "IfcBuildingStorey"
if not self.props.total_elements:
row = self.layout.row()
row.label(text="No Contained Elements", icon="FILE_3D")
return
row = self.layout.row(align=True)
row.label(text=f"{self.props.total_elements} Contained Elements", icon="FILE_3D")
row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="")
self.layout.template_list(
"BIM_UL_elements",
"",
self.props,
"elements",
self.props,
"active_element_index",
)
row = self.layout.row()
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")
op.container = self.props.containers[self.props.active_container_index].ifc_definition_id
class BIM_UL_containers_manager(UIList):
@@ -146,22 +167,52 @@ class BIM_UL_containers_manager(UIList):
if item:
row = layout.row(align=True)
self.draw_hierarchy(row, item)
split1 = row.split(factor=0.7)
split1.prop(item, "name", emboss=False, text="")
split2 = row.split(factor=1)
split2.label(icon="BLANK1", text=tool.Unit.blender_format_unit(item.elevation))
row.prop(item, "name", emboss=False, text="")
row.prop(item, "long_name", emboss=False, text="")
col = row.column()
col.alignment = "RIGHT"
col.prop(item, "elevation", emboss=False, text="")
def draw_hierarchy(self, row, item):
for i in range(0, item.level_index):
row.label(text="", icon="BLANK1")
if item.has_children:
if item.is_expanded:
row.operator(
"bim.contract_container", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
).container = item.ifc_definition_id
row.operator("bim.contract_container", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN").container = (
item.ifc_definition_id
)
else:
row.operator(
"bim.expand_container", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).container = item.ifc_definition_id
row.operator("bim.expand_container", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT").container = (
item.ifc_definition_id
)
else:
row.label(text="", icon="DOT")
row.label(text="", icon="BLANK1")
if item.ifc_class == "IfcSite":
row.label(text="", icon="WORLD")
elif item.ifc_class == "IfcBuilding":
row.label(text="", icon="HOME")
elif item.ifc_class == "IfcBuildingStorey":
row.label(text="", icon="LINENUMBERS_OFF")
elif item.ifc_class == "IfcSpace":
row.label(text="", icon="ANTIALIASED")
else:
row.label(text="", icon="META_PLANE")
class BIM_UL_elements(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
if item.is_class:
row.label(text="", icon="DISCLOSURE_TRI_DOWN")
row.label(text=item.name)
col = row.column()
col.alignment = "RIGHT"
col.label(text=str(item.total))
elif item.is_type:
row.label(text="", icon="BLANK1")
row.label(text="", icon="DISCLOSURE_TRI_DOWN")
row.label(text=item.name)
col = row.column()
col.alignment = "RIGHT"
col.label(text=str(item.total))
@@ -64,16 +64,10 @@ def add_layout_hotkey(layout, text, hotkey, description):
tool.Blender.add_layout_hotkey_operator(*args)
# NOTES before adding new operators:
# - add scene.BIMSpatialProperties
# - add SpatialData
class SpatialToolUI:
@classmethod
def draw(cls, context, layout):
cls.layout = layout
# cls.props = context.scene.BIMSpatialProperties
cls.model_props = context.scene.BIMModelProperties
row = cls.layout.row(align=True)
@@ -151,12 +145,10 @@ class Hotkey(bpy.types.Operator, Operator):
return operator.description or ""
def _execute(self, context):
# self.props = context.scene.BIMSpatialProperties
getattr(self, f"hotkey_{self.hotkey}")()
def invoke(self, context, event):
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
# self.props = context.scene.BIMSpatialProperties
return self.execute(context)
def draw(self, context):
@@ -24,6 +24,7 @@ classes = (
operator.AddPresentationStyle,
operator.AddStyle,
operator.AddSurfaceTexture,
operator.AssignStyleToSelected,
operator.BrowseExternalStyle,
operator.RemoveTextureMap,
operator.ChooseTextureMapPath,
@@ -22,6 +22,8 @@ import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.style as core
import ifcopenshell.api
import ifcopenshell.api.style
import ifcopenshell.util.representation
from blenderbim.bim.module.style.prop import switch_shading
from pathlib import Path
@@ -160,13 +162,6 @@ class UpdateCurrentStyle(bpy.types.Operator):
update_all: bpy.props.BoolProperty(name="Update All", default=False, options={"SKIP_SAVE"})
style_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"})
@classmethod
def poll(cls, context):
if not context.selected_objects:
cls.poll_message_set("No objects selected")
return False
return True
def invoke(self, context, event):
# updating all styles on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
@@ -957,3 +952,49 @@ class SaveUVToStyle(bpy.types.Operator, tool.Ifc.Operator):
self.report({"INFO"}, f"UV saved to the style {style.Name}")
return {"FINISHED"}
class AssignStyleToSelected(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_style_to_selected"
bl_label = "Assign Style To Selected"
bl_description = "Assign style to the selected objects' active representations"
bl_options = {"REGISTER", "UNDO"}
style_id: bpy.props.IntProperty(name="Style ID")
@classmethod
def poll(cls, context):
if not context.selected_objects:
cls.poll_message_set("No objects selected")
return False
return True
def _execute(self, context):
if self.style_id == 0:
self.report({"ERROR"}, "No style provided")
return {"CANCELLED"}
ifc_file = tool.Ifc.get()
style = ifc_file.by_id(self.style_id)
representations: dict[ifcopenshell.entity_instance, bpy.types.Object] = {}
for obj in context.selected_objects:
representation = tool.Geometry.get_active_representation(obj)
if not representation:
continue
representation = tool.Geometry.resolve_mapped_representation(representation)
representations.setdefault(representation, obj)
if not representations:
self.report({"INFO"}, "No IFC objects with representations selected.")
return {"FINISHED"}
for representation in representations:
ifcopenshell.api.style.assign_representation_styles(
ifc_file,
shape_representation=representation,
styles=[style],
should_use_presentation_style_assignment=tool.Geometry.should_use_presentation_style_assignment(),
)
tool.Geometry.reload_representation(representations.values())
return {"FINISHED"}
@@ -65,6 +65,8 @@ class BIM_PT_styles(Panel):
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.assign_style_to_selected", text="", icon="BRUSH_DATA")
op.style_id = 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")
-1
View File
@@ -263,7 +263,6 @@ class Attribute(PropertyGroup):
is_optional: BoolProperty(name="Is Optional")
is_uri: BoolProperty(name="Is Uri", default=False)
is_selected: BoolProperty(name="Is Selected", default=False)
has_calculator: BoolProperty(name="Has Calculator", default=False)
value_min: FloatProperty(description="This is used to validate int_value and float_value")
value_min_constraint: BoolProperty(default=False, description="True if the numerical value has a lower bound")
value_max: FloatProperty(description="This is used to validate int_value and float_value")
+40 -13
View File
@@ -194,19 +194,17 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout_svg_command: StringProperty(name="Layout SVG Command", description='E.g. [["firefox", "path"]]')
pdf_command: StringProperty(name="PDF Command", description='E.g. [["firefox", "path"]]')
spreadsheet_command: StringProperty(name="Spreadsheet Command", description='E.g. [["libreoffice", "path"]]')
openlca_port: IntProperty(name="OpenLCA IPC Port", default=8080)
should_hide_empty_props: BoolProperty(name="Should Hide Empty Properties", default=True)
should_setup_workspace: BoolProperty(name="Should Setup Workspace Layout for BIM", default=True)
should_hide_empty_props: BoolProperty(name="Hide Empty Properties", default=True)
should_setup_workspace: BoolProperty(name="Setup Workspace Layout for BIM", default=True)
activate_workspace: BoolProperty(name="Activate BIM Workspace on Startup", default=True)
should_setup_toolbar: BoolProperty(
name="Always Show Toolbar In 3D Viewport",
default=True,
description="If disabled, the toolbar will only load when an IFC model is active",
)
should_play_chaching_sound: BoolProperty(
name="Should Make A Cha-Ching Sound When Project Costs Updates", default=False
)
lock_grids_on_import: BoolProperty(name="Should Lock Grids By Default", default=True)
spatial_elements_unselectable: BoolProperty(name="Should Make Spatial Elements Unselectable By Default", default=True)
should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False)
lock_grids_on_import: BoolProperty(name="Lock Grids By Default", default=True)
spatial_elements_unselectable: BoolProperty(name="Make Spatial Elements Unselectable By Default", default=True)
decorations_colour: bpy.props.FloatVectorProperty(
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
)
@@ -277,12 +275,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row = layout.row()
row.prop(self, "spreadsheet_command")
row = layout.row()
row.prop(self, "openlca_port")
row = layout.row()
row.prop(self, "should_hide_empty_props")
row = layout.row()
row.prop(self, "should_setup_workspace")
row = layout.row()
row.prop(self, "activate_workspace")
row = layout.row()
row.prop(self, "should_setup_toolbar")
row = layout.row()
row.prop(self, "should_play_chaching_sound")
@@ -407,7 +405,7 @@ class BIM_PT_tabs(Panel):
if blenderbim.last_error:
box = self.layout.box()
box.alert=True
box.alert = True
row = box.row(align=True)
row.label(text="BlenderBIM experienced an error :(", icon="ERROR")
row.operator("bim.close_error", text="", icon="CANCEL")
@@ -442,6 +440,20 @@ class BIM_PT_tab_project_info(Panel):
pass
class BIM_PT_tab_project_tree(Panel):
bl_label = "Project Tree"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "PROJECT")
def draw(self, context):
pass
class BIM_PT_tab_project_setup(Panel):
bl_label = "Project Setup"
bl_space_type = "PROPERTIES"
@@ -490,7 +502,7 @@ class BIM_PT_tab_grouping_and_filtering(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED","HEADER_LAYOUT_EXPAND"}
bl_options = {"DEFAULT_CLOSED", "HEADER_LAYOUT_EXPAND"}
@classmethod
def poll(cls, context):
@@ -503,8 +515,9 @@ class BIM_PT_tab_grouping_and_filtering(Panel):
# Draws help button on the right
row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row
row.operator("bim.open_uri", text="", icon="HELP").uri = \
row.operator("bim.open_uri", text="", icon="HELP").uri = (
"https://docs.ifcopenshell.org/ifcopenshell-python/selector_syntax.html"
)
class BIM_PT_tab_geometry(Panel):
@@ -535,6 +548,20 @@ class BIM_PT_tab_status(Panel):
pass
class BIM_PT_tab_qto(Panel):
bl_label = "Quantity Take-off"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get()
def draw(self, context):
pass
class BIM_PT_tab_resources(Panel):
bl_label = "Resources"
bl_space_type = "PROPERTIES"
+46 -7
View File
@@ -16,20 +16,37 @@
# 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 __future__ import annotations
from typing import TYPE_CHECKING, Optional, Union
def enable_editing_aggregate(aggregator, obj=None):
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def enable_editing_aggregate(aggregator: tool.Aggregate, obj: bpy.types.Object) -> None:
aggregator.enable_editing(obj)
def disable_editing_aggregate(aggregator, obj=None):
def disable_editing_aggregate(aggregator: tool.Aggregate, obj: bpy.types.Object) -> None:
aggregator.disable_editing(obj)
def assign_object(ifc, aggregator, collector, relating_obj=None, related_obj=None):
def assign_object(
ifc: tool.Ifc,
aggregator: tool.Aggregate,
collector: tool.Collector,
relating_obj: Optional[bpy.types.Object] = None,
related_obj: Optional[bpy.types.Object] = None,
) -> Union[ifcopenshell.entity_instance, None]:
if not aggregator.can_aggregate(relating_obj, related_obj):
return
raise IncompatibleAggregateError
relating_object = ifc.get_entity(relating_obj)
if aggregator.has_physical_body_representation(relating_object):
raise AggregateRepresentationError
rel = ifc.run(
"aggregate.assign_object", products=[ifc.get_entity(related_obj)], relating_object=ifc.get_entity(relating_obj)
"aggregate.assign_object", products=[ifc.get_entity(related_obj)], relating_object=relating_object
)
collector.assign(relating_obj)
collector.assign(related_obj)
@@ -37,7 +54,13 @@ def assign_object(ifc, aggregator, collector, relating_obj=None, related_obj=Non
return rel
def unassign_object(ifc, aggregate, collector, relating_obj=None, related_obj=None):
def unassign_object(
ifc: tool.Ifc,
aggregate: tool.Aggregate,
collector: tool.Collector,
relating_obj: Optional[bpy.types.Object] = None,
related_obj: Optional[bpy.types.Object] = None,
) -> None:
related_element = ifc.get_entity(related_obj)
container = aggregate.get_container(related_element)
if not relating_obj:
@@ -52,7 +75,23 @@ def unassign_object(ifc, aggregate, collector, relating_obj=None, related_obj=No
collector.assign(related_obj)
def add_part_to_object(ifc, aggregator, collector, blender, obj, part_class, part_name=None):
def add_part_to_object(
ifc: tool.Ifc,
aggregator: tool.Aggregate,
collector: tool.Collector,
blender: tool.Blender,
obj: bpy.types.Object,
part_class: str,
part_name: Optional[str] = None,
) -> None:
part_obj = blender.create_ifc_object(ifc_class=part_class, name=part_name)
assign_object(ifc, aggregator, collector, relating_obj=obj, related_obj=part_obj)
blender.set_active_object(obj)
class IncompatibleAggregateError(Exception):
pass
class AggregateRepresentationError(Exception):
pass
@@ -484,12 +484,4 @@ def activate_drawing_view(
drawing_tool.import_annotations_in_group(drawing_tool.get_drawing_group(drawing))
blender.activate_camera(camera)
drawing_tool.isolate_camera_collection(camera)
try:
blender.set_active_object(camera)
except:
raise CameraNotAvailableError()
drawing_tool.activate_drawing(camera)
class CameraNotAvailableError(Exception):
pass
+41 -15
View File
@@ -16,15 +16,33 @@
# 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 __future__ import annotations
from typing import TYPE_CHECKING, Optional, Union
def unlink_material(ifc, obj=None):
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def unlink_material(ifc: tool.Ifc, obj: bpy.types.Material) -> None:
ifc.unlink(obj=obj)
def add_material(ifc, material, style, obj=None, name=None, category=None, description=None):
def add_material(
ifc: tool.Ifc,
material: tool.Material,
style: tool.Style,
obj: Optional[bpy.types.Material] = None,
name: Optional[str] = None,
category: Optional[str] = None,
description: Optional[str] = None,
) -> ifcopenshell.entity_instance:
if not obj:
obj = material.add_default_material_object(name)
ifc_material = ifc.run("material.add_material", name=material.get_name(obj), category=category, description=description)
ifc_material = ifc.run(
"material.add_material", name=material.get_name(obj), category=category, description=description
)
ifc.link(ifc_material, obj)
ifc_style = style.get_style(obj)
if ifc_style:
@@ -36,14 +54,16 @@ def add_material(ifc, material, style, obj=None, name=None, category=None, descr
return ifc_material
def add_material_set(ifc, material, set_type=None):
def add_material_set(ifc: tool.Ifc, material: tool.Material, set_type: str) -> ifcopenshell.entity_instance:
ifc_material = ifc.run("material.add_material_set", name="Unnamed", set_type=set_type)
if material.is_editing_materials():
material.import_material_definitions(material.get_active_material_type())
return ifc_material
def remove_material(ifc, material_tool, style, material=None) -> bool:
def remove_material(
ifc: tool.Ifc, material_tool: tool.Material, style: tool.Style, material: ifcopenshell.entity_instance
) -> bool:
"""returns True after deleting False,\n
returns False if material used in material sets and cannot be removed"""
if material_tool.is_material_used_in_sets(material):
@@ -58,31 +78,33 @@ def remove_material(ifc, material_tool, style, material=None) -> bool:
return True
def remove_material_set(ifc, material_tool, material=None):
def remove_material_set(ifc: tool.Ifc, material_tool: tool.Material, material: ifcopenshell.entity_instance) -> None:
ifc.run("material.remove_material_set", material=material)
if material_tool.is_editing_materials():
material_tool.import_material_definitions(material_tool.get_active_material_type())
def load_materials(material, material_type=None):
def load_materials(material: tool.Material, material_type: str) -> None:
material.import_material_definitions(material_type)
material.enable_editing_materials()
def disable_editing_materials(material):
def disable_editing_materials(material: tool.Material) -> None:
material.disable_editing_materials()
def select_by_material(material_tool, spatial, material=None):
def select_by_material(
material_tool: tool.Material, spatial: tool.Spatial, material: ifcopenshell.entity_instance
) -> None:
spatial.select_products(material_tool.get_elements_by_material(material))
def enable_editing_material(material_tool, material):
def enable_editing_material(material_tool: tool.Material, material: ifcopenshell.entity_instance) -> None:
material_tool.load_material_attributes(material)
material_tool.enable_editing_material(material)
def edit_material(ifc, material_tool, material):
def edit_material(ifc: tool.Ifc, material_tool: tool.Material, material: ifcopenshell.entity_instance) -> None:
attributes = material_tool.get_material_attributes()
ifc.run("material.edit_material", material=material, attributes=attributes)
material_tool.sync_blender_material_name(material)
@@ -92,11 +114,13 @@ def edit_material(ifc, material_tool, material):
material_tool.enable_editing_materials()
def disable_editing_material(material_tool):
def disable_editing_material(material_tool: tool.Material) -> None:
material_tool.disable_editing_material()
def assign_material(ifc, material_tool, material_type, objects):
def assign_material(
ifc: tool.Ifc, material_tool: tool.Material, material_type: Union[str, None], objects: list[bpy.types.Object]
) -> None:
material_type = material_type or material_tool.get_active_object_material()
material = material_tool.get_active_material()
for obj in objects:
@@ -109,7 +133,7 @@ def assign_material(ifc, material_tool, material_type, objects):
material_tool.add_material_to_set(material_set=assigned_material, material=material)
def unassign_material(ifc, material_tool, objects):
def unassign_material(ifc: tool.Ifc, material_tool: tool.Material, objects: list[bpy.types.Object]) -> None:
for obj in objects:
element = ifc.get_entity(obj)
if element:
@@ -125,7 +149,9 @@ def unassign_material(ifc, material_tool, objects):
ifc.run("material.unassign_material", products=[element])
def patch_non_parametric_mep_segment(ifc, material_tool, profile_tool, obj):
def patch_non_parametric_mep_segment(
ifc: tool.Ifc, material_tool: tool.Material, profile_tool: tool.Profile, obj: bpy.types.Object
) -> None:
element = ifc.get_entity(obj)
if not element:
return
+8 -46
View File
@@ -16,53 +16,15 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING
def calculate_circle_radius(qto, obj=None):
if TYPE_CHECKING:
import bpy
import blenderbim.tool as tool
def calculate_circle_radius(qto: tool.Qto, obj: bpy.types.Object) -> float:
result = qto.get_radius_of_selected_vertices(obj)
qto.set_qto_result(result)
return result
def assign_objects_base_qto(ifc, qto, selected_objects):
for obj in selected_objects:
assign_object_base_qto(ifc, qto, obj)
def assign_object_base_qto(ifc, qto, obj):
product = ifc.get_entity(obj)
if not product:
return
base_quantity_name = qto.get_applicable_base_quantity_name(product)
if not base_quantity_name:
return
ifc.run(
"pset.add_qto",
product=product,
name=base_quantity_name,
)
def calculate_objects_base_quantities(ifc, cost, qto, calculator, selected_objects):
if selected_objects:
for obj in selected_objects:
calculate_object_base_quantities(ifc, cost, qto, calculator, obj)
def calculate_object_base_quantities(ifc, cost, qto, calculator, obj):
product = ifc.get_entity(obj)
if not product:
return
base_quantity_name = qto.get_applicable_base_quantity_name(product)
if not base_quantity_name:
print(f"There is no base quantity")
return
base_qto = qto.get_base_qto(product)
if not base_qto:
base_qto = ifc.run(
"pset.add_qto",
product=product,
name=base_quantity_name,
)
calculated_quantities = qto.get_calculated_object_quantities(calculator, base_quantity_name, obj)
ifc.run("pset.edit_qto", qto=base_qto, properties=calculated_quantities)
cost.update_cost_items(product=product)
+5 -16
View File
@@ -533,20 +533,6 @@ def go_to_task(sequence: tool.Sequence, task: ifcopenshell.entity_instance) -> U
return "Work schedule is not active"
def highlight_product_related_task(sequence: tool.Sequence, spatial: tool.Spatial, product_type=None) -> None:
products = spatial.get_selected_products()
if products:
if product_type == "Output":
tasks = sequence.find_related_output_tasks(products[0])
elif product_type == "Input":
tasks = sequence.find_related_input_tasks(products[0])
for task in tasks:
work_schedule = sequence.get_work_schedule(task)
is_work_schedule_active = sequence.is_work_schedule_active(work_schedule)
if is_work_schedule_active:
sequence.go_to_task(task)
def guess_date_range(sequence: tool.Sequence, work_schedule: ifcopenshell.entity_instance) -> None:
start, finish = sequence.guess_date_range(work_schedule)
sequence.update_visualisation_date(start, finish)
@@ -593,17 +579,20 @@ def generate_gantt_chart(sequence: tool.Sequence, work_schedule: ifcopenshell.en
sequence.generate_gantt_browser_chart(json, work_schedule)
def load_product_related_tasks(sequence: tool.Sequence, product: ifcopenshell.entity_instance) -> Union[None, str]:
def load_product_related_tasks(
sequence: tool.Sequence, product: ifcopenshell.entity_instance
) -> Union[list[ifcopenshell.entity_instance], str]:
filter_by_schedule = sequence.is_filter_by_active_schedule()
if filter_by_schedule:
work_schedule = sequence.get_active_work_schedule()
if work_schedule:
task_inputs, task_ouputs = sequence.get_tasks_for_product(product, work_schedule)
else:
return "No active work schedule"
return "No active work schedule."
else:
task_inputs, task_ouputs = sequence.get_tasks_for_product(product)
sequence.load_product_related_tasks(task_inputs, task_ouputs)
return task_inputs + task_ouputs
def reorder_task_nesting(
+31 -13
View File
@@ -16,8 +16,16 @@
# 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 __future__ import annotations
from typing import TYPE_CHECKING, Optional, Any
def add_style(ifc, style, obj=None):
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def add_style(ifc: tool.Ifc, style: tool.Style, obj: bpy.types.Material) -> ifcopenshell.entity_instance:
element = ifc.run("style.add_style", name=style.get_name(obj))
ifc.link(element, obj)
if style.can_support_rendering_style(obj):
@@ -33,18 +41,26 @@ def add_style(ifc, style, obj=None):
return element
def add_external_style(ifc, style, obj, attributes):
def add_external_style(ifc: tool.Ifc, style: tool.Style, obj: bpy.types.Material, attributes: dict[str, Any]) -> None:
element = style.get_style(obj)
ifc.run(
"style.add_surface_style", style=element, ifc_class="IfcExternallyDefinedSurfaceStyle", attributes=attributes
)
def update_external_style(ifc, style, external_style, attributes):
# TODO: unused `style` argument?
def update_external_style(
ifc: tool.Ifc,
style: ifcopenshell.entity_instance,
external_style: ifcopenshell.entity_instance,
attributes: dict[str, Any],
) -> None:
ifc.run("style.edit_surface_style", style=external_style, attributes=attributes)
def remove_style(ifc, material, style_tool, style=None):
def remove_style(
ifc: tool.Ifc, material: tool.Material, style_tool: tool.Style, style: ifcopenshell.entity_instance
) -> None:
obj = ifc.get_object(style)
ifc.unlink(obj=obj, element=style)
ifc.run("style.remove_style", style=style)
@@ -54,7 +70,7 @@ def remove_style(ifc, material, style_tool, style=None):
style_tool.import_presentation_styles(style_tool.get_active_style_type())
def update_style_colours(ifc, style, obj=None, verbose=False):
def update_style_colours(ifc: tool.Ifc, style: tool.Style, obj: bpy.types.Material, verbose: bool = False) -> None:
element = style.get_style(obj)
if style.can_support_rendering_style(obj):
@@ -91,7 +107,9 @@ def update_style_colours(ifc, style, obj=None, verbose=False):
style.record_shading(obj)
def update_style_textures(ifc, style, obj=None, representation=None):
def update_style_textures(
ifc: tool.Ifc, style: tool.Style, obj: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
) -> None:
element = style.get_style(obj)
uv_maps = style.get_uv_maps(representation)
@@ -111,34 +129,34 @@ def update_style_textures(ifc, style, obj=None, representation=None):
ifc.run("style.remove_surface_style", style=texture_style)
def unlink_style(ifc, style=None):
def unlink_style(ifc: tool.Ifc, style: ifcopenshell.entity_instance) -> None:
obj = ifc.get_object(style)
ifc.unlink(obj=obj, element=style)
def enable_editing_style(style, obj=None):
def enable_editing_style(style: tool.Style, obj: bpy.types.Material) -> None:
style.enable_editing(obj)
style.import_surface_attributes(style.get_style(obj), obj)
def disable_editing_style(style, obj=None):
def disable_editing_style(style: tool.Style, obj: bpy.types.Material) -> None:
style.disable_editing(obj)
def edit_style(ifc, style, obj=None):
def edit_style(ifc: tool.Ifc, style: tool.Style, obj: bpy.types.Material) -> None:
attributes = style.export_surface_attributes(obj)
ifc.run("style.edit_presentation_style", style=style.get_style(obj), attributes=attributes)
style.disable_editing(obj)
def load_styles(style, style_type=None):
def load_styles(style: tool.Style, style_type: str) -> None:
style.import_presentation_styles(style_type)
style.enable_editing_styles()
def disable_editing_styles(style):
def disable_editing_styles(style: tool.Style) -> None:
style.disable_editing_styles()
def select_by_style(style_tool, spatial, style=None):
def select_by_style(style_tool: tool.Style, spatial: tool.Spatial, style: ifcopenshell.entity_instance) -> None:
spatial.select_products(style_tool.get_elements_by_style(style))
+3 -10
View File
@@ -637,16 +637,9 @@ class Pset:
@interface
class Qto:
def get_radius_of_selected_vertices(cls, obj): pass
def set_qto_result(cls, result): pass
def get_applicable_quantity_names(cls, qto_name): pass
def get_applicable_base_quantity_name(cls, product): pass
def get_rounded_value(cls, new_quantity): pass
def get_calculated_object_quantities(cls, calculator, baste_qto, object): pass
def add_object_base_qto(cls, object): pass
def add_product_base_qto(cls, product): pass
def get_new_calculated_quantity(cls, qto_name, quantity_name, object): pass
def get_new_guessed_quantity(cls, object, qto_name, quantity_name, ): pass
def get_related_cost_item_quantities(cls, product): pass
def get_rounded_value(cls, new_quantity): pass
def set_qto_result(cls, result): pass
@interface
@@ -838,7 +831,7 @@ class Spatial:
def can_reference(cls, structure, element): pass
def contract_container(cls, container): pass
def copy_xy(cls, src_obj, destination_obj): pass
def create_new_storey_li(cls, element, level_index): pass
def import_spatial_structure(cls, element, level_index): pass
def deselect_objects(cls): pass
def disable_editing(cls, obj): pass
def duplicate_object_and_data(cls, obj): pass
+17 -7
View File
@@ -20,6 +20,7 @@ import bpy
import blenderbim.core.tool
import blenderbim.tool as tool
import ifcopenshell.util.element
from typing import Union
class Aggregate(blenderbim.core.tool.Aggregate):
@@ -29,9 +30,9 @@ class Aggregate(blenderbim.core.tool.Aggregate):
related_object = tool.Ifc.get_entity(related_obj)
if not relating_object or not related_object:
return False
if (relating_object.is_a("IfcElement") or relating_object.is_a("IfcElementType")) and related_object.is_a("IfcElement"):
if relating_obj.data: # See #3973
return False
if (relating_object.is_a("IfcElement") or relating_object.is_a("IfcElementType")) and related_object.is_a(
"IfcElement"
):
return True
if tool.Ifc.get_schema() == "IFC2X3":
if relating_object.is_a("IfcSpatialStructureElement") and related_object.is_a("IfcSpatialStructureElement"):
@@ -46,19 +47,28 @@ class Aggregate(blenderbim.core.tool.Aggregate):
return False
@classmethod
def disable_editing(cls, obj):
def has_physical_body_representation(cls, element: ifcopenshell.entity_instance) -> bool:
if element.is_a("IfcElement") or element.is_a("IfcElementType"): # See 3973
if ifcopenshell.util.representation.get_representation(element, "Model", "Body"):
return True
return False
@classmethod
def disable_editing(cls, obj: bpy.types.Object) -> None:
obj.BIMObjectAggregateProperties.is_editing = False
@classmethod
def enable_editing(cls, obj):
def enable_editing(cls, obj: bpy.types.Object) -> None:
obj.BIMObjectAggregateProperties.is_editing = True
@classmethod
def get_container(cls, element):
def get_container(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.element.get_container(element)
@classmethod
def get_relating_object(cls, related_element):
def get_relating_object(
cls, related_element: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
for rel in related_element.Decomposes:
if rel.is_a("IfcRelAggregates"):
return rel.RelatingObject
+23
View File
@@ -21,6 +21,7 @@ import bmesh
import json
import ifcopenshell.api
import ifcopenshell.util.element
import blenderbim.core.tool
import blenderbim.tool as tool
import blenderbim.bim
import addon_utils
@@ -196,6 +197,21 @@ class Blender(blenderbim.core.tool.Blender):
return False
return False
@classmethod
def is_valid_data_block(cls, data_block: bpy.types.ID) -> bool:
"""Check if Blender data-block is still valid.
If Blender data-block (e.g. an Object) is removed then it's
python object gets invalidated and accessing any of it's attributes
leads to ReferenceError: StructRNA of type Object has been removed.
This method helps avoiding try / except ReferenceError constructions.
"""
try:
data_block.bl_rna
return True
except ReferenceError:
return False
@classmethod
def show_info_message(cls, text: str, message_type: Literal["INFO", "ERROR"] = "INFO") -> None:
"""useful for showing error messages outside blender operators
@@ -459,6 +475,13 @@ class Blender(blenderbim.core.tool.Blender):
obj.select_set(False)
context.view_layer.objects.active = active_object
active_object.select_set(True)
@classmethod
def select_object(cls, obj: bpy.types.Object):
try:
obj.select_set(True)
except RuntimeError: # Trying to select a hidden object throws an error
pass
@classmethod
def set_objects_selection(
+6 -3
View File
@@ -95,9 +95,12 @@ class Collector(blenderbim.core.tool.Collector):
# NOTE: won't allow assigning IfcElements to the IfcProject directly
# and some elements might get missing in other viewers if they're don't support displaying
# elements without hierarchy
blenderbim.core.aggregate.assign_object(
tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=parent_obj, related_obj=obj
)
try:
blenderbim.core.aggregate.assign_object(
tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=parent_obj, related_obj=obj
)
except blenderbim.core.aggregate.IncompatibleAggregateError:
pass
@classmethod
def assign(cls, obj: bpy.types.Object) -> None:
@@ -1752,6 +1752,8 @@ class Drawing(blenderbim.core.tool.Drawing):
@classmethod
def activate_drawing(cls, camera: bpy.types.Object) -> None:
selected_objects_before = bpy.context.selected_objects
# Sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude
drawing = tool.Ifc.get_entity(camera)
@@ -1839,6 +1841,10 @@ class Drawing(blenderbim.core.tool.Drawing):
cls.import_camera_props(drawing, camera)
for obj in selected_objects_before:
obj.hide_set(False)
obj.select_set(True)
@classmethod
def get_elements_in_camera_view(
cls, camera: bpy.types.Object, objs: list[ifcopenshell.entity_instance]
+41 -18
View File
@@ -40,7 +40,7 @@ import blenderbim.bim.import_ifc
from math import radians, pi
from mathutils import Vector, Matrix
from blenderbim.bim.ifc import IfcStore
from typing import Union
from typing import Union, Iterable
class Geometry(blenderbim.core.tool.Geometry):
@@ -725,11 +725,11 @@ class Geometry(blenderbim.core.tool.Geometry):
return False
@classmethod
def should_use_presentation_style_assignment(cls):
def should_use_presentation_style_assignment(cls) -> bool:
return bpy.context.scene.BIMGeometryProperties.should_use_presentation_style_assignment
@classmethod
def get_model_representations(cls):
def get_model_representations(cls) -> list[ifcopenshell.entity_instance]:
return tool.Ifc.get().by_type("IfcShapeRepresentation")
@classmethod
@@ -755,21 +755,44 @@ class Geometry(blenderbim.core.tool.Geometry):
bpy.context.view_layer.update()
@classmethod
def reload_representation(cls, obj):
"""reload `obj` active representation"""
if not obj.data:
return
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
def reload_representation(cls, obj_or_objs: Union[bpy.types.Object, Iterable[bpy.types.Object]]) -> None:
"""Reload object/objects active representation.
Ensures that same representations won't be reloaded multiple times.
"""
objs = obj_or_objs if isinstance(obj_or_objs, Iterable) else [obj_or_objs]
ifc_file = tool.Ifc.get()
# Find all objects that use the same representation
# as there are possibility that some of them have openings
# (each representation with opening has a unique Mesh)
# and therefore reloading Mesh of it's type or occurrence
# might not be enough.
elements = set()
for obj in objs:
representation = tool.Geometry.get_active_representation(obj)
if not representation:
continue
representation = tool.Geometry.resolve_mapped_representation(representation)
elements.update(ifcopenshell.util.element.get_elements_by_representation(ifc_file, representation))
# Filter out unique meshes to avoid
# reloading the same representation multiple times.
meshes_to_objects: dict[bpy.types.Mesh, bpy.types.Object]
meshes_to_objects = {(obj:=tool.Ifc.get_object(element)).data: obj for element in elements}
for obj in meshes_to_objects.values():
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
@classmethod
def remove_representation_item(cls, representation_item):
+76 -38
View File
@@ -16,48 +16,72 @@
# 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 __future__ import annotations
import bpy
import ifcopenshell
import blenderbim.core.tool
import blenderbim.core.material
import blenderbim.tool as tool
import blenderbim.bim.helper
import ifcopenshell.util.unit
import ifcopenshell.util.element
from collections import defaultdict
from typing import Union, Any, TYPE_CHECKING
if TYPE_CHECKING:
# Avoid circular imports.
from blenderbim.bim.module.material.prop import Material as MaterialItem
class Material(blenderbim.core.tool.Material):
@classmethod
def add_default_material_object(cls, name):
def add_default_material_object(cls, name: Union[str, None]) -> bpy.types.Material:
return bpy.data.materials.new(name or "Default")
@classmethod
def delete_object(cls, obj):
def delete_object(cls, obj: bpy.types.Material) -> None:
bpy.data.materials.remove(obj)
@classmethod
def disable_editing_materials(cls):
def disable_editing_materials(cls) -> None:
bpy.context.scene.BIMMaterialProperties.is_editing = False
@classmethod
def enable_editing_materials(cls):
def enable_editing_materials(cls) -> None:
bpy.context.scene.BIMMaterialProperties.is_editing = True
@classmethod
def get_active_material_type(cls):
def get_active_material_type(cls) -> str:
return bpy.context.scene.BIMMaterialProperties.material_type
@classmethod
def get_elements_by_material(cls, material):
def get_elements_by_material(cls, material: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material)
@classmethod
def get_name(cls, obj):
def get_name(cls, obj: bpy.types.Material) -> str:
return obj.name
@classmethod
def import_material_definitions(cls, material_type):
def get_active_material_item(cls) -> Union[MaterialItem, None]:
"""Get active material props item if index is valid, otherwise, return None."""
props = bpy.context.scene.BIMMaterialProperties
expanded_categories = {m.name for m in props.materials if m.is_expanded}
if 0 <= props.active_material_index < len(props.materials):
return props.materials[props.active_material_index]
return None
@classmethod
def import_material_definitions(cls, material_type: str) -> None:
props = bpy.context.scene.BIMMaterialProperties
# Store active category name to reselect it later.
# Occurs when we expand/contract all categories.
active_item = cls.get_active_material_item()
previously_selected_category = None
if active_item and active_item.is_category:
previously_selected_category = active_item.name
expanded_categories = {m.name for m in props.materials if m.is_category and m.is_expanded}
props.materials.clear()
get_name = lambda x: x.Name or "Unnamed"
if material_type == "IfcMaterialLayerSet":
@@ -65,14 +89,25 @@ class Material(blenderbim.core.tool.Material):
elif material_type == "IfcMaterialList":
get_name = lambda x: "Unnamed"
materials = sorted(tool.Ifc.get().by_type(material_type), key=get_name)
categories = {}
categories = defaultdict(list)
if material_type == "IfcMaterial":
[categories.setdefault(getattr(m, "Category", "Uncategorised"), []).append(m) for m in materials]
category_index_to_reselect = None
for m in materials:
# IfcMaterial has Category since IFC4.
category = getattr(m, "Category", None)
category = category or "Uncategorised"
categories[category].append(m)
for category, mats in categories.items():
cat = props.materials.add()
cat.name = category or ""
cat.name = category
cat.is_category = True
cat.is_expanded = cat.name in expanded_categories
if previously_selected_category == category:
category_index_to_reselect = len(props.materials) - 1
for material in mats if cat.is_expanded else []:
new = props.materials.add()
new.ifc_definition_id = material.id()
@@ -81,6 +116,9 @@ class Material(blenderbim.core.tool.Material):
ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material)
)
new.has_style = bool(material.HasRepresentation)
if category_index_to_reselect is not None:
props.active_material_index = category_index_to_reselect
return
for material in materials:
new = props.materials.add()
@@ -89,11 +127,11 @@ class Material(blenderbim.core.tool.Material):
new.total_elements = len(ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material))
@classmethod
def is_editing_materials(cls):
def is_editing_materials(cls) -> bool:
return bpy.context.scene.BIMMaterialProperties.is_editing
@classmethod
def is_material_used_in_sets(cls, material):
def is_material_used_in_sets(cls, material: ifcopenshell.entity_instance) -> bool:
for inverse in tool.Ifc.get().get_inverse(material):
if inverse.is_a() in [
"IfcMaterialProfile",
@@ -105,48 +143,50 @@ class Material(blenderbim.core.tool.Material):
return False
@classmethod
def load_material_attributes(cls, material):
def load_material_attributes(cls, material: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMMaterialProperties
props.material_attributes.clear()
blenderbim.bim.helper.import_attributes2(material, props.material_attributes)
@classmethod
def enable_editing_material(cls, material):
def enable_editing_material(cls, material: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMMaterialProperties
props.active_material_id = material.id()
props.editing_material_type = "ATTRIBUTES"
@classmethod
def get_material_attributes(cls):
def get_material_attributes(cls) -> dict[str, Any]:
return blenderbim.bim.helper.export_attributes(bpy.context.scene.BIMMaterialProperties.material_attributes)
@classmethod
def disable_editing_material(cls):
def disable_editing_material(cls) -> None:
props = bpy.context.scene.BIMMaterialProperties
props.active_material_id = 0
props.editing_material_type = ""
@classmethod
def get_type(cls, element):
def get_type(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.element.get_type(element)
@classmethod
def get_active_object_material(cls):
def get_active_object_material(cls) -> Union[str, None]:
active_obj = bpy.context.active_object
if not active_obj:
return
return active_obj.BIMObjectMaterialProperties.material_type
@classmethod
def get_active_material(cls):
def get_active_material(cls) -> ifcopenshell.entity_instance:
return tool.Ifc.get().by_id(int(bpy.context.active_object.BIMObjectMaterialProperties.material))
@classmethod
def get_material(cls, element, should_inherit=False):
def get_material(
cls, element: ifcopenshell.entity_instance, should_inherit: bool = False
) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.element.get_material(element, should_inherit=should_inherit)
@classmethod
def is_a_material_set(cls, material):
def is_a_material_set(cls, material: ifcopenshell.entity_instance) -> bool:
return material.is_a() in [
"IfcMaterialConstituentSet",
"IfcMaterialLayerSet",
@@ -154,7 +194,9 @@ class Material(blenderbim.core.tool.Material):
]
@classmethod
def add_material_to_set(cls, material_set, material):
def add_material_to_set(
cls, material_set: ifcopenshell.entity_instance, material: ifcopenshell.entity_instance
) -> None:
if material_set.is_a("IfcMaterialConstituentSet"):
if not material_set.MaterialConstituents:
tool.Ifc.run(
@@ -195,7 +237,7 @@ class Material(blenderbim.core.tool.Material):
)
@classmethod
def has_material_profile(cls, element):
def has_material_profile(cls, element: ifcopenshell.entity_instance) -> bool:
material = cls.get_material(element, should_inherit=False)
inherited_material = cls.get_material(element, should_inherit=True)
if material and "Profile" in material.is_a():
@@ -205,11 +247,13 @@ class Material(blenderbim.core.tool.Material):
return False
@classmethod
def is_a_flow_segment(cls, element):
def is_a_flow_segment(cls, element: ifcopenshell.entity_instance) -> bool:
return element.is_a("IfcFlowSegment")
@classmethod
def replace_material_with_material_profile(cls, element):
def replace_material_with_material_profile(
cls, element: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
old_material = cls.get_material(element, should_inherit=False)
old_inherited_material = cls.get_material(element, should_inherit=True)
material = old_material if old_material and old_material.is_a("IfcMaterial") else None
@@ -225,20 +269,14 @@ class Material(blenderbim.core.tool.Material):
return material_profile
@classmethod
def update_elements_using_material(cls, material):
def update_elements_using_material(cls, material: ifcopenshell.entity_instance) -> None:
# update elements that are using this material
elements = ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material)
# filter only unique representations to avoid reloading same representations multiple times
meshes_to_objects = dict()
for e in elements:
obj = tool.Ifc.get_object(e)
mesh = obj.data
meshes_to_objects[mesh] = obj
for obj in meshes_to_objects.values():
tool.Geometry.reload_representation(obj)
objects = [tool.Ifc.get_object(e) for e in elements]
tool.Geometry.reload_representation(objects)
@classmethod
def sync_blender_material_name(cls, material):
def sync_blender_material_name(cls, material: ifcopenshell.entity_instance) -> None:
name = material.Name or "Unnamed"
obj = tool.Ifc.get_object(material)
if obj:
@@ -251,7 +289,7 @@ class Material(blenderbim.core.tool.Material):
obj.name = name
@classmethod
def get_style(cls, material):
def get_style(cls, material: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
for material_representation in material.HasRepresentation:
for representation in material_representation.Representations:
for item in representation.Items:
+10 -5
View File
@@ -17,18 +17,23 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.element
import ifcopenshell.util.unit
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import blenderbim.core.tool
import blenderbim.tool as tool
import PIL.ImageDraw
from blenderbim.bim.module.model.decorator import ProfileDecorator
from typing import Union
class Profile(blenderbim.core.tool.Profile):
@classmethod
def draw_image_for_ifc_profile(cls, draw, profile, size):
def draw_image_for_ifc_profile(
cls, draw: PIL.ImageDraw.ImageDraw, profile: ifcopenshell.entity_instance, size: float
) -> None:
"""generates image based on `profile` using `PIL.ImageDraw`"""
settings = ifcopenshell.geom.settings()
settings.set(settings.INCLUDE_CURVES, True)
@@ -57,11 +62,11 @@ class Profile(blenderbim.core.tool.Profile):
draw.line((tuple(grouped_verts[e[0]]), tuple(grouped_verts[e[1]])), fill="white", width=2)
@classmethod
def is_editing_profile(cls):
return ProfileDecorator.installed
def is_editing_profile(cls) -> bool:
return bool(ProfileDecorator.installed)
@classmethod
def get_profile(cls, element):
def get_profile(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
representations = element.Representation
for representation in representations.Representations:
if not representation.is_a("IfcShapeRepresentation"):
@@ -74,7 +79,7 @@ class Profile(blenderbim.core.tool.Profile):
return None
@classmethod
def get_model_profiles(cls):
def get_model_profiles(cls) -> list[ifcopenshell.entity_instance]:
return tool.Ifc.get().by_type("IfcProfileDef")
@classmethod
-2
View File
@@ -23,7 +23,6 @@ import blenderbim.core.tool
import blenderbim.tool as tool
import blenderbim.bim.schema
from typing import Union
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
class Pset(blenderbim.core.tool.Pset):
@@ -177,7 +176,6 @@ class Pset(blenderbim.core.tool.Pset):
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 = cls.get_prop_template_primitive_type(prop_template)
special_type = ""
+19 -93
View File
@@ -16,20 +16,17 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from types import ClassMethodDescriptorType
import bpy
import blenderbim.core.tool
import blenderbim.bim.schema
import blenderbim.tool as tool
import ifcopenshell
from mathutils import Vector
from ifcopenshell import util
import ifcopenshell.util.unit
import ifcopenshell.util.element
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator, QuanityTypes
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
import blenderbim.bim.schema
from mathutils import Vector
from typing import Optional, Union, Literal
QuantityTypes = Literal["Q_LENGTH", "Q_AREA", "Q_VOLUME"]
class Qto(blenderbim.core.tool.Qto):
@classmethod
@@ -45,77 +42,17 @@ class Qto(blenderbim.core.tool.Qto):
def set_qto_result(cls, result: float) -> None:
bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3))
@classmethod
def add_object_base_qto(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
product = tool.Ifc.get_entity(obj)
return cls.add_product_base_qto(product)
@classmethod
def add_product_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
base_quantity_name = cls.get_applicable_base_quantity_name(product)
if base_quantity_name:
return tool.Ifc.run(
"pset.add_qto",
product=product,
name=base_quantity_name,
)
@classmethod
def get_applicable_quantity_names(cls, qto_name: str) -> list[str]:
pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(qto_name)
return (
[property.Name for property in pset_template.HasPropertyTemplates]
if hasattr(pset_template, "HasPropertyTemplates")
else []
)
@classmethod
def get_applicable_base_quantity_name(
cls, product: Optional[ifcopenshell.entity_instance] = None
) -> Union[str, None]:
if not product:
return
applicable_qto_names = blenderbim.bim.schema.ifc.psetqto.get_applicable_names(
product.is_a(), ifcopenshell.util.element.get_predefined_type(product), qto_only=True
)
return next((qto_name for qto_name in applicable_qto_names if "Qto_" in qto_name and "Base" in qto_name), None)
@classmethod
def get_new_calculated_quantity(cls, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
return QtoCalculator().calculate_quantity(qto_name, quantity_name, obj)
@classmethod
def get_new_guessed_quantity(
cls, obj: bpy.types.Object, quantity_name: str, alternative_prop_names: list[str]
) -> Union[float, None]:
return QtoCalculator().guess_quantity(quantity_name, alternative_prop_names, obj)
@classmethod
def get_rounded_value(cls, new_quantity: float) -> float:
return round(new_quantity, 3)
@classmethod
def get_calculated_object_quantities(
cls, calculator: QtoCalculator, qto_name: str, obj: bpy.types.Object
) -> dict[str, float]:
return {
quantity_name: cls.get_rounded_value(value)
for quantity_name in cls.get_applicable_quantity_names(qto_name) or []
if cls.has_calculator(qto_name, quantity_name)
and (value := calculator.calculate_quantity(qto_name, quantity_name, obj)) is not None
}
@classmethod
def has_calculator(cls, qto_name: str, quantity_name: str) -> bool:
return bool(mapper.get(qto_name, {}).get(quantity_name, None))
@classmethod
def convert_to_project_units(
cls,
value: float,
qto_name: Optional[str] = None,
quantity_name: Optional[str] = None,
quantity_type: Optional[QuanityTypes] = None,
quantity_type: Optional[QuantityTypes] = None,
) -> Union[float, None]:
"""You can either specify `quantity_type` or provide `qto_name/quantity_name`
to let method figure the `quantity_type` from the templates
@@ -147,38 +84,27 @@ class Qto(blenderbim.core.tool.Qto):
)
return value
@classmethod
def get_guessed_quantities(
cls, obj: bpy.types.Object, pset_qto_properties: list[ifcopenshell.entity_instance]
) -> dict[str, float]:
calculated_quantities = {}
for pset_qto_property in pset_qto_properties:
quantity_name = pset_qto_property.get_info()["Name"]
alternative_prop_names = [p.get_info()["Name"] for p in pset_qto_properties]
new_quantity = cls.get_new_guessed_quantity(obj, quantity_name, alternative_prop_names)
if not new_quantity:
new_quantity = 0
else:
new_quantity = cls.get_rounded_value(new_quantity)
calculated_quantities[quantity_name] = new_quantity
return calculated_quantities
@classmethod
def get_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
if not hasattr(product, "IsDefinedBy"):
return
base_qto_definition = None
base_qto_definition_name: Union[str, None] = None
for rel in product.IsDefinedBy or []:
if not (
rel.is_a("IfcRelDefinesByProperties")
and "Base" in rel.RelatingPropertyDefinition.Name
and "Qto_" in rel.RelatingPropertyDefinition.Name
):
definition = rel.RelatingPropertyDefinition
if not rel.is_a("IfcRelDefinesByProperties"):
continue
return rel.RelatingPropertyDefinition
definition = rel.RelatingPropertyDefinition
definition_name = definition.Name
if "Qto_" not in definition_name:
continue
if "Base" in definition_name:
return definition
if base_qto_definition and "BodyGeometryValidation" not in base_qto_definition_name:
continue
base_qto_definition = definition
base_qto_definition_name = definition_name
return base_qto_definition
@classmethod
def get_related_cost_item_quantities(cls, product: ifcopenshell.entity_instance) -> list[dict]:
@@ -264,6 +264,7 @@ class Resource(blenderbim.core.tool.Resource):
props = bpy.context.scene.BIMResourceProperties
props.active_resource_id = resource.id()
props.editing_resource_type = "QUANTITY"
props.active_resource_class = resource.is_a()
@classmethod
def enable_editing_resource_quantity(cls, resource_quantity: ifcopenshell.entity_instance) -> None:
+34 -19
View File
@@ -21,6 +21,7 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.representation
import ifcopenshell.util.element
import ifcopenshell.util.placement
import blenderbim.core.tool
import blenderbim.core.aggregate
import blenderbim.core.geometry
@@ -29,17 +30,17 @@ import blenderbim.core.style
import blenderbim.tool as tool
from mathutils import Vector
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
from typing import Union, Optional
from typing import Union, Optional, Any
class Root(blenderbim.core.tool.Root):
@classmethod
def add_tracked_opening(cls, obj):
def add_tracked_opening(cls, obj: bpy.types.Object) -> None:
new = bpy.context.scene.BIMModelProperties.openings.add()
new.obj = obj
@classmethod
def assign_body_styles(cls, element, obj):
def assign_body_styles(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None:
# Should this even be here? Should it be in the geometry tool?
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
@@ -56,7 +57,7 @@ class Root(blenderbim.core.tool.Root):
)
@classmethod
def copy_representation(cls, source, dest):
def copy_representation(cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance) -> None:
def exclude_callback(attribute):
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
@@ -80,11 +81,13 @@ class Root(blenderbim.core.tool.Root):
]
@classmethod
def does_type_have_representations(cls, element):
def does_type_have_representations(cls, element: ifcopenshell.entity_instance) -> bool:
return bool(element.RepresentationMaps)
@classmethod
def get_decomposition_relationships(cls, objs):
def get_decomposition_relationships(
cls, objs: list[bpy.types.Object]
) -> dict[ifcopenshell.entity_instance, dict[str, Any]]:
relationships = {}
for obj in objs:
element = tool.Ifc.get_entity(obj)
@@ -96,7 +99,9 @@ class Root(blenderbim.core.tool.Root):
return relationships
@classmethod
def get_connection_relationships(cls, objs):
def get_connection_relationships(
cls, objs: list[bpy.types.Object]
) -> dict[ifcopenshell.entity_instance, dict[str, Any]]:
relationships = {}
for obj in objs:
element = tool.Ifc.get_entity(obj)
@@ -119,7 +124,9 @@ class Root(blenderbim.core.tool.Root):
return relationships
@classmethod
def get_element_representation(cls, element, context):
def get_element_representation(
cls, element: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
if context.is_a("IfcGeometricRepresentationSubContext"):
return ifcopenshell.util.representation.get_representation(
element,
@@ -134,13 +141,13 @@ class Root(blenderbim.core.tool.Root):
return ifcopenshell.util.element.get_type(element)
@classmethod
def get_object_name(cls, obj):
def get_object_name(cls, obj: bpy.types.Object) -> None:
if "." in obj.name and obj.name.split(".")[-1].isnumeric():
return ".".join(obj.name.split(".")[:-1])
return obj.name
@classmethod
def get_object_representation(cls, obj):
def get_object_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
if obj.data and obj.data.BIMMeshProperties.ifc_definition_id:
return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
element = tool.Ifc.get_entity(obj)
@@ -152,19 +159,21 @@ class Root(blenderbim.core.tool.Root):
return element.Representation.Representations[0]
@classmethod
def get_representation_context(cls, representation):
def get_representation_context(cls, representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
return representation.ContextOfItems
@classmethod
def is_element_a(cls, element, ifc_class):
def is_element_a(cls, element: ifcopenshell.entity_instance, ifc_class: str) -> bool:
return element.is_a(ifc_class)
@classmethod
def link_object_data(cls, source_obj, destination_obj):
def link_object_data(cls, source_obj: bpy.types.Object, destination_obj: bpy.types.Object) -> None:
destination_obj.data = source_obj.data
@classmethod
def recreate_decompositions(cls, relationships, old_to_new):
def recreate_decompositions(
cls, relationships, old_to_new: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]
) -> None:
for subelement, data in relationships.items():
new_subelements = old_to_new.get(subelement)
new_elements = old_to_new.get(data["element"])
@@ -227,7 +236,11 @@ class Root(blenderbim.core.tool.Root):
)
@classmethod
def recreate_connections(cls, relationship, old_to_new):
def recreate_connections(
cls,
relationship: dict[ifcopenshell.entity_instance, dict[str, Any]],
old_to_new: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]],
) -> None:
for element, data in relationship.items():
try:
new_relating_element = old_to_new.get(data["relating_element"])[0]
@@ -244,7 +257,9 @@ class Root(blenderbim.core.tool.Root):
)
@classmethod
def recreate_aggregate(cls, old_to_new):
def recreate_aggregate(
cls, old_to_new: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]
) -> None:
for old, new in old_to_new.items():
old_aggregate = ifcopenshell.util.element.get_aggregate(old)
if old_aggregate:
@@ -301,7 +316,7 @@ class Root(blenderbim.core.tool.Root):
)
@classmethod
def set_object_name(cls, obj, element):
def set_object_name(cls, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
# This disables the Blender name event handler
obj.BIMObjectProperties.is_renaming = True
name = getattr(element, "Name", getattr(element, "AxisTag", None))
@@ -309,13 +324,13 @@ class Root(blenderbim.core.tool.Root):
obj.BIMObjectProperties.is_renaming = False
@classmethod
def unlink_object(cls, obj):
def unlink_object(cls, obj: bpy.types.Object) -> None:
tool.Ifc.unlink(obj=obj)
if hasattr(obj.data, "BIMMeshProperties"):
obj.data.BIMMeshProperties.ifc_definition_id = 0
for material_slot in obj.material_slots:
if material_slot.material:
blenderbim.core.style.unlink_style(tool.Ifc, tool.Style, obj=material_slot.material)
blenderbim.core.style.unlink_style(tool.Ifc, style=material_slot.material)
blenderbim.core.material.unlink_material(tool.Ifc, obj=material_slot.material)
if "Ifc" in obj.name and "/" in obj.name:
obj.name = obj.name.split("/", 1)[1]
+7
View File
@@ -86,6 +86,9 @@ class Search(blenderbim.core.tool.Search):
elif ifc_filter.type == "group":
comparison, value = cls.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"group{comparison}{value}")
elif ifc_filter.type == "parent":
comparison, value = cls.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"parent{comparison}{value}")
elif ifc_filter.type == "query":
keys = cls.wrap_value(ifc_filter, ifc_filter.name)
comparison, value = cls.get_comparison_and_value(ifc_filter)
@@ -201,6 +204,10 @@ class ImportFilterQueryTransformer(lark.Transformer):
comparison, value = args
return {"type": "group", "value": f"{comparison}{value}"}
def parent(self, args):
comparison, value = args
return {"type": "parent", "value": f"{comparison}{value}"}
def query(self, args):
keys, comparison, value = args
return {"type": "query", "name": keys, "value": f"{comparison}{value}"}
+65 -31
View File
@@ -207,45 +207,79 @@ class Spatial(blenderbim.core.tool.Spatial):
src_obj.location = (destination_obj.location[0], destination_obj.location[1], z)
@classmethod
def load_container_manager(cls):
cls.props = bpy.context.scene.BIMSpatialManagerProperties
previous_container_index = cls.props.active_container_index
cls.props.containers.clear()
cls.contracted_containers = json.loads(cls.props.contracted_containers)
cls.props.is_container_update_enabled = False
parent = tool.Ifc.get().by_type("IfcProject")[0]
def load_contained_elements(cls):
props = bpy.context.scene.BIMProjectTreeProperties
props.elements.clear()
if not (container := props.active_container):
return
for object in ifcopenshell.util.element.get_parts(parent) or []:
if object.is_a("IfcSpatialElement") or object.is_a("IfcSpatialStructureElement"):
cls.create_new_storey_li(object, 0)
cls.props.is_container_update_enabled = True
# triggers spatial manager props setup
cls.props.active_container_index = min(previous_container_index, len(cls.props.containers) - 1)
container = tool.Ifc.get().by_id(container.ifc_definition_id)
results = {}
for element in ifcopenshell.util.element.get_contained(container):
element_type = ifcopenshell.util.element.get_type(element)
ifc_class = element.is_a()
type_name = element_type.Name or "Unnamed" if element_type else "Untyped"
results.setdefault(ifc_class, {}).setdefault(type_name, 0)
results[ifc_class][type_name] += 1
total_elements = 0
for ifc_class in sorted(results.keys()):
new = props.elements.add()
new.name = ifc_class
new.is_class = True
total = 0
for type_name in sorted(results[ifc_class].keys()):
new2 = props.elements.add()
new2.name = type_name
new2.is_type = True
new2.total = results[ifc_class][type_name]
total += new2.total
new.total = total
total_elements += total
props.total_elements = total_elements
@classmethod
def create_new_storey_li(cls, element, level_index):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
new = cls.props.containers.add()
new.name = element.Name or "Unnamed"
new.long_name = element.LongName or ""
new.has_decomposition = bool(element.IsDecomposedBy)
new.ifc_definition_id = element.id()
new.elevation = ifcopenshell.util.placement.get_storey_elevation(element) * si_conversion
def load_container_manager(cls):
props = bpy.context.scene.BIMProjectTreeProperties
previous_container_index = props.active_container_index
props.containers.clear()
cls.contracted_containers = json.loads(props.contracted_containers)
props.is_container_update_enabled = False
parent = tool.Ifc.get().by_type("IfcProject")[0]
for subelement in ifcopenshell.util.element.get_parts(parent) or []:
if subelement.is_a("IfcSpatialElement") or subelement.is_a("IfcSpatialStructureElement"):
cls.import_spatial_structure(subelement, 0)
props.is_container_update_enabled = True
# triggers spatial manager props setup
props.active_container_index = min(previous_container_index, len(props.containers) - 1)
@classmethod
def import_spatial_structure(cls, element, level_index):
props = bpy.context.scene.BIMProjectTreeProperties
new = props.containers.add()
new.ifc_class = element.is_a()
new.name = element.Name or "Unnamed"
new.description = element.Description or ""
new.long_name = element.LongName or ""
new.elevation = ifcopenshell.util.placement.get_storey_elevation(element)
new.is_expanded = element.id() not in cls.contracted_containers
new.level_index = level_index
if new.has_decomposition:
new.has_children = True
if new.is_expanded:
for related_object in ifcopenshell.util.element.get_parts(element) or []:
if related_object.is_a("IfcSpatialElement") or related_object.is_a("IfcSpatialStructureElement"):
cls.create_new_storey_li(related_object, level_index + 1)
children = ifcopenshell.util.element.get_parts(element)
new.has_children = bool(children)
new.ifc_definition_id = element.id()
if new.is_expanded:
for child in children or []:
cls.import_spatial_structure(child, level_index + 1)
@classmethod
def edit_container_attributes(cls, entity):
# TODO
obj = tool.Ifc.get_object(entity)
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
name = bpy.context.scene.BIMSpatialManagerProperties.container_name
name = bpy.context.scene.BIMProjectTreeProperties.container_name
if name != entity.Name:
cls.edit_container_name(entity, name)
@@ -255,21 +289,21 @@ class Spatial(blenderbim.core.tool.Spatial):
@classmethod
def get_active_container(cls):
props = bpy.context.scene.BIMSpatialManagerProperties
props = bpy.context.scene.BIMProjectTreeProperties
if props.active_container_index < len(props.containers):
container = tool.Ifc.get().by_id(props.containers[props.active_container_index].ifc_definition_id)
return container
@classmethod
def contract_container(cls, container):
props = bpy.context.scene.BIMSpatialManagerProperties
props = bpy.context.scene.BIMProjectTreeProperties
contracted_containers = json.loads(props.contracted_containers)
contracted_containers.append(container.id())
props.contracted_containers = json.dumps(contracted_containers)
@classmethod
def expand_container(cls, container):
props = bpy.context.scene.BIMSpatialManagerProperties
props = bpy.context.scene.BIMProjectTreeProperties
contracted_containers = json.loads(props.contracted_containers)
contracted_containers.remove(container.id())
props.contracted_containers = json.dumps(contracted_containers)
+44 -38
View File
@@ -20,11 +20,12 @@ import bpy
import numpy as np
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
import blenderbim.core.tool
import blenderbim.tool as tool
import blenderbim.bim.helper
from mathutils import Color
from typing import Union
from typing import Union, Any, Optional
# fmt: off
TEXTURE_MAPS_BY_METHODS = {
@@ -45,19 +46,19 @@ STYLE_PROPS_MAP = {
class Style(blenderbim.core.tool.Style):
@classmethod
def can_support_rendering_style(cls, obj):
def can_support_rendering_style(cls, obj: bpy.types.Material) -> bool:
return obj.use_nodes and hasattr(obj.node_tree, "nodes")
@classmethod
def disable_editing(cls, obj):
def disable_editing(cls, obj: bpy.types.Material) -> None:
obj.BIMStyleProperties.is_editing = False
@classmethod
def disable_editing_external_style(cls, obj):
def disable_editing_external_style(cls, obj: bpy.types.Material) -> None:
obj.BIMStyleProperties.is_editing_external_style = False
@classmethod
def disable_editing_styles(cls):
def disable_editing_styles(cls) -> None:
bpy.context.scene.BIMStylesProperties.is_editing = False
@classmethod
@@ -67,39 +68,40 @@ class Style(blenderbim.core.tool.Style):
return new_style
@classmethod
def enable_editing(cls, obj):
def enable_editing(cls, obj: bpy.types.Material) -> None:
obj.BIMStyleProperties.is_editing = True
@classmethod
def enable_editing_external_style(cls, obj):
def enable_editing_external_style(cls, obj: bpy.types.Material) -> None:
obj.BIMStyleProperties.is_editing_external_style = True
@classmethod
def enable_editing_styles(cls):
def enable_editing_styles(cls) -> None:
bpy.context.scene.BIMStylesProperties.is_editing = True
@classmethod
def export_surface_attributes(cls, obj):
def export_surface_attributes(cls, obj: bpy.types.Material) -> dict[str, Any]:
return blenderbim.bim.helper.export_attributes(obj.BIMStyleProperties.attributes)
@classmethod
def export_external_style_attributes(cls, obj):
def export_external_style_attributes(cls, obj: bpy.types.Material) -> dict[str, Any]:
return blenderbim.bim.helper.export_attributes(obj.BIMStyleProperties.external_style_attributes)
@classmethod
def get_active_style_type(cls):
def get_active_style_type(cls) -> str:
return bpy.context.scene.BIMStylesProperties.style_type
# TODO: `obj` argument is unused?
@classmethod
def get_context(cls, obj):
def get_context(cls, obj) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
@classmethod
def get_elements_by_style(cls, style):
def get_elements_by_style(cls, style: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return ifcopenshell.util.element.get_elements_by_style(tool.Ifc.get(), style)
@classmethod
def get_name(cls, obj):
def get_name(cls, obj: bpy.types.Material) -> str:
return obj.name
@classmethod
@@ -126,7 +128,7 @@ class Style(blenderbim.core.tool.Style):
return style_elements
@classmethod
def get_shading_style_data_from_props(cls) -> dict:
def get_shading_style_data_from_props(cls) -> dict[str, Any]:
"""returns style data from blender props in similar way to `Loader.surface_style_to_dict`
to be compatible with `Loader.create_surface_style_rendering`"""
surface_style_data = dict()
@@ -154,7 +156,7 @@ class Style(blenderbim.core.tool.Style):
return surface_style_data
@classmethod
def get_texture_style_data_from_props(cls) -> list[dict]:
def get_texture_style_data_from_props(cls) -> list[dict[str, Any]]:
"""returns style data from blender props in similar way to `Loader.surface_texture_to_dict`
to be compatible with `Loader.create_surface_style_with_textures`"""
props = bpy.context.scene.BIMStylesProperties
@@ -174,7 +176,7 @@ class Style(blenderbim.core.tool.Style):
return textures
@classmethod
def set_surface_style_props(cls):
def set_surface_style_props(cls) -> None:
"""set blender style props based on currently edited IfcSurfaceStyle,
reset unrelated props to default values"""
@@ -241,7 +243,7 @@ class Style(blenderbim.core.tool.Style):
props["update_graph"] = prev_update_graph_value
@classmethod
def get_surface_rendering_attributes(cls, obj, verbose=False):
def get_surface_rendering_attributes(cls, obj: bpy.types.Material, verbose: bool = False) -> dict[str, Any]:
report = (lambda *x: print(*x)) if verbose else (lambda *x: None)
def color_to_ifc_format(color):
@@ -403,22 +405,22 @@ class Style(blenderbim.core.tool.Style):
return attributes
@classmethod
def get_surface_rendering_style(cls, obj):
def get_surface_rendering_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
style_elements = cls.get_style_elements(obj)
return style_elements.get("IfcSurfaceStyleRendering", None)
@classmethod
def get_texture_style(cls, obj):
def get_texture_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
style_elements = cls.get_style_elements(obj)
return style_elements.get("IfcSurfaceStyleWithTextures", None)
@classmethod
def get_external_style(cls, obj):
def get_external_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
style_elements = cls.get_style_elements(obj)
return style_elements.get("IfcExternallyDefinedSurfaceStyle", None)
@classmethod
def get_surface_shading_attributes(cls, obj):
def get_surface_shading_attributes(cls, obj: bpy.types.Material) -> dict[str, Any]:
data = {
"SurfaceColour": {
"Name": None,
@@ -433,7 +435,7 @@ class Style(blenderbim.core.tool.Style):
return data
@classmethod
def get_surface_shading_style(cls, obj):
def get_surface_shading_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
if obj.BIMMaterialProperties.ifc_style_id:
style = tool.Ifc.get().by_id(obj.BIMMaterialProperties.ifc_style_id)
items = [s for s in style.Styles if s.is_a() == "IfcSurfaceStyleShading"]
@@ -441,7 +443,7 @@ class Style(blenderbim.core.tool.Style):
return items[0]
@classmethod
def get_surface_texture_style(cls, obj):
def get_surface_texture_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
if obj.BIMMaterialProperties.ifc_style_id:
style = tool.Ifc.get().by_id(obj.BIMMaterialProperties.ifc_style_id)
items = [s for s in style.Styles if s.is_a("IfcSurfaceStyleWithTextures")]
@@ -449,7 +451,7 @@ class Style(blenderbim.core.tool.Style):
return items[0]
@classmethod
def get_uv_maps(cls, representation):
def get_uv_maps(cls, representation: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
items = []
for item in representation.Items:
if item.is_a("IfcMappedItem"):
@@ -464,7 +466,7 @@ class Style(blenderbim.core.tool.Style):
return results
@classmethod
def get_style_ui_props_attributes(cls, style_type):
def get_style_ui_props_attributes(cls, style_type: str) -> Union[bpy.types.PropertyGroup, None]:
props = bpy.context.scene.BIMStylesProperties
if style_type == "IfcExternallyDefinedSurfaceStyle":
return props.external_style_attributes
@@ -474,7 +476,7 @@ class Style(blenderbim.core.tool.Style):
return props.lighting_style_colours
@classmethod
def import_presentation_styles(cls, style_type):
def import_presentation_styles(cls, style_type: str) -> None:
color_to_tuple = lambda x: (x.Red, x.Green, x.Blue)
props = bpy.context.scene.BIMStylesProperties
props.styles.clear()
@@ -497,13 +499,13 @@ class Style(blenderbim.core.tool.Style):
new.total_elements = len(ifcopenshell.util.element.get_elements_by_style(tool.Ifc.get(), style))
@classmethod
def import_surface_attributes(cls, style, obj):
def import_surface_attributes(cls, style: ifcopenshell.entity_instance, obj: bpy.types.Material) -> None:
attributes = obj.BIMStyleProperties.attributes
attributes.clear()
blenderbim.bim.helper.import_attributes2(style, attributes)
@classmethod
def import_external_style_attributes(cls, style, obj):
def import_external_style_attributes(cls, style: ifcopenshell.entity_instance, obj: bpy.types.Material) -> None:
attributes = obj.BIMStyleProperties.external_style_attributes
attributes.clear()
blenderbim.bim.helper.import_attributes2(style, attributes)
@@ -514,26 +516,26 @@ class Style(blenderbim.core.tool.Style):
return bool(external_style and external_style.Location and external_style.Location.endswith(".blend"))
@classmethod
def is_editing_styles(cls):
def is_editing_styles(cls) -> bool:
return bpy.context.scene.BIMStylesProperties.is_editing
@classmethod
def record_shading(cls, obj):
def record_shading(cls, obj: bpy.types.Material) -> None:
obj.BIMMaterialProperties.shading_checksum = repr(np.array(obj.diffuse_color).tobytes())
@classmethod
def select_elements(cls, elements):
def select_elements(cls, elements: list[ifcopenshell.entity_instance]) -> None:
for element in elements:
obj = tool.Ifc.get_object(element)
if obj:
obj.select_set(True)
@classmethod
def change_current_style_type(cls, blender_material, style_type):
def change_current_style_type(cls, blender_material: bpy.types.Material, style_type: str) -> None:
blender_material.BIMStyleProperties.active_style_type = style_type
@classmethod
def get_styled_items(cls, style):
def get_styled_items(cls, style: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
ifc_file = tool.Ifc.get()
inverses = list(ifc_file.get_inverse(style))
@@ -554,13 +556,15 @@ class Style(blenderbim.core.tool.Style):
return items
@classmethod
def assign_style_to_object(cls, style, obj):
def assign_style_to_object(cls, style: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None:
"""assigns `style` to `object` current representation"""
representation = tool.Geometry.get_active_representation(obj)
tool.Ifc.run("style.assign_representation_styles", shape_representation=representation, styles=[style])
@classmethod
def assign_style_to_representation_item(cls, representation_item, style=None):
def assign_style_to_representation_item(
cls, representation_item: ifcopenshell.entity_instance, style: Optional[ifcopenshell.entity_instance] = None
) -> None:
ifc_file = tool.Ifc.get()
if not representation_item.StyledByItem:
if style is None:
@@ -574,12 +578,14 @@ class Style(blenderbim.core.tool.Style):
styled_item.Styles = (style,)
@classmethod
def get_representation_item_style(cls, representation_item):
def get_representation_item_style(
cls, representation_item: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
for inverse in tool.Ifc.get().get_inverse(representation_item):
if inverse.is_a("IfcStyledItem"):
for style in inverse.Styles:
return style
@classmethod
def reload_material_from_ifc(cls, blender_material):
def reload_material_from_ifc(cls, blender_material: bpy.types.Material) -> None:
blender_material.BIMStyleProperties.active_style_type = blender_material.BIMStyleProperties.active_style_type
+1 -1
View File
@@ -63,7 +63,7 @@ section img {
background-color: var(--color-admonition-title-background);
border-radius: 5px;
font-style: italic;
color: var(--color-admonition-title);
color: var(--color-admonition-text);
}
img.icon {
width: auto;
+2
View File
@@ -99,6 +99,7 @@ html_theme_options = {
"color-link--visited": "#39b54a",
"color-link--hover": "#d98014",
"color-link--visited--hover": "#d98014",
"color-admonition-text": "#651fff",
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
},
"dark_css_variables": {
@@ -113,6 +114,7 @@ html_theme_options = {
"color-link--visited": "#39b54a",
"color-link--hover": "#d98014",
"color-link--visited--hover": "#d98014",
"color-admonition-text": "#EEEEEC",
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
},
@@ -25,6 +25,9 @@ You will need to choose which build to download.
- Choose ``linux``, ``macos`` (Apple Intel), ``macosm1`` (Apple Silicon), or
``win`` depending on your operating system
For users who don't follow the `VFX Platform <https://vfxplatform.com/>`_
standard, we also provide py312 builds.
Sometimes, a build may be delayed, or contain broken code. We try to avoid this,
but it happens.
+1 -1
View File
@@ -36,7 +36,7 @@ Some of these add-ons are not shipped with Blender:
import GIS data, grab elevation data from the web, and generate TINs from
survey points and contours.
- `Ladybug Tools for Blender
<https://github.com/Andrej730/ladybug-blender/releases/download/ladybug-blender-240419/ladybug-blender-240419.zip>`__ - Ladybug Tools
<https://github.com/ladybug-tools/ladybug-blender/releases/download/ladybug-blender-240529/ladybug-blender-240529.zip>`__ - Ladybug Tools
is an extension of Sverchok for environmental analysis and building physics
simulation. It allows analysis of solar, daylight, energy, and CFD.
- `Topologic <https://topologic.app/>`__ - Perform spatial and topological
+27
View File
@@ -0,0 +1,27 @@
import json
import ifcopenshell.util.pset
import ifcopenshell.util.type
def order_dict(dictionary):
# https://stackoverflow.com/questions/22721579/sorting-a-nested-ordereddict-by-key-recursively
return {k: order_dict(v) if isinstance(v, dict) else v for k, v in sorted(dictionary.items())}
results = {}
psetqto = ifcopenshell.util.pset.get_template("IFC4")
for template in psetqto.templates:
for pset_template in template.by_type("IfcPropertySetTemplate"):
if not pset_template.Name.startswith("Qto_"):
continue
query = pset_template.ApplicableEntity
results.setdefault(query, {})
results[query].setdefault(pset_template.Name, {})
for quantity in pset_template.HasPropertyTemplates:
results[query][pset_template.Name][quantity.Name] = None
results = order_dict(results)
print(results)
with open("results.json", "w") as f:
json.dump(results, f, indent=4)
+19 -8
View File
@@ -18,6 +18,9 @@
import bpy
import ifcopenshell
import ifcopenshell.api.root
import ifcopenshell.api.unit
import ifcopenshell.api.context
import blenderbim.core.tool
import blenderbim.tool as tool
from test.bim.bootstrap import NewFile
@@ -92,16 +95,24 @@ class TestCanAggregate(NewFile):
subelement_obj = bpy.data.objects.new("Object", None)
assert subject.can_aggregate(element_obj, subelement_obj) is False
def test_aggregates_with_meshes_are_invalid(self):
class TestHasPhysicalBodyRepresentation(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcElementAssembly()
element_obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
tool.Ifc.link(element, element_obj)
subelement = ifc.createIfcBeam()
subelement_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(subelement, subelement_obj)
assert subject.can_aggregate(element_obj, subelement_obj) is False
assert subject.has_physical_body_representation(element) is False
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
ifcopenshell.api.unit.assign_unit(ifc)
context = ifcopenshell.api.context.add_context(ifc, context_type="Model")
body = ifcopenshell.api.context.add_context(
ifc, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=context
)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc)
origin = builder.create_axis2_placement_3d()
block = ifc.createIfcCsgSolid(ifc.createIfcBlock(origin, 200, 200, 200))
rep = builder.get_representation(context=body, items=[block])
ifcopenshell.api.geometry.assign_representation(ifc, product=element, representation=rep)
assert subject.has_physical_body_representation(element) is True
class TestDisableEditing(NewFile):
+32 -67
View File
@@ -19,12 +19,13 @@
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.pset
import test.bim.bootstrap
import blenderbim.core.tool
import blenderbim.core.root
import blenderbim.tool as tool
import blenderbim.bim.module.qto.calculator as calculator
from blenderbim.tool.qto import Qto as subject
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
class TestImplementsTool(test.bim.bootstrap.NewFile):
@@ -44,35 +45,6 @@ class TestSetQtoResult(test.bim.bootstrap.NewFile):
assert bpy.context.scene.BIMQtoProperties.qto_result == "123.457"
class TestGetApplicableQuantityNames(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
schema = ifc.schema
properties_templates = (
ifcopenshell.util.pset.PsetQto(schema)
.get_by_name("Qto_WallBaseQuantities")
.get_info()["HasPropertyTemplates"]
)
applicable_quantity_names = [a.Name for a in properties_templates]
assert subject.get_applicable_quantity_names("Qto_WallBaseQuantities") == applicable_quantity_names
class TestGetApplicableBaseQuantityName(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall = ifc.createIfcWall()
assert subject.get_applicable_base_quantity_name(wall) == "Qto_WallBaseQuantities"
def test_no_base_quantity(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
product = ifc.by_type("IfcProject")[0]
assert subject.get_applicable_base_quantity_name(product) == None
class TestGetRoundedValue(test.bim.bootstrap.NewFile):
def test_run(self):
quantity = 1.2345
@@ -83,12 +55,14 @@ class TestGetCalculatedObjectQuantities(test.bim.bootstrap.NewFile):
def setup_file(self):
self.ifc = ifcopenshell.file()
tool.Ifc.set(self.ifc)
project = ifcopenshell.api.run("root.create_entity", self.ifc, ifc_class="IfcProject", name="My Project")
ifcopenshell.api.run("root.create_entity", self.ifc, ifc_class="IfcProject", name="My Project")
def setup_units(self, units):
ifcopenshell.api.run("unit.assign_unit", self.ifc, **units)
def calculate_quantities(self, obj):
import ifc5d.qto
context = ifcopenshell.api.run("context.add_context", self.ifc, context_type="Model")
bpy.ops.mesh.primitive_cube_add(location=(0.0, 0.0, 0.0), size=2)
obj = bpy.context.active_object
@@ -101,12 +75,32 @@ class TestGetCalculatedObjectQuantities(test.bim.bootstrap.NewFile):
predefined_type="ELEMENTEDWALL",
context=context,
)
calculator = QtoCalculator()
base_qto = ifcopenshell.api.run("pset.add_qto", self.ifc, product=element, name="Qto_WallBaseQuantities")
quantities = subject.get_calculated_object_quantities(
calculator=calculator, qto_name="Qto_WallBaseQuantities", obj=obj
)
return quantities
rules = {
"calculators": {
"Blender": {
"IfcWall": {
"Qto_WallBaseQuantities": {
"GrossFootprintArea": "get_gross_footprint_area",
"GrossSideArea": "get_gross_side_area",
"GrossVolume": "get_gross_volume",
"GrossWeight": "get_gross_weight",
"Height": "get_height",
"Length": "get_length",
"NetFootprintArea": "get_net_footprint_area",
"NetSideArea": "get_net_side_area",
"NetVolume": "get_net_volume",
"NetWeight": "get_net_weight",
"Width": "get_width",
}
},
}
}
}
ifc_file = tool.Ifc.get()
results = ifc5d.qto.quantify(ifc_file, {element}, rules)
return {k: round(v, 3) for k, v in results[element]["Qto_WallBaseQuantities"].items() if v is not None}
def test_meters_project_unit(self):
self.setup_file()
@@ -172,35 +166,6 @@ class TestGetCalculatedObjectQuantities(test.bim.bootstrap.NewFile):
assert quantities["NetVolume"] == 282.517
class TestAddObjectBaseQto(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
project = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject", name="My Project")
context = ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
bpy.ops.mesh.primitive_cube_add(location=(0.0, 0.0, 0.0), size=2)
obj = bpy.context.active_object
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class="IfcWall",
predefined_type="ELEMENTEDWALL",
context=context,
)
assert subject.add_object_base_qto(obj).Name == "Qto_WallBaseQuantities"
class TestAddProductBaseQto(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall = ifc.createIfcWall()
base_qto = subject.add_product_base_qto(wall)
assert base_qto.Name == "Qto_WallBaseQuantities"
class TestGetBaseQto(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
@@ -213,7 +178,7 @@ class TestGetBaseQto(test.bim.bootstrap.NewFile):
assert subject.get_base_qto(product).id() == pset_qto.get_info()["id"]
assert subject.get_base_qto(product).Name == pset_qto.Name
def test_isempty(self):
def test_no_quantities(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall = ifc.createIfcWall()
+637
View File
@@ -0,0 +1,637 @@
{
"name": "IFC4 Base Quantities - IfcOpenShell",
"description": "This ruleset quantifies every single possible standardised base quantity in IFC4 using only IfcOpenShell as a geometry processor.",
"calculators": {
"IfcOpenShell": {
"IfcActuator": {
"Qto_ActuatorBaseQuantities": {
"GrossWeight": null
}
},
"IfcAirTerminal": {
"Qto_AirTerminalBaseQuantities": {
"GrossWeight": null,
"Perimeter": null,
"TotalSurfaceArea": null
}
},
"IfcAirTerminalBox": {
"Qto_AirTerminalBoxTypeBaseQuantities": {
"GrossWeight": null
}
},
"IfcAirToAirHeatRecovery": {
"Qto_AirToAirHeatRecoveryBaseQuantities": {
"GrossWeight": null
}
},
"IfcAlarm": {
"Qto_AlarmBaseQuantities": {
"GrossWeight": null
}
},
"IfcAudioVisualAppliance": {
"Qto_AudioVisualApplianceBaseQuantities": {
"GrossWeight": null
}
},
"IfcBeam": {
"Qto_BeamBaseQuantities": {
"CrossSectionArea": null,
"GrossSurfaceArea": "gross_get_area",
"GrossVolume": "gross_get_volume",
"GrossWeight": null,
"Length": "net_get_z",
"NetSurfaceArea": "net_get_area",
"NetVolume": "net_get_volume",
"NetWeight": null,
"OuterSurfaceArea": "net_get_outer_surface_area"
}
},
"IfcBoiler": {
"Qto_BoilerBaseQuantities": {
"GrossWeight": null,
"NetWeight": null,
"TotalSurfaceArea": null
}
},
"IfcBuilding": {
"Qto_BuildingBaseQuantities": {
"EavesHeight": null,
"FootprintArea": null,
"GrossFloorArea": null,
"GrossVolume": null,
"Height": null,
"NetFloorArea": null,
"NetVolume": null
}
},
"IfcBuildingElementProxy": {
"Qto_BuildingElementProxyQuantities": {
"NetSurfaceArea": null,
"NetVolume": null
}
},
"IfcBuildingStorey": {
"Qto_BuildingStoreyBaseQuantities": {
"GrossFloorArea": null,
"GrossHeight": null,
"GrossPerimeter": null,
"GrossVolume": null,
"NetFloorArea": null,
"NetHeigtht": null,
"NetVolume": null
}
},
"IfcBurner": {
"Qto_BurnerBaseQuantities": {
"GrossWeight": null
}
},
"IfcCableCarrierFitting": {
"Qto_CableCarrierFittingBaseQuantities": {
"GrossWeight": null
}
},
"IfcCableCarrierSegment": {
"Qto_CableCarrierSegmentBaseQuantities": {
"CrossSectionArea": null,
"GrossWeight": null,
"Length": null,
"OuterSurfaceArea": null
}
},
"IfcCableFitting": {
"Qto_CableFittingBaseQuantities": {
"GrossWeight": null
}
},
"IfcCableSegment": {
"Qto_CableSegmentBaseQuantities": {
"CrossSectionArea": null,
"GrossWeight": null,
"Length": null,
"OuterSurfaceArea": null
}
},
"IfcChiller": {
"Qto_ChillerBaseQuantities": {
"GrossWeight": null
}
},
"IfcChimney": {
"Qto_ChimneyBaseQuantities": {
"Length": null
}
},
"IfcCoil": {
"Qto_CoilBaseQuantities": {
"GrossWeight": null
}
},
"IfcColumn": {
"Qto_ColumnBaseQuantities": {
"CrossSectionArea": null,
"GrossSurfaceArea": "gross_get_area",
"GrossVolume": "gross_get_volume",
"GrossWeight": null,
"Length": "net_get_z",
"NetSurfaceArea": "net_get_area",
"NetVolume": "net_get_volume",
"NetWeight": null,
"OuterSurfaceArea": "net_get_outer_surface_area"
}
},
"IfcCommunicationsAppliance": {
"Qto_CommunicationsApplianceBaseQuantities": {
"GrossWeight": null
}
},
"IfcCompressor": {
"Qto_CompressorBaseQuantities": {
"GrossWeight": null
}
},
"IfcCondenser": {
"Qto_CondenserBaseQuantities": {
"GrossWeight": null
}
},
"IfcConstructionEquipmentResource": {
"Qto_ConstructionEquipmentResourceBaseQuantities": {
"OperatingTime": null,
"UsageTime": null
}
},
"IfcConstructionMaterialResource": {
"Qto_ConstructionMaterialResourceBaseQuantities": {
"GrossVolume": null,
"GrossWeight": null,
"NetVolume": null,
"NetWeight": null
}
},
"IfcController": {
"Qto_ControllerBaseQuantities": {
"GrossWeight": null
}
},
"IfcCooledBeam": {
"Qto_CooledBeamBaseQuantities": {
"GrossWeight": null
}
},
"IfcCoolingTower": {
"Qto_CoolingTowerBaseQuantities": {
"GrossWeight": null
}
},
"IfcCovering": {
"Qto_CoveringBaseQuantities": {
"GrossArea": "gross_get_max_side_area",
"NetArea": "net_get_max_side_area",
"Width": "gross_get_min_xyz"
}
},
"IfcCurtainWall": {
"Qto_CurtainWallQuantities": {
"GrossSideArea": null,
"Height": null,
"Length": null,
"NetSideArea": null,
"Width": null
}
},
"IfcDamper": {
"Qto_DamperBaseQuantities": {
"GrossWeight": null
}
},
"IfcDistributionChamberElement": {
"Qto_DistributionChamberElementBaseQuantities": {
"GrossSurfaceArea": null,
"GrossVolume": null,
"NetSurfaceArea": null,
"NetVolume": null
}
},
"IfcDoor": {
"Qto_DoorBaseQuantities": {
"Area": null,
"Height": null,
"Perimeter": null,
"Width": null
}
},
"IfcDuctFitting": {
"Qto_DuctFittingBaseQuantities": {
"GrossCrossSectionArea": null,
"GrossWeight": null,
"Length": null,
"NetCrossSectionArea": null,
"OuterSurfaceArea": null
}
},
"IfcDuctSegment": {
"Qto_DuctSegmentBaseQuantities": {
"GrossCrossSectionArea": null,
"GrossWeight": null,
"Length": null,
"NetCrossSectionArea": null,
"OuterSurfaceArea": null
}
},
"IfcDuctSilencer": {
"Qto_DuctSilencerBaseQuantities": {
"GrossWeight": null
}
},
"IfcElectricAppliance": {
"Qto_ElectricApplianceBaseQuantities": {
"GrossWeight": null
}
},
"IfcElectricDistributionBoard": {
"Qto_ElectricDistributionBoardBaseQuantities": {
"GrossWeight": null,
"NumberOfCircuits": null
}
},
"IfcElectricFlowStorageDevice": {
"Qto_ElectricFlowStorageDeviceBaseQuantities": {
"GrossWeight": null
}
},
"IfcElectricGenerator": {
"Qto_ElectricGeneratorBaseQuantities": {
"GrossWeight": null
}
},
"IfcElectricMotor": {
"Qto_ElectricMotorBaseQuantities": {
"GrossWeight": null
}
},
"IfcElectricTimeControl": {
"Qto_ElectricTimeControlBaseQuantities": {
"GrossWeight": null
}
},
"IfcEvaporativeCooler": {
"Qto_EvaporativeCoolerBaseQuantities": {
"GrossWeight": null
}
},
"IfcEvaporator": {
"Qto_EvaporatorBaseQuantities": {
"GrossWeight": null
}
},
"IfcFan": {
"Qto_FanBaseQuantities": {
"GrossWeight": null
}
},
"IfcFilter": {
"Qto_FilterBaseQuantities": {
"GrossWeight": null
}
},
"IfcFireSuppressionTerminal": {
"Qto_FireSuppressionTerminalBaseQuantities": {
"GrossWeight": null
}
},
"IfcFlowInstrument": {
"Qto_FlowInstrumentBaseQuantities": {
"GrossWeight": null
}
},
"IfcFlowMeter": {
"Qto_FlowMeterBaseQuantities": {
"GrossWeight": null
}
},
"IfcFooting": {
"Qto_FootingBaseQuantities": {
"CrossSectionArea": null,
"GrossSurfaceArea": null,
"GrossVolume": null,
"GrossWeight": null,
"Height": null,
"Length": null,
"NetVolume": null,
"NetWeight": null,
"OuterSurfaceArea": null,
"Width": null
}
},
"IfcHeatExchanger": {
"Qto_HeatExchangerBaseQuantities": {
"GrossWeight": null
}
},
"IfcHumidifier": {
"Qto_HumidifierBaseQuantities": {
"GrossWeight": null
}
},
"IfcInterceptor": {
"Qto_InterceptorBaseQuantities": {
"GrossWeight": null
}
},
"IfcJunctionBox": {
"Qto_JunctionBoxBaseQuantities": {
"GrossWeight": null,
"NumberOfGangs": null
}
},
"IfcLaborResource": {
"Qto_LaborResourceBaseQuantities": {
"OvertimeWork": null,
"StandardWork": null
}
},
"IfcLamp": {
"Qto_LampBaseQuantities": {
"GrossWeight": null
}
},
"IfcLightFixture": {
"Qto_LightFixtureBaseQuantities": {
"GrossWeight": null
}
},
"IfcMember": {
"Qto_MemberBaseQuantities": {
"CrossSectionArea": null,
"GrossSurfaceArea": "gross_get_area",
"GrossVolume": "gross_get_volume",
"GrossWeight": null,
"Length": "net_get_z",
"NetSurfaceArea": "net_get_area",
"NetVolume": "net_get_volume",
"NetWeight": null,
"OuterSurfaceArea": "net_get_outer_surface_area"
}
},
"IfcMotorConnection": {
"Qto_MotorConnectionBaseQuantities": {
"GrossWeight": null
}
},
"IfcOpeningElement": {
"Qto_OpeningElementBaseQuantities": {
"Area": "gross_get_max_side_area",
"Depth": "gross_get_z",
"Height": "gross_get_y",
"Volume": "gross_get_volume",
"Width": "gross_get_x"
}
},
"IfcOutlet": {
"Qto_OutletBaseQuantities": {
"GrossWeight": null
}
},
"IfcPile": {
"Qto_PileBaseQuantities": {
"CrossSectionArea": null,
"GrossSurfaceArea": null,
"GrossVolume": null,
"GrossWeight": null,
"Length": null,
"NetVolume": null,
"NetWeight": null,
"OuterSurfaceArea": null
}
},
"IfcPipeFitting": {
"Qto_PipeFittingBaseQuantities": {
"GrossCrossSectionArea": null,
"GrossWeight": null,
"Length": null,
"NetCrossSectionArea": null,
"NetWeight": null,
"OuterSurfaceArea": null
}
},
"IfcPipeSegment": {
"Qto_PipeSegmentBaseQuantities": {
"GrossCrossSectionArea": null,
"GrossWeight": null,
"Length": null,
"NetCrossSectionArea": null,
"NetWeight": null,
"OuterSurfaceArea": null
}
},
"IfcPlate": {
"Qto_PlateBaseQuantities": {
"GrossArea": "gross_get_max_side_area",
"GrossVolume": "gross_get_volume",
"GrossWeight": null,
"NetArea": "net_get_max_side_area",
"NetVolume": "net_get_volume",
"NetWeight": null,
"Perimeter": null,
"Width": "net_get_min_xyz"
}
},
"IfcProjectionElement": {
"Qto_ProjectionElementBaseQuantities": {
"Area": null,
"Volume": null
}
},
"IfcProtectiveDevice": {
"Qto_ProtectiveDeviceBaseQuantities": {
"GrossWeight": null
}
},
"IfcProtectiveDeviceTrippingUnit": {
"Qto_ProtectiveDeviceTrippingUnitBaseQuantities": {
"GrossWeight": null
}
},
"IfcPump": {
"Qto_PumpBaseQuantities": {
"GrossWeight": null
}
},
"IfcRailing": {
"Qto_RailingBaseQuantities": {
"Length": null
}
},
"IfcRampFlight": {
"Qto_RampFlightBaseQuantities": {
"GrossArea": null,
"GrossVolume": null,
"Length": null,
"NetArea": null,
"NetVolume": null,
"Width": null
}
},
"IfcReinforcingElement": {
"Qto_ReinforcingElementBaseQuantities": {
"Count": null,
"Length": null,
"Weight": null
}
},
"IfcRoof": {
"Qto_RoofBaseQuantities": {
"GrossArea": null,
"NetArea": null,
"ProjectedArea": null
}
},
"IfcSanitaryTerminal": {
"Qto_SanitaryTerminalBaseQuantities": {
"GrossWeight": null
}
},
"IfcSensor": {
"Qto_SensorBaseQuantities": {
"GrossWeight": null
}
},
"IfcSite": {
"Qto_SiteBaseQuantities": {
"GrossArea": null,
"GrossPerimeter": null
}
},
"IfcSlab": {
"Qto_SlabBaseQuantities": {
"Depth": "net_get_z",
"GrossArea": "gross_get_footprint_area",
"GrossVolume": "gross_get_volume",
"GrossWeight": null,
"Length": "net_get_x",
"NetArea": "net_get_footprint_area",
"NetVolume": "net_get_volume",
"NetWeight": null,
"Perimeter": "net_get_footprint_perimeter",
"Width": "net_get_y"
}
},
"IfcSolarDevice": {
"Qto_SolarDeviceBaseQuantities": {
"GrossArea": null,
"GrossWeight": null
}
},
"IfcSpace": {
"Qto_SpaceBaseQuantities": {
"FinishCeilingHeight": null,
"FinishFloorHeight": null,
"GrossCeilingArea": null,
"GrossFloorArea": null,
"GrossPerimeter": null,
"GrossVolume": null,
"GrossWallArea": null,
"Height": null,
"NetCeilingArea": null,
"NetFloorArea": null,
"NetPerimeter": null,
"NetVolume": null,
"NetWallArea": null
}
},
"IfcSpaceHeater": {
"Qto_SpaceHeaterBaseQuantities": {
"GrossWeight": null,
"Length": null,
"NetWeight": null
}
},
"IfcStackTerminal": {
"Qto_StackTerminalBaseQuantities": {
"GrossWeight": null
}
},
"IfcStairFlight": {
"Qto_StairFlightBaseQuantities": {
"GrossVolume": null,
"Length": "net_get_max_xy",
"NetVolume": "net_get_volume"
}
},
"IfcSwitchingDevice": {
"Qto_SwitchingDeviceBaseQuantities": {
"GrossWeight": null
}
},
"IfcTank": {
"Qto_TankBaseQuantities": {
"GrossWeight": null,
"NetWeight": null,
"TotalSurfaceArea": null
}
},
"IfcTransformer": {
"Qto_TransformerBaseQuantities": {
"GrossWeight": null
}
},
"IfcTubeBundle": {
"Qto_TubeBundleBaseQuantities": {
"GrossWeight": null,
"NetWeight": null
}
},
"IfcUnitaryControlElement": {
"Qto_UnitaryControlElementBaseQuantities": {
"GrossWeight": null
}
},
"IfcUnitaryEquipment": {
"Qto_UnitaryEquipmentBaseQuantities": {
"GrossWeight": null
}
},
"IfcValve": {
"Qto_ValveBaseQuantities": {
"GrossWeight": null
}
},
"IfcVibrationIsolator": {
"Qto_VibrationIsolatorBaseQuantities": {
"GrossWeight": null
}
},
"IfcWall": {
"Qto_WallBaseQuantities": {
"GrossFootprintArea": null,
"GrossSideArea": "gross_get_side_area",
"GrossVolume": "gross_get_volume",
"GrossWeight": null,
"Height": "net_get_z",
"Length": "net_get_x",
"NetFootprintArea": null,
"NetSideArea": "net_get_side_area",
"NetVolume": "net_get_volume",
"NetWeight": null,
"Width": "net_get_y"
}
},
"IfcWasteTerminal": {
"Qto_WasteTerminalBaseQuantities": {
"GrossWeight": null
}
},
"IfcWindow": {
"Qto_WindowBaseQuantities": {
"Area": "net_get_max_side_area",
"Height": "net_get_z",
"Perimeter": null,
"Width": "net_get_x"
}
}
}
}
}
@@ -0,0 +1,637 @@
{
"name": "IFC4 Base Quantities - Blender",
"description": "This ruleset quantifies every single possible standardised base quantity in IFC4 using Blender.",
"calculators": {
"Blender": {
"IfcActuator": {
"Qto_ActuatorBaseQuantities": {
"GrossWeight": null
}
},
"IfcAirTerminal": {
"Qto_AirTerminalBaseQuantities": {
"GrossWeight": null,
"Perimeter": null,
"TotalSurfaceArea": null
}
},
"IfcAirTerminalBox": {
"Qto_AirTerminalBoxTypeBaseQuantities": {
"GrossWeight": null
}
},
"IfcAirToAirHeatRecovery": {
"Qto_AirToAirHeatRecoveryBaseQuantities": {
"GrossWeight": null
}
},
"IfcAlarm": {
"Qto_AlarmBaseQuantities": {
"GrossWeight": null
}
},
"IfcAudioVisualAppliance": {
"Qto_AudioVisualApplianceBaseQuantities": {
"GrossWeight": null
}
},
"IfcBeam": {
"Qto_BeamBaseQuantities": {
"CrossSectionArea": "get_cross_section_area",
"GrossSurfaceArea": "get_gross_surface_area",
"GrossVolume": "get_gross_volume",
"GrossWeight": "get_gross_weight",
"Length": "get_length",
"NetSurfaceArea": "get_net_surface_area",
"NetVolume": "get_net_volume",
"NetWeight": "get_net_weight",
"OuterSurfaceArea": "get_outer_surface_area"
}
},
"IfcBoiler": {
"Qto_BoilerBaseQuantities": {
"GrossWeight": null,
"NetWeight": null,
"TotalSurfaceArea": null
}
},
"IfcBuilding": {
"Qto_BuildingBaseQuantities": {
"EavesHeight": null,
"FootprintArea": null,
"GrossFloorArea": null,
"GrossVolume": null,
"Height": null,
"NetFloorArea": null,
"NetVolume": null
}
},
"IfcBuildingElementProxy": {
"Qto_BuildingElementProxyQuantities": {
"NetSurfaceArea": "get_net_surface_area",
"NetVolume": "get_net_volume"
}
},
"IfcBuildingStorey": {
"Qto_BuildingStoreyBaseQuantities": {
"GrossFloorArea": null,
"GrossHeight": null,
"GrossPerimeter": null,
"GrossVolume": null,
"NetFloorArea": null,
"NetHeigtht": null,
"NetVolume": null
}
},
"IfcBurner": {
"Qto_BurnerBaseQuantities": {
"GrossWeight": null
}
},
"IfcCableCarrierFitting": {
"Qto_CableCarrierFittingBaseQuantities": {
"GrossWeight": null
}
},
"IfcCableCarrierSegment": {
"Qto_CableCarrierSegmentBaseQuantities": {
"CrossSectionArea": null,
"GrossWeight": null,
"Length": null,
"OuterSurfaceArea": null
}
},
"IfcCableFitting": {
"Qto_CableFittingBaseQuantities": {
"GrossWeight": null
}
},
"IfcCableSegment": {
"Qto_CableSegmentBaseQuantities": {
"CrossSectionArea": null,
"GrossWeight": null,
"Length": "get_length",
"OuterSurfaceArea": "get_outer_surface_area"
}
},
"IfcChiller": {
"Qto_ChillerBaseQuantities": {
"GrossWeight": null
}
},
"IfcChimney": {
"Qto_ChimneyBaseQuantities": {
"Length": "get_height"
}
},
"IfcCoil": {
"Qto_CoilBaseQuantities": {
"GrossWeight": null
}
},
"IfcColumn": {
"Qto_ColumnBaseQuantities": {
"CrossSectionArea": "get_cross_section_area",
"GrossSurfaceArea": "get_gross_surface_area",
"GrossVolume": "get_gross_volume",
"GrossWeight": "get_gross_weight",
"Length": "get_length",
"NetSurfaceArea": "get_net_surface_area",
"NetVolume": "get_net_volume",
"NetWeight": "get_net_weight",
"OuterSurfaceArea": "get_outer_surface_area"
}
},
"IfcCommunicationsAppliance": {
"Qto_CommunicationsApplianceBaseQuantities": {
"GrossWeight": null
}
},
"IfcCompressor": {
"Qto_CompressorBaseQuantities": {
"GrossWeight": null
}
},
"IfcCondenser": {
"Qto_CondenserBaseQuantities": {
"GrossWeight": null
}
},
"IfcConstructionEquipmentResource": {
"Qto_ConstructionEquipmentResourceBaseQuantities": {
"OperatingTime": null,
"UsageTime": null
}
},
"IfcConstructionMaterialResource": {
"Qto_ConstructionMaterialResourceBaseQuantities": {
"GrossVolume": "get_gross_volume",
"GrossWeight": null,
"NetVolume": "get_net_volume",
"NetWeight": null
}
},
"IfcController": {
"Qto_ControllerBaseQuantities": {
"GrossWeight": null
}
},
"IfcCooledBeam": {
"Qto_CooledBeamBaseQuantities": {
"GrossWeight": null
}
},
"IfcCoolingTower": {
"Qto_CoolingTowerBaseQuantities": {
"GrossWeight": null
}
},
"IfcCovering": {
"Qto_CoveringBaseQuantities": {
"GrossArea": "get_covering_gross_area",
"NetArea": "get_covering_net_area",
"Width": "get_covering_width"
}
},
"IfcCurtainWall": {
"Qto_CurtainWallQuantities": {
"GrossSideArea": null,
"Height": null,
"Length": null,
"NetSideArea": null,
"Width": null
}
},
"IfcDamper": {
"Qto_DamperBaseQuantities": {
"GrossWeight": null
}
},
"IfcDistributionChamberElement": {
"Qto_DistributionChamberElementBaseQuantities": {
"GrossSurfaceArea": "get_gross_surface_area",
"GrossVolume": "get_gross_volume",
"NetSurfaceArea": "get_net_surface_area",
"NetVolume": "get_net_volume"
}
},
"IfcDoor": {
"Qto_DoorBaseQuantities": {
"Area": "get_net_side_area",
"Height": "get_height",
"Perimeter": "get_rectangular_perimeter",
"Width": "get_length"
}
},
"IfcDuctFitting": {
"Qto_DuctFittingBaseQuantities": {
"GrossCrossSectionArea": null,
"GrossWeight": null,
"Length": "get_length",
"NetCrossSectionArea": null,
"OuterSurfaceArea": "get_outer_surface_area"
}
},
"IfcDuctSegment": {
"Qto_DuctSegmentBaseQuantities": {
"GrossCrossSectionArea": null,
"GrossWeight": null,
"Length": "get_length",
"NetCrossSectionArea": null,
"OuterSurfaceArea": "get_outer_surface_area"
}
},
"IfcDuctSilencer": {
"Qto_DuctSilencerBaseQuantities": {
"GrossWeight": null
}
},
"IfcElectricAppliance": {
"Qto_ElectricApplianceBaseQuantities": {
"GrossWeight": null
}
},
"IfcElectricDistributionBoard": {
"Qto_ElectricDistributionBoardBaseQuantities": {
"GrossWeight": null,
"NumberOfCircuits": null
}
},
"IfcElectricFlowStorageDevice": {
"Qto_ElectricFlowStorageDeviceBaseQuantities": {
"GrossWeight": null
}
},
"IfcElectricGenerator": {
"Qto_ElectricGeneratorBaseQuantities": {
"GrossWeight": null
}
},
"IfcElectricMotor": {
"Qto_ElectricMotorBaseQuantities": {
"GrossWeight": null
}
},
"IfcElectricTimeControl": {
"Qto_ElectricTimeControlBaseQuantities": {
"GrossWeight": null
}
},
"IfcEvaporativeCooler": {
"Qto_EvaporativeCoolerBaseQuantities": {
"GrossWeight": null
}
},
"IfcEvaporator": {
"Qto_EvaporatorBaseQuantities": {
"GrossWeight": null
}
},
"IfcFan": {
"Qto_FanBaseQuantities": {
"GrossWeight": null
}
},
"IfcFilter": {
"Qto_FilterBaseQuantities": {
"GrossWeight": null
}
},
"IfcFireSuppressionTerminal": {
"Qto_FireSuppressionTerminalBaseQuantities": {
"GrossWeight": null
}
},
"IfcFlowInstrument": {
"Qto_FlowInstrumentBaseQuantities": {
"GrossWeight": null
}
},
"IfcFlowMeter": {
"Qto_FlowMeterBaseQuantities": {
"GrossWeight": null
}
},
"IfcFooting": {
"Qto_FootingBaseQuantities": {
"CrossSectionArea": "get_cross_section_area",
"GrossSurfaceArea": "get_gross_surface_area",
"GrossVolume": "get_gross_volume",
"GrossWeight": "get_gross_weight",
"Height": "get_height",
"Length": "get_length",
"NetVolume": "get_net_volume",
"NetWeight": "get_net_weight",
"OuterSurfaceArea": "get_outer_surface_area",
"Width": "get_width"
}
},
"IfcHeatExchanger": {
"Qto_HeatExchangerBaseQuantities": {
"GrossWeight": null
}
},
"IfcHumidifier": {
"Qto_HumidifierBaseQuantities": {
"GrossWeight": null
}
},
"IfcInterceptor": {
"Qto_InterceptorBaseQuantities": {
"GrossWeight": null
}
},
"IfcJunctionBox": {
"Qto_JunctionBoxBaseQuantities": {
"GrossWeight": null,
"NumberOfGangs": null
}
},
"IfcLaborResource": {
"Qto_LaborResourceBaseQuantities": {
"OvertimeWork": null,
"StandardWork": null
}
},
"IfcLamp": {
"Qto_LampBaseQuantities": {
"GrossWeight": null
}
},
"IfcLightFixture": {
"Qto_LightFixtureBaseQuantities": {
"GrossWeight": null
}
},
"IfcMember": {
"Qto_MemberBaseQuantities": {
"CrossSectionArea": "get_cross_section_area",
"GrossSurfaceArea": "get_gross_surface_area",
"GrossVolume": "get_gross_volume",
"GrossWeight": "get_gross_weight",
"Length": "get_length",
"NetSurfaceArea": "get_net_surface_area",
"NetVolume": "get_net_volume",
"NetWeight": "get_net_weight",
"OuterSurfaceArea": "get_outer_surface_area"
}
},
"IfcMotorConnection": {
"Qto_MotorConnectionBaseQuantities": {
"GrossWeight": null
}
},
"IfcOpeningElement": {
"Qto_OpeningElementBaseQuantities": {
"Area": "get_opening_mapping_area",
"Depth": "get_opening_depth",
"Height": "get_opening_height",
"Volume": "get_net_volume",
"Width": "get_length"
}
},
"IfcOutlet": {
"Qto_OutletBaseQuantities": {
"GrossWeight": null
}
},
"IfcPile": {
"Qto_PileBaseQuantities": {
"CrossSectionArea": "get_cross_section_area",
"GrossSurfaceArea": "get_gross_surface_area",
"GrossVolume": "get_gross_volume",
"GrossWeight": "get_gross_weight",
"Length": "get_length",
"NetVolume": "get_net_volume",
"NetWeight": "get_net_weight",
"OuterSurfaceArea": "get_outer_surface_area"
}
},
"IfcPipeFitting": {
"Qto_PipeFittingBaseQuantities": {
"GrossCrossSectionArea": null,
"GrossWeight": null,
"Length": null,
"NetCrossSectionArea": null,
"NetWeight": null,
"OuterSurfaceArea": null
}
},
"IfcPipeSegment": {
"Qto_PipeSegmentBaseQuantities": {
"GrossCrossSectionArea": null,
"GrossWeight": "get_gross_weight",
"Length": "get_length",
"NetCrossSectionArea": "get_cross_section_area",
"NetWeight": "get_net_weight",
"OuterSurfaceArea": "get_outer_surface_area"
}
},
"IfcPlate": {
"Qto_PlateBaseQuantities": {
"GrossArea": "get_gross_footprint_area",
"GrossVolume": "get_gross_volume",
"GrossWeight": "get_gross_weight",
"NetArea": "get_net_footprint_area",
"NetVolume": "get_net_volume",
"NetWeight": "get_net_weight",
"Perimeter": "get_gross_perimeter",
"Width": "get_height"
}
},
"IfcProjectionElement": {
"Qto_ProjectionElementBaseQuantities": {
"Area": "get_net_side_area",
"Volume": "get_net_volume"
}
},
"IfcProtectiveDevice": {
"Qto_ProtectiveDeviceBaseQuantities": {
"GrossWeight": null
}
},
"IfcProtectiveDeviceTrippingUnit": {
"Qto_ProtectiveDeviceTrippingUnitBaseQuantities": {
"GrossWeight": null
}
},
"IfcPump": {
"Qto_PumpBaseQuantities": {
"GrossWeight": null
}
},
"IfcRailing": {
"Qto_RailingBaseQuantities": {
"Length": "get_length"
}
},
"IfcRampFlight": {
"Qto_RampFlightBaseQuantities": {
"GrossArea": "get_gross_stair_area",
"GrossVolume": "get_gross_volume",
"Length": "get_stair_length",
"NetArea": "get_net_stair_area",
"NetVolume": "get_net_volume",
"Width": "get_width"
}
},
"IfcReinforcingElement": {
"Qto_ReinforcingElementBaseQuantities": {
"Count": null,
"Length": "get_length",
"Weight": null
}
},
"IfcRoof": {
"Qto_RoofBaseQuantities": {
"GrossArea": "get_gross_top_area",
"NetArea": "get_net_top_area",
"ProjectedArea": null
}
},
"IfcSanitaryTerminal": {
"Qto_SanitaryTerminalBaseQuantities": {
"GrossWeight": null
}
},
"IfcSensor": {
"Qto_SensorBaseQuantities": {
"GrossWeight": null
}
},
"IfcSite": {
"Qto_SiteBaseQuantities": {
"GrossArea": "get_gross_footprint_area",
"GrossPerimeter": "get_gross_perimeter"
}
},
"IfcSlab": {
"Qto_SlabBaseQuantities": {
"Depth": "get_height",
"GrossArea": "get_gross_footprint_area",
"GrossVolume": "get_gross_volume",
"GrossWeight": "get_gross_weight",
"Length": "get_length",
"NetArea": "get_net_footprint_area",
"NetVolume": "get_net_volume",
"NetWeight": "get_net_weight",
"Perimeter": "get_gross_perimeter",
"Width": "get_width"
}
},
"IfcSolarDevice": {
"Qto_SolarDeviceBaseQuantities": {
"GrossArea": null,
"GrossWeight": null
}
},
"IfcSpace": {
"Qto_SpaceBaseQuantities": {
"FinishCeilingHeight": "get_finish_ceiling_height",
"FinishFloorHeight": "get_finish_floor_height",
"GrossCeilingArea": "get_gross_ceiling_area",
"GrossFloorArea": "get_gross_footprint_area",
"GrossPerimeter": "get_gross_perimeter",
"GrossVolume": "get_gross_volume",
"GrossWallArea": null,
"Height": "get_height",
"NetCeilingArea": "get_net_ceiling_area",
"NetFloorArea": "get_net_floor_area",
"NetPerimeter": null,
"NetVolume": "get_space_net_volume",
"NetWallArea": null
}
},
"IfcSpaceHeater": {
"Qto_SpaceHeaterBaseQuantities": {
"GrossWeight": null,
"Length": "get_length",
"NetWeight": null
}
},
"IfcStackTerminal": {
"Qto_StackTerminalBaseQuantities": {
"GrossWeight": null
}
},
"IfcStairFlight": {
"Qto_StairFlightBaseQuantities": {
"GrossVolume": "get_gross_volume",
"Length": "get_stair_length",
"NetVolume": "get_net_volume"
}
},
"IfcSwitchingDevice": {
"Qto_SwitchingDeviceBaseQuantities": {
"GrossWeight": null
}
},
"IfcTank": {
"Qto_TankBaseQuantities": {
"GrossWeight": null,
"NetWeight": null,
"TotalSurfaceArea": "get_outer_surface_area"
}
},
"IfcTransformer": {
"Qto_TransformerBaseQuantities": {
"GrossWeight": null
}
},
"IfcTubeBundle": {
"Qto_TubeBundleBaseQuantities": {
"GrossWeight": null,
"NetWeight": null
}
},
"IfcUnitaryControlElement": {
"Qto_UnitaryControlElementBaseQuantities": {
"GrossWeight": null
}
},
"IfcUnitaryEquipment": {
"Qto_UnitaryEquipmentBaseQuantities": {
"GrossWeight": null
}
},
"IfcValve": {
"Qto_ValveBaseQuantities": {
"GrossWeight": null
}
},
"IfcVibrationIsolator": {
"Qto_VibrationIsolatorBaseQuantities": {
"GrossWeight": null
}
},
"IfcWall": {
"Qto_WallBaseQuantities": {
"GrossFootprintArea": "get_gross_footprint_area",
"GrossSideArea": "get_gross_side_area",
"GrossVolume": "get_gross_volume",
"GrossWeight": "get_gross_weight",
"Height": "get_height",
"Length": "get_length",
"NetFootprintArea": "get_net_footprint_area",
"NetSideArea": "get_net_side_area",
"NetVolume": "get_net_volume",
"NetWeight": "get_net_weight",
"Width": "get_width"
}
},
"IfcWasteTerminal": {
"Qto_WasteTerminalBaseQuantities": {
"GrossWeight": null
}
},
"IfcWindow": {
"Qto_WindowBaseQuantities": {
"Area": "get_net_side_area",
"Height": "get_height",
"Perimeter": "get_rectangular_perimeter",
"Width": "get_length"
}
}
}
}
}
+1 -1
View File
@@ -185,7 +185,7 @@ class Csv2Ifc:
"cost.add_cost_item_quantity", self.file, cost_item=cost_item["ifc"], ifc_class=quantity_class
)
# 3 IfcPhysicalSimpleQuantity Value
quantity[3] = cost_item["Quantity"]
quantity[3] = int(cost_item["Quantity"]) if quantity_class == "IfcQuantityCount" else cost_item["Quantity"]
if prop_name:
quantity.Name = prop_name
+34 -29
View File
@@ -26,7 +26,7 @@ import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.cost
import ifcopenshell.util.date
from typing import Union, Optional
from typing import Union, Optional, Any
class IfcDataGetter:
@@ -41,17 +41,17 @@ class IfcDataGetter:
]
@staticmethod
def canonicalise_time(time):
def canonicalise_time(time: Union[datetime.datetime, None]) -> str:
if not time:
return "-"
return time.strftime("%d/%m/%y")
@staticmethod
def get_root_costs(cost_schedule):
def get_root_costs(cost_schedule: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return [obj for rel in cost_schedule.Controls or [] for obj in rel.RelatedObjects or []]
@staticmethod
def get_cost_item_values(cost_item=None):
def get_cost_item_values(cost_item: Union[ifcopenshell.entity_instance, None]) -> Union[list[dict[str, Any]], None]:
if not cost_item:
return None
values = []
@@ -71,14 +71,14 @@ class IfcDataGetter:
return values
@staticmethod
def process_categories(cost_item, categories):
def process_categories(cost_item: ifcopenshell.entity_instance, categories: set[str]) -> set[str]:
for cost_value in cost_item.CostValues or []:
if cost_value.Category:
categories.add("{}{}".format(cost_value.Category, " Cost"))
return categories
@staticmethod
def process_cost_item_categories(cost_item, categories):
def process_cost_item_categories(cost_item: ifcopenshell.entity_instance, categories: set[str]) -> set[str]:
IfcDataGetter.process_categories(cost_item, categories)
for rel in cost_item.IsNestedBy or []:
for child in rel.RelatedObjects or []:
@@ -86,14 +86,20 @@ class IfcDataGetter:
return categories
@staticmethod
def get_cost_rates_categories(schedule):
def get_cost_rates_categories(schedule: ifcopenshell.entity_instance) -> set[str]:
categories = set()
for cost_item in IfcDataGetter.get_root_costs(schedule):
IfcDataGetter.process_cost_item_categories(cost_item, categories)
return categories
@staticmethod
def process_cost_data(file, cost_item, cost_items_data, index, hierarchy="1"):
def process_cost_data(
file: ifcopenshell.file,
cost_item: ifcopenshell.entity_instance,
cost_items_data: list[dict[str, Any]],
index: int,
hierarchy: str = "1",
) -> None:
def listToString(s):
return ", ".join([str(i) for i in s])
@@ -134,7 +140,7 @@ class IfcDataGetter:
)
@staticmethod
def get_cost_items_data(file, schedule):
def get_cost_items_data(file: ifcopenshell.file, schedule: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
cost_items_data = []
index = 0
for cost_item in IfcDataGetter.get_root_costs(schedule):
@@ -142,7 +148,7 @@ class IfcDataGetter:
return cost_items_data
@staticmethod
def format_unit(unit):
def format_unit(unit: ifcopenshell.entity_instance) -> str:
if unit.is_a("IfcContextDependentUnit"):
return f"{unit.UnitType} / {unit.Name}"
else:
@@ -152,7 +158,7 @@ class IfcDataGetter:
return f"{unit.UnitType} / {name}"
@staticmethod
def get_cost_value_unit(cost_value=None):
def get_cost_value_unit(cost_value: Optional[ifcopenshell.entity_instance] = None) -> Union[str, None]:
if not cost_value:
return None
unit = cost_value.UnitBasis
@@ -161,9 +167,9 @@ class IfcDataGetter:
return IfcDataGetter.format_unit(unit.UnitComponent)
@staticmethod
def get_cost_item_quantity(file, cost_item=None):
def get_cost_item_quantity(file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance) -> dict[str, Any]:
# TODO: handle multiple quantities, THOSE WHHICH ARE JUYST ASSIGNED TO THE COST ITEM DIRECTLY, NOT THROUGH OBJECTS.
def add_quantity(quantity, take_off_name):
def add_quantity(quantity: ifcopenshell.entity_instance, take_off_name: str) -> float:
accounted_for.append(quantity)
if take_off_name == "":
take_off_name = quantity[0]
@@ -171,26 +177,25 @@ class IfcDataGetter:
take_off_name = "mixed-takeoff-quantities"
return quantity[3]
if not cost_item:
return None
take_off_name = ""
has_changed_name = False
total_cost_quantity = 0
accounted_for = []
for rel in cost_item.Controls or []:
for related_object in rel.RelatedObjects:
qtos = ifcopenshell.util.element.get_psets(related_object, qtos_only=True)
for quantities in qtos.values() or []:
qto = file.by_id(quantities["id"])
for quantity in qto.Quantities:
if not quantity in cost_item.CostQuantities:
continue
total_cost_quantity += add_quantity(quantity, take_off_name)
accounted_for.append(quantity)
cost_item_quantities = cost_item.CostQuantities
if cost_item_quantities:
for rel in cost_item.Controls or []:
for related_object in rel.RelatedObjects:
qtos = ifcopenshell.util.element.get_psets(related_object, qtos_only=True)
for quantities in qtos.values() or []:
qto = file.by_id(quantities["id"])
for quantity in qto.Quantities:
if quantity not in cost_item_quantities:
continue
total_cost_quantity += add_quantity(quantity, take_off_name)
accounted_for.append(quantity)
for quantity in cost_item.CostQuantities or []:
if not quantity in accounted_for:
total_cost_quantity += add_quantity(quantity, take_off_name)
for quantity in cost_item_quantities:
if not quantity in accounted_for:
total_cost_quantity += add_quantity(quantity, take_off_name)
return {
"id": cost_item.id(),
+273
View File
@@ -0,0 +1,273 @@
# Ifc5D - IFC costing utility
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Ifc5D.
#
# Ifc5D is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ifc5D is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Ifc5D. If not, see <http://www.gnu.org/licenses/>.
import os
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.util.unit
import ifcopenshell.util.selector
import multiprocessing
from collections import namedtuple
Function = namedtuple("Function", ["measure", "name", "description"])
rules = {}
cwd = os.path.dirname(os.path.realpath(__file__))
for name in ("IFC4QtoBaseQuantities", "IFC4QtoBaseQuantitiesBlender"):
with open(os.path.join(cwd, name + ".json"), "r") as f:
rules[name] = json.load(f)
def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], rules: dict) -> dict:
results = {}
for calculator, queries in rules["calculators"].items():
calculator = calculators[calculator]
for query, qtos in queries.items():
filtered_elements = ifcopenshell.util.selector.filter_elements(ifc_file, query, elements)
if filtered_elements:
calculator.calculate(ifc_file, filtered_elements, qtos, results)
return results
def edit_qtos(ifc_file, results) -> None:
for element, qtos in results.items():
for name, quantities in qtos.items():
qto = ifcopenshell.util.element.get_pset(element, name, should_inherit=False)
if qto:
qto = ifc_file.by_id(qto["id"])
else:
qto = ifcopenshell.api.pset.add_qto(ifc_file, element, name)
ifcopenshell.api.pset.edit_qto(ifc_file, qto=qto, properties=quantities)
class SI2ProjectUnitConverter:
def __init__(self, ifc_file):
self.project_units = {
"IfcAreaMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "AREAUNIT"),
"IfcLengthMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "LENGTHUNIT"),
"IfcMassMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "MASSUNIT"),
"IfcTimeMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "TIMEUNIT"),
"IfcVolumeMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "VOLUMEUNIT"),
}
for key, value in self.project_units.items():
if value:
self.project_units[key] = (getattr(value, "Prefix", "None"), value.Name)
self.si_names = {
"IfcAreaMeasure": "SQUARE_METRE",
"IfcLengthMeasure": "METRE",
"IfcMassMeasure": "GRAM",
"IfcTimeMeasure": "SECOND",
"IfcVolumeMeasure": "CUBIC_METRE",
}
def convert(self, value, measure):
if measure_unit := self.project_units.get(measure, None):
return ifcopenshell.util.unit.convert(value, None, self.si_names[measure], *measure_unit)
return value
class IfcOpenShell:
"""Calculates Model body context geometry using the default IfcOpenShell
iterator on triangulation elements."""
raw_functions = {
# IfcLengthMeasure
"get_x": Function("IfcLengthMeasure", "X", "Calculates the length along the local X axis"),
"get_y": Function("IfcLengthMeasure", "Y", "Calculates the length along the local Y axis"),
"get_z": Function("IfcLengthMeasure", "Z", "Calculates the length along the local Z axis"),
"get_max_xyz": Function("IfcLengthMeasure", "Max XYZ", "The maximum X, Y, or Z local dimension"),
"get_min_xyz": Function("IfcLengthMeasure", "Min XYZ", "The minimum X, Y, or Z local dimension"),
"get_top_elevation": Function("IfcLengthMeasure", "Top elevation", "The local maximum Z ordinate"),
"get_bottom_elevation": Function("IfcLengthMeasure", "Bottom Elevation", "The local minimum Z ordinate"),
"get_footprint_perimeter": Function(
"IfcLengthMeasure",
"Footprint Perimeter",
"The perimeter if the object's faces were projected along the Z-axis and seen top down",
),
# IfcAreaMeasure
"get_area": Function("IfcAreaMeasure", "Area", "The total surface area of the element"),
"get_footprint_area": Function(
"IfcAreaMeasure",
"Footprint Area",
"The area if the object's faces were projected along the Z-axis and seen top down",
),
"get_max_side_area": Function(
"IfcAreaMeasure",
"Max Side Area",
"The maximum side area when seen from either X, Y, or Z directions",
),
"get_outer_surface_area": Function(
"IfcAreaMeasure",
"Outer Surface Area",
"The total surface area except for the top or bottom, such as the ends of columns or beams",
),
"get_side_area": Function(
"IfcAreaMeasure",
"Side area",
"The side (non-projected) are of the shape as seen from the local Y-axis",
),
# IfcVolumeMeasure
"get_volume": Function("IfcVolumeMeasure", "Volume", "Calculates the volume of a manifold shape"),
}
functions = {}
for k, v in raw_functions.items():
functions[f"gross_{k}"] = Function(v.measure, f"Gross {v.name}", v.description)
functions[f"net_{k}"] = Function(v.measure, f"Net {v.name}", v.description)
@staticmethod
def calculate(
ifc_file: ifcopenshell.file,
elements: set[ifcopenshell.entity_instance],
qtos: dict,
results: dict,
) -> None:
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.shape
formula_functions = {}
gross_settings = ifcopenshell.geom.settings()
gross_settings.set(gross_settings.DISABLE_OPENING_SUBTRACTIONS, True)
net_settings = ifcopenshell.geom.settings()
gross_qtos = {}
net_qtos = {}
for name, quantities in qtos.items():
for quantity, formula in quantities.items():
if not formula:
continue
if formula.startswith("gross_"):
formula = formula[6:]
gross_qtos.setdefault(name, {})[quantity] = formula
formula_functions[formula] = getattr(ifcopenshell.util.shape, formula)
elif formula.startswith("net_"):
formula = formula[4:]
net_qtos.setdefault(name, {})[quantity] = formula
formula_functions[formula] = getattr(ifcopenshell.util.shape, formula)
tasks = []
if gross_qtos:
tasks.append((IfcOpenShell.create_iterator(ifc_file, gross_settings, list(elements)), gross_qtos))
if net_qtos:
tasks.append((IfcOpenShell.create_iterator(ifc_file, net_settings, list(elements)), net_qtos))
unit_converter = SI2ProjectUnitConverter(ifc_file)
for iterator, qtos in tasks:
if iterator.initialize():
while True:
shape = iterator.get()
element = ifc_file.by_id(shape.id)
results.setdefault(element, {})
for name, quantities in qtos.items():
results[element].setdefault(name, {})
for quantity, formula in quantities.items():
results[element][name][quantity] = unit_converter.convert(
formula_functions[formula](shape.geometry), IfcOpenShell.raw_functions[formula].measure
)
if not iterator.next():
break
@staticmethod
def create_iterator(
ifc_file: ifcopenshell.file, settings: ifcopenshell.geom.settings, elements: list[ifcopenshell.entity_instance]
) -> ifcopenshell.geom.iterator:
return ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=elements)
class Blender:
"""Calculates geometry based on currently loaded Blender objects."""
functions = {
# IfcLengthMeasure
"get_covering_width": Function("IfcLengthMeasure", "Covering Width", ""),
"get_finish_ceiling_height": Function("IfcLengthMeasure", "Finish Ceiling Height", ""),
"get_finish_floor_height": Function("IfcLengthMeasure", "Finish Floor Height", ""),
"get_gross_perimeter": Function("IfcLengthMeasure", "Gross Perimeter", ""),
"get_height": Function("IfcLengthMeasure", "Height", ""),
"get_length": Function("IfcLengthMeasure", "Length", ""),
"get_opening_depth": Function("IfcLengthMeasure", "Opening Depth", ""),
"get_opening_height": Function("IfcLengthMeasure", "Opening Height", ""),
"get_rectangular_perimeter": Function("IfcLengthMeasure", "Rectangular Perimeter", ""),
"get_stair_length": Function("IfcLengthMeasure", "Stair Length", ""),
"get_width": Function("IfcLengthMeasure", "Width", ""),
# IfcAreaMeasure
"get_covering_gross_area": Function("IfcAreaMeasure", "Covering Gross Area", ""),
"get_covering_net_area": Function("IfcAreaMeasure", "Covering Net Area", ""),
"get_cross_section_area": Function("IfcAreaMeasure", "Cross Section Area", ""),
"get_gross_ceiling_area": Function("IfcAreaMeasure", "Gross Ceiling Area", ""),
"get_gross_footprint_area": Function("IfcAreaMeasure", "Gross Footprint Area", ""),
"get_gross_side_area": Function("IfcAreaMeasure", "Gross Side Area", ""),
"get_gross_stair_area": Function("IfcAreaMeasure", "Gross Stair Area", ""),
"get_gross_surface_area": Function("IfcAreaMeasure", "Gross Surface Area", ""),
"get_gross_top_area": Function("IfcAreaMeasure", "Gross Top Area", ""),
"get_net_ceiling_area": Function("IfcAreaMeasure", "Net Ceiling Area", ""),
"get_net_floor_area": Function("IfcAreaMeasure", "Net Floor Area", ""),
"get_net_footprint_area": Function("IfcAreaMeasure", "Net Footprint Area", ""),
"get_net_side_area": Function("IfcAreaMeasure", "Net Side Area", ""),
"get_net_stair_area": Function("IfcAreaMeasure", "Net Stair Area", ""),
"get_net_surface_area": Function("IfcAreaMeasure", "Net Surface Area", ""),
"get_net_top_area": Function("IfcAreaMeasure", "Net Top Area", ""),
"get_opening_mapping_area": Function("IfcAreaMeasure", "Opening Mapping Area", ""),
"get_outer_surface_area": Function("IfcAreaMeasure", "Outer Surface Area", ""),
# IfcVolumeMeasure
"get_gross_volume": Function("IfcVolumeMeasure", "Gross Volume", ""),
"get_net_volume": Function("IfcVolumeMeasure", "Net Volume", ""),
"get_space_net_volume": Function("IfcVolumeMeasure", "Space Net Volume", ""),
# IfcMassMeasure
"get_gross_weight": Function("IfcMassMeasure", "Gross Weight", ""),
"get_net_weight": Function("IfcMassMeasure", "Net Weight", ""),
}
@staticmethod
def calculate(
ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], qtos: dict, results: dict
) -> None:
import blenderbim.tool as tool
import blenderbim.bim.module.qto.calculator as calculator
unit_converter = SI2ProjectUnitConverter(ifc_file)
formula_functions = {}
for element in elements:
obj = tool.Ifc.get_object(element)
if not obj or obj.type != "MESH":
continue
results.setdefault(element, {})
for name, quantities in qtos.items():
results[element].setdefault(name, {})
for quantity, formula in quantities.items():
if not formula:
continue
if not (formula_function := formula_functions.get(formula)):
formula_function = formula_functions[formula] = getattr(calculator, formula)
results[element][name][quantity] = unit_converter.convert(
formula_function(obj), Blender.functions[formula].measure
)
calculators = {"Blender": Blender, "IfcOpenShell": IfcOpenShell}
+2 -33
View File
@@ -1,35 +1,4 @@
# IFCCityJSON
Converter for CityJSON files and IFC. Currently only supports one-way conversion from CityJSON to IFC.
## Dependencies
- [IfcOpenShell](https://github.com/IfcOpenShell/IfcOpenShell) (also IfcOpenShell api is needed)
- [CJIO](https://github.com/cityjson/cjio) (>=0.8, <1.0)
## Usage of IFCCityJSON
An extended ifccityjson tutorial can be found on [the OSARCH wiki](https://wiki.osarch.org/index.php?title=Ifccityjson)
Following command will execute a conversion from CityJSON to IFC
python ifccityjson.py [-i input file] [-o output file] [-n name of identification attribute]
The example file that could be used is example/3D_BAG_example.json
python ifccityjson.py -i example/3DBAG_example.json -o example/3DBAG_example.ifc -n identificatie
## Implemented geometries
- [x] "MultiPoint"
- [x] "MultiLineString"
- [x] "MultiSurface"
- [x] "CompositeSurface"
- [x] "Solid": exterior shell
- [ ] "Solid": interior shell
- [x] "MultiSolid"
- [x] "CompositeSolid"
- [ ] "GeometryInstance"
## TODO
- [x] CityJSON Attributes as IFC properties in 'CityJSON_attributes' pset
- [x] Implement georeferencing
- [x] Do not use template IFC for new IFC file, but make IFC file from scratch
- [x] Create mapping to IFC for all CityJSON object types & semantic surfaces
- [ ] Implement conversion of all CitYJSON geometries
- [x] Implement conversion of all LODs instead of only the most detailed
IfcCityJSON is a converter for CityJSON files and IFC. It currently only
supports one-way conversion from CityJSON to IFC.
@@ -18,7 +18,7 @@
import argparse
from cjio import cityjson
from cityjson2ifc.cityjson2ifc import Cityjson2ifc
from .cityjson2ifc import Cityjson2ifc
def cmdline():
# Example:
@@ -51,4 +51,4 @@ def cmdline():
converter.convert(city_model)
if __name__ == '__main__':
cmdline()
cmdline()
@@ -146,7 +146,7 @@ class Cityjson2ifc:
# Georeferencing
self.properties["local_translation"] = None
self.properties["local_scale"] = None
if not self.city_model.is_transformed:
if self.city_model.is_transformed:
self.properties["local_scale"] = self.city_model.transform["scale"]
local_translation = self.city_model.transform["translate"]
self.properties["local_translation"] = {
+5 -5
View File
@@ -265,7 +265,7 @@ config = {
"AreaUnit",
"Phase",
],
"colours": "ppppreeeeesss",
"colours": "ppprrreeeeesss",
"sort": [{"name": "Name", "order": "ASC"}],
"get_category_elements": get_facilities,
"get_element_data": get_facility_data,
@@ -283,7 +283,7 @@ config = {
"ModelID",
"Elevation",
],
"colours": "ppreeees",
"colours": "prrreeees",
"sort": [{"name": "Elevation", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
"get_category_elements": get_storeys,
"get_element_data": get_storey_data,
@@ -304,7 +304,7 @@ config = {
"GrossFloorArea",
"NetFloorArea",
],
"colours": "ppprreeess",
"colours": "pprrrreeesss",
"sort": [{"name": "StoreyName", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
"get_category_elements": get_spaces,
"get_element_data": get_space_data,
@@ -336,7 +336,7 @@ config = {
"PointOfContact",
"WarrantyPeriod",
],
"colours": "pppreeeeesssss",
"colours": "pprrreeeeesssss",
"sort": [{"name": "ModelObject", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
"get_category_elements": get_element_types,
"get_element_data": get_element_type_data,
@@ -379,7 +379,7 @@ config = {
"ModelSoftware",
"ModelID",
],
"colours": "pppreee",
"colours": "pprrreee",
"sort": [{"name": "Name", "order": "ASC"}],
"get_category_elements": get_systems,
"get_element_data": get_system_data,
+1 -1
View File
@@ -89,7 +89,7 @@ endif
mkdir -p dist/ifcopenshell
cp -r ifcopenshell/* dist/ifcopenshell/
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/ifcopenshell_wrapper.py dist/ifcopenshell/
ifeq ($(PLATFORM), win)
@@ -3,3 +3,62 @@ IfcCityJSON
IfcCityJSON is a converter for CityJSON files and IFC. It currently only
supports one-way conversion from CityJSON to IFC.
Source installation
-------------------
1. :doc:`Install IfcOpenShell <ifcopenshell-python/installation>`
2. `Clone the source code <https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/ifccityjson>`_.
3. ``cd /path/to/IfcOpenShell/src/ifccityjson``
4. ``pip install .``
Usage
-----
An extended ifccityjson tutorial can be found on `the OSARCH wiki
<https://wiki.osarch.org/index.php?title=Ifccityjson>`_. Here's the built-in
documentation:
.. code-block:: console
$ python -m ifccityjson -h
usage: __main__.py [-h] -i INPUT [-o OUTPUT] [-n NAME] [--split-lod] [--no-split-lod] [--lod LOD]
options:
-h, --help show this help message and exit
-i INPUT, --input INPUT
input CityJSON file
-o OUTPUT, --output OUTPUT
output IFC file. Standard is output.ifc
-n NAME, --name NAME Attribute containing the name
--split-lod Split the file in multiple LoDs
--no-split-lod Do not split the file in multiple LoDs
--lod LOD extract LOD value (example: 1.2)
The example file that could be used is example/3D_BAG_example.json
.. code-block:: console
python ifccityjson.py -i example/geometries.json -o output.ifc -n identificatie
The following geometries are implemented:
- [x] "MultiPoint"
- [x] "MultiLineString"
- [x] "MultiSurface"
- [x] "CompositeSurface"
- [x] "Solid": exterior shell
- [ ] "Solid": interior shell
- [x] "MultiSolid"
- [x] "CompositeSolid"
- [ ] "GeometryInstance"
TODO:
- [x] CityJSON Attributes as IFC properties in 'CityJSON_attributes' pset
- [x] Implement georeferencing
- [x] Do not use template IFC for new IFC file, but make IFC file from scratch
- [x] Create mapping to IFC for all CityJSON object types & semantic surfaces
- [ ] Implement conversion of all CitYJSON geometries
- [x] Implement conversion of all LODs instead of only the most detailed

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