Merge remote-tracking branch 'upstream/v0.7.0' into pset_custom_units_test

This commit is contained in:
Carlos Villagrasa
2023-03-19 22:33:01 +01:00
13 changed files with 266 additions and 16 deletions
@@ -133,9 +133,9 @@ mapper = {
'NetWeight' : None, 'NetWeight' : None,
}, },
'Qto_CoveringBaseQuantities' : { 'Qto_CoveringBaseQuantities' : {
'Width' : "get_height", 'Width' : "get_covering_width",
'GrossArea' : "get_gross_footprint_area", 'GrossArea' : "get_covering_gross_area",
'NetArea' : "get_net_footprint_area", 'NetArea' : "get_covering_net_area",
}, },
'Qto_PipeSegmentBaseQuantities' : { 'Qto_PipeSegmentBaseQuantities' : {
'Length' : "get_length", 'Length' : "get_length",
@@ -136,6 +136,48 @@ class QtoCalculator:
OBB_gross_footprint_area = self.get_gross_footprint_area(OBB_obj) OBB_gross_footprint_area = self.get_gross_footprint_area(OBB_obj)
return OBB_gross_footprint_area return OBB_gross_footprint_area
def get_parametric_axis(self, obj):
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(obj))
if relating_type:
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
if parametric:
layer_set_direction = None
layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction)
if layer_set_direction == "AXIS2":
return "AXIS2"
elif layer_set_direction == "AXIS3":
return "AXIS3"
else:
return None
return None
def get_covering_gross_area(self, obj):
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_gross_footprint_area(obj)
elif get_parametric_axis == "AXIS2":
return self.get_gross_side_area(obj)
elif get_parametric_axis == "AXIS3":
return self.get_gross_footprint_area(obj)
def get_covering_net_area(self, obj):
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_net_footprint_area(obj)
elif get_parametric_axis == "AXIS2":
return self.get_net_side_area(obj)
elif get_parametric_axis == "AXIS3":
return self.get_net_footprint_area(obj)
def get_covering_width(self, obj):
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_height(obj)
elif get_parametric_axis == "AXIS2":
return self.get_width(obj)
elif get_parametric_axis == "AXIS3":
return self.get_height(obj)
def get_width(self, o): def get_width(self, o):
"""_summary_: Returns the width of the object bounding box """_summary_: Returns the width of the object bounding box
@@ -0,0 +1,75 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.tool as tool
def refresh():
QtoData.is_loaded = False
class QtoData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {
"has_cost_item" : cls.has_cost_item(),
"relating_cost_items" : cls.relating_cost_items(),
}
cls.is_loaded = True
@classmethod
def has_cost_item(cls):
return False if not cls.relating_cost_items() else True
@classmethod
def relating_cost_items(cls):
obj = bpy.context.active_object
if not obj:
return
element = tool.Ifc.get_entity(obj)
if not element:
return []
results = []
relating_cost_items = tool.Qto.get_related_cost_item_quantities(element)
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'],
}
)
return results
+31 -2
View File
@@ -16,10 +16,11 @@
# You should have received a copy of the GNU General Public License # 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/>. # along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from bpy.types import Panel import bpy
from blenderbim.bim.module.qto.data import QtoData
class BIM_PT_qto_utilities(Panel): class BIM_PT_qto_utilities(bpy.types.Panel):
bl_idname = "BIM_PT_qto_utilities" bl_idname = "BIM_PT_qto_utilities"
bl_label = "Quantity Take-off" bl_label = "Quantity Take-off"
bl_options = {"DEFAULT_CLOSED"} bl_options = {"DEFAULT_CLOSED"}
@@ -28,6 +29,9 @@ class BIM_PT_qto_utilities(Panel):
bl_category = "BlenderBIM" bl_category = "BlenderBIM"
def draw(self, context): def draw(self, context):
if not QtoData.is_loaded:
QtoData.load()
layout = self.layout layout = self.layout
props = context.scene.BIMQtoProperties props = context.scene.BIMQtoProperties
@@ -57,3 +61,28 @@ class BIM_PT_qto_utilities(Panel):
row = layout.row(align=True) row = layout.row(align=True)
row.operator("bim.calculate_all_quantities", icon="MOD_EDGESPLIT") row.operator("bim.calculate_all_quantities", icon="MOD_EDGESPLIT")
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")
+1
View File
@@ -518,6 +518,7 @@ class Qto:
def add_product_base_qto(cls, product): pass def add_product_base_qto(cls, product): pass
def get_new_calculated_quantity(cls, qto_name, quantity_name, object): 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_new_guessed_quantity(cls, object, qto_name, quantity_name, ): pass
def get_related_cost_item_quantities(cls, product): pass
@interface @interface
+51
View File
@@ -127,3 +127,54 @@ class Qto(blenderbim.core.tool.Qto):
): ):
continue continue
return rel.RelatingPropertyDefinition return rel.RelatingPropertyDefinition
@classmethod
def get_related_cost_item_quantities(cls, product):
"""_summary_: Returns the related cost item and related quantities of the product
:param ifc-instance product: ifc instance
:type product: ifcopenshell.entity_instance.entity_instance
:return list of dictionaries in the form [
{
"cost_item_id" : XX,
"cost_item_name" : XX,
"quantity_id" : XX,
"quantity_name" : XX,
"quantity_value" : XX,
"quantity_type" : XX
}]
:rtype list
Example:
.. code::Python
import blenderbim.tool as tool
relating_cost_items = tool.Qto.relating_cost_items(my_beautiful_wall)
for relating_cost_item in relating_cost_items:
print(f"RELATING COST ITEM NAME: {relating_cost_item["cost_item_name"]}")
print(f"RELATING COST QUANTITY NAME: {relating_cost_item["quantity_name"]}")
...
"""
quantities = cls.get_base_qto(product).Quantities
model = tool.Ifc.get()
cost_items = model.by_type("IfcCostItem")
result = []
for cost_item in cost_items:
cost_item_quantities = cost_item.CostQuantities if cost_item.CostQuantities is not None else []
for cost_item_quantity in cost_item_quantities:
for quantity in quantities:
if quantity == cost_item_quantity:
result.append(
{
"cost_item_id" : cost_item.id(),
"cost_item_name" : cost_item.Name,
"quantity_id" : quantity.id(),
"quantity_name" : quantity.Name,
"quantity_value" : quantity[3],
"quantity_type" : quantity.is_a(),
}
)
return result
+16
View File
@@ -142,3 +142,19 @@ class TestGetBaseQto(test.bim.bootstrap.NewFile):
product = tool.Ifc.get_entity(wall_obj) product = tool.Ifc.get_entity(wall_obj)
assert not subject.get_base_qto(product) == True assert not subject.get_base_qto(product) == True
class TestGetRelatedCostItemQuantities(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall = ifc.createIfcWall()
product = tool.Ifc.get_entity(wall)
schedule = ifcopenshell.api.run("cost.add_cost_schedule", ifc)
item = ifcopenshell.api.run("cost.add_cost_item", ifc, cost_schedule=schedule)
ifcopenshell.api.run("cost.edit_cost_item", ifc, cost_item=item, attributes={"Name": "Foo"})
qto = ifcopenshell.api.run("pset.add_qto", ifc, product=wall, name="Qto_WallBaseQuantities")
ifcopenshell.api.run("pset.edit_qto", ifc, qto=qto, properties={"NetVolume": 42.0})
ifcopenshell.api.run("cost.assign_cost_item_quantity", ifc, cost_item=item, products=[wall], prop_name="NetVolume")
assert subject.get_related_cost_item_quantities(wall)[0]['cost_item_name'] == "Foo"
assert subject.get_related_cost_item_quantities(wall)[0]['quantity_name'] == "NetVolume"
assert subject.get_related_cost_item_quantities(wall)[0]['quantity_value'] == 42
assert subject.get_related_cost_item_quantities(wall)[0]['quantity_type'] == "IfcQuantityVolume"
@@ -808,7 +808,7 @@ def copy(ifc_file, element):
return new return new
def copy_deep(ifc_file, element, exclude=None, exclude_callback=None): def copy_deep(ifc_file, element, exclude=None, exclude_callback=None, copied_entities=None):
""" """
Recursively copy an element and all of its directly related subelements. Recursively copy an element and all of its directly related subelements.
@@ -825,10 +825,22 @@ def copy_deep(ifc_file, element, exclude=None, exclude_callback=None):
:param exclude_callback: A callback to determine whether or not to exclude :param exclude_callback: A callback to determine whether or not to exclude
an entity or not. Returns True to exclude and False to exclude. an entity or not. Returns True to exclude and False to exclude.
:type exclude_callback: function,optional :type exclude_callback: function,optional
:param copied_entities: A dictionary of IDs as keys and entities as values
to reuse when coming across the same entity twice. This can typically
be left as None.
:type copied_entities: dict[int:ifcopenshell.entity_instance.entity_instance]
:return: The newly copied element :return: The newly copied element
:rtype: ifcopenshell.entity_instance.entity_instance :rtype: ifcopenshell.entity_instance.entity_instance
""" """
if copied_entities is None:
copied_entities = {}
else:
copied_entity = copied_entities.get(element.id(), None)
if copied_entity:
return copied_entity
new = ifc_file.create_entity(element.is_a()) new = ifc_file.create_entity(element.is_a())
if element.id():
copied_entities[element.id()] = new
for i, attribute in enumerate(element): for i, attribute in enumerate(element):
if attribute is None: if attribute is None:
continue continue
@@ -838,7 +850,7 @@ def copy_deep(ifc_file, element, exclude=None, exclude_callback=None):
elif exclude_callback and exclude_callback(attribute): elif exclude_callback and exclude_callback(attribute):
pass pass
else: else:
attribute = copy_deep(ifc_file, attribute, exclude=exclude) attribute = copy_deep(ifc_file, attribute, exclude=exclude, copied_entities=copied_entities)
elif isinstance(attribute, tuple) and attribute and isinstance(attribute[0], ifcopenshell.entity_instance): elif isinstance(attribute, tuple) and attribute and isinstance(attribute[0], ifcopenshell.entity_instance):
if exclude and any([attribute[0].is_a(e) for e in exclude]): if exclude and any([attribute[0].is_a(e) for e in exclude]):
pass pass
@@ -847,7 +859,13 @@ def copy_deep(ifc_file, element, exclude=None, exclude_callback=None):
else: else:
attribute = list(attribute) attribute = list(attribute)
for j, item in enumerate(attribute): for j, item in enumerate(attribute):
attribute[j] = copy_deep(ifc_file, item, exclude=exclude, exclude_callback=exclude_callback) attribute[j] = copy_deep(
ifc_file,
item,
exclude=exclude,
exclude_callback=exclude_callback,
copied_entities=copied_entities,
)
if new.attribute_name(i) == "GlobalId": if new.attribute_name(i) == "GlobalId":
new[i] = ifcopenshell.guid.new() new[i] = ifcopenshell.guid.new()
else: else:
@@ -125,7 +125,7 @@ def assert_valid_inverse(attr, val, schema):
aggr = attr.type_of_aggregation_string().upper() aggr = attr.type_of_aggregation_string().upper()
if aggr: if aggr:
aggr_str = f'{aggr} [{b1}:{b2}] OF ' aggr_str = f'{aggr} [{b1}:{"?" if b2 == -1 else b2}] OF '
else: else:
aggr_str = '' aggr_str = ''
@@ -707,3 +707,19 @@ class TestCopyDeepIFC4(test.bootstrap.IFC4):
rel.RelatedObjects = [element] rel.RelatedObjects = [element]
rel2 = subject.copy_deep(self.file, rel, exclude_callback=lambda x: x.is_a("IfcWall")) rel2 = subject.copy_deep(self.file, rel, exclude_callback=lambda x: x.is_a("IfcWall"))
assert rel.RelatedObjects == rel2.RelatedObjects assert rel.RelatedObjects == rel2.RelatedObjects
def test_copying_and_reusing_element_references(self):
points = self.file.createIfcCartesianPointList2D()
subelement1 = self.file.createIfcIndexedPolyCurve(points)
subelement2 = self.file.createIfcIndexedPolyCurve(points)
element = self.file.createIfcGeometricCurveSet([subelement1, subelement2])
element2 = subject.copy_deep(self.file, element)
assert element2.Elements[0].Points.id() == element2.Elements[1].Points.id()
def test_copying_primitive_entities(self):
element = self.file.createIfcIndexedPolyCurve(
Segments=(self.file.createIfcLineIndex((1, 2)), self.file.createIfcLineIndex((3, 4)))
)
element2 = subject.copy_deep(self.file, element)
assert element2.Segments[0][0] == (1, 2)
assert element2.Segments[1][0] == (3, 4)
+2 -2
View File
@@ -29,10 +29,10 @@ bl_info = {
} }
import importlib import importlib
from sverchok.utils.logging import info, debug import logging
logger = logging.getLogger('sverchok.ifc')
from sverchok.ui.nodeview_space_menu import add_node_menu from sverchok.ui.nodeview_space_menu import add_node_menu
def nodes_index(): def nodes_index():
return [ return [
( (
+3 -1
View File
@@ -23,6 +23,8 @@ import ifcsverchok.helper
from bpy.props import StringProperty, EnumProperty from bpy.props import StringProperty, EnumProperty
from sverchok.node_tree import SverchCustomTreeNode from sverchok.node_tree import SverchCustomTreeNode
from sverchok.data_structure import updateNode from sverchok.data_structure import updateNode
import logging
logger = logging.getLogger('sverchok.ifc')
def update_usecase(self, context): def update_usecase(self, context):
@@ -98,8 +100,8 @@ class SvIfcApi(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCor
def register(): def register():
bpy.utils.register_class(SvIfcTooltip)
bpy.utils.register_class(SvIfcApi) bpy.utils.register_class(SvIfcApi)
bpy.utils.register_class(SvIfcTooltip)
def unregister(): def unregister():
+2 -2
View File
@@ -26,8 +26,8 @@ from sverchok.data_structure import updateNode
import importlib import importlib
class SvIfcTooltip(bpy.types.Operator): class SvIfcTooltipWIP(bpy.types.Operator):
bl_idname = "node.sv_ifc_tooltip" bl_idname = "node.sv_ifc_tooltipWIP"
bl_label = "IFC Info" bl_label = "IFC Info"
tooltip: bpy.props.StringProperty() tooltip: bpy.props.StringProperty()