diff --git a/src/blenderbim/blenderbim/bim/module/brick/__init__.py b/src/blenderbim/blenderbim/bim/module/brick/__init__.py index ca3e084128..ac53d3eb41 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/brick/__init__.py @@ -35,7 +35,6 @@ classes = ( operator.ViewBrickItem, operator.SerializeBrick, operator.AddBrickNamespace, - operator.SetBrickListRoot, operator.RemoveBrickRelation, prop.Brick, prop.BIMBrickProperties, diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 5ca949ffc4..01a66418b9 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -37,6 +37,7 @@ class LoadBrickProject(bpy.types.Operator, Operator): bl_idname = "bim.load_brick_project" bl_label = "Load Brickschema Project" bl_options = {"REGISTER", "UNDO"} + bl_description = "Load in a Brick project from a file" filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"}) @@ -56,6 +57,7 @@ class ViewBrickClass(bpy.types.Operator, Operator): bl_idname = "bim.view_brick_class" bl_label = "View Brick Class" bl_options = {"REGISTER", "UNDO"} + bl_description = "Inspect the subclasses of this class" brick_class: bpy.props.StringProperty(name="Brick Class") split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) @@ -67,6 +69,7 @@ class ViewBrickItem(bpy.types.Operator, Operator): bl_idname = "bim.view_brick_item" bl_label = "View Brick Item" bl_options = {"REGISTER", "UNDO"} + bl_description = "Inspect this entity in the viewer" item: bpy.props.StringProperty(name="Brick Item") split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) @@ -78,6 +81,7 @@ class RewindBrickClass(bpy.types.Operator, Operator): bl_idname = "bim.rewind_brick_class" bl_label = "Rewind Brick Class" bl_options = {"REGISTER", "UNDO"} + bl_description = "Go back to the previous list view" split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) def _execute(self, context): @@ -88,6 +92,7 @@ class CloseBrickProject(bpy.types.Operator, Operator): bl_idname = "bim.close_brick_project" bl_label = "Close Brick Project" bl_options = {"REGISTER", "UNDO"} + bl_description = "Close the Brick project" def _execute(self, context): core.close_brick_project(tool.Brick) @@ -122,6 +127,7 @@ class AddBrick(bpy.types.Operator, Operator): bl_idname = "bim.add_brick" bl_label = "Add Brick" bl_options = {"REGISTER", "UNDO"} + bl_description = "Create the Brick entity" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -143,6 +149,7 @@ class AddBrickRelation(bpy.types.Operator, Operator): bl_idname = "bim.add_brick_relation" bl_label = "Add Brick Relation" bl_options = {"REGISTER", "UNDO"} + bl_description = "Create the Brick relationship" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -178,6 +185,7 @@ class NewBrickFile(bpy.types.Operator): bl_idname = "bim.new_brick_file" bl_label = "New Brick File" bl_options = {"REGISTER", "UNDO"} + bl_description = "Create a Brick project from scratch" def execute(self, context): IfcStore.begin_transaction(self) @@ -210,16 +218,18 @@ class RefreshBrickViewer(bpy.types.Operator, Operator): bl_idname = "bim.refresh_brick_viewer" bl_label = "Refresh Brick Viewer" bl_options = {"REGISTER", "UNDO"} - split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) + bl_description = "Refresh the list view" def _execute(self, context): - core.refresh_brick_viewer(tool.Brick, split_screen=self.split_screen) + core.refresh_brick_viewer(tool.Brick) + core.refresh_brick_viewer(tool.Brick, split_screen=True) class RemoveBrick(bpy.types.Operator, Operator): bl_idname = "bim.remove_brick" bl_label = "Remove Brick" bl_options = {"REGISTER", "UNDO"} + bl_description = "Delete this entity" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -262,6 +272,7 @@ class SerializeBrick(bpy.types.Operator): class AddBrickNamespace(bpy.types.Operator, Operator): bl_idname = "bim.add_brick_namespace" bl_label = "Add Brick Namespace" + bl_description = "Bind a new namespace to the Brick project" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -270,24 +281,11 @@ class AddBrickNamespace(bpy.types.Operator, Operator): core.add_namespace(tool.Brick, alias=alias, uri=uri) -class SetBrickListRoot(bpy.types.Operator, Operator): - bl_idname = "bim.set_brick_list_root" - bl_label = "Set Brick View Type" - bl_options = {"REGISTER", "UNDO"} - split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) - - def _execute(self, context): - if self.split_screen: - root = context.scene.BIMBrickProperties.split_screen_brick_list_root - else: - root = context.scene.BIMBrickProperties.brick_list_root - core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=self.split_screen) - - class RemoveBrickRelation(bpy.types.Operator, Operator): bl_idname = "bim.remove_brick_relation" bl_label = "Remove Relation" bl_options = {"REGISTER", "UNDO"} + bl_description = "Delete this relationship" predicate: bpy.props.StringProperty(name="Relation") object: bpy.props.StringProperty(name="Object") diff --git a/src/blenderbim/blenderbim/bim/module/brick/prop.py b/src/blenderbim/blenderbim/bim/module/brick/prop.py index 2396b65a59..fed515b044 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/prop.py +++ b/src/blenderbim/blenderbim/bim/module/brick/prop.py @@ -30,6 +30,8 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +import blenderbim.core.brick as core +import blenderbim.tool.brick as tool from blenderbim.tool.brick import BrickStore def update_active_brick_index(self, context): @@ -65,6 +67,15 @@ def get_brick_relations(self, context): return BrickStore.relationships +def update_view(self, context): + root = context.scene.BIMBrickProperties.brick_list_root + core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False) + +def split_screen_update_view(self, context): + root = context.scene.BIMBrickProperties.split_screen_brick_list_root + core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True) + + class Brick(PropertyGroup): name: StringProperty(name="Name") label: StringProperty(name="Label") @@ -79,7 +90,7 @@ class BIMBrickProperties(PropertyGroup): active_brick_index: IntProperty(name="Active Brick Index", update=update_active_brick_index) libraries: EnumProperty(name="Libraries", items=get_libraries) set_list_root_toggled: BoolProperty(name="Set List Root Toggled", default=False) - brick_list_root: EnumProperty(name="Brick List Root", items=get_brick_roots) + brick_list_root: EnumProperty(name="Brick List Root", items=get_brick_roots, update=update_view) # namespace manager namespace: EnumProperty(name="Namespace", items=get_namespaces) brick_settings_toggled: BoolProperty(name="Brick Settings Toggled", default=False) @@ -102,4 +113,4 @@ class BIMBrickProperties(PropertyGroup): split_screen_active_brick_index: IntProperty(name="Split Screen Active Brick Index", update=update_active_brick_index) split_screen_active_brick_class: StringProperty(name="Split Screen Active Brick Class") split_screen_brick_breadcrumbs: CollectionProperty(name="Split Screen Brick Breadcrumbs", type=StrProperty) - split_screen_brick_list_root: EnumProperty(name="Split Screen Brick List Root", items=get_brick_roots) \ No newline at end of file + split_screen_brick_list_root: EnumProperty(name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index b19e7152d8..a32ec17bf9 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -42,9 +42,17 @@ class BIM_PT_brickschema(Panel): row.operator("bim.load_brick_project", text="Load Project") return + row = self.layout.row(align=True) if BrickStore.path: - row = self.layout.row(align=True) row.label(text=BrickStore.path, icon="FILEBROWSER") + else: + row.label(text="No file", icon="FILEBROWSER") + + row = self.layout.row(align=True) + if BrickStore.last_saved: + row.label(text=BrickStore.last_saved, icon="TIME") + else: + row.label(text="Not saved", icon="TIME") row = self.layout.row(align=True) op = row.operator("bim.serialize_brick", icon="EXPORT", text="Save") @@ -55,7 +63,7 @@ class BIM_PT_brickschema(Panel): row = self.layout.row(align=True) row.prop(data=self.props, property="brick_settings_toggled", text="", icon="PREFERENCES") - + if self.props.brick_settings_toggled: box = self.layout.box() row = box.row(align=True) @@ -86,47 +94,43 @@ class BIM_PT_brickschema(Panel): row.prop(data=self.props, property="new_brick_label", text="") prop_with_search(row, self.props, "brick_entity_class", text="") row.operator("bim.add_brick", text="", icon="ADD") - # row.operator("bim.refresh_brick_viewer", text="", icon="FILE_REFRESH") row = self.layout.row(align=True) col = row.column() col.alignment = "RIGHT" + row.prop(data=self.props, property="set_list_root_toggled", text="", icon="OUTLINER") row.prop(data=self.props, property="split_screen_toggled", text="", icon="WINDOW") + row.operator("bim.refresh_brick_viewer", text="", icon="FILE_REFRESH") grid = self.layout.grid_flow(even_columns=True) - grid1 = grid.column(align=True) - row = grid1.row(align=True) + grid_left = grid.column(align=True) + row = grid_left.row(align=True) if len(self.props.brick_breadcrumbs): op = row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV") op.split_screen = False - row.prop(data=self.props, property="set_list_root_toggled", text="", icon="OUTLINER") row.label(text=self.props.active_brick_class) if self.props.set_list_root_toggled: - row = grid1.row(align=True) - op = row.operator("bim.set_brick_list_root", text="Set View") - op.split_screen = False + row = grid_left.row(align=True) row.prop(data=self.props, property="brick_list_root", text="") - row = grid1.row() + row = grid_left.row() BIM_UL_bricks.split_screen = False row.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index") if self.props.split_screen_toggled: - grid2 = grid.column(align=True) - row = grid2.row(align=True) + grid_right = grid.column(align=True) + row = grid_right.row(align=True) if len(self.props.split_screen_brick_breadcrumbs): op = row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV") op.split_screen = True row.label(text=self.props.split_screen_active_brick_class) if self.props.set_list_root_toggled: - row = grid2.row(align=True) - op = row.operator("bim.set_brick_list_root", text="Set View") - op.split_screen = True + row = grid_right.row(align=True) row.prop(data=self.props, property="split_screen_brick_list_root", text="") - row = grid2.row() + row = grid_right.row() BIM_UL_bricks.split_screen = True row.template_list("BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index") @@ -165,12 +169,10 @@ class BIM_PT_brickschema(Panel): prop_with_search(row, self.props, "new_brick_relation_type", text="") row.prop(data=self.props, property="new_brick_relation_object", text="") row.operator("bim.add_brick_relation", text="", icon="ADD") - if self.props.brick_create_relations_toggled and self.props.add_relation_failed: row = self.layout.row(align=True) row.label(text="Failed to find this entity!", icon="ERROR") - for relation in BrickschemaData.data["active_relations"]: row = self.layout.row(align=True) diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index 8a9ef49798..d279ad317c 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -20,64 +20,65 @@ import bpy from . import ui, prop, operator classes = ( + operator.AddCostColumn, + operator.AddCostItem, + operator.AddCostItemQuantity, operator.AddCostSchedule, - operator.RemoveCostSchedule, - operator.EditCostSchedule, + operator.AddCostValue, + operator.AddCurrency, + operator.AddSummaryCostItem, + operator.AssignCostItemQuantity, + operator.AssignCostItemType, + operator.AssignCostValue, + operator.CalculateCostItemResourceValue, + operator.ChangeParentCostItem, + operator.ClearCostItemAssignments, + operator.ContractCostItem, + operator.ContractCostItemRate, + operator.ContractCostItems, + operator.CopyCostItem, + operator.CopyCostItemValues, + operator.DisableEditingCostItem, + operator.DisableEditingCostItemQuantity, + operator.DisableEditingCostItemValue, + operator.DisableEditingCostSchedule, operator.EditCostItem, operator.EditCostItemQuantity, operator.EditCostItemValue, operator.EditCostItemValueFormula, - operator.EnableEditingCostSchedule, - operator.EnableEditingCostItems, + operator.EditCostSchedule, operator.EnableEditingCostItem, - operator.ExportCostSchedules, - operator.ExpandCostItems, operator.EnableEditingCostItemQuantities, operator.EnableEditingCostItemQuantity, - operator.EnableEditingCostItemValues, + operator.EnableEditingCostItems, operator.EnableEditingCostItemValue, operator.EnableEditingCostItemValueFormula, - operator.DisableEditingCostItem, - operator.DisableEditingCostSchedule, - operator.DisableEditingCostItemQuantity, - operator.DisableEditingCostItemValue, - operator.AddCostColumn, - operator.RemoveCostColumn, - operator.AddCostItem, - operator.AddSummaryCostItem, + operator.EnableEditingCostItemValues, + operator.EnableEditingCostSchedule, operator.ExpandCostItem, - operator.ContractCostItem, + operator.ExpandCostItemRate, + operator.ExpandCostItems, + operator.ExportCostSchedules, + operator.HighlightProductCostItem, + operator.ImportCostScheduleCsv, + operator.LoadCostItemElementQuantities, + operator.LoadCostItemQuantities, + operator.LoadCostItemResourceQuantities, + operator.LoadCostItemTaskQuantities, + operator.LoadCostItemTypes, + operator.LoadProductCostItems, + operator.LoadScheduleOfRates, + operator.RemoveCostColumn, operator.RemoveCostItem, - operator.AssignCostItemType, - operator.UnassignCostItemType, - operator.AssignCostItemQuantity, - operator.UnassignCostItemQuantity, - operator.AddCostItemQuantity, operator.RemoveCostItemQuantity, - operator.AddCostValue, operator.RemoveCostItemValue, - operator.CopyCostItemValues, + operator.RemoveCostSchedule, + operator.ReorderCostItem, operator.SelectCostItemProducts, operator.SelectCostScheduleProducts, - operator.ImportCostScheduleCsv, - operator.LoadCostItemQuantities, - operator.LoadCostItemTypes, - operator.AssignCostValue, - operator.LoadScheduleOfRates, - operator.ExpandCostItemRate, - operator.ContractCostItemRate, - operator.CalculateCostItemResourceValue, - operator.ClearCostItemAssignments, - operator.HighlightProductCostItem, - operator.LoadProductCostItems, - operator.ReorderCostItem, operator.SelectUnassignedProducts, - operator.LoadCostItemElementQuantities, - operator.LoadCostItemTaskQuantities, - operator.LoadCostItemResourceQuantities, - operator.ChangeParentCostItem, - operator.CopyCostItem, - operator.AddCurrency, + operator.UnassignCostItemQuantity, + operator.UnassignCostItemType, prop.CostItem, prop.CostItemQuantity, prop.CostItemType, diff --git a/src/blenderbim/blenderbim/bim/module/cost/data.py b/src/blenderbim/blenderbim/bim/module/cost/data.py index c713b9eb39..9786151c6b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/data.py +++ b/src/blenderbim/blenderbim/bim/module/cost/data.py @@ -117,6 +117,7 @@ class CostSchedulesData: data["TotalAppliedValue"] = 0.0 data["TotalCost"] = 0.0 has_unit_basis = False + is_sum = False if root_element.is_a("IfcCostItem"): values = root_element.CostValues elif root_element.is_a("IfcConstructionResource"): @@ -130,6 +131,11 @@ class CostSchedulesData: data["UnitBasisValueComponent"] = cost_value_data["UnitBasis"]["ValueComponent"] data["UnitBasisUnitSymbol"] = cost_value_data["UnitBasis"]["UnitSymbol"] has_unit_basis = True + else: + data["UnitBasisValueComponent"] = 1 + data["UnitBasisUnitSymbol"] = "U" + if cost_value.Category == "*": + is_sum = True if has_unit_basis: data["TotalCost"] = data["TotalAppliedValue"] / data["UnitBasisValueComponent"] else: @@ -137,7 +143,8 @@ class CostSchedulesData: data["TotalCost"] = data["TotalAppliedValue"] * data["TotalCostQuantity"] else: data["TotalCost"] = data["TotalAppliedValue"] - data["TotalAppliedValue"] = None + if is_sum: + data["TotalAppliedValue"] = None @classmethod def _load_cost_item_quantities(cls, cost_item, data): @@ -154,7 +161,7 @@ class CostSchedulesData: if unit: data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit) else: - data["UnitSymbol"] = None + data["UnitSymbol"] = "U" # same_unit_nested_cost_item = set() # data["DerivedTotalCostQuantity"] = None diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index 02c7b2e9a8..91bf921347 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -156,6 +156,17 @@ class ContractCostItem(bpy.types.Operator, tool.Ifc.Operator): core.contract_cost_item(tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item)) +class ContractCostItems(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.contract_cost_items" + bl_label = "Contract Cost Item" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Collapse cost item tree" + cost_item: bpy.props.IntProperty() + + def _execute(self, context): + core.contract_cost_items(tool.Cost) + + class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_cost_item" bl_label = "Remove Cost Item" diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 832e69c871..bdf2e7ef8b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -139,8 +139,8 @@ class BIM_PT_cost_schedules(Panel): row = self.layout.row(align=True) row.alignment = "RIGHT" row.operator("bim.add_summary_cost_item", text="Add Summary Cost", icon="ADD") - row.operator("bim.expand_all_tasks", text="Expand All") - row.operator("bim.contract_all_tasks", text="Contract All") + row.operator("bim.expand_cost_items", text="Expand All") + row.operator("bim.contract_cost_items", text="Contract All") row = self.layout.row(align=True) row.alignment = "RIGHT" if self.props.cost_items and self.props.active_cost_item_index < len(self.props.cost_items): @@ -620,18 +620,22 @@ class BIM_UL_cost_items_trait: else: row.label(text="", icon="DOT") - def draw_total_cost_column(self, layout, cost_item): - format_numbers = "{0:,.2f}".format(cost_item["TotalCost"]).replace(",", " ") - currency = CostSchedulesData.data["currency"] - text = "{} {}".format(format_numbers, currency["name"]) if currency else format_numbers - layout.label(text=text) - def draw_quantity_column(self, layout, cost_item): if CostSchedulesData.data["is_editing_rates"]: self.draw_uom_column(layout, cost_item) else: self.draw_total_quantity_column(layout, cost_item) + def draw_uom_column(self, layout, cost_item): + layout.label(text=cost_item["UnitBasisUnitSymbol"]) + + def draw_total_quantity_column(self, layout, cost_item): + if cost_item["TotalCostQuantity"]: + label = "{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}" + layout.label(text=label) + else: + layout.label(text="-") + def draw_value_column(self, layout, cost_item): if cost_item["TotalAppliedValue"]: text = "{0:,.2f}".format(cost_item["TotalAppliedValue"]).replace(",", " ") @@ -643,8 +647,11 @@ class BIM_UL_cost_items_trait: else: layout.label(text="-") - def draw_uom_column(self, layout, cost_item): - layout.label(text=cost_item["UnitBasisUnitSymbol"] or "-" if cost_item["UnitBasisValueComponent"] else "-") + def draw_total_cost_column(self, layout, cost_item): + format_numbers = "{0:,.2f}".format(cost_item["TotalCost"]).replace(",", " ") + currency = CostSchedulesData.data["currency"] + text = "{} {}".format(format_numbers, currency["name"]) if currency else format_numbers + layout.label(text=text) def draw_order_operator(self, row, ifc_definition_id, cost_item): if cost_item["NestingIndex"] is not None: @@ -657,19 +664,7 @@ class BIM_UL_cost_items_trait: op.cost_item = ifc_definition_id op.new_index = cost_item["NestingIndex"] - 1 - def draw_total_quantity_column(self, layout, cost_item): - if cost_item["TotalCostQuantity"]: - label = "{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}" - layout.label(text=label) - else: - layout.label(text="-") - # if cost_item["DerivedTotalCostQuantity"] not in [None, 0]: - # layout.label(text="{0:.2f}".format(cost_item["DerivedTotalCostQuantity"]) + f" {cost_item['DerivedUnitSymbol'] or '-'}") - # else: - # if cost_item["TotalCostQuantity"] == 0: - # layout.label(text="-") - # else: - # layout.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}") + class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList): diff --git a/src/blenderbim/blenderbim/bim/module/csv/operator.py b/src/blenderbim/blenderbim/bim/module/csv/operator.py index 71dabc5b21..152c671f39 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/operator.py +++ b/src/blenderbim/blenderbim/bim/module/csv/operator.py @@ -129,9 +129,8 @@ class ExportCsvAttributes(bpy.types.Operator): class ExportIfcCsv(bpy.types.Operator): bl_idname = "bim.export_ifccsv" bl_label = "Export IFC" - #filename_ext = ".csv" + filename_ext = ".csv" filepath: bpy.props.StringProperty(subtype="FILE_PATH") - def invoke(self, context, event): props = context.scene.CsvProperties @@ -149,8 +148,7 @@ class ExportIfcCsv(bpy.types.Operator): ifc_file = IfcStore.get_file() else: ifc_file = ifcopenshell.open(props.csv_ifc_file) - selector = ifcopenshell.util.selector.Selector() - results = selector.parse(ifc_file, props.ifc_selector) + results = ifcopenshell.util.selector.filter_elements(ifc_file, props.ifc_selector) ifc_csv = ifccsv.IfcCsv() attributes = [a.name for a in props.csv_attributes] sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter @@ -196,9 +194,12 @@ class EyedropIfcCsv(bpy.types.Operator): global_ids = [] self.file = IfcStore.get_file() for obj in context.selected_objects: - if hasattr(obj, "BIMObjectProperties") and obj.BIMObjectProperties.ifc_definition_id: - global_ids.append("#" + self.file.by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId) - context.scene.CsvProperties.ifc_selector = "|".join(global_ids) + element = tool.Ifc.get_entity(obj) + if element: + global_id = getattr(element, "GlobalId", None) + if global_id: + global_ids.append(global_id) + context.scene.CsvProperties.ifc_selector = ",".join(global_ids) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/debug/operator.py b/src/blenderbim/blenderbim/bim/module/debug/operator.py index e8cb0efed8..2b67815858 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/operator.py +++ b/src/blenderbim/blenderbim/bim/module/debug/operator.py @@ -154,6 +154,8 @@ class ValidateIfcFile(bpy.types.Operator): logger = logging.getLogger("validate") logger.setLevel(logging.DEBUG) ifcopenshell.validate.validate(IfcStore.get_file(), logger, express_rules=True) + + self.report({"INFO"}, "Check validation results in the system console.") return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index 9daf4ede50..07ce208e95 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -248,8 +248,13 @@ class BaseDecorator: if check_mode and obj.data.is_editmode: return self.get_editmesh_geom(obj) - vertices = [obj.matrix_world @ v.co for v in obj.data.vertices] - indices = [e.vertices for e in obj.data.edges] + bm = bmesh.new() + bm.from_mesh(obj.data) + vertices = [obj.matrix_world @ v.co for v in bm.verts] + # In object mode, it's nicer to not show "internal edges". Most will be dissolved anyway. + indices = [[v.index for v in e.verts] for e in bm.edges if len(e.link_faces) != 2] + bm.free() + return vertices, indices def get_editmesh_geom(self, obj): diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index c0bf829553..d85cd2dbe9 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -889,6 +889,21 @@ class CreateDrawing(bpy.types.Operator): ) return classes + def is_manifold(self, obj): + result = self.is_manifold_cache.get(obj.data.name, None) + if result is not None: + return result + + bm = bmesh.new() + bm.from_mesh(obj.data) + for edge in bm.edges: + if not edge.is_manifold: + bm.free() + self.is_manifold_cache[obj.data.name] = False + return False + self.is_manifold_cache[obj.data.name] = True + return True + def merge_linework_and_add_metadata(self, root): join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria") if join_criteria: @@ -899,6 +914,7 @@ class CreateDrawing(bpy.types.Operator): group = root.findall(".//{http://www.w3.org/2000/svg}g")[0] joined_paths = {} + self.is_manifold_cache = {} ifc = tool.Ifc.get() for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"): @@ -908,6 +924,10 @@ class CreateDrawing(bpy.types.Operator): classes.append("cut") el.set("class", " ".join(classes)) + obj = tool.Ifc.get_object(element) + if not self.is_manifold(obj): + continue + # An element group will contain a bunch of paths representing the # cut of that element. However IfcOpenShell may not correctly # create closed paths. We post-process all paths with shapely to diff --git a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py index db501355d3..d7338fd382 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py @@ -351,7 +351,7 @@ class Scheduler: wrap_text: if True, text will be wrapped to fit in cell cell_width: width of cell, used for wrapping text """ - text_lines = [str(p).upper() for p in p_tags] + text_lines = [str(p) for p in p_tags] box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment) text_params = { "font-size": font_size, diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py index 25155f5096..e96eb9defe 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py @@ -314,7 +314,7 @@ class BIM_PT_references(Panel): if not self.props.is_editing_references: row = self.layout.row(align=True) - row.label(text=f"{DocumentsData.data['total_references']} References Found", icon="LONGDISPLAY") + row.label(text=f"{DocumentsData.data['total_references']} References Found", icon="OBJECT_HIDDEN") row.operator("bim.load_references", text="", icon="IMPORT") return diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 59e47c7c02..8414c6d018 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -637,6 +637,11 @@ class OverrideDuplicateMove(bpy.types.Operator): element = tool.Ifc.get_entity(obj) if element and element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": continue # For now, don't copy drawings until we stabilise a bit more. It's tricky. + + # Prior to duplicating, sync the object placement to make decomposition recreation more stable. + if tool.Ifc.is_moved(obj): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + new_obj = obj.copy() if obj.data: new_obj.data = obj.data.copy() @@ -705,6 +710,10 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator): relationships = tool.Root.get_decomposition_relationships(context.selected_objects) old_to_new = {} for obj in context.selected_objects: + # Prior to duplicating, sync the object placement to make decomposition recreation more stable. + if tool.Ifc.is_moved(obj): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + new_obj = obj.copy() if obj.data: new_obj.data = obj.data.copy() diff --git a/src/blenderbim/blenderbim/bim/module/model/space.py b/src/blenderbim/blenderbim/bim/module/model/space.py index fb52923279..2af8ae020c 100644 --- a/src/blenderbim/blenderbim/bim/module/model/space.py +++ b/src/blenderbim/blenderbim/bim/module/model/space.py @@ -95,19 +95,19 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): gross_settings.set(gross_settings.DISABLE_OPENING_SUBTRACTIONS, True) for obj in bpy.context.visible_objects: - element = tool.Ifc.get_entity(obj) + visible_element = tool.Ifc.get_entity(obj) if ( - not element + not visible_element or obj.type != "MESH" - or not self.is_bounding_class(element) + or not self.is_bounding_class(visible_element) or not tool.Drawing.is_intersecting_plane(obj, self.cut_point, self.cut_normal) ): continue old_mesh = None - if element.HasOpenings: - new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, element)) + if visible_element.HasOpenings: + new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, visible_element)) old_mesh = obj.data obj.data = new_mesh diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index df38a8c86d..2bd50dd132 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -41,6 +41,7 @@ classes = ( operator.SaveLibraryFile, operator.SelectLibraryFile, operator.ToggleFilterCategories, + operator.ToggleLinkSelectability, operator.ToggleLinkVisibility, operator.UnassignLibraryDeclaration, operator.UnlinkIfc, diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index a929cb638e..b510494fd7 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -880,6 +880,27 @@ class LoadLink(bpy.types.Operator): return {"FINISHED"} +class ToggleLinkSelectability(bpy.types.Operator): + bl_idname = "bim.toggle_link_selectability" + bl_label = "Toggle Link Selectability" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Toggle selectability" + link: bpy.props.StringProperty() + + def execute(self, context): + props = context.scene.BIMProjectProperties + link = props.links.get(self.link) + for collection in self.get_linked_collections(): + collection.hide_select = not collection.hide_select + link.is_selectable = not collection.hide_select + return {"FINISHED"} + + def get_linked_collections(self): + return [ + c for c in bpy.data.collections if "IfcProject" in c.name and c.library and c.library.filepath == self.link + ] + + class ToggleLinkVisibility(bpy.types.Operator): bl_idname = "bim.toggle_link_visibility" bl_label = "Toggle Link Visibility" diff --git a/src/blenderbim/blenderbim/bim/module/project/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py index e65f5c4e48..bbc3c64f00 100644 --- a/src/blenderbim/blenderbim/bim/module/project/prop.py +++ b/src/blenderbim/blenderbim/bim/module/project/prop.py @@ -93,6 +93,7 @@ class FilterCategory(PropertyGroup): class Link(PropertyGroup): name: StringProperty(name="Name") is_loaded: BoolProperty(name="Is Loaded", default=False) + is_selectable: BoolProperty(name="Is Selectable", default=True) is_wireframe: BoolProperty(name="Is Wireframe", default=False) is_hidden: BoolProperty(name="Is Hidden", default=False) diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 788769b159..7558fc3762 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -350,6 +350,13 @@ class BIM_UL_links(UIList): row = layout.row(align=True) if item.is_loaded: row.label(text=item.name) + op = row.operator( + "bim.toggle_link_selectability", + text="", + icon="RESTRICT_SELECT_OFF" if item.is_selectable else "RESTRICT_SELECT_ON", + emboss=False, + ) + op.link = item.name op = row.operator( "bim.toggle_link_visibility", text="", diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py index b0b411e23a..5b60c151e7 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -123,6 +123,7 @@ class BIM_PT_object_systems(Panel): "IfcDistributionSystem": "NETWORK_DRIVE", "IfcDistributionCircuit": "DRIVER", "IfcBuildingSystem": "MOD_BUILD", + "IfcBuiltSystem": "MOD_BUILD", "IfcZone": "CUBE", } for system in ObjectSystemData.data["systems"]: @@ -284,6 +285,7 @@ class BIM_UL_systems(UIList): "IfcDistributionSystem": "NETWORK_DRIVE", "IfcDistributionCircuit": "DRIVER", "IfcBuildingSystem": "MOD_BUILD", + "IfcBuiltSystem": "MOD_BUILD", "IfcZone": "CUBE", } if item: @@ -316,6 +318,7 @@ class BIM_UL_object_systems(UIList): "IfcDistributionSystem": "NETWORK_DRIVE", "IfcDistributionCircuit": "DRIVER", "IfcBuildingSystem": "MOD_BUILD", + "IfcBuiltSystem": "MOD_BUILD", "IfcZone": "CUBE", } if item: diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index dace67569d..821348a3b8 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -51,6 +51,8 @@ def close_brick_project(brick): brick.clear_project() brick.clear_brick_browser() brick.clear_brick_browser(split_screen=True) + brick.clear_breadcrumbs() + brick.clear_breadcrumbs(split_screen=True) def convert_brick_project(ifc, brick): @@ -132,9 +134,7 @@ def add_namespace(brick, alias=None, uri=None): def set_brick_list_root(brick, brick_root=None, split_screen=False): - brick.clear_brick_browser(split_screen=split_screen) - brick.import_brick_classes(brick_root, split_screen=split_screen) - brick.set_active_brick_class(brick_root, split_screen=split_screen) + brick.run_view_brick_class(brick_class=brick_root, split_screen=split_screen) brick.clear_breadcrumbs(split_screen=split_screen) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 8f308d1590..f16e959c4a 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -23,6 +23,7 @@ import ifcopenshell.util.brick import blenderbim.core.tool import blenderbim.tool as tool from contextlib import contextmanager +import datetime try: import brickschema @@ -308,6 +309,8 @@ class Brick(blenderbim.core.tool.Brick): with BrickStore.graph.new_changeset("PROJECT") as cs: cs.load_file(filepath) BrickStore.path = filepath + cls.set_last_saved() + BrickStore.load_sub_roots() BrickStore.load_namespaces() BrickStore.load_entity_classes() BrickStore.load_relationships() @@ -321,6 +324,7 @@ class Brick(blenderbim.core.tool.Brick): with BrickStore.graph.new_changeset("SCHEMA") as cs: cs.load_file(BrickStore.schema) BrickStore.graph.bind("digitaltwin", Namespace("https://example.org/digitaltwin#")) + BrickStore.load_sub_roots() BrickStore.load_namespaces() BrickStore.load_entity_classes() BrickStore.load_relationships() @@ -340,10 +344,9 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def remove_brick(cls, brick_uri): - if BrickStore.graph.triples((URIRef(brick_uri), None, None)): - with BrickStore.new_changeset() as cs: - for triple in BrickStore.graph.triples((URIRef(brick_uri), None, None)): - cs.remove(triple) + with BrickStore.new_changeset() as cs: + for triple in BrickStore.graph.triples((URIRef(brick_uri), None, None)): + cs.remove(triple) @classmethod def run_assign_brick_reference(cls, element=None, library=None, brick_uri=None): @@ -379,6 +382,7 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def serialize_brick(cls): BrickStore.get_project().serialize(destination=BrickStore.path, format="turtle") + cls.set_last_saved() @classmethod def add_namespace(cls, alias, uri): @@ -392,22 +396,24 @@ class Brick(blenderbim.core.tool.Brick): else: bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.clear() + @classmethod + def set_last_saved(cls): + save = os.path.getmtime(BrickStore.path) + save = datetime.datetime.fromtimestamp(save) + BrickStore.last_saved = f"{save.year}-{save.month}-{save.day} {save.hour}:{save.minute}" + class BrickStore: schema = None # this is now a os path path = None # file path if the project was loaded in graph = None # this is the VersionedGraphCollection with 2 arbitrarily named graphs: "schema" and "project" # "SCHEMA" holds the Brick.ttl metadata; "PROJECT" holds all the authored entities + last_saved = None history = [] future = [] current_changesets = 0 history_size = 64 namespaces = [] - root_classes = ["Equipment", - "Electrical_Equipment", "Fire_Safety_Equipment", "HVAC_Equipment", "Lighting_Equipment", "Meter", - "Location", - "System", - "Point", - "Alarm", "Command", "Parameter", "Sensor", "Setpoint", "Status"] + root_classes = ["Equipment", "Location", "System", "Point"] entity_classes = {} relationships = [] @@ -416,7 +422,9 @@ class BrickStore: BrickStore.schema = None BrickStore.graph = None BrickStore.path = None + BrickStore.last_saved = None BrickStore.namespaces = [] + BrickStore.root_classes = ["Equipment", "Location", "System", "Point"] BrickStore.entity_classes = {} BrickStore.relationships = [] @@ -424,10 +432,36 @@ class BrickStore: def get_project(cls): return BrickStore.graph.graph_at(graph="PROJECT") + @classmethod + def load_sub_roots(cls): + query = BrickStore.graph.query( + """ + PREFIX brick: + PREFIX rdfs: + SELECT ?subRoot ?subClasses WHERE { + { + SELECT ?subRoot (COUNT(?subClass) as ?subClasses) WHERE { + { + ?subRoot rdfs:subClassOf brick:Equipment . + } UNION { + ?subRoot rdfs:subClassOf brick:Point . + } + ?subClass rdfs:subClassOf* ?subRoot . + } + GROUP BY ?subRoot + } + FILTER(?subClasses > 3) + } + """ + ) + for row in query: + sub_root = row.get("subRoot").toPython().split("#")[-1] + BrickStore.root_classes.append(sub_root) + @classmethod def load_namespaces(cls): BrickStore.namespaces = [] - keyword_filter = ["brickschema.org", "schema.org", "w3.org", "purl.org", "rdfs.org", "qudt.org", "ashrae.org"] + keyword_filter = ["brickschema.org", "schema.org", "w3.org", "purl.org", "rdfs.org", "qudt.org", "ashrae.org", "usefulinc.com", "xmlns.com", "opengis.net"] for alias, uri in BrickStore.graph.namespaces(): ignore_namespace = False for keyword in keyword_filter: @@ -444,8 +478,12 @@ class BrickStore: """ PREFIX brick: PREFIX rdfs: + PREFIX owl: SELECT ?class WHERE { ?class rdfs:subClassOf* brick:{root_class} . + FILTER NOT EXISTS { + ?class owl:deprecated true . + } } """.replace( "{root_class}", root_class diff --git a/src/blenderbim/blenderbim/tool/cost.py b/src/blenderbim/blenderbim/tool/cost.py index f6c83b051c..698625bc33 100644 --- a/src/blenderbim/blenderbim/tool/cost.py +++ b/src/blenderbim/blenderbim/tool/cost.py @@ -96,11 +96,11 @@ class Cost(blenderbim.core.tool.Cost): props.contracted_cost_items = json.dumps(cls.contracted_cost_items) @classmethod - def contract_cost_item(cls, cost_item_id): + def contract_cost_item(cls, cost_item): props = bpy.context.scene.BIMCostProperties if not hasattr(cls, "contracted_cost_items"): cls.contracted_cost_items = json.loads(props.contracted_cost_items) - cls.contracted_cost_items.append(cost_item_id) + cls.contracted_cost_items.append(cost_item.id()) props.contracted_cost_items = json.dumps(cls.contracted_cost_items) @classmethod diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 9ae057a57b..903794d4fa 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -1511,8 +1511,8 @@ class Drawing(blenderbim.core.tool.Drawing): else: base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement")) elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"} - annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing)) - elements.update(annotations) + annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing)) + elements.update(annotations) exclude = pset.get("Exclude", None) if exclude: diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index a4e9295d72..904f7019ed 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -358,7 +358,7 @@ class Model(blenderbim.core.tool.Model): is_closed = False if curve.Segments: for segment in curve.Segments: - if len(segment[0]) == 3: # IfcArcIndex + if segment.is_a("IfcArcIndex"): is_arc = True local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1]) global_point = position @ Vector(local_point).to_3d() @@ -368,12 +368,13 @@ class Model(blenderbim.core.tool.Model): cls.vertices.append(global_point) cls.arcs.append([len(cls.vertices) - 2, len(cls.vertices) - 1]) else: - local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1]) - global_point = position @ Vector(local_point).to_3d() - cls.vertices.append(global_point) - if is_arc: - cls.arcs[-1].append(len(cls.vertices) - 1) - is_arc = False + for segment_index in segment[0][0:-1]: + local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment_index - 1]) + global_point = position @ Vector(local_point).to_3d() + cls.vertices.append(global_point) + if is_arc: + cls.arcs[-1].append(len(cls.vertices) - 1) + is_arc = False if curve.Segments[0][0][0] == curve.Segments[-1][0][-1]: is_closed = True diff --git a/src/blenderbim/blenderbim/tool/root.py b/src/blenderbim/blenderbim/tool/root.py index b7424ff3bc..ce198c9f00 100644 --- a/src/blenderbim/blenderbim/tool/root.py +++ b/src/blenderbim/blenderbim/tool/root.py @@ -23,6 +23,7 @@ import blenderbim.core.tool import blenderbim.core.geometry import blenderbim.tool as tool from mathutils import Vector +from blenderbim.bim.module.model.opening import FilledOpeningGenerator class Root(blenderbim.core.tool.Root): @@ -57,7 +58,10 @@ class Root(blenderbim.core.tool.Root): if not source.Representation: return dest.Representation = ifcopenshell.util.element.copy_deep( - tool.Ifc.get(), source.Representation, exclude=["IfcGeometricRepresentationContext"], exclude_callback=exclude_callback + tool.Ifc.get(), + source.Representation, + exclude=["IfcGeometricRepresentationContext"], + exclude_callback=exclude_callback, ) elif dest.is_a("IfcTypeProduct"): if not source.RepresentationMaps: @@ -140,9 +144,58 @@ class Root(blenderbim.core.tool.Root): for i, new_subelement in enumerate(new_subelements): new_element = new_elements[i] if data["type"] == "fill": - obj1 = tool.Ifc.get_object(new_element) - obj2 = tool.Ifc.get_object(new_subelement) - bpy.ops.bim.add_filled_opening(voided_obj=obj1.name, filling_obj=obj2.name) + element = new_element + filling = new_subelement + voided_obj = tool.Ifc.get_object(new_element) + filling_obj = tool.Ifc.get_object(new_subelement) + + existing_opening_occurrence = subelement.FillsVoids[0].RelatingOpeningElement + opening = ifcopenshell.api.run( + "root.copy_class", tool.Ifc.get(), product=existing_opening_occurrence + ) + ifcopenshell.api.run( + "geometry.edit_object_placement", + tool.Ifc.get(), + product=opening, + matrix=ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement), + is_si=False, + ) + + representation = ifcopenshell.util.representation.get_representation( + existing_opening_occurrence, "Model", "Body", "MODEL_VIEW" + ) + representation = ifcopenshell.util.representation.resolve_representation(representation) + mapped_representation = ifcopenshell.api.run( + "geometry.map_representation", tool.Ifc.get(), representation=representation + ) + ifcopenshell.api.run( + "geometry.assign_representation", + tool.Ifc.get(), + product=opening, + representation=mapped_representation, + ) + ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=opening, element=element) + ifcopenshell.api.run("void.add_filling", tool.Ifc.get(), opening=opening, element=filling) + + voided_objs = [voided_obj] + # Openings affect all subelements of an aggregate + for subelement in ifcopenshell.util.element.get_decomposition(element): + subobj = tool.Ifc.get_object(subelement) + if subobj: + voided_objs.append(subobj) + + for voided_obj in voided_objs: + if voided_obj.data: + representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id) + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=voided_obj, + representation=representation, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) @classmethod def run_geometry_add_representation( diff --git a/src/blenderbim/test/bim/feature/cost.feature b/src/blenderbim/test/bim/feature/cost.feature index 1098c96716..aefe3e5ff9 100644 --- a/src/blenderbim/test/bim/feature/cost.feature +++ b/src/blenderbim/test/bim/feature/cost.feature @@ -3,6 +3,7 @@ Feature: Cost Scenario: Add cost schedule Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" When I press "bim.add_cost_schedule" Then nothing happens @@ -111,6 +112,53 @@ Scenario: Add cost item When I press "bim.add_cost_item(cost_item={cost_item})" Then nothing happens +Scenario: Contract Cost Item + Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item()" + And I press "bim.add_cost_item(cost_item={cost_item})" + When I press "bim.contract_cost_item(cost_item={cost_item})" + Then nothing happens + +Scenario: Contract All Cost Items + Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item()" + And I press "bim.add_cost_item(cost_item={cost_item})" + When I press "bim.contract_cost_items" + Then nothing happens + +Scenario: Expand Cost Item + Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item()" + And I press "bim.add_cost_item(cost_item={cost_item})" + And I press "bim.contract_cost_item(cost_item={cost_item})" + When I press "bim.expand_cost_item(cost_item={cost_item})" + Then nothing happens + + +Scenario: Expand All Cost Items + Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item()" + And I press "bim.add_cost_item(cost_item={cost_item})" + When I press "bim.expand_cost_items" + Then nothing happens + + Scenario: Enable editing cost item quantities Given an empty IFC project And I press "bim.add_cost_schedule" diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py index 6497fe3671..d69f890df8 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -58,51 +58,25 @@ class IfcAttributeSetter: return element if "." not in key: return element - if key[0:3] == "Qto": - qto_name, prop = key.split(".", 1) - qto = IfcAttributeSetter.get_element_qto(element, qto_name) - if qto: - IfcAttributeSetter.set_qto_property(qto, prop, value) - return element pset_name, prop = key.split(".", 1) - pset = IfcAttributeSetter.get_element_pset(element, pset_name) + pset = ifcopenshell.util.element.get_pset(element, pset_name, should_inherit=True) if pset: - IfcAttributeSetter.set_pset_property(ifc_file, pset, prop, value) - return element + pset = ifc_file.by_id(pset["id"]) + if pset.is_a("IfcElementQuantity"): + IfcAttributeSetter.set_qto_property(pset, prop, value) + else: + IfcAttributeSetter.set_pset_property(ifc_file, pset, prop, value) return element - @staticmethod - def get_element_qto(element, name): - for relationship in element.IsDefinedBy: - if ( - relationship.is_a("IfcRelDefinesByProperties") - and relationship.RelatingPropertyDefinition.is_a("IfcElementQuantity") - and relationship.RelatingPropertyDefinition.Name == name - ): - return relationship.RelatingPropertyDefinition - @staticmethod def set_qto_property(qto, name, value): for prop in qto.Quantities: if prop.Name != name: continue - setattr(prop, prop.is_a()[len("IfcQuantity") :] + "Value", value) - - @staticmethod - def get_element_pset(element, name): - if element.is_a("IfcTypeObject"): - if element.HasPropertySets: - for pset in element.HasPropertySets: - if pset.is_a("IfcPropertySet") and pset.Name == name: - return pset - else: - for relationship in element.IsDefinedBy: - if ( - relationship.is_a("IfcRelDefinesByProperties") - and relationship.RelatingPropertyDefinition.is_a("IfcPropertySet") - and relationship.RelatingPropertyDefinition.Name == name - ): - return relationship.RelatingPropertyDefinition + try: + setattr(prop, prop.is_a()[len("IfcQuantity") :] + "Value", float(value)) + except: + pass @staticmethod def set_pset_property(ifc_file, pset, name, value): @@ -302,7 +276,7 @@ if __name__ == "__main__": parser.add_argument("-i", "--ifc", type=str, required=True, help="The IFC file") parser.add_argument("-s", "--spreadsheet", type=str, default="data.csv", help="The spreadsheet file") parser.add_argument("-f", "--format", type=str, default="csv", help="The format, chosen from csv, ods, or xlsx") - parser.add_argument("-q", "--query", type=str, default="", help='Specify a IFC query selector, such as ".IfcWall"') + parser.add_argument("-q", "--query", type=str, default="", help='Specify a IFC query selector, such as "IfcWall"') parser.add_argument( "-a", "--arguments", @@ -315,7 +289,7 @@ if __name__ == "__main__": if args.export: ifc_file = ifcopenshell.open(args.ifc) - results = ifcopenshell.util.selector.Selector.parse(ifc_file, args.query) + results = ifcopenshell.util.selector.filter_elements(ifc_file, args.query) ifc_csv = IfcCsv() ifc_csv.export(ifc_file, results, args.arguments or [], output=args.spreadsheet, format=args.format) elif getattr(args, "import"): diff --git a/src/ifcfm/ifcfm/parser.py b/src/ifcfm/ifcfm/parser.py index d055063f69..f74ce4ad78 100644 --- a/src/ifcfm/ifcfm/parser.py +++ b/src/ifcfm/ifcfm/parser.py @@ -22,9 +22,278 @@ import ifcopenshell.util.fm import ifcopenshell.util.selector import ifcopenshell.util.date import ifcopenshell.util.schema +import ifcopenshell.util.system +import ifcopenshell.util.placement import ifcopenshell.util.classification +class Parser2: + def __init__(self, preset="BASIC"): + self.file = None + self.categories = {} + self.get_category_elements = {} + self.get_element_data = {} + self.get_custom_element_data = {} + self.duplicate_keys = [] + + if preset == "BASIC": + self.get_category_elements = { + "actors": get_actors, + "facilities": get_facilities, + "storeys": get_storeys, + "spaces": get_spaces, + "zones": get_zones, + "types": get_types, + "elements": get_elements, + "systems": get_systems, + } + self.get_element_data = { + "actors": get_actor_data, + "facilities": get_facility_data, + "storeys": get_storey_data, + "spaces": get_space_data, + "zones": get_zone_data, + "types": get_type_data, + "elements": get_element_data, + "systems": get_system_data, + } + + def parse(self, ifc_file): + for category_name, get_category_elements in self.get_category_elements.items(): + self.categories.setdefault(category_name, {}) + for element in get_category_elements(ifc_file): + data = self.get_element_data[category_name](ifc_file, element) or {} + custom_data = ( + self.get_custom_element_data.get(category_name, lambda x, y: None)(ifc_file, element) or {} + ) + data.update(custom_data) + + if data: + if data["key"] in self.categories[category_name]: + self.duplicate_keys.append((self.categories[category_name][data["key"]], data)) + self.categories[category_name][data["key"]] = data + + +def get_actors(ifc_file): + return ifc_file.by_type("IfcActor") + + +def get_facilities(ifc_file): + return ifc_file.by_type("IfcBuilding") + + +def get_storeys(ifc_file): + return ifc_file.by_type("IfcBuildingStorey") + + +def get_spaces(ifc_file): + return ifc_file.by_type("IfcSpace") + + +def get_zones(ifc_file): + zones = [] + for zone in ifc_file.by_type("IfcZone"): + for rel in zone.IsGroupedBy: + zones.extend([(zone, space) for space in rel.RelatedObjects]) + return zones + + +def get_types(ifc_file): + return ifcopenshell.util.fm.get_fmhem_types(ifc_file) + + +def get_elements(ifc_file): + elements = set() + for element_type in ifcopenshell.util.fm.get_fmhem_types(ifc_file): + elements.update(ifcopenshell.util.element.get_types(element_type)) + return elements + + +def get_systems(ifc_file): + return ifc_file.by_type("IfcSystem") + + +def get_actor_data(ifc_file, element): + return { + "key": element.TheActor.Name, + "Name": element.TheActor.Name, + "Category": get_classification(element), + "Email": get_actor_address(element, "ElectronicMailAddresses"), + "Phone": get_actor_address(element, "TelephoneNumbers"), + "CompanyURL": get_actor_address(element, "WWWHomePageURL"), + "Department": get_actor_address(element, "InternalLocation"), + "Address1": get_actor_address(element, "AddressLines"), + "Address2": get_actor_address(element, "Town"), + "StateRegion": get_actor_address(element, "Region"), + "PostalCode": get_actor_address(element, "PostalCode"), + "Country": get_actor_address(element, "Country"), + } + + +def get_facility_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(ifc_file.by_type("IfcProject")[0]), + "Category": get_classification(element), + "ProjectName": ifc_file.by_type("IfcProject")[0].Name, + "SiteName": getattr(get_facility_parent(element, "IfcSite"), "Name", None), + "LinearUnits": "millimeters", + "AreaUnits": "square meters", + "AreaMeasurement": "BIM Software", + "Phase": ifc_file.by_type("IfcProject")[0].Phase, + "ModelSoftware": get_owner_application(element), + "ModelProjectID": ifc_file.by_type("IfcProject")[0].GlobalId, + "ModelSiteID": getattr(get_facility_parent(element, "IfcSite"), "GlobalId", None), + "ModelBuildingID": element.GlobalId, + } + + +def get_storey_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": "Level", + "ModelSoftware": get_owner_application(element), + "ModelObject": element.is_a(), + "ModelID": element.GlobalId, + "Elevation": ifcopenshell.util.placement.get_storey_elevation(element), + } + + +def get_space_data(ifc_file, element): + psets = ifcopenshell.util.element.get_psets(element) + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": get_classification(element), + "LevelName": getattr(get_facility_parent(element, "IfcBuildingStorey"), "Name", None), + "Description": element.LongName, + "ModelSoftware": get_owner_application(element), + "ModelID": element.GlobalId, + "AreaGross": get_property(psets, "Qto_SpaceBaseQuantities", "GrossFloorArea", decimals=2), + "AreaNet": get_property(psets, "Qto_SpaceBaseQuantities", "NetFloorArea", decimals=2), + } + + +def get_zone_data(ifc_file, element): + zone, space = element + return { + "key": (element.Name or "Unnamed") + (space.Name or "Unnamed"), + "Name": zone.Name, + "AuthorOrganizationName": get_owner_name(zone), + "AuthorDate": get_owner_creation_date(zone), + "SpaceName": space.Name, + "ModelSoftware": get_owner_application(zone), + "ModelID": zone.GlobalId, + } + + +def get_type_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": get_classification(element), + "Description": element.Description, + "ModelSoftware": get_owner_application(element), + "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), + "ModelTag": element.Tag, + "ModelID": element.GlobalId, + } + + +def get_element_data(ifc_file, element): + space = ifcopenshell.util.element.get_container(element) + space_name = space.Name if space.is_a("IfcSpace") else None + systems = ifcopenshell.util.system.get_element_systems(element) + system = systems[0].Name if systems else None + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "TypeName": ifcopenshell.util.element.get_type(element).Name, + "SpaceName": space_name, + "SystemName": system, + "ModelSoftware": get_owner_application(element), + "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), + "ModelID": element.GlobalId, + } + + +def get_system_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "Description": element.Description, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": get_classification(element), + "ModelSoftware": get_owner_application(element), + "ModelID": element.GlobalId, + } + + +def get_owner_name(element): + if not getattr(element, "OwnerHistory", None): + return + return element.OwnerHistory.OwningUser.TheOrganization.Name + + +def get_owner_creation_date(element): + if not getattr(element, "OwnerHistory", None): + return + return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat() + + +def get_owner_application(element): + if not getattr(element, "OwnerHistory", None): + return + return element.OwnerHistory.OwningApplication.ApplicationFullName + + +def get_facility_parent(element, ifc_class): + parent = ifcopenshell.util.element.get_aggregate(element) + while parent: + if parent.is_a(ifc_class): + return parent + if parent.is_a("IfcProject"): + return + parent = ifcopenshell.util.element.get_aggregate(parent) + + +def get_classification(element): + references = list(ifcopenshell.util.classification.get_references(element)) + if references: + if hasattr(references[0], "Identification"): + return "{}:{}".format(references[0].Identification, references[0].Name) + return "{}:{}".format(references[0].ItemReference, references[0].Name) + + +def get_actor_address(element, name): + for address in element.TheActor.Addresses or []: + if hasattr(address, name) and getattr(address, name, None): + result = getattr(address, name) + if isinstance(result, tuple): + return result[0] + return result + + +def get_property(psets, pset_name, prop_name, decimals=None): + if pset_name in psets: + result = psets[pset_name].get(prop_name, None) + if decimals is None or result is None: + return result + return round(result, decimals) + + class Parser: def __init__(self, logger): self.logger = logger diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst index 043fa0689c..439d4cec69 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst @@ -1,16 +1,19 @@ Installation ============ -There are different methods of installation, depending on your situation. +There are different methods of installation, depending on your situation. If +you aren't sure which to choose, if you're a programmer, go for the **Pre-built +packages**. If you aren't a programmer, go for the **BlenderBIM Add-on**. 1. **Pre-built packages** is recommended for users wanting to use the latest IfcOpenShell builds. 2. **PyPI** is recommended for developers using Pip. 3. **Conda** is recommended for developers using Anaconda. 4. **Docker** is recommended for developers using Docker. 5. **AWS Lambda** is recommended for developers using AWS Lambda functions. -6. **Using the BlenderBIM Add-on** is recommended for non-developers wanting a graphical interface. -7. **From source with precompiled binaries** is recommended for developers actively working with the Python code. -8. **Compiling from source** is recommended for developers actively working with the C++ core. +6. **Google Colab** is recommended for developers using Google Colab. +7. **Using the BlenderBIM Add-on** is recommended for non-developers wanting a graphical interface. +8. **From source with precompiled binaries** is recommended for developers actively working with the Python code. +9. **Compiling from source** is recommended for developers actively working with the C++ core. Pre-built packages ------------------ @@ -182,6 +185,18 @@ Gateways, etc. the AWS documentation. Some tools that could be useful are AWS CloudFormaton, AWS CDK, pulumi or terraform. +Google Colab +------------ + +The Google Colab environment is based on the distribution from PyPI, but lets +you run it in an online notebook without any local setup required. This is +great for educators and those wanting to try it out without control on their +local system. + +`Click here +`__ +to launch a simple notebook. + Using the BlenderBIM Add-on --------------------------- diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py index 754b8b02ca..b09e7f07fa 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py @@ -77,28 +77,41 @@ class Usecase: cost_item=item, products=[slab], prop_name="NetVolume") """ self.file = file - self.settings = {"cost_item": cost_item, "products": products or [], "prop_name": prop_name} + self.settings = { + "cost_item": cost_item, + "products": products or [], + "prop_name": prop_name, + } def execute(self): if self.settings["prop_name"]: self.quantities = set(self.settings["cost_item"].CostQuantities or []) for product in self.settings["products"]: - ifcopenshell.api.run( - "control.assign_control", - self.file, - related_object=product, - relating_control=self.settings["cost_item"], + self.assign_cost_control( + related_object=product, cost_item=self.settings["cost_item"] ) if self.settings["prop_name"]: + if ( + self.settings["cost_item"].CostQuantities + and self.settings["cost_item"].CostQuantities[0].Name.lower() + != self.settings["prop_name"].lower() + ) or not product.is_a("IfcObject"): + continue self.add_quantity_from_related_object(product) if self.settings["prop_name"]: self.settings["cost_item"].CostQuantities = list(self.quantities) else: self.update_cost_item_count() + def assign_cost_control(self, related_object, cost_item): + return ifcopenshell.api.run( + "control.assign_control", + self.file, + related_object=related_object, + relating_control=cost_item, + ) + def add_quantity_from_related_object(self, element): - if not element.is_a("IfcObject"): - return for relationship in element.IsDefinedBy: if relationship.is_a("IfcRelDefinesByProperties"): self.add_quantity_from_qto(relationship.RelatingPropertyDefinition) @@ -107,14 +120,17 @@ class Usecase: if not qto.is_a("IfcElementQuantity"): return for prop in qto.Quantities: - if prop.is_a("IfcPhysicalSimpleQuantity") and prop.Name.lower() == self.settings["prop_name"].lower(): + if ( + prop.is_a("IfcPhysicalSimpleQuantity") + and prop.Name.lower() == self.settings["prop_name"].lower() + ): self.quantities.add(prop) def update_cost_item_count(self): # This is a bold assumption # https://forums.buildingsmart.org/t/how-does-a-cost-item-know-that-it-is-counting-a-controlled-product/3564 if not self.settings["cost_item"].CostQuantities: - return ifcopenshell.api.run( + ifcopenshell.api.run( "cost.add_cost_item_quantity", self.file, cost_item=self.settings["cost_item"], diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py index e4b48bafa0..967cec4162 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py @@ -98,4 +98,7 @@ class Usecase: count = 0 for rel in self.settings["cost_item"].Controls: count += len(rel.RelatedObjects) - quantity[3] = count + if count: + quantity[3] = count + else: + self.file.remove(quantity) diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py b/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py index 9506c51478..64fba5975c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py @@ -46,10 +46,17 @@ class Usecase: def execute(self): for reference in self.settings["information"].HasDocumentReferences or []: ifcopenshell.api.run("document.remove_reference", self.file, reference=reference) + for rel in self.settings["information"].IsPointer or []: for information in rel.RelatedDocuments: ifcopenshell.api.run("document.remove_information", self.file, information=information) self.file.remove(rel) + + # remove IfcDocumentInformationRelationship so it won't become invalid + for rel in self.settings["information"].IsPointedTo or []: + if rel.RelatedDocuments == (self.settings["information"],): + self.file.remove(rel) + for rel in self.settings["information"].DocumentInfoForObjects or []: self.file.remove(rel) self.file.remove(self.settings["information"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py index a6fecfb55c..0e784ad207 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py @@ -69,7 +69,7 @@ class Usecase: outer_curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in outer_points]) inner_curves = [] for inner_point in inner_points: - inner_curves.append(self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in inner_point]) + inner_curves.append(self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in inner_point])) else: outer_curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList3D(outer_points)) inner_curves = [] diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 2e8669b75d..bf0ddf616b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -339,6 +339,8 @@ def get_material(element, should_skip_usage=False, should_inherit=True): The material may be a single material, material set (layered, profiled, or constituent), or a material set usage. + :param element: The element to get the material of. + :type element: ifcopenshell.entity_instance.entity_instance :param should_skip_usage: If set to True, if the material is a material set usage, the material set itself will be returned. Useful if you don't care about occurrence usage parameters. If False, the usage will be @@ -378,6 +380,8 @@ def get_materials(element, should_inherit=True): If the element has a material set, the individual materials of that set are returned as a list. + :param element: The element to get the materials of. + :type element: ifcopenshell.entity_instance.entity_instance :param should_inherit: If True, any inherited materials from associated types will be considered. :return: The associated materials of the element. @@ -403,6 +407,51 @@ def get_materials(element, should_inherit=True): return [c.Material for c in material.MaterialConstituents] +def get_styles(element): + """Retrieves the styles used in an element's representation. + + Styles may be retreived from the material or the body representation. + + :param element: The element to get the styles of. + :type element: ifcopenshell.entity_instance.entity_instance + :return: A list of surface styles + :rtype: list[ifcopenshell.entity_instance.entity_instance] + + Example: + + .. code:: python + + wall = file.by_type("IfcWall")[0] + styles = ifcopenshell.util.element.get_styles(wall) + """ + styles = [] + + materials = ifcopenshell.util.element.get_materials(element) + for material in materials: + for material_definition_representation in material.HasRepresentation or []: + for representation in material_definition_representation.Representations: + for item in representation.Items: + styles.extend([s for s in item.Styles if s.is_a("IfcSurfaceStyle")]) + + body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + if not body: + return styles + + for representation in [body]: + queue = list(representation.Items) + while queue: + item = queue.pop() + if item.is_a("IfcMappedItem"): + queue.extend(item.MappingSource.MappedRepresentation.Items) + if item.is_a("IfcBooleanResult"): + queue.append(item.FirstOperand) + queue.append(item.SecondOperand) + if item.StyledByItem: + styles.extend([s for s in item.StyledByItem[0].Styles if s.is_a("IfcSurfaceStyle")]) + return styles + + + def get_elements_by_material(ifc_file, material): """Retrieves the elements related to a material. diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 6f940bdf8b..f44160fbf6 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -520,6 +520,10 @@ class Selector: value = ifcopenshell.util.element.get_type(value) elif key in ("material", "mat"): value = ifcopenshell.util.element.get_material(value, should_skip_usage=True) + elif key in ("materials", "mats"): + value = ifcopenshell.util.element.get_materials(value) + elif key == "styles": + value = ifcopenshell.util.element.get_styles(value) elif key in ("item", "i"): if value.is_a("IfcMaterialLayerSet"): value = value.MaterialLayers diff --git a/src/ifcopenshell-python/test/api/document/test_remove_information.py b/src/ifcopenshell-python/test/api/document/test_remove_information.py index 4df4857052..d03a7341ad 100644 --- a/src/ifcopenshell-python/test/api/document/test_remove_information.py +++ b/src/ifcopenshell-python/test/api/document/test_remove_information.py @@ -37,6 +37,19 @@ class TestRemoveInformation(test.bootstrap.IFC4): assert len(self.file.by_type("IfcDocumentReference")) == 0 assert len(self.file.by_type("IfcRelAssociatesDocument")) == 0 + # test removing relationship to another information if it was the only relating element + information = ifcopenshell.api.run("document.add_information", self.file, parent=None) + information1 = ifcopenshell.api.run("document.add_information", self.file, parent=information) + information2 = ifcopenshell.api.run("document.add_information", self.file, parent=information) + + ifcopenshell.api.run("document.remove_information", self.file, information=information1) + assert len(self.file.by_type("IfcDocumentInformation")) == 2 + assert len(self.file.by_type("IfcDocumentInformationRelationship")) == 1 + + ifcopenshell.api.run("document.remove_information", self.file, information=information2) + assert len(self.file.by_type("IfcDocumentInformation")) == 1 + assert len(self.file.by_type("IfcDocumentInformationRelationship")) == 0 + def test_removing_all_subdocuments_and_their_references_too(self): project = self.file.createIfcProject() information = ifcopenshell.api.run("document.add_information", self.file, parent=None) diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index c4e8652472..499f448567 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -386,6 +386,53 @@ class TestGetMaterial(test.bootstrap.IFC4): assert subject.get_material(element, should_inherit=False) is None +class TestGetMaterials(test.bootstrap.IFC4): + def test_getting_the_materials_of_a_product(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + material = ifcopenshell.api.run("material.add_material", self.file) + ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + assert subject.get_materials(element) == [material] + + +class TestGetStyles(test.bootstrap.IFC4): + def test_getting_the_styles_of_a_product(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + assert subject.get_styles(element) == [] + + model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model") + body = ifcopenshell.api.run("context.add_context", self.file, + context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model) + + material = ifcopenshell.api.run("material.add_material", self.file) + ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + + style = ifcopenshell.api.run("style.add_style", self.file) + ifcopenshell.api.run("style.add_surface_style", self.file, + style=style, ifc_class="IfcSurfaceStyleShading", attributes={ + "SurfaceColour": { "Name": None, "Red": 1.0, "Green": 0.8, "Blue": 0.8 }, + "Transparency": 0., # 0 is opaque, 1 is transparent + }) + ifcopenshell.api.run("style.assign_material_style", self.file, material=material, style=style, context=body) + + assert subject.get_styles(element) == [style] + + style2 = ifcopenshell.api.run("style.add_style", self.file) + ifcopenshell.api.run("style.add_surface_style", self.file, + style=style2, ifc_class="IfcSurfaceStyleShading", attributes={ + "SurfaceColour": { "Name": None, "Red": 1.0, "Green": 0.8, "Blue": 0.8 }, + "Transparency": 0., # 0 is opaque, 1 is transparent + }) + + representation = ifcopenshell.api.run("geometry.add_wall_representation", self.file, + context=body, length=5, height=3, thickness=0.118) + + ifcopenshell.api.run("geometry.assign_representation", self.file, product=element, representation=representation) + ifcopenshell.api.run("style.assign_representation_styles", self.file, shape_representation=representation, styles=[style2]) + + assert subject.get_styles(element) == [style, style2] + + class TestGetElementsByMaterial(test.bootstrap.IFC4): def test_getting_elements_of_a_material(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")