Compare commits

...

47 Commits

Author SHA1 Message Date
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
78 changed files with 9190 additions and 7887 deletions
@@ -170,6 +170,7 @@ classes = [
ui.BIM_PT_tab_structural,
# Construction scheduling
ui.BIM_PT_tab_status,
ui.BIM_PT_tab_qto,
ui.BIM_PT_tab_resources,
ui.BIM_PT_tab_cost,
ui.BIM_PT_tab_sequence,
@@ -5,6 +5,7 @@ FILE_NAME('EQto_BodyGeometryValidation.ifc','2020-01-01T00:00:00',(),(),'EQto_Bo
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
/* This quantity set is Qto_BodyGeometryValidation backport from IFC4X3 to support IFC4. */
#1=IFCPROPERTYSETTEMPLATE('2KS9su6r517uLXTGKwwDdn',$,'EQto_BodyGeometryValidation','Quantities supplied for validating the correct interpretation of the body shape representation at import. In case of multiple representation items, the quantities are summed for each of the items (irrespective of any overlap). Choosing a suitable tolerance value for comparing the supplied numbers to the numbers calculated from the reconstructed geometry is at the discretion of the importing application.',.QTO_OCCURRENCEDRIVEN.,'IfcProduct',(#2,#3,#4,#5,#6,#7));
#2=IFCSIMPLEPROPERTYTEMPLATE('3iCdOOLRjCwQ_HRTB41Xh8',$,'GrossSurfaceArea','Total gross area of the object, normally generated as perimeter * length + 2 * cross section area. It is the sum of OuterSurfaceArea + (2 x CrossSectionArea) and shall only be given, if the OuterSurfaceArea and CrossSectionArea cannot be established separately.\X2\000A000A\X0\Total gross surface area of the element before applying product-level geometric features such as openings and projections.',.Q_AREA.,$,$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('0juemEqF5889vg7HwR87Fv',$,'NetSurfaceArea','Net surface area of the object, normally generated as perimeter * length + 2 * cross section area taking into account possible processing features (cut-out''s, etc.) or openings and recesses.\X2\000A000A\X0\Total net surface area of the element after applying product-level geometric features such as openings and projections.',.Q_AREA.,$,$,$,$,$,$,.READWRITE.);
+2 -1
View File
@@ -301,7 +301,8 @@ def load_post(scene):
if bpy.context.preferences.addons["blenderbim"].preferences.should_setup_workspace:
if "BIM" in bpy.data.workspaces:
bpy.context.window.workspace = bpy.data.workspaces["BIM"]
if bpy.context.preferences.addons["blenderbim"].preferences.activate_workspace:
bpy.context.window.workspace = bpy.data.workspaces["BIM"]
else:
bpy.ops.workspace.append_activate(idname="BIM", filepath=os.path.join(cwd, "data", "workspace.blend"))
-9
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
@@ -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
@@ -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()
@@ -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
@@ -713,6 +714,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()
@@ -1683,20 +1688,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 +1754,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):
@@ -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:
@@ -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):
@@ -24,12 +24,13 @@ classes = (
operator.CalculateEdgeLengths,
operator.CalculateFaceAreas,
operator.CalculateObjectVolumes,
operator.ExecuteQtoMethod,
operator.QuantifyObjects,
operator.AssignBaseQto,
operator.CalculateAllQuantities,
operator.CalculateSingleQuantity,
operator.PerformQuantityTakeOff,
prop.BIMQtoProperties,
ui.BIM_PT_qto_utilities,
ui.BIM_PT_qto,
ui.BIM_PT_qto_manual,
ui.BIM_PT_qto_simple,
ui.BIM_PT_qto_cost,
)
File diff suppressed because it is too large Load Diff
@@ -19,9 +19,11 @@
import bpy
import blenderbim.tool as tool
def refresh():
QtoData.is_loaded = False
class QtoData:
data = {}
is_loaded = False
@@ -29,9 +31,9 @@ class QtoData:
@classmethod
def load(cls):
cls.data = {
"has_cost_item" : cls.has_cost_item(),
"relating_cost_items" : cls.relating_cost_items(),
}
"has_cost_item": cls.has_cost_item(),
"relating_cost_items": cls.relating_cost_items(),
}
cls.is_loaded = True
@@ -53,23 +55,13 @@ class QtoData:
for relating_cost_item in relating_cost_items:
results.append(
{
'cost_item_id' : relating_cost_item['cost_item_id'],
'cost_item_name' : relating_cost_item['cost_item_name'],
'quantity_id' : relating_cost_item['quantity_id'],
'quantity_name' : relating_cost_item['quantity_name'],
'quantity_value' : relating_cost_item['quantity_value'],
'quantity_type' : relating_cost_item['quantity_type'],
"cost_item_id": relating_cost_item["cost_item_id"],
"cost_item_name": relating_cost_item["cost_item_name"],
"quantity_id": relating_cost_item["quantity_id"],
"quantity_name": relating_cost_item["quantity_name"],
"quantity_value": relating_cost_item["quantity_value"],
"quantity_type": relating_cost_item["quantity_type"],
}
)
return results
@@ -23,7 +23,6 @@ import blenderbim.tool as tool
import blenderbim.core.qto as core
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.qto import helper
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
class CalculateCircleRadius(bpy.types.Operator):
@@ -85,115 +84,67 @@ class CalculateObjectVolumes(bpy.types.Operator):
return {"FINISHED"}
class ExecuteQtoMethod(bpy.types.Operator):
bl_idname = "bim.execute_qto_method"
bl_label = "Execute Qto Method"
class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.calculate_single_quantity"
bl_label = "Calculate Single Quantity"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def execute(self, context):
selected_mesh_objects = [o for o in context.selected_objects if o.type == "MESH"]
props = context.scene.BIMQtoProperties
result = 0
if props.qto_methods == "HEIGHT":
for obj in selected_mesh_objects:
result += helper.calculate_height(obj)
elif props.qto_methods == "VOLUME":
result = helper.calculate_volumes(selected_mesh_objects, context)
elif props.qto_methods == "FORMWORK":
result = helper.calculate_formwork_area(selected_mesh_objects, context)
elif props.qto_methods == "SIDE_FORMWORK":
result = helper.calculate_side_formwork_area(selected_mesh_objects, context)
elif props.qto_methods == "NetFootprintArea":
result = QtoCalculator().get_net_footprint_area(selected_mesh_objects[0])
elif props.qto_methods == "NetRoofprintArea":
result = QtoCalculator().get_net_roofprint_area(selected_mesh_objects[0])
elif props.qto_methods == "LateralArea":
result = QtoCalculator().get_lateral_area(selected_mesh_objects[0])
elif props.qto_methods == "TotalSurfaceArea":
result = QtoCalculator().get_total_surface_area(selected_mesh_objects[0])
elif props.qto_methods == "OpeningArea":
result = QtoCalculator().get_opening_area(selected_mesh_objects[0])
elif props.qto_methods == "GrossTopArea":
result = QtoCalculator().get_gross_top_area(selected_mesh_objects[0])
elif props.qto_methods == "NetTopArea":
result = QtoCalculator().get_net_top_area(selected_mesh_objects[0])
elif props.qto_methods == "ProjectedArea":
result = QtoCalculator().get_projected_area(selected_mesh_objects[0])
elif props.qto_methods == "TotalContactArea":
result = QtoCalculator().get_total_contact_area(selected_mesh_objects[0])
elif props.qto_methods == "ContactArea":
result = QtoCalculator().get_contact_area(selected_mesh_objects[0], selected_mesh_objects[1])
props.qto_result = str(round(result, 3))
return {"FINISHED"}
class QuantifyObjects(bpy.types.Operator):
bl_idname = "bim.quantify_objects"
bl_label = "Quantify Objects"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return IfcStore.get_file() and context.selected_objects
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
props = context.scene.BIMQtoProperties
self.file = IfcStore.get_file()
for obj in (o for o in context.selected_objects if o.type == "MESH"):
if not obj.BIMObjectProperties.ifc_definition_id:
continue
result = 0
if props.qto_methods == "HEIGHT":
result = helper.calculate_height(obj)
elif props.qto_methods == "VOLUME":
result = helper.calculate_volumes([obj], context)
elif props.qto_methods == "FORMWORK":
result = helper.calculate_formwork_area([obj], context)
elif props.qto_methods == "SIDE_FORMWORK":
result = helper.calculate_side_formwork_area([obj], context)
if not result:
continue
result = round(result, 3)
qto = ifcopenshell.api.run(
"pset.add_qto",
self.file,
product=self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
name=props.qto_name,
)
ifcopenshell.api.run("pset.edit_qto", self.file, qto=qto, properties={props.prop_name: result})
return {"FINISHED"}
class AssignBaseQto(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_objects_base_qto"
bl_label = "Assign IFC Object Quantity Set"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Assign IFC quantity set to selected object"
bl_description = "Calculate a single quantity using a function on the selected objects"
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.selected_objects
def _execute(self, context):
core.assign_objects_base_qto(tool.Ifc, tool.Qto, selected_objects = context.selected_objects)
import ifc5d.qto
props = context.scene.BIMQtoProperties
elements = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element:
elements.add(element)
rules = {
"calculators": {
props.calculator: {
"IfcProduct": {props.qto_name: {props.prop_name: props.calculator_function}},
}
}
}
ifc_file = tool.Ifc.get()
results = ifc5d.qto.quantify(ifc_file, elements, rules)
ifc5d.qto.edit_qtos(ifc_file, results)
return {"FINISHED"}
class CalculateAllQuantities(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.calculate_all_quantities"
bl_label = "Calculate All Quantities"
class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.perform_quantity_take_off"
bl_label = "Perform Quantity Take-off"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Calculate all possible quantities and assign them to selected object"
bl_description = "Perform a quantity take off based of a QTO rule configuration"
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.selected_objects
return tool.Ifc.get()
def _execute(self, context):
core.calculate_objects_base_quantities(tool.Ifc, tool.Cost, tool.Qto, QtoCalculator(), selected_objects = context.selected_objects)
import ifc5d.qto
props = context.scene.BIMQtoProperties
if context.selected_objects:
elements = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element:
elements.add(element)
else:
elements = set(tool.Ifc.get().by_type("IfcElement"))
rules = ifc5d.qto.rules[props.qto_rule]
ifc_file = tool.Ifc.get()
results = ifc5d.qto.quantify(ifc_file, elements, rules)
ifc5d.qto.edit_qtos(ifc_file, results)
return {"FINISHED"}
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifc5d.qto
from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
@@ -31,34 +32,37 @@ from bpy.props import (
)
def get_qto_rule(self, context):
results = []
for rule_id, rule in ifc5d.qto.rules.items():
results.append((rule_id, rule["name"], rule["description"]))
return results
def get_calculator(self, context):
results = []
for name, calculator in ifc5d.qto.calculators.items():
results.append((name, name, calculator.__doc__))
return results
def get_calculator_function(self, context):
calculator = ifc5d.qto.calculators[self.calculator]
results = []
previous_measure = None
for function_id, function in calculator.functions.items():
measure = function.measure.split("Measure")[0][3:]
if previous_measure is not None and measure != previous_measure:
results.append(None)
results.append((function_id, f"{measure}: {function.name}", function.description))
previous_measure = measure
return results
class BIMQtoProperties(PropertyGroup):
qto_rule: EnumProperty(items=get_qto_rule, name="Qto Rule")
calculator: EnumProperty(items=get_calculator, name="Calculator")
calculator_function: EnumProperty(items=get_calculator_function, name="Calculator Function")
qto_result: StringProperty(default="", name="Qto Result")
qto_methods: EnumProperty(
items=[
("HEIGHT", "Height", "Calculate the Z height of an object"),
("VOLUME", "Volume", "Calculate the volume of an object"),
(
"FORMWORK",
"Formwork",
"Calculate the exposed formwork for all bottoms and sides (e.g. for beams and slabs) of one or more objects",
),
(
"SIDE_FORMWORK",
"Side Formwork",
"Calculate the exposed formwork for all sides only (e.g. for columns) of one or more objects",
),
("NetFootprintArea", "Net footprint area", "Calculate the net footprint area"),
("NetRoofprintArea", "Net roofprint area", "Calculate the net roofprint area"),
("LateralArea", "Lateral area", "Calculate the lateral area"),
("TotalSurfaceArea", "Total surface area", "Calculate the total surface area"),
("OpeningArea", "Opening area", "Calculate the opening area"),
("GrossTopArea", "Gross top area", "Calculate the gross top area"),
("NetTopArea", "Net top area", "Calculate the net top area"),
("ProjectedArea", "Projected area", "Calculate the projected area"),
("TotalContactArea", "Total contact area", "Get the total contact area"),
("ContactArea", "Contact area between two objects", "Get the contact area")
],
name="Qto Methods",
)
qto_name: StringProperty(name="Qto Name")
prop_name: StringProperty(name="Prop Name")
qto_name: StringProperty(name="Qto Name", default="My_Qto")
prop_name: StringProperty(name="Prop Name", default="MyDimension")
+92 -41
View File
@@ -20,18 +20,67 @@ import bpy
from blenderbim.bim.module.qto.data import QtoData
class BIM_PT_qto_utilities(bpy.types.Panel):
bl_idname = "BIM_PT_qto_utilities"
class BIM_PT_qto(bpy.types.Panel):
bl_idname = "BIM_PT_qto"
bl_label = "Quantity Take-off"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_qto"
bl_options = {"HIDE_HEADER"}
def draw(self, context):
if not QtoData.is_loaded:
QtoData.load()
layout = self.layout
props = context.scene.BIMQtoProperties
row = layout.row()
if context.selected_objects:
row.label(text=f"Quantifying {len(context.selected_objects)} Selected Objects", icon="MOD_EDGESPLIT")
else:
row.label(text="Quantifying All Objects", icon="MOD_EDGESPLIT")
row = layout.row()
row.prop(props, "qto_rule", text="")
row = layout.row()
row.operator("bim.perform_quantity_take_off")
class BIM_PT_qto_manual(bpy.types.Panel):
bl_idname = "BIM_PT_qto_manual"
bl_label = "Manual Quantification"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_qto"
def draw(self, context):
layout = self.layout
props = context.scene.BIMQtoProperties
row = layout.row()
row.prop(props, "calculator")
row = layout.row()
row.prop(props, "calculator_function", text="Function")
row = layout.row(align=True)
row.prop(props, "qto_name", text="")
row.prop(props, "prop_name", text="")
row = layout.row()
row.operator("bim.calculate_single_quantity")
class BIM_PT_qto_simple(bpy.types.Panel):
bl_idname = "BIM_PT_qto_simple"
bl_label = "Simple Quantity Calculator"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_qto"
def draw(self, context):
layout = self.layout
props = context.scene.BIMQtoProperties
@@ -47,42 +96,44 @@ class BIM_PT_qto_utilities(bpy.types.Panel):
row = layout.row(align=True)
row.operator("bim.calculate_object_volumes")
row = layout.row(align=True)
row.prop(props, "qto_methods", text="")
row.operator("bim.execute_qto_method", icon="PROPERTIES", text="")
row = layout.row(align=True)
row.prop(props, "qto_name", text="")
row.prop(props, "prop_name", text="")
row.operator("bim.quantify_objects", icon="COPYDOWN", text="")
class BIM_PT_qto_cost(bpy.types.Panel):
bl_idname = "BIM_PT_qto_cost"
bl_label = "Parametric Cost Relationships"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_qto"
row = layout.row(align=True)
row.operator("bim.assign_objects_base_qto")
def draw(self, context):
if not QtoData.is_loaded:
QtoData.load()
row = layout.row(align=True)
row.operator("bim.calculate_all_quantities", icon="MOD_EDGESPLIT")
if not context.selected_objects:
row = self.layout.row()
row.label(text="No Selected Object")
return
if context.selected_objects:
row = layout.row(align=True)
row.label(text=f"Relating Cost Item:")
if QtoData.data['has_cost_item']:
for relating_cost_item in QtoData.data['relating_cost_items']:
row.label(text=f"\n")
row = layout.row(align=True)
row.label(text=f"Cost item name:")
row.label(text=f"{relating_cost_item['cost_item_name']}")
row = layout.row(align=True)
row.label(text=f"Quantity name:")
row.label(text=f"{relating_cost_item['quantity_name']}")
row = layout.row(align=True)
row.label(text=f"Quantity value:")
row.label(text=f"{relating_cost_item['quantity_value']}")
row = layout.row(align=True)
row.label(text=f"Quantity type:")
row.label(text=f"{relating_cost_item['quantity_type']}")
row = layout.row(align=True)
else:
row = layout.row(align=True)
row.label(text = f"No cost item related")
if not QtoData.data["has_cost_item"]:
row = self.layout.row()
row.label(text="No Related Cost Item")
return
row = self.layout.row(align=True)
row.label(text="Relating Cost Item:")
for relating_cost_item in QtoData.data["relating_cost_items"]:
row.label(text="\n")
row = self.layout.row(align=True)
row.label(text="Cost item name:")
row.label(text=f"{relating_cost_item['cost_item_name']}")
row = self.layout.row(align=True)
row.label(text="Quantity name:")
row.label(text=f"{relating_cost_item['quantity_name']}")
row = self.layout.row(align=True)
row.label(text="Quantity value:")
row.label(text=f"{relating_cost_item['quantity_value']}")
row = self.layout.row(align=True)
row.label(text="Quantity type:")
row.label(text=f"{relating_cost_item['quantity_type']}")
row = self.layout.row(align=True)
@@ -37,13 +37,29 @@ from bpy.props import (
CollectionProperty,
)
quantitytypes_enum = []
quantitytypes_enum = {}
def purge():
global quantitytypes_enum
quantitytypes_enum = []
def setup_quantity_types_enum():
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcConstructionResource.htm#Table-7.3.3.7.1.3.H
resources = {
"IfcCrewResource": ("IfcQuantityTime",),
"IfcLaborResource": ("IfcQuantityTime",),
"IfcSubContractResource": ("IfcQuantityTime",),
"IfcConstructionEquipmentResource": ("IfcQuantityTime",),
"IfcConstructionMaterialResource": (
"IfcQuantityVolume",
"IfcQuantityArea",
"IfcQuantityLength",
"IfcQuantityWeight",
),
"IfcConstructionProductResource": ("IfcQuantityCount",),
}
for resource, quantities in resources.items():
quantitytypes_enum[resource] = [(q, q, "") for q in quantities]
setup_quantity_types_enum()
def updateResourceName(self, context):
@@ -63,15 +79,7 @@ def updateResourceName(self, context):
def get_quantity_types(self, context):
global quantitytypes_enum
if len(quantitytypes_enum) == 0 and IfcStore.get_schema():
quantitytypes_enum.extend(
[
(t.name(), t.name(), "")
for t in IfcStore.get_schema().declaration_by_name("IfcPhysicalSimpleQuantity").subtypes()
]
)
return quantitytypes_enum
return quantitytypes_enum[self.active_resource_class]
def update_active_resource_index(self, context):
@@ -88,9 +96,7 @@ def updateResourceUsage(self, context):
resource = tool.Ifc.get().by_id(self.ifc_definition_id)
if resource.Usage and resource.Usage.ScheduleUsage == self.schedule_usage:
return
tool.Resource.run_edit_resource_time(resource, attributes={
"ScheduleUsage": self.schedule_usage
})
tool.Resource.run_edit_resource_time(resource, attributes={"ScheduleUsage": self.schedule_usage})
tool.Sequence.load_task_properties()
tool.Resource.load_resource_properties()
tool.Sequence.refresh_task_resources()
@@ -127,12 +133,21 @@ class BIMResourceProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
active_resource_index: IntProperty(name="Active Resource Index", update=update_active_resource_index)
active_resource_id: IntProperty(name="Active Resource Id")
active_resource_class: StringProperty(name="Active Resource Type")
contracted_resources: StringProperty(name="Contracted Resources", default="[]")
is_resource_update_enabled: BoolProperty(name="Is Resource Update Enabled", default=True)
is_loaded: BoolProperty(name="Is Editing")
active_resource_time_id: IntProperty(name="Active Resource Usage Id")
resource_time_attributes: CollectionProperty(name="Resource Usage Attributes", type=Attribute)
editing_resource_type: StringProperty(name="Editing Resource Type")
editing_resource_type: EnumProperty(
name="Editing Resource Type",
items=(
("ATTRIBUTES", "", ""),
("USAGE", "", ""),
("COSTS", "", ""),
("QUANTITY", "", ""),
),
)
cost_types: EnumProperty(
items=[
("FIXED", "Fixed", "The cost value is a fixed number"),
@@ -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"
@@ -1409,9 +1409,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,6 +22,7 @@ import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.style as core
import ifcopenshell.api
import ifcopenshell.util.representation
from blenderbim.bim.module.style.prop import switch_shading
from pathlib import Path
-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")
+22 -5
View File
@@ -195,18 +195,19 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
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
name="Play 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)
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
)
@@ -283,6 +284,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
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")
@@ -535,6 +538,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"
+33 -5
View File
@@ -16,16 +16,30 @@
# 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
rel = ifc.run(
@@ -37,7 +51,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 +72,15 @@ 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)
+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))
+2 -9
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
+7 -4
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):
@@ -46,19 +47,21 @@ class Aggregate(blenderbim.core.tool.Aggregate):
return False
@classmethod
def disable_editing(cls, obj):
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
+22
View File
@@ -196,6 +196,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 +474,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(
+30 -16
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):
@@ -755,21 +755,35 @@ 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]
# 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 = dict()
for obj in objs:
mesh = obj.data
if not mesh:
continue
meshes_to_objects.setdefault(mesh, obj)
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):
+36 -34
View File
@@ -19,43 +19,45 @@
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 typing import Union, Any
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 import_material_definitions(cls, material_type: str) -> None:
props = bpy.context.scene.BIMMaterialProperties
expanded_categories = {m.name for m in props.materials if m.is_expanded}
props.materials.clear()
@@ -89,11 +91,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 +107,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 +158,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 +201,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 +211,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 +233,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 +253,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]
+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
+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)
+56 -2
View File
@@ -19,6 +19,7 @@
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.pset
import test.bim.bootstrap
import blenderbim.core.tool
import blenderbim.core.root
@@ -65,13 +66,26 @@ class TestGetApplicableBaseQuantityName(test.bim.bootstrap.NewFile):
wall = ifc.createIfcWall()
assert subject.get_applicable_base_quantity_name(wall) == "Qto_WallBaseQuantities"
def test_no_base_quantity(self):
def test_no_quantities(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
def test_anomaly_named_quantities(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
product = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBuildingElementProxy")
# Prioritized over Qto_BodyGeometryValidation.
assert subject.get_applicable_base_quantity_name(product) == "Qto_BuildingElementProxyQuantities"
def test_prioritize_base_over_other_qto(self):
ifc = ifcopenshell.file(schema="IFC4X3")
tool.Ifc.set(ifc)
product = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
assert subject.get_applicable_base_quantity_name(product) == "Qto_WallBaseQuantities"
class TestGetRoundedValue(test.bim.bootstrap.NewFile):
def test_run(self):
@@ -213,7 +227,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()
@@ -222,6 +236,46 @@ class TestGetBaseQto(test.bim.bootstrap.NewFile):
product = tool.Ifc.get_entity(wall_obj)
assert not subject.get_base_qto(product) == True
def test_anomaly_named_quantities(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
product = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBuildingElementProxy")
tool.Ifc.run(
"pset.add_qto",
product=product,
name="EQto_BodyGeometryValidation",
)
tool.Ifc.run(
"pset.add_qto",
product=product,
name="Qto_BuildingElementProxyQuantities",
)
# Prioritized over Qto_BodyGeometryValidation.
base_qto_name = subject.get_base_qto(product).Name
assert base_qto_name == "Qto_BuildingElementProxyQuantities"
# Ensure methods are in sync.
assert base_qto_name == subject.get_applicable_base_quantity_name(product)
def test_prioritize_base_over_other_qto(self):
ifc = ifcopenshell.file(schema="IFC4X3")
tool.Ifc.set(ifc)
product = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
tool.Ifc.run(
"pset.add_qto",
product=product,
name="Qto_BodyGeometryValidation",
)
tool.Ifc.run(
"pset.add_qto",
product=product,
name="Qto_WallBaseQuantities",
)
# Prioritized over Qto_BodyGeometryValidation.
base_qto_name = subject.get_base_qto(product).Name
assert base_qto_name == "Qto_WallBaseQuantities"
# Ensure methods are in sync.
assert base_qto_name == subject.get_applicable_base_quantity_name(product)
class TestGetRelatedCostItemQuantities(test.bim.bootstrap.NewFile):
def test_run(self):
+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": null,
"GrossVolume": null,
"GrossWeight": null,
"Length": null,
"NetSurfaceArea": null,
"NetVolume": null,
"NetWeight": null,
"OuterSurfaceArea": null
}
},
"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": null,
"GrossVolume": null,
"GrossWeight": null,
"Length": null,
"NetSurfaceArea": null,
"NetVolume": null,
"NetWeight": null,
"OuterSurfaceArea": null
}
},
"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": null,
"NetArea": null,
"Width": null
}
},
"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": null,
"GrossVolume": null,
"GrossWeight": null,
"Length": null,
"NetSurfaceArea": null,
"NetVolume": null,
"NetWeight": null,
"OuterSurfaceArea": null
}
},
"IfcMotorConnection": {
"Qto_MotorConnectionBaseQuantities": {
"GrossWeight": null
}
},
"IfcOpeningElement": {
"Qto_OpeningElementBaseQuantities": {
"Area": null,
"Depth": null,
"Height": null,
"Volume": null,
"Width": null
}
},
"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": null,
"GrossVolume": null,
"GrossWeight": null,
"NetArea": null,
"NetVolume": null,
"NetWeight": null,
"Perimeter": null,
"Width": null
}
},
"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": null,
"GrossArea": null,
"GrossVolume": null,
"GrossWeight": null,
"Length": null,
"NetArea": null,
"NetVolume": null,
"NetWeight": null,
"Perimeter": null,
"Width": null
}
},
"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": null,
"NetVolume": null
}
},
"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": null,
"GrossVolume": null,
"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": null,
"Height": null,
"Perimeter": null,
"Width": null
}
}
}
}
}
@@ -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"
}
}
}
}
}
+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(),
+271
View File
@@ -0,0 +1,271 @@
# 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": "CUBIE_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_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,
@@ -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
+18
View File
@@ -63,3 +63,21 @@ with this data structure that IfcFM supports:
installation. As expected, the majority of these are already referenced in
named standards. "Vanilla" IFC offers a "specification agnostic" approach
towards facility management data collection.
COBie 2.4 vs COBie 2.4 Legacy
-----------------------------
In collaboration with leading UK consultancy BuildData Group (formerly Bond
Bryan Digital), and inventor of COBie 2.4 Bill East (Prarie Sky Consulting), an
effort was made to preserve the original IFC mapping, as well as modernise the
mapping with the following goals:
1. For anyone delivering **COBie 2.4** data using best practices from graphical
BIM software, there must be *no difference* between **COBie 2.4** and
**COBie 2.4 Legacy**. It must not contradict any specifications in the
official NBIMS-US and BS standards.
2. Update compatibility to IFC4X3.
3. Prioritise organisational data instead of personal data with discourage PII.
4. Prioritise bSI standardised property sets over custom ones.
5. Prevent needless repetition of data / fallback defaults that may result in
invalid or unexpected data in obscure edge cases.
@@ -228,6 +228,7 @@ Here is a simple example in Python:
if iterator.initialize():
while True:
shape = iterator.get()
element = ifc_file.by_id(shape.id)
matrix = shape.transformation.matrix.data
faces = shape.geometry.faces
edges = shape.geometry.edges
@@ -235,6 +235,6 @@ nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce
"``title({{value}})``", "``title(""foo"")``", "``Foo``", "Titlecases a string."
"``concat({{value}}[, {{value2}}]*)``", "``concat(""foo"", ""bar"")``", "``foobar``", "Concatenates two or more strings."
"``round({{value}}, {{precision}})``", "``round(3.123, 0.1)``", "``3.1``", "Rounds ``{{value}}`` to the nearest ``{{precision}}``."
"``number({{value}}[, {{decimal_separator}}[, {{thousands_separator}}]])``", "``number(1234.56, "","", ""."")``", "1.234,56", "Formats {{value}} with an optional custom {{decimal_separator}} and {{thousands_separator}}. The default separators are ``.`` and ``,``."
"``number({{value}}[, {{decimal_separator}}[, {{thousands_separator}}]])``", "``number(1234.56, "","", ""."")``", "``1.234,56``", "Formats {{value}} with an optional custom {{decimal_separator}} and {{thousands_separator}}. The default separators are ``.`` and ``,``."
"``metric_length({{value}}, {{precision}}, {{decimals}})``", "``metric_length(3.123, 0.1, 2)``", "``3.10``", "Rounds ``{{value}}`` to the nearest ``{{precision}}`` then displays using a certain amount of decimal places."
"``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}})``", "``imperial_length(3.22, 4, ""foot"")``", "``3' - 3 3/4""``", "``The {{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is then rounded to the nearest ``1/{{precision}}`` inch then formatted using fractional feet and inches if ``{{output_unit}}`` is set to ``foot`` or just inches if ``{{output_unit}}`` is set to ``inch``."
@@ -19,7 +19,7 @@
import datetime
from re import findall
from dateutil import parser
from typing import Literal, Union, Any
from typing import Literal, Union, Any, overload
try:
import isodate
@@ -105,8 +105,11 @@ def readable_ifc_duration(string):
return final_string
@overload
def datetime2ifc(dt: None, ifc_type: Any) -> None: ...
@overload
def datetime2ifc(
dt: Union[datetime.date, str],
dt: Union[datetime.date, str, None],
ifc_type: Literal[
"IfcDuration",
"IfcTimeStamp",
@@ -116,7 +119,19 @@ def datetime2ifc(
"IfcCalendarDate",
"IfcLocalTime",
],
) -> Union[int, str, dict[str, Any]]:
) -> Union[int, str, dict[str, Any], None]: ...
def datetime2ifc(
dt: Union[datetime.date, str, None],
ifc_type: Literal[
"IfcDuration",
"IfcTimeStamp",
"IfcDateTime",
"IfcDate",
"IfcTime",
"IfcCalendarDate",
"IfcLocalTime",
],
) -> Union[int, str, dict[str, Any], None]:
if isinstance(dt, str):
if ifc_type == "IfcDuration":
return dt
@@ -124,6 +139,8 @@ def datetime2ifc(
dt = datetime.datetime.fromisoformat(dt)
except:
dt = datetime.time.fromisoformat(dt)
elif dt is None:
return
if ifc_type == "IfcDuration":
return isodate.duration_isoformat(dt)
@@ -157,7 +174,8 @@ def datetime2ifc(
"MinuteComponent": dt.minute,
"SecondComponent": dt.second,
}
raise TypeError(f"Unsupported ifc_type for conversion from datetime.datetime = {ifc_type}.")
raise TypeError(f"Unsupported ifc_type for conversion from datetime.datetime = {ifc_type}, value = {dt}")
def string_to_date(string):
@@ -833,7 +833,7 @@ def get_layers(
def get_container(
element: ifcopenshell.entity_instance, should_get_direct: bool = False, ifc_class: Optional[str] = None
) -> ifcopenshell.entity_instance:
) -> Union[ifcopenshell.entity_instance, None]:
"""
Retrieves the spatial structure container of an element.
@@ -849,7 +849,7 @@ def get_container(
example, you may be after the storey, not a space.
:type ifc_class: str, optional
:return: The direct or indirect container of the element or None.
:rtype: ifcopenshell.entity_instance
:rtype: Union[ifcopenshell.entity_instance, None]
Example:
@@ -18,6 +18,7 @@
RUN_FROM_DEV_REPO = False
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.attribute
@@ -33,9 +34,9 @@ if not RUN_FROM_DEV_REPO:
import shutil
BASE_MODULE_PATH = Path(__file__).parent
IFC4x3_HTML_LOCATION = BASE_MODULE_PATH / "IFC4.3-html-iso-release"
if not RUN_FROM_DEV_REPO:
IFC4x3_HTML_ZIP_LOCATION = BASE_MODULE_PATH / "annex-a-psd.zip"
IFC4x3_OUTPUT_PATH = BASE_MODULE_PATH / "schema/Pset_IFC4X3.ifc"
else:
IFC4x3_PSD_LOCATION = BASE_MODULE_PATH / "../output/psd"
@@ -67,20 +68,18 @@ class PsetTemplatesGenerator:
print("Starting parsing data for IFC4X3...")
if not RUN_FROM_DEV_REPO:
if not IFC4x3_HTML_LOCATION.is_dir():
if not IFC4x3_HTML_ZIP_LOCATION.is_file():
raise Exception(
f'ISO release for Ifc4.3.0.1 expected to be in folder "{IFC4x3_HTML_LOCATION.resolve()}\\"\n'
f'ISO release for Ifc4.3.2.0 expected to be located in "{IFC4x3_HTML_ZIP_LOCATION.resolve()}"\n'
"For generating ifc pset library please either setup docs as described above \n"
"or change IFC4x3_HTML_LOCATION in the script accordingly.\n"
"or change IFC4x3_HTML_ZIP_LOCATION in the script accordingly.\n"
"You can download docs from the repository: \n"
"https://github.com/buildingSMART/IFC4.3-html/releases/tag/sep-13-release"
"https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/HTML/annex-a-psd.zip"
)
# unzip the data
if not RUN_FROM_DEV_REPO:
pset_data_zip = IFC4x3_HTML_LOCATION / "IFC/RELEASE/IFC4x3/HTML/annex-a-psd.zip"
pset_data_location = BASE_MODULE_PATH / "temp/annex-a-psd"
with zipfile.ZipFile(pset_data_zip, "r") as fi_zip:
fi_zip.extractall(pset_data_location)
pset_data_location = BASE_MODULE_PATH / "temp/annex-a-psd"
with zipfile.ZipFile(IFC4x3_HTML_ZIP_LOCATION, "r") as fi_zip:
fi_zip.extractall(pset_data_location)
else:
if not IFC4x3_PSD_LOCATION.is_dir():
raise Exception(
@@ -100,7 +99,7 @@ class PsetTemplatesGenerator:
print("Starting parsing data for IFC2X3...")
if not IFC2x3_HTML_ZIP_LOCATION.is_file():
raise Exception(
f'ISO release for IFC2x3 TC1 expected to be in folder "{IFC2x3_HTML_ZIP_LOCATION.resolve()}\\"\n'
f'ISO release for IFC2x3 TC1 expected to be located in "{IFC2x3_HTML_ZIP_LOCATION.resolve()}"\n'
"For doc extraction please either setup docs as described above \n"
"or change IFC2x3_HTML_LOCATION in the script accordingly.\n"
"You can download docs from the url: \n"
@@ -176,8 +175,12 @@ class PsetTemplatesGenerator:
TemplateType=root_xml.get("templatetype"),
Name=pset_name,
Description=root_xml.find("Definition").text,
ApplicableEntity=",".join(applicable_entities),
ApplicableEntity=",".join(applicable_entities).strip(),
)
if project_name.startswith("IFC2X3"):
pset.TemplateType = self.get_pset_template_type_ifc2x3(pset)
else:
pset.TemplateType = root_xml.get("templatetype")
# NOTE: there is also Applicability tag
# but it's seems always empty in ifc4 and ifc4x3
@@ -369,6 +372,43 @@ class PsetTemplatesGenerator:
pset_property.PrimaryMeasureType = primary_measure_type
def get_pset_template_type_ifc2x3(self, pset_template: ifcopenshell.entity_instance) -> str:
def declaration_is_a(declaration: ifcopenshell_wrapper.declaration, ifc_class: str) -> bool:
if declaration.name() == ifc_class:
return True
super_type = declaration.supertype()
if not super_type:
return False
return declaration_is_a(super_type, ifc_class)
name = pset_template.Name
applicability = pset_template.ApplicableEntity
schema = ifcopenshell.schema_by_name("IFC2X3")
if "PHistory" in name:
return "PSET_PERFORMANCEDRIVEN"
applicable_types = applicability.replace(", ", ",").split(",")
for applicable_type in applicable_types:
if not applicable_type:
continue
parts = applicable_type.split("/")
assert 3 > len(parts) > 0
if parts[0].isupper(): # IFC2X3 thing
applicable_type = parts[1]
else:
applicable_type = parts[0]
applicable_type = applicable_type.strip()
declaration = schema.declaration_by_name(applicable_type)
if declaration_is_a(declaration, "IfcTypeObject"):
return "PSET_TYPEDRIVENOVERRIDE"
# ifc4x3+
elif declaration_is_a(declaration, "IfcProfileDef"):
return "PSET_PROFILEDRIVEN"
elif declaration_is_a(declaration, "IfcMaterialDefinition"):
return "PSET_MATERIALDRIVEN"
return "PSET_OCCURRENCEDRIVEN"
if __name__ == "__main__":
templates_generator = PsetTemplatesGenerator()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -291,9 +291,10 @@ def filter_elements(
:type ifc_file: ifcopenshell.file
:param query: Query to execute
:type query: str
:param elements: Base set of IFC elements for the query.
If provided, new elements found for the current query will be added to `elements`.
Elements explicitly excluded in the `query` will also be excluded from `elements`
:param elements: Base set of IFC elements for the query. If not provided,
all elements in the IFC are queried. If provided, the query will be
applied to this set of elements, so the result will be a subset of
elements.
:type elements: set[ifcopenshell.entity_instance], optional
:param edit_in_place: If `True`, mutate the provided `elements` in place. Defaults to `False`
:type edit_in_place: bool
@@ -497,11 +498,17 @@ def set_element_value(
f"Failed to set value for element '{original_element}' with query '{query}' (invalid or unsupported query)."
)
class FacetTransformer(lark.Transformer):
def __init__(self, ifc_file: ifcopenshell.file, elements: Optional[set[ifcopenshell.entity_instance]] = None):
self.file = ifc_file
self.results = []
self.elements = set() if elements is None else elements
if elements is None:
self.base_elements = None
self.elements = set()
else:
self.base_elements = elements.copy()
self.elements = set()
self.container_parents = {}
self.container_trees = {}
@@ -517,28 +524,44 @@ class FacetTransformer(lark.Transformer):
self.elements = set()
def instance(self, args):
if args[0].data == "globalid":
try:
self.elements.add(self.file.by_guid(args[0].children[0].value))
except:
pass
if self.base_elements is None:
if args[0].data == "globalid":
try:
self.elements.add(self.file.by_guid(args[0].children[0].value))
except:
pass
else:
try:
self.elements.remove(self.file.by_guid(args[1].children[0].value))
except:
pass
else:
try:
self.elements.remove(self.file.by_guid(args[1].children[0].value))
except:
pass
if args[0].data == "globalid":
self.elements |= {
e for e in self.base_elements if getattr(e, "GlobalId", None) == args[0].children[0].value
}
else:
self.elements -= {
e for e in self.base_elements if getattr(e, "GlobalId", None) == args[1].children[0].value
}
def entity(self, args):
if args[0].data == "ifc_class":
try:
self.elements |= set(self.file.by_type(args[0].children[0].value))
except:
pass
if self.base_elements is None:
if args[0].data == "ifc_class":
try:
self.elements |= set(self.file.by_type(args[0].children[0].value))
except:
pass
else:
try:
self.elements -= set(self.file.by_type(args[1].children[0].value))
except:
pass
else:
try:
self.elements -= set(self.file.by_type(args[1].children[0].value))
except:
pass
if args[0].data == "ifc_class":
self.elements |= {e for e in self.base_elements if e.is_a(args[0].children[0].value)}
else:
self.elements -= {e for e in self.base_elements if e.is_a(args[1].children[0].value)}
def attribute(self, args):
name, comparison, value = args
@@ -117,6 +117,28 @@ def get_z(geometry) -> float:
return max(z_values) - min(z_values)
def get_max_xyz(geometry) -> float:
"""Gets the maximum X, Y, or Z length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell
:type geometry: geometry
:return: The maximum possible value out of the X, Y, and Z dimension
:rtype: float
"""
return max(get_x(geometry), get_y(geometry), get_z(geometry))
def get_min_xyz(geometry) -> float:
"""Gets the minimum X, Y, or Z length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell
:type geometry: geometry
:return: The minimum possible value out of the X, Y, and Z dimension
:rtype: float
"""
return min(get_x(geometry), get_y(geometry), get_z(geometry))
def get_shape_matrix(shape) -> MatrixType:
"""Formats the transformation matrix of a shape as a 4x4 numpy array
@@ -486,6 +508,19 @@ def get_side_area(
return get_area_vf(vertices, filtered_faces)
def get_max_side_area(geometry) -> float:
"""Returns the maximum X, Y, or Z side area
See :func:`get_side_area` for how side area is calculated.
:param geometry: Geometry output calculated by IfcOpenShell
:type geometry: geometry
:return: The maximum surface area from either the X, Y, or Z axis.
:rtype: float
"""
return max(get_side_area(geometry, axis="X"), get_side_area(geometry, axis="Y"), get_side_area(geometry, axis="Z"))
def get_footprint_area(
geometry,
axis: AXIS_LITERAL = "Z",
@@ -421,8 +421,8 @@ def get_project_unit(ifc_file: ifcopenshell.file, unit_type: str) -> Union[ifcop
:param unit_type: The type of unit, taken from the list of IFC unit types,
such as "LENGTHUNIT".
:type unit_type: str
:return: The IFC unit entity, or nothing if there is no default project unit
defined.
:return: The IFC unit entity, or nothing if there is no default project
unit defined.
:rtype: Union[ifcopenshell.entity_instance, None]
"""
unit_assignment = get_unit_assignment(ifc_file)
@@ -435,6 +435,20 @@ def get_project_unit(ifc_file: ifcopenshell.file, unit_type: str) -> Union[ifcop
def get_property_unit(
prop: ifcopenshell.entity_instance, ifc_file: ifcopenshell.file
) -> Union[ifcopenshell.entity_instance, None]:
"""Gets the unit definition of a property or quantity
Properties and quantities in psets and qtos can be associated with a unit.
This unit may be defined at the property itself explicitly, or if not
specified, fallback to the project default.
:param prop: The property instance. You can fetch this via the instance ID
if doing :func:`ifcopenshell.util.element.get_psets` with
``verbose=True``.
:param ifc_file: The IFC file being used. This is necessary to check
default project units.
:return: The IFC unit entity, or nothing if there is no default project
unit defined.
"""
unit = getattr(prop, "Unit", None)
if unit:
return unit
@@ -488,6 +502,16 @@ def get_property_unit(
def get_unit_measure_class(unit_type: str) -> MEASURE_CLASS:
"""Get the IFC measure class for a unit type.
IFC has specific classes used to measure different units. An example of an
IFC measure class is ``IfcLengthMeasure``. An example of the correlating
unit type (i.e. the IfcUnitEnum) is ``LENGTHUNIT``.
The inverse function of this is :func:`get_measure_unit_type`
:param unit_type: A string chosen from IfcUnitEnum, such as LENGTHUNIT
"""
if unit_type == "USERDEFINED":
# See https://github.com/buildingSMART/IFC4.3.x-development/issues/71
return "IfcNumericMeasure"
@@ -495,6 +519,20 @@ def get_unit_measure_class(unit_type: str) -> MEASURE_CLASS:
def get_measure_unit_type(measure_class: MEASURE_CLASS) -> str:
"""Get the unit type of an IFC measure class
IFC has different unit types which can be associated with units (e.g. SI
units, imperial units, derived units, etc). An example of a unit type (i.e.
an IfcUnitEnum) is ``LENGTHUNIT``. An example of the correlating measure
class used to store length data is ``IfcLengthMeasure``.
The inverse fucntion of this is :func:`get_unit_measure_class`
:param measure_class: The measure class, such as ``IfcLengthMeasure``. If
you have an ``IfcPropertySingleValue``, you can get this using
``prop.NominalValue.is_a()``.
:return: The unit type, as an uppercase value of IfcUnitEnum.
"""
if measure_class == "IfcNumericMeasure":
# See https://github.com/buildingSMART/IFC4.3.x-development/issues/71
return "USERDEFINED"
@@ -25,41 +25,41 @@ import ifcopenshell.util.pset
import numpy as np
class TestFormat():
class TestFormat:
def test_no_formatting(self):
assert subject.format("123") == "123"
assert subject.format('\"123\"') == "123"
assert subject.format('\"foo\"') == "foo"
assert subject.format('"123"') == "123"
assert subject.format('"foo"') == "foo"
def test_string_formatting(self):
assert subject.format('upper(\"fOo\")') == "FOO"
assert subject.format('lower(\"fOo\")') == "foo"
assert subject.format('title(\"fOo\")') == "Foo"
assert subject.format('concat(\"fOo\", \"bar\")') == "fOobar"
assert subject.format('upper(concat(\"fOo\", \"bar\"))') == "FOOBAR"
assert subject.format('substr(\"foobar\", 3)') == "bar"
assert subject.format('substr(\"foobar\", 1, 2)') == "o"
assert subject.format('substr(\"foobar\", 1, -1)') == "ooba"
assert subject.format('upper("fOo")') == "FOO"
assert subject.format('lower("fOo")') == "foo"
assert subject.format('title("fOo")') == "Foo"
assert subject.format('concat("fOo", "bar")') == "fOobar"
assert subject.format('upper(concat("fOo", "bar"))') == "FOOBAR"
assert subject.format('substr("foobar", 3)') == "bar"
assert subject.format('substr("foobar", 1, 2)') == "o"
assert subject.format('substr("foobar", 1, -1)') == "ooba"
def test_number_formatting(self):
assert subject.format("round(123, 5)") == "125"
assert subject.format('round(\"123\", 5)') == "125"
assert subject.format('number(123)') == "123"
assert subject.format('number(1234.56)') == "1,234.56"
assert subject.format('round("123", 5)') == "125"
assert subject.format("number(123)") == "123"
assert subject.format("number(1234.56)") == "1,234.56"
assert subject.format('number(123, ".")') == "123"
assert subject.format('number(\"123\", ".")') == "123"
assert subject.format('number("123", ".")') == "123"
assert subject.format('number(123.12, ".")') == "123.12"
assert subject.format('number(123.12, ",")') == "123,12"
assert subject.format('number(1234.12, ",", ".")') == "1.234,12"
assert subject.format('metric_length(123, 5, 2)') == "125.00"
assert subject.format('metric_length(123.123, 0.1, 2)') == "123.10"
assert subject.format('metric_length(\"123\", 5, 2)') == "125.00"
assert subject.format('imperial_length(1, 1)') == "1'"
assert subject.format('imperial_length(3.123, 1)') == "3' - 1\""
assert subject.format('imperial_length(3.123, 2)') == "3' - 1 1/2\""
assert subject.format('imperial_length(\"3.123\", 2)') == "3' - 1 1/2\""
assert subject.format('imperial_length(\"123.123\", 2, \"inch\", \"foot\")') == "10' - 3\""
assert subject.format('imperial_length(\"123.123\", 2, \"inch\", \"inch\")') == "123\""
assert subject.format("metric_length(123, 5, 2)") == "125.00"
assert subject.format("metric_length(123.123, 0.1, 2)") == "123.10"
assert subject.format('metric_length("123", 5, 2)') == "125.00"
assert subject.format("imperial_length(1, 1)") == "1'"
assert subject.format("imperial_length(3.123, 1)") == "3' - 1\""
assert subject.format("imperial_length(3.123, 2)") == "3' - 1 1/2\""
assert subject.format('imperial_length("3.123", 2)') == "3' - 1 1/2\""
assert subject.format('imperial_length("123.123", 2, "inch", "foot")') == "10' - 3\""
assert subject.format('imperial_length("123.123", 2, "inch", "inch")') == '123"'
class TestGetElementValue(test.bootstrap.IFC4):
@@ -269,17 +269,19 @@ class TestFilterElements(test.bootstrap.IFC4):
def test_using_elements_argument(self):
wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
slab = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab")
door = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcDoor")
elements = {wall, slab}
results = subject.filter_elements(self.file, "IfcWall", {wall, slab})
assert results != elements
assert results == {wall}
elements = {wall, slab, door}
assert subject.filter_elements(self.file, "IfcWall, IfcSlab", elements) == {wall, slab}
# keep elements unaffected by expression
assert subject.filter_elements(self.file, "IfcWall", {slab}) == {wall, slab}
# filter out excluded elements
assert subject.filter_elements(self.file, "IfcWall, ! IfcSlab", {slab}) == {wall}
# edit_in_place to update original set
original_set = set()
def test_editing_in_place(self):
wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
original_set = {wall}
new_set = subject.filter_elements(self.file, "IfcWall", original_set, edit_in_place=True)
assert new_set == original_set
assert new_set == original_set == {wall}
class TestSetElementValue(test.bootstrap.IFC4):
@@ -46,10 +46,10 @@ class Patcher:
.. code:: python
# Convert to millimeters
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["MILLIMETER"]})
model = ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["MILLIMETER"]})
# Convert to feet
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FOOT"]})
model = ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FOOT"]})
"""
self.src = src
self.file = file