diff --git a/conda/bld.bat b/conda/bld.bat index b25f46c9d6..c426d75916 100644 --- a/conda/bld.bat +++ b/conda/bld.bat @@ -4,6 +4,7 @@ set MY_PY_VER=%PY_VER:.=% set LIBXML2="%LIBRARY_PREFIX%/lib/libxml2.lib" cmake -G "Ninja" ^ + -D SCHEMA_VERSIONS="2x3;4;4x1;4x3;4x3_add1" ^ -D CMAKE_BUILD_TYPE:STRING=Release ^ -D CMAKE_INSTALL_PREFIX:FILEPATH="%LIBRARY_PREFIX%" ^ -D CMAKE_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^ diff --git a/conda/build.sh b/conda/build.sh index 77c9d54e8f..275e2cbfc5 100644 --- a/conda/build.sh +++ b/conda/build.sh @@ -13,6 +13,7 @@ if [ `uname` == Darwin ]; then fi cmake -G Ninja \ + -DSCHEMA_VERSIONS="2x3;4;4x1;4x3;4x3_add1" \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=$PREFIX \ ${CMAKE_PLATFORM_FLAGS[@]} \ diff --git a/conda/meta.yaml b/conda/meta.yaml index 38c4d2bbc5..5afdd5a56c 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -36,10 +36,10 @@ requirements: run: - python - - boost-cpp - - occt ==7.7.0 + - {{ pin_compatible('occt', max_pin='x.x.x') }} + - {{ pin_compatible('cgal-cpp', max_pin='x.x.x') }} + - {{ pin_compatible('boost-cpp', max_pin='x.x.x') }} - libxml2 - - cgal-cpp - hdf5 - mpfr - gmp # [unix] diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 761ed00193..cfcd70e376 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -145,6 +145,8 @@ classes = [ ui.BIM_PT_tab_services_object, # Structural analysis ui.BIM_PT_tab_structural, + # Construction scheduling + ui.BIM_PT_tab_4D5D, # Facility management ui.BIM_PT_tab_handover, ui.BIM_PT_tab_operations, 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 351e9ae0e9..bdf2e7ef8b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -21,7 +21,6 @@ import blenderbim.bim.module.cost.prop as CostProp from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.cost.data import CostSchedulesData -import blenderbim.tool as tool class BIM_PT_cost_schedules(Panel): @@ -30,10 +29,12 @@ class BIM_PT_cost_schedules(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3" + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): if not CostSchedulesData.is_loaded: @@ -46,8 +47,8 @@ class BIM_PT_cost_schedules(Panel): row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT") row.operator("bim.export_cost_schedules", text="Export as spreadsheet", icon="EXPORT") else: - row.label(text="No Cost Schedules found.", icon="COMMUNITY") - row = self.layout.row() + row.label(text="No Cost Schedules found.", icon="TEXT") + row = self.layout.row(align=True) row.alignment = "RIGHT" row.prop(self.props, "cost_schedule_predefined_types") row.operator("bim.add_cost_schedule", icon="ADD", text="Add") @@ -138,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): @@ -619,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(",", " ") @@ -642,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: @@ -656,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/annotation.py b/src/blenderbim/blenderbim/bim/module/drawing/annotation.py index c49dbd8c9d..10e1e59e5a 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/annotation.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/annotation.py @@ -166,6 +166,7 @@ class Annotator: center = camera.matrix_world.inverted() @ bpy.context.scene.cursor.location center.z = 0 + center = camera.matrix_world @ center return ( center + z_offset, diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index e392084f8d..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): @@ -520,7 +525,11 @@ class BaseDecorator: return matrix.inverted()[i].to_3d().normalized() text_dir_world_x_axis = get_basis_vector(obj.matrix_world) - text_dir = (camera.matrix_world.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() + camera_matrix = camera.matrix_world.copy() + camera_matrix[0][0] = 1 + camera_matrix[1][1] = 1 + camera_matrix[2][2] = 1 + text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() pos = location_3d_to_region_2d(region, region3d, text_world_position) props = obj.BIMTextProperties diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 2717eae142..d85cd2dbe9 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -485,7 +485,7 @@ class CreateDrawing(bpy.types.Operator): drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element) - self.setup_serialiser(ifc) + self.setup_serialiser(ifc, target_view) cache = IfcStore.get_cache() [cache.remove(guid) for guid in invalidated_guids] tree = ifcopenshell.geom.tree() @@ -829,7 +829,7 @@ class CreateDrawing(bpy.types.Operator): return svg_path - def setup_serialiser(self, ifc): + def setup_serialiser(self, ifc, target_view): self.svg_settings = ifcopenshell.geom.settings( DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True ) @@ -853,6 +853,8 @@ class CreateDrawing(bpy.types.Operator): self.serialiser.setScale(self.scale) self.serialiser.setSubtractionSettings(ifcopenshell.ifcopenshell_wrapper.ALWAYS) self.serialiser.setUsePrefiltering(True) # See #3359 + if target_view == "REFLECTED_PLAN_VIEW": + self.serialiser.setMirrorY(True) # tree = ifcopenshell.geom.tree() # This instructs the tree to explode BReps into faces and return # the style of the face when running tree.select_ray() @@ -887,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: @@ -897,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]"): @@ -906,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 @@ -1512,6 +1534,9 @@ class RemoveDrawing(bpy.types.Operator, Operator): tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected ] else: + if not self.drawing: + self.report({"ERROR"}, "No drawing selected") + return {"CANCELLED"} drawings = [tool.Ifc.get().by_id(self.drawing)] removed_drawings = [drawing.id() for drawing in drawings] @@ -1862,7 +1887,7 @@ class RemoveSchedule(bpy.types.Operator, Operator): schedule: bpy.props.IntProperty() def _execute(self, context): - core.remove_document(tool.Ifc, tool.Drawing, "SCHEDULE", schedule=tool.Ifc.get().by_id(self.schedule)) + core.remove_document(tool.Ifc, tool.Drawing, "SCHEDULE", document=tool.Ifc.get().by_id(self.schedule)) class OpenSchedule(bpy.types.Operator, Operator): diff --git a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py index db501355d3..a703075772 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py @@ -118,6 +118,25 @@ class Scheduler: if cell_style: related_styles.append((style_name, cell_style)) + # sometimes there are no column styles (e.g. in ODS from IfcCSV) + # and we just use some constant number for column widths + if not column_widths: + row_columns = [] + for tr in table.getElementsByType(TableRow): + row_cols = 0 + for td in tr.getElementsByType(TableCell): + column_span = td.getAttribute("numbercolumnsspanned") + column_span = int(column_span) if column_span else 1 + + col_repeat = td.getAttribute("numbercolumnsrepeated") + col_repeat = int(col_repeat) if col_repeat else 1 + row_cols += column_span * col_repeat + row_columns.append(row_cols) + + n_columns = max(row_columns) + column_widths = [25] * n_columns # some constant width value 👀 + column_styles = [None] * n_columns + # collect rows height row_heights = [] # TODO: never used yet because unsure about priority for row styles @@ -351,7 +370,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/svgwriter.py b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py index a0ed721fa5..9b3fad9ee8 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py @@ -791,7 +791,14 @@ class SvgWriter: return matrix.inverted()[i].to_3d().normalized() text_dir_world_x_axis = get_basis_vector(text_obj.matrix_world) - text_dir = (self.camera.matrix_world.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() + + # RCP cameras may be scaled, so reset scales. + camera_matrix = self.camera.matrix_world.copy() + camera_matrix[0][0] = 1 + camera_matrix[1][1] = 1 + camera_matrix[2][2] = 1 + + text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() angle = math.degrees(-text_dir.angle_signed(Vector((1, 0)))) classes = self.get_attribute_classes(text_obj) 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/georeference/__init__.py b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py index 146ce8b1c3..6a1c3fb1b6 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py @@ -34,6 +34,7 @@ classes = ( operator.GetCursorLocation, operator.SetCursorLocation, operator.ConvertAngleToCoordinates, + operator.ImportPlot, prop.BIMGeoreferenceProperties, ui.BIM_PT_gis, ui.BIM_PT_gis_utilities, diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index cbcfe930ea..eec2dab9ca 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -110,7 +110,7 @@ class SetCursorLocation(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.set_cursor_location" bl_label = "Set Cursor Location" bl_options = {"REGISTER", "UNDO"} - bl_description = "Move curson location to the specified coordinates" + bl_description = "Move cursor location to the specified coordinates" @classmethod def poll(cls, context): @@ -187,3 +187,20 @@ class ConvertAngleToCoordinates(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.convert_angle_to_coord(tool.Georeference, type=self.type) + + +class ImportPlot(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.import_plot" + bl_label = "Import Plot" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Import plot" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) + + def execute(self, context): + core.import_plot(tool.Georeference, filepath=self.filepath) + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} diff --git a/src/blenderbim/blenderbim/bim/module/misc/ui.py b/src/blenderbim/blenderbim/bim/module/misc/ui.py index 9fe4e178cc..a2c9ebb0ec 100644 --- a/src/blenderbim/blenderbim/bim/module/misc/ui.py +++ b/src/blenderbim/blenderbim/bim/module/misc/ui.py @@ -50,8 +50,9 @@ class BIM_PT_misc_utilities(bpy.types.Panel): row.operator("bim.clean_wireframes") row = layout.row() row.operator("bim.patch_non_parametric_mep_segment") - row = layout.row(align=True) row.operator("bim.enable_editing_sketch_extrusion_profile", text="Start Sketching") row.operator("bim.edit_sketch_extrusion_profile", text="", icon="FILE_REFRESH") row.operator("bim.disable_editing_sketch_extrusion_profile", text="", icon="CANCEL") + row = layout.row() + row.operator("bim.import_plot", text="Import Plot Coordinates", icon="FILE_FOLDER") diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 9d732a9141..ee5224b157 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -177,6 +177,7 @@ classes = ( roof.RemoveRoof, roof.SetGableRoofEdgeAngle, mep.MEPAddObstruction, + mep.MEPAddTransition, ) addon_keymaps = [] diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index ccbde59675..4274e1c6cd 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -18,7 +18,10 @@ import bpy import math +import collections import bmesh +import re +import json import ifcopenshell import ifcopenshell.api import ifcopenshell.util.unit @@ -31,13 +34,13 @@ import blenderbim.core.type import blenderbim.core.root import blenderbim.core.geometry import blenderbim.tool as tool -from math import pi, degrees +from math import pi, degrees, radians +from copy import copy from mathutils import Vector, Matrix -import re +from ifcopenshell.util.shape_builder import ShapeBuilder from blenderbim.bim.module.model.profile import DumbProfileJoiner V = lambda *x: Vector([float(i) for i in x]) -float_is_zero = lambda f: 0.0001 >= f >= -0.0001 class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator): @@ -70,29 +73,68 @@ class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator): [e for e in ifcopenshell.util.system.get_connected_from(element) if e not in processed_elements] ) - if len(connected) == 1: - extend_branch(list(connected)[0], branch, element) - else: - for connected_element in connected: - branch_element["children"].append(extend_branch(connected_element, [], element)) + for connected_element in connected: + branch_element["children"].append(extend_branch(connected_element, [], element)) return branch - queue = extend_branch(current_element, [])[0]["children"] + extended_branch = extend_branch(current_element, []) + queue = extended_branch[0]["children"] # import pprint # pprint.pprint(queue) + def get_connected_ports_between(element1, element2): + ports1 = tool.System.get_ports(element1) + ports2 = tool.System.get_ports(element2) + + for p in ports1: + connected_port = tool.System.get_connected_port(p) + # in IFC2X3 there is no PredefinedType + if getattr(p, "PredefinedType", None) == "WIRELESS": + continue + if connected_port in ports2: + return p, connected_port + + return None, None + + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + def process_branch(branch): for branch_element in branch: element = branch_element["element"] - print('processing', element) + print("processing", element) predecessor = branch_element["predecessor"] - if False: # If the element does not need to be transformed, return early. - return + # Perform the extend, translate, rotate, etc the element as necessary based on the predecessor. - # For segments, prioritise extensions instead of translations. - # For everything else, only translate. No rotation. + # For everything besides segments, only translate. No rotation. + + obj = tool.Ifc.get_object(element) + obj_pred = tool.Ifc.get_object(predecessor) + if tool.Ifc.is_moved(obj): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + if tool.Ifc.is_moved(obj_pred): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj_pred) + + port, port_pred = get_connected_ports_between(element, predecessor) + port_matrix_pred = tool.Model.get_element_matrix(port_pred) + + # Only segments can be extended + # extension for them takes priority over translation + if element.is_a("IfcFlowSegment"): + DumbProfileJoiner().join_E(obj, port_matrix_pred.translation * si_conversion) + context.view_layer.update() # update since extrusion might involve changing object's location + + port_martix = tool.Model.get_element_matrix(port) + port_location = port_martix.translation + port_location_pred = port_matrix_pred.translation + if not tool.Cad.are_vectors_equal(port_location, port_location_pred): + obj.location += (port_location_pred - port_location) * si_conversion + context.view_layer.update() # otherwise tool.Ifc.is_moved won't get triggered + else: + # If the element does not need to be transformed, return early. + return + for child_branch in branch_element["children"]: process_branch(child_branch) @@ -107,6 +149,9 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): + # TODO: need to add ui for parameters: + # - obstruction cap thickness + # - start/end thickness and angle for transition selected_objs = [] selected_profiles = [] @@ -162,6 +207,7 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator): elif total_profiles == 2: if is_parallel: fitting_type = "TRANSITION" + bpy.ops.bim.mep_add_transition() elif total_selected_objs == 3: if total_profiles > 1: @@ -207,12 +253,7 @@ class MEPGenerator: ports = tool.System.get_ports(segment) if segment.is_a("IfcFlowSegment") and not ports: - for mat in [start_port_matrix, end_port_matrix]: - # TODO: specify PredefinedType based on the segment type - port = tool.Ifc.run("system.add_port", element=segment) - port.FlowDirection = "NOTDEFINED" - port.PredefinedType = self.get_port_predefined_type(segment) - tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=mat, is_si=True) + tool.System.add_ports(obj) return # adjust current segment ports and related flow segments @@ -228,6 +269,10 @@ class MEPGenerator: if port_position == "end_port": tool.Model.edit_element_placement(port, end_port_matrix) + continue + + # NOTE: currently this functionality is moved to bim.regenerate_distribution_element + connected_port = tool.System.get_connected_port(port) if not connected_port: continue @@ -248,7 +293,6 @@ class MEPGenerator: ): if port_position == "start_port": if segment.is_a("IfcFlowFitting"): - profile_joiner = DumbProfileJoiner() connected_element_length = ( tool.Model.get_flow_segment_axis(connected_obj)[0] - tool.Model.get_flow_segment_axis(obj)[0] @@ -269,45 +313,103 @@ class MEPGenerator: extrusion_depth = segment_object.dimensions.z end_point = segment_object.matrix_world @ V(0, 0, extrusion_depth) segment_data = { - "start_point": start_point, - "end_point": end_point, + "start_point": start_point.copy().freeze(), + "end_point": end_point.freeze(), "ports": ports, "extrusion_depth": extrusion_depth, } for port in ports: port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates) - if float_is_zero(port_local_position.length): + if tool.Cad.is_x(port_local_position.length, 0.0): segment_data["start_port"] = port else: segment_data["end_port"] = port return segment_data - def get_port_predefined_type(self, segment): - split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x) - class_name = "".join(split_camel_case(segment.is_a())[1:-1]).upper() - if class_name == "CONVEYOR": - return "NOTDEFINED" - return class_name - def get_mep_element_class_name(self, element, mep_class_type): split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x) class_name = "".join(split_camel_case(element.is_a())[:-1] + [mep_class_type]) return class_name - def get_compatible_fitting_type(self, segment, predefined_type): - """We find compatible fitting only by checking if they were - already used with that segment type before. + def get_compatible_fitting_type(self, segment_or_segments, port_or_ports, predefined_type): + """ + returns a dict of compatible fitting_type and start_port_match flag to correctly place the fitting. + + We find compatible fitting only by checking + if they were already used with that segment type before + and fitting's ports should match `port_or_ports` by PredefinedType and SystemType. + + If port from `port_or_ports` has PredefinedType/SystemType == None/NOTDEFINED then + those parameters won't be taken into account checking compatibility. There lies the problem that it won't be - able to identify the fittings that were not connected to any segments yet. + able to identify the fittings that were not yet connected to any segments yet. """ - segment_type = ifcopenshell.util.element.get_type(segment) - if not segment_type: - return None + if not isinstance(segment_or_segments, collections.abc.Iterable): + segments = [segment_or_segments] + ports = [port_or_ports] + else: + segments = segment_or_segments + ports = port_or_ports - fitting_types = tool.Ifc.get().by_type(self.get_mep_element_class_name(segment, "Fitting")) + segments_data = [] + for segment, port in zip(segments, ports, strict=True): + segment_type = ifcopenshell.util.element.get_type(segment) + # if segment doesn't have type we cannot check compatibility by available occurences + if segment_type is None: + return + segments_data.append((segment_type, port.PredefinedType, port.SystemType)) + + def are_connected_elements_compatible(segments_data, fitting_data): + # prevent arguments mutation, not using deepcopy because of the errors with ifc elements + segments_data = [copy(i) for i in segments_data] + fitting_data = [copy(i) for i in fitting_data] + not_defined_values = {"NOTDEFINED", None} + + if len(segments_data) != len(fitting_data): + return False + + def are_segments_compatible(test_segment_data, base_segment_data): + segment_type, predefined_type, system_type = test_segment_data + base_segment_type, base_predefined_type, base_system_type = base_segment_data + + if segment_type != base_segment_type: + return False + + if predefined_type not in not_defined_values and predefined_type != base_predefined_type: + return False + + if system_type not in not_defined_values and system_type != base_system_type: + return False + + return True + + # NOTE: I have a feeling that there are cases where order + # in which we're checking the segments is important + # but I couldn't pin it down exact cases + for test_segment_data in fitting_data[:]: + for base_segment_data in segments_data: + if not are_segments_compatible(test_segment_data, base_segment_data): + continue + segments_data.remove(test_segment_data) + + # all segments were sorted + return len(segments_data) == 0 + + def pack_return_data(fitting_type, ports, segments_data): + for port in ports: + port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates) + if tool.Cad.is_x(port_local_position.length, 0.0): + start_port = port + break + connected_port = tool.System.get_connected_port(start_port) + connected_element = tool.System.get_port_relating_element(connected_port) + element_type = ifcopenshell.util.element.get_type(connected_element) + return {"fitting_type": fitting_type, "start_port_match": element_type == segments_data[0][0]} + + fitting_types = tool.Ifc.get().by_type(self.get_mep_element_class_name(segments[0], "FittingType")) for fitting_type in fitting_types: if fitting_type.PredefinedType != predefined_type: continue @@ -315,13 +417,24 @@ class MEPGenerator: if not fittings: continue fitting = fittings[0] - elements = set( - ifcopenshell.util.system.get_connected_to(fitting) - + ifcopenshell.util.system.get_connected_from(fitting) - ) - for element in elements: - if element.IsTypedBy and element.IsTypedBy[0].RelatingType == segment_type: - return fitting_type + + ports = ifcopenshell.util.system.get_ports(fitting) + fitting_data = [] + fitting_connected_to_none_type = False + for port in ports: + connected_port = tool.System.get_connected_port(port) + connected_element = tool.System.get_port_relating_element(connected_port) + element_type = ifcopenshell.util.element.get_type(connected_element) + if element_type is None: + fitting_connected_to_none_type = True + break + fitting_data.append((element_type, port.PredefinedType, port.SystemType)) + + if fitting_connected_to_none_type: + continue + + if are_connected_elements_compatible(segments_data, fitting_data): + return pack_return_data(fitting_type, ports, segments_data) def create_obstruction_type(self, segment): # code is very similar to "bim.add_type" @@ -333,7 +446,8 @@ class MEPGenerator: ifc_file = tool.Ifc.get() body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") - obj = bpy.data.objects.new("Fitting", None) + obj = bpy.data.objects.new("Obstruction", None) + # TODO: OBSTRUCTION predefined type is available only for IfcDuctFitting and IfcPipeFitting element = blenderbim.core.root.assign_class( tool.Ifc, tool.Collector, @@ -377,7 +491,8 @@ class MEPGenerator: segment_obj = tool.Ifc.get_object(segment) segment_matrix = segment_obj.matrix_world segment_rotation = segment_matrix.to_quaternion() - obstruction_type = self.get_compatible_fitting_type(segment, "OBSTRUCTION") + fitting_data = self.get_compatible_fitting_type(segment, related_port, "OBSTRUCTION") + obstruction_type = fitting_data["fitting_type"] if fitting_data else None if not obstruction_type: obstruction_type = self.create_obstruction_type(segment) @@ -389,17 +504,11 @@ class MEPGenerator: obstruction_obj.matrix_world = segment_matrix profile_joiner.set_depth(obstruction_obj, length) - obstruction = tool.Ifc.get_entity(obstruction_obj) - # TODO: specify PredefinedType based on the segment type - obstruction_port = tool.Ifc.run("system.add_port", element=obstruction) - obstruction_port.PredefinedType = self.get_port_predefined_type(obstruction) - port_local_position = Matrix.Translation((0, 0, length)) if at_segment_start else Matrix() - tool.Ifc.run( - "geometry.edit_object_placement", - product=obstruction_port, - matrix=segment_matrix @ port_local_position, - is_si=True, - ) + obstruction_port = tool.System.add_ports( + obstruction_obj, + add_start_port=not at_segment_start, + add_end_port=at_segment_start, + )[0] # change segment length new_segment_length = segment_data["extrusion_depth"] - length @@ -411,6 +520,7 @@ class MEPGenerator: obstruction_obj.location += segment_rotation @ V(0, 0, new_segment_length) tool.Ifc.run("system.connect_port", port1=related_port, port2=obstruction_port, direction="NOTDEFINED") + obstruction = tool.Ifc.get_entity(obstruction_obj) return obstruction, None @@ -449,3 +559,153 @@ class MEPAddObstruction(bpy.types.Operator, tool.Ifc.Operator): return {"CANCELLED"} return {"FINISHED"} + + +class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.mep_add_transition" + bl_label = "Add Transition" + bl_description = ( + "Adds transition between two MEP elements. Elements are either provided by ID or selected in Blender" + ) + bl_options = {"REGISTER", "UNDO"} + start_length: bpy.props.FloatProperty( + name="Start Length", description="Transition start length in SI units", default=0.1, subtype="DISTANCE" + ) + end_length: bpy.props.FloatProperty( + name="End Length", description="Transition end length in SI units", default=0.1, subtype="DISTANCE" + ) + start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0) + end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0) + + def _execute(self, context): + start_element, end_element = None, None + ifc_file = tool.Ifc.get() + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) + + if self.start_segment_id and self.end_segment_id: + start_element = ifc_file.by_id(self.start_segment_id) + end_element = ifc_file.by_id(self.end_segment_id) + start_object = tool.Ifc.get_object(start_element) + end_object = tool.Ifc.get_object(end_element) + + elif len(context.selected_objects) == 2: + start_object = context.active_object + end_object = next(o for o in context.selected_objects if o != context.active_object) + start_element = tool.Ifc.get_entity(start_object) + end_element = tool.Ifc.get_entity(end_object) + if not start_element or not end_element: + self.report({"ERROR"}, f"Two IFC elements should be selected for the transition") + return {"CANCELLED"} + + else: + self.report({"ERROR"}, f"Two IFC elements should be provided for the transition") + return {"CANCELLED"} + + # TODO: support IfcFlowTerminal + def is_mep(element): + return element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting") + + if not is_mep(start_element) or not is_mep(end_element): + self.report( + {"ERROR"}, + f"Failed to add transition - some object is not a MEP element: {start_element.is_a()}, {end_element.is_a()}.", + ) + return {"CANCELLED"} + + start_axis = tool.Model.get_flow_segment_axis(start_object) + end_axis = tool.Model.get_flow_segment_axis(end_object) + + # TODO: support cases when segments are partially or completely overlapping each other + if not tool.Cad.are_edges_collinear(start_axis, end_axis): + self.report({"ERROR"}, f"Failed to add transition - non collinear segments are not yet supported.") + return {"CANCELLED"} + + start_segment_data = MEPGenerator().get_segment_data(start_element) + end_segment_data = MEPGenerator().get_segment_data(end_element) + end_port = end_segment_data["start_port"] + start_port = start_segment_data["end_port"] + + points_ports_map = { + start_segment_data["start_point"]: start_segment_data["start_port"], + start_segment_data["end_point"]: start_segment_data["end_port"], + end_segment_data["start_point"]: end_segment_data["start_port"], + end_segment_data["end_point"]: end_segment_data["end_port"], + } + + start_point, end_point = tool.Cad.closest_points( + (start_segment_data["start_point"], start_segment_data["end_point"]), + (end_segment_data["start_point"], end_segment_data["end_point"]), + ) + transition_dir = (end_point - start_point).normalized() + start_port = points_ports_map[start_point] + end_port = points_ports_map[end_point] + + # add transition representation + builder = ShapeBuilder(ifc_file) + rep, transition_data = builder.mep_transition_shape( + start_element, end_element, self.start_length / si_conversion, self.end_length / si_conversion + ) + + if not rep: + self.report({"ERROR"}, f"Failed to add transition - this kind of profiles is not yet supported.") + return {"CANCELLED"} + + middle_point = (start_point + end_point) / 2 + full_transition_length = transition_data["full_transition_length"] * si_conversion + start_segment_extend_point = middle_point - transition_dir * full_transition_length / 2 + end_segment_extend_point = middle_point + transition_dir * full_transition_length / 2 + DumbProfileJoiner().join_E(start_object, start_segment_extend_point) + DumbProfileJoiner().join_E(end_object, end_segment_extend_point) + + fitting_data = MEPGenerator().get_compatible_fitting_type( + [start_element, end_element], [start_port, end_port], "TRANSITION" + ) + + transition_type = fitting_data["fitting_type"] if fitting_data else None + start_port_match = fitting_data["start_port_match"] if fitting_data else True + + if not transition_type: + mesh = bpy.data.meshes.new("Transition") + obj = bpy.data.objects.new("Transition", mesh) + transition_type = blenderbim.core.root.assign_class( + tool.Ifc, + tool.Collector, + tool.Root, + obj=obj, + ifc_class=MEPGenerator().get_mep_element_class_name(start_element, "FittingType"), + predefined_type="TRANSITION", + should_add_representation=False, + ) + body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + tool.Model.replace_object_ifc_representation(body, obj, rep) + pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=transition_type, name="BBIM_Fitting") + ifcopenshell.api.run( + "pset.edit_pset", + tool.Ifc.get(), + pset=pset, + properties={"Data": json.dumps(transition_data, default=list)}, + ) + + # NOTE: at this point we loose current blender objects selection + bpy.ops.bim.add_constr_type_instance(relating_type_id=transition_type.id()) + transition_obj = bpy.context.active_object + + # adjust transition segment rotation and location + transition_obj.matrix_world = start_object.matrix_world + context.view_layer.update() + transition_obj_dir = tool.Cad.get_edge_direction(tool.Model.get_flow_segment_axis(transition_obj)) + direction_match = tool.Cad.are_vectors_equal(transition_obj_dir, transition_dir) + + # if there are no mismatches or everything matches up we don't need to flip the transition + if start_port_match != direction_match: + transition_obj.matrix_world = start_object.matrix_world @ Matrix.Rotation(radians(180), 4, "X") + transition_obj.location = start_segment_extend_point if start_port_match else end_segment_extend_point + + # add ports and connect them + ports = tool.System.add_ports(transition_obj) + if not start_port_match: + start_port, end_port = end_port, start_port + tool.Ifc.run("system.connect_port", port1=ports[0], port2=start_port, direction="NOTDEFINED") + tool.Ifc.run("system.connect_port", port1=ports[1], port2=end_port, direction="NOTDEFINED") + + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/roof.py b/src/blenderbim/blenderbim/bim/module/model/roof.py index 06a1b67069..cb7a1a30d1 100644 --- a/src/blenderbim/blenderbim/bim/module/model/roof.py +++ b/src/blenderbim/blenderbim/bim/module/model/roof.py @@ -40,10 +40,6 @@ from pprint import pprint # https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRoofType.htm -def float_is_zero(f): - return 0.0001 >= f >= -0.0001 - - def bm_mesh_clean_up(bm): # remove internal edges and faces # adding missing faces so we could rely on `e.is_boundary` later @@ -99,7 +95,7 @@ def is_valid_roof_footprint(bm): # should be bmesh to support edit mode bm.verts.ensure_lookup_table() base_z = bm.verts[0].co.z - all_verts_same_level = all([float_is_zero(v.co.z - base_z) for v in bm.verts[1:]]) + all_verts_same_level = all([tool.Cad.is_x(v.co.z - base_z, 0) for v in bm.verts[1:]]) if not all_verts_same_level: return ( {"ERROR"}, @@ -202,7 +198,7 @@ def generate_hiped_roof_bmesh( def find_identical_new_vert(co): for v in bm.verts: - if float_is_zero((co - v.co).length): + if tool.Cad.is_x((co - v.co).length, 0): return v def find_other_polygon_verts(edge): @@ -234,7 +230,7 @@ def generate_hiped_roof_bmesh( bottom_chords_to_remove = [] def is_footprint_vert(v): - return float_is_zero(v.co.z - footprint_z) + return tool.Cad.is_x(v.co.z - footprint_z, 0) def is_footprint_edge(edge): return all(is_footprint_vert(v) for v in edge.verts) @@ -326,7 +322,7 @@ def generate_hiped_roof_bmesh( default_offset_dir = Vector([0, 0, 1]) * roof_thickness footprint_verts = set() - if not float_is_zero(rafter_edge_angle): + if not tool.Cad.is_x(rafter_edge_angle, 0): footprint_edges = [] for edge in extruded_edges: if is_footprint_edge(edge): @@ -398,7 +394,7 @@ def update_roof_modifier_ifc_data(context): if not angle_layer: return False for edge_angle in angle_layer: - if float_is_zero(edge_angle - pi / 2): + if tool.Cad.is_x(edge_angle - pi / 2, 0): return True return False 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/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 351bb5631c..090aa0f8c4 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -20,20 +20,20 @@ import blenderbim.bim.helper from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.resource.data import ResourceData -import blenderbim.tool as tool class BIM_PT_resources(Panel): bl_label = "Resources" bl_idname = "BIM_PT_resources" - bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3" + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): self.props = context.scene.BIMResourceProperties diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 8f6fbe4d6c..65d1fb887e 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -20,12 +20,11 @@ import bpy from . import ui, prop, operator classes = ( - operator.ExpandAllTasks, - operator.ContractAllTasks, + operator.AddAnimationCamera, operator.AddSummaryTask, operator.AddTask, - operator.AddTaskColumn, operator.AddTaskBars, + operator.AddTaskColumn, operator.AddTimePeriod, operator.AddWorkCalendar, operator.AddWorkPlan, @@ -42,11 +41,15 @@ classes = ( operator.BlenderBIM_DatePickerSetDate, operator.BlenderBIM_RedrawDatePicker, operator.CalculateTaskDuration, + operator.ClearPreviousAnimation, + operator.ContractAllTasks, operator.ContractTask, - operator.CopyTaskAttribute, operator.CopyTask, + operator.CopyTaskAttribute, + operator.CreateBaseline, operator.DisableEditingSequence, operator.DisableEditingTask, + operator.DisableEditingTaskAnimationColors, operator.DisableEditingTaskTime, operator.DisableEditingWorkCalendar, operator.DisableEditingWorkPlan, @@ -67,23 +70,27 @@ classes = ( operator.EnableEditingTaskCalendar, operator.EnableEditingTaskSequence, operator.EnableEditingTaskTime, - operator.EnableEditingWorkScheduleTasks, operator.EnableEditingWorkCalendar, operator.EnableEditingWorkCalendarTimes, operator.EnableEditingWorkPlan, operator.EnableEditingWorkPlanSchedules, operator.EnableEditingWorkSchedule, + operator.EnableEditingWorkScheduleTasks, operator.EnableEditingWorkTime, + operator.ExpandAllTasks, operator.ExpandTask, operator.ExportMSP, operator.ExportP6, operator.GenerateGanttChart, operator.GuessDateRange, - operator.ImportMSP, + operator.HighlightTask, operator.ImportCSV, + operator.ImportMSP, operator.ImportP6, operator.ImportP6XER, operator.ImportPP, + operator.LoadProductTasks, + operator.LoadTaskAnimationColors, operator.LoadTaskInputs, operator.LoadTaskOutputs, operator.LoadTaskProperties, @@ -98,10 +105,10 @@ classes = ( operator.RemoveWorkSchedule, operator.RemoveWorkTime, operator.ReorderTask, - operator.SelectTaskRelatedProducts, operator.SelectTaskRelatedInputs, - operator.SelectWorkScheduleProducts, + operator.SelectTaskRelatedProducts, operator.SelectUnassignedWorkScheduleProducts, + operator.SelectWorkScheduleProducts, operator.SetTaskSortColumn, operator.SetupDefaultTaskColumns, operator.UnassignLagTime, @@ -113,11 +120,6 @@ classes = ( operator.UnassignWorkSchedule, operator.VisualiseWorkScheduleDate, operator.VisualiseWorkScheduleDateRange, - operator.LoadTaskAnimationColors, - operator.DisableEditingTaskAnimationColors, - operator.LoadProductTasks, - operator.HighlightTask, - operator.CreateBaseline, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, @@ -137,6 +139,7 @@ classes = ( ui.BIM_PT_work_schedules, ui.BIM_PT_work_calendars, ui.BIM_PT_task_icom, + ui.BIM_PT_animation_tools, ui.BIM_UL_task_columns, ui.BIM_UL_task_inputs, ui.BIM_UL_task_resources, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/data.py b/src/blenderbim/blenderbim/bim/module/sequence/data.py index 123e51977c..f08a5d79ed 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/data.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/data.py @@ -19,7 +19,6 @@ import bpy import blenderbim.tool as tool import ifcopenshell -from ifcopenshell.util.doc import get_predefined_type_doc import ifcopenshell.util.date as dateutil @@ -37,7 +36,6 @@ class SequenceData: @classmethod def load(cls): cls.data = { - "predefined_types": cls.get_work_schedule_types(), "has_work_plans": cls.has_work_plans(), "has_work_schedules": cls.has_work_schedules(), "has_work_calendars": cls.has_work_calendars(), @@ -230,22 +228,6 @@ class SequenceData: data["NestingIndex"] = rel.RelatedObjects.index(task) cls.data["tasks"][task.id()] = data - @classmethod - def get_work_schedule_types(cls): - results = [] - declaration = tool.Ifc().schema().declaration_by_name("IfcWorkSchedule") - version = tool.Ifc.get_schema() - for attribute in declaration.attributes(): - if attribute.name() == "PredefinedType": - results.extend( - [ - (e, e, get_predefined_type_doc(version, "IfcWorkSchedule", e)) - for e in attribute.type_of_attribute().declared_type().enumeration_items() - ] - ) - break - return results - class WorkScheduleData: data = {} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 735430b403..bb6f9d17ab 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -129,9 +129,21 @@ class AddWorkSchedule(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_work_schedule" bl_label = "Add Work Schedule" bl_options = {"REGISTER", "UNDO"} + name: bpy.props.StringProperty() def _execute(self, context): - core.add_work_schedule(tool.Ifc) + core.add_work_schedule(tool.Ifc, tool.Sequence, name=self.name) + + def draw(self, context): + layout = self.layout + layout.prop(self, "name", text="Name") + self.props = context.scene.BIMWorkScheduleProperties + layout.prop(self.props, "work_schedule_predefined_types", text="Type") + if self.props.work_schedule_predefined_types == "USERDEFINED": + layout.prop(self.props,"object_type", text="Object type") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) class EditWorkSchedule(bpy.types.Operator, tool.Ifc.Operator): @@ -1389,3 +1401,22 @@ class CreateBaseline(bpy.types.Operator, tool.Ifc.Operator): def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) + + +class ClearPreviousAnimation(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.clear_previous_animation" + bl_label = "Clear Previous Animation" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.clear_previous_animation(tool.Sequence) + + +class AddAnimationCamera(bpy.types.Operator): + bl_idname = "bim.add_animation_camera" + bl_label = "Add Camera to Scene" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + core.add_animation_camera(tool.Sequence) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 07929a4771..28a77506b4 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -20,6 +20,7 @@ import bpy import isodate import ifcopenshell.api import ifcopenshell.util.attribute +from ifcopenshell.util.doc import get_predefined_type_doc import blenderbim.tool as tool import blenderbim.core.sequence as core from blenderbim.bim.ifc import IfcStore @@ -208,26 +209,31 @@ def updateTaskDuration(self, context): self.duration = "-" return - self.file = tool.Ifc.get() - task = self.file.by_id(self.ifc_definition_id) + task = tool.Ifc.get().by_id(self.ifc_definition_id) if task.TaskTime: task_time = task.TaskTime else: - task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=task) - ifcopenshell.api.run( - "sequence.edit_task_time", - self.file, - **{"task_time": task_time, "attributes": {"ScheduleDuration": duration}}, - ) + task_time = tool.Ifc.run("sequence.add_task_time", task=task) + tool.Ifc.run("sequence.edit_task_time", task_time=task_time, attributes={"ScheduleDuration": duration}) SequenceData.load() bpy.ops.bim.load_task_properties() def get_schedule_predefined_types(self, context): - if not SequenceData.is_loaded: - SequenceData.load() - return SequenceData.data["predefined_types"] - + results = [] + declaration = tool.Ifc().schema().declaration_by_name("IfcWorkSchedule") + version = tool.Ifc.get_schema() + for attribute in declaration.attributes(): + if attribute.name() == "PredefinedType": + results.extend( + [ + (e, e, get_predefined_type_doc(version, "IfcWorkSchedule", e)) + for e in attribute.type_of_attribute().declared_type().enumeration_items() + if e != "BASELINE" + ] + ) + break + return results def update_visualisation_start(self, context): update_visualisation_start_finish(self, context, "visualisation_start") @@ -292,6 +298,14 @@ def update_filter_by_active_schedule(self, context): tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id) ) +def switch_options(self, context): + if self.should_show_visualisation_ui: + self.should_show_snapshot_ui = False + +def switch_options2(self, context): + if self.should_show_snapshot_ui: + self.should_show_visualisation_ui = False + class Task(PropertyGroup): name: StringProperty(name="Name", update=updateTaskName) identification: StringProperty(name="Identification", update=updateTaskIdentification) @@ -352,6 +366,7 @@ class BIMWorkScheduleProperties(PropertyGroup): work_schedule_predefined_types: EnumProperty( items=get_schedule_predefined_types, name="Predefined Type", default=None ) + object_type: StringProperty(name="Object Type") durations_attributes: CollectionProperty(name="Durations Attributes", type=ISODuration) work_calendars: EnumProperty(items=getWorkCalendars, name="Work Calendars") work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute) @@ -362,9 +377,9 @@ class BIMWorkScheduleProperties(PropertyGroup): active_task_index: IntProperty(name="Active Task Index", update=update_active_task_index) active_task_id: IntProperty(name="Active Task Id") task_attributes: CollectionProperty(name="Task Attributes", type=Attribute) - should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=False) + should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=True, update=switch_options) should_show_task_bar_selection: BoolProperty(name="Add to task bar", default=False) - should_show_snapshot_ui: BoolProperty(name="Should Show Snapshot UI", default=False) + should_show_snapshot_ui: BoolProperty(name="Should Show Snapshot UI", default=False, update=switch_options2) should_show_column_ui: BoolProperty(name="Should Show Column UI", default=False) columns: CollectionProperty(name="Columns", type=Attribute) active_column_index: IntProperty(name="Active Column Index") @@ -397,9 +412,9 @@ class BIMWorkScheduleProperties(PropertyGroup): visualisation_start: StringProperty(name="Visualisation Start", update=update_visualisation_start) visualisation_finish: StringProperty(name="Visualisation Finish", update=update_visualisation_finish) speed_multiplier: FloatProperty(name="Speed Multiplier", default=10000) - speed_animation_duration: StringProperty(name="Speed Animation Duration", default="PT1S") + speed_animation_duration: StringProperty(name="Speed Animation Duration", default="1 s") speed_animation_frames: IntProperty(name="Speed Animation Frames", default=24) - speed_real_duration: StringProperty(name="Speed Real Duration", default="P1W") + speed_real_duration: StringProperty(name="Speed Real Duration", default="1 w") speed_types: EnumProperty( items=[ ("FRAME_SPEED", "Frame-based", "e.g. 25 frames = 1 real week"), diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 572977d379..64ed3399f7 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -22,7 +22,6 @@ from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from blenderbim.bim.helper import draw_attributes from blenderbim.bim.module.sequence.data import WorkPlansData, WorkScheduleData, SequenceData, TaskICOMData -import blenderbim.tool as tool class BIM_PT_work_plans(Panel): @@ -32,10 +31,12 @@ class BIM_PT_work_plans(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3" + file = IfcStore.get_file() + return file and file.schema != "IFC2X3" def draw(self, context): if not WorkPlansData.is_loaded: @@ -54,11 +55,10 @@ class BIM_PT_work_plans(Panel): def draw_work_plan_ui(self, work_plan): row = self.layout.row(align=True) row.label(text=work_plan["name"], icon="TEXT") - if self.props.active_work_plan_id == work_plan["id"]: if self.props.editing_type == "ATTRIBUTES": row.operator("bim.edit_work_plan", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_work_plan", text="", icon="CANCEL") + row.operator("bim.disable_editing_work_plan", text="Cancel", icon="CANCEL") elif self.props.active_work_plan_id: row.operator("bim.remove_work_plan", text="", icon="X").work_plan = work_plan["id"] else: @@ -101,10 +101,12 @@ class BIM_PT_work_schedules(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3" + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): if not SequenceData.is_loaded: @@ -113,10 +115,9 @@ class BIM_PT_work_schedules(Panel): WorkScheduleData.load() self.props = context.scene.BIMWorkScheduleProperties self.tprops = context.scene.BIMTaskTreeProperties - self.animation_props = context.scene.BIMAnimationProperties if not self.props.active_work_schedule_id: - row = self.layout.row() + row = self.layout.row(align=True) if SequenceData.data["has_work_schedules"]: row.label( text="{} Work Schedules Found".format(SequenceData.data["number_of_work_schedules_loaded"]), @@ -124,25 +125,25 @@ class BIM_PT_work_schedules(Panel): ) else: row.label(text="No Work Schedules found.", icon="TEXT") - row = self.layout.row(align=True) - row.alignment = "RIGHT" - row.prop(self.props, "work_schedule_predefined_types") row.operator("bim.add_work_schedule", text="Add", icon="ADD") for work_schedule_id, work_schedule in SequenceData.data["work_schedules"].items(): - self.draw_work_schedule_ui(work_schedule_id, work_schedule) def draw_work_schedule_ui(self, work_schedule_id, work_schedule): - if not work_schedule["PredefinedType"] == "BASELINE": + if work_schedule["PredefinedType"] == "BASELINE": + self.draw_readonly_work_schedule_ui(work_schedule_id) + else: row = self.layout.row(align=True) - row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") if self.props.active_work_schedule_id == work_schedule_id: + row.label( + text="Currently editing: {}[{}]".format(work_schedule["Name"], work_schedule["PredefinedType"]), + icon="LINENUMBERS_ON", + ) if self.props.editing_type == "WORK_SCHEDULE": - row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK") + row.operator("bim.edit_work_schedule", text="Apply", icon="CHECKMARK") elif self.props.editing_type == "TASKS": grid = self.layout.grid_flow(columns=2, even_columns=True) - col = grid.column() row1 = col.row(align=True) row1.alignment = "LEFT" @@ -177,33 +178,23 @@ class BIM_PT_work_schedules(Panel): row1.alignment = "RIGHT" row1.prop(self.props, "should_show_column_ui", text="Schedule Columns", icon="SHORTDISPLAY") row2 = col.row(align=True) - row2.prop( - self.props, "should_show_visualisation_ui", text="Animation Options", icon="CAMERA_STEREO" - ) - row2.prop(self.props, "should_show_snapshot_ui", text="Snapshot Options", icon="CAMERA_STEREO") - row.operator("bim.disable_editing_work_schedule", text="Disable editing", icon="CANCEL") - else: + row.operator("bim.disable_editing_work_schedule", text="Cancel", icon="CANCEL") + if not self.props.active_work_schedule_id: + row.label(text="{}[{}]".format(work_schedule["Name"], work_schedule["PredefinedType"]) or "Unnamed", icon="LINENUMBERS_ON") row.operator( - "bim.enable_editing_work_schedule_tasks", text="", icon="ACTION" + "bim.enable_editing_work_schedule_tasks", text="Tasks", icon="ACTION" ).work_schedule = work_schedule_id row.operator( - "bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL" + "bim.enable_editing_work_schedule", text="Attributes", icon="GREASEPENCIL" ).work_schedule = work_schedule_id - row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id - + row.operator("bim.remove_work_schedule", text="Delete", icon="X").work_schedule = work_schedule_id if self.props.active_work_schedule_id == work_schedule_id: if self.props.editing_type == "WORK_SCHEDULE": self.draw_editable_work_schedule_ui() elif self.props.editing_type == "TASKS": self.draw_baseline_ui(work_schedule_id) self.draw_column_ui() - if self.props.should_show_visualisation_ui: - self.draw_visualisation_ui() - if self.props.should_show_snapshot_ui: - self.draw_snapshot_ui() self.draw_editable_task_ui(work_schedule_id) - else: - self.draw_readonly_work_schedule_ui(work_schedule_id) def draw_task_operators(self): row = self.layout.row(align=True) @@ -218,7 +209,7 @@ class BIM_PT_work_schedules(Panel): row.operator("bim.edit_task_time", text="", icon="CHECKMARK") elif self.props.editing_task_type == "ATTRIBUTES": row.operator("bim.edit_task", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_task", text="", icon="CANCEL") + row.operator("bim.disable_editing_task", text="Cancel", icon="CANCEL") else: row.prop(self.props, "show_task_operators", text="Edit", icon="GREASEPENCIL") if self.props.show_task_operators: @@ -242,7 +233,7 @@ class BIM_PT_work_schedules(Panel): if not self.props.should_show_column_ui: return row = self.layout.row() - row.operator("bim.setup_default_task_columns", text="Add Default Columns", icon="ANCHOR_BOTTOM") + row.operator("bim.setup_default_task_columns", text="Setup Default Columns", icon="ANCHOR_BOTTOM") row.alignment = "RIGHT" row = self.layout.row(align=True) row.prop(self.props, "column_types", text="") @@ -268,103 +259,6 @@ class BIM_PT_work_schedules(Panel): self.layout.template_list("BIM_UL_task_columns", "", self.props, "columns", self.props, "active_column_index") - def draw_visualisation_ui(self): - row = self.layout.row(align=True) - row.label(text="Start Date/ Date Range:") - row = self.layout.row(align=True) - op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Start Date", icon="REW") - op.target_prop = "BIMWorkScheduleProperties.visualisation_start" - op = row.operator("bim.datepicker", text=self.props.visualisation_finish or "Finish Date", icon="FF") - op.target_prop = "BIMWorkScheduleProperties.visualisation_finish" - op = row.operator("bim.guess_date_range", text="Guess", icon="FILE_REFRESH") - op.work_schedule = self.props.active_work_schedule_id - - row = self.layout.row(align=True) - row.label(text="Speed Settings") - row = self.layout.row(align=True) - row.prop(self.props, "speed_types", text="") - if self.props.speed_types == "FRAME_SPEED": - row.prop(self.props, "speed_animation_frames", text="") - row.prop(self.props, "speed_real_duration", text="") - elif self.props.speed_types == "DURATION_SPEED": - row.prop(self.props, "speed_animation_duration", text="") - row.prop(self.props, "speed_real_duration", text="") - elif self.props.speed_types == "MULTIPLIER_SPEED": - row.prop(self.props, "speed_multiplier", text="") - row = self.layout.row(align=True) - row.label(text="Display Settings") - row = self.layout.row(align=True) - if not self.animation_props.is_editing: - op = row.operator( - "bim.enable_editing_task_animation_colors", text="Customize Object Colors", icon="SEQUENCE_COLOR_04" - ) - else: - op = row.operator( - "bim.disable_editing_task_animation_colors", text="Hide Object Colors", icon="SEQUENCE_COLOR_01" - ) - - row.prop(self.animation_props, "should_show_task_bar_options", text="Task Bar", icon="NLA_PUSHDOWN") - if self.animation_props.should_show_task_bar_options: - row = self.layout.row() - row.label(text="Task Bar Options", icon="NLA_PUSHDOWN") - row.alignment = "LEFT" - row = self.layout.row(align=True) - row.prop(self.props, "should_show_task_bar_selection", text="Enable Selection", icon="NLA_PUSHDOWN") - row.operator("bim.add_task_bars", text="Generate bars", icon="NLA_PUSHDOWN") - - grid = self.layout.grid_flow(columns=2, even_columns=True) - # Column1 - col = grid.column() - - row = col.row(align=True) - row.prop(self.animation_props, "color_progress") - - row = col.row(align=True) - row.prop(self.animation_props, "color_full") - - if self.animation_props.is_editing: - self.draw_visualisation_settings_ui() - - row = self.layout.row(align=True) - op = row.operator("bim.visualise_work_schedule_date_range", text="Create Animation", icon="OUTLINER_OB_CAMERA") - op.work_schedule = self.props.active_work_schedule_id - - def draw_snapshot_ui(self): - row = self.layout.row(align=True) - row.label(text="Create Construction Snapshot:") - row = self.layout.row(align=True) - op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Date", icon="REW") - op.target_prop = "BIMWorkScheduleProperties.visualisation_start" - op = row.operator("bim.visualise_work_schedule_date", text="Create SnapShot", icon="RESTRICT_RENDER_OFF") - op.work_schedule = self.props.active_work_schedule_id - - def draw_visualisation_settings_ui(self): - grid = self.layout.grid_flow(columns=2, even_columns=True) - col = grid.column() - row1 = col.row(align=True) - row1.label(text="INPUT COLORS", icon="COLLECTION_COLOR_01") - row1 = col.row() - row1.template_list( - "BIM_UL_animation_colors", - "", - self.animation_props, - "task_colors_components_inputs", - self.animation_props, - "active_color_component_inputs_index", - ) - col = grid.column() - row1 = col.row(align=True) - row1.label(text="OUTPUT COLORS", icon="COLLECTION_COLOR_04") - row1 = col.row() - row1.template_list( - "BIM_UL_animation_colors", - "", - self.animation_props, - "task_colors_components_outputs", - self.animation_props, - "active_color_component_outputs_index", - ) - def draw_editable_work_schedule_ui(self): draw_attributes(self.props.work_schedule_attributes, self.layout) @@ -421,12 +315,12 @@ class BIM_PT_work_schedules(Panel): if self.props.active_sequence_id == sequence["id"]: if self.props.editing_sequence_type == "ATTRIBUTES": row.operator("bim.edit_sequence_attributes", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_sequence", text="", icon="CANCEL") + row.operator("bim.disable_editing_sequence", text="Cancel", icon="CANCEL") self.draw_editable_sequence_attributes_ui() elif self.props.editing_sequence_type == "LAG_TIME": op = row.operator("bim.edit_sequence_lag_time", text="", icon="CHECKMARK") op.lag_time = sequence["TimeLag"] - row.operator("bim.disable_editing_sequence", text="", icon="CANCEL") + row.operator("bim.disable_editing_sequence", text="Cancel", icon="CANCEL") self.draw_editable_sequence_lag_time_ui() else: if sequence["TimeLag"]: @@ -491,7 +385,7 @@ class BIM_PT_work_schedules(Panel): "id" ] baseline_row.operator( - "bim.enable_editing_work_schedule_tasks", text="", icon="ACTION" + "bim.enable_editing_work_schedule_tasks", text="Display Schedule", icon="ACTION" ).work_schedule = baseline["id"] baseline_row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = baseline["id"] @@ -521,6 +415,151 @@ class BIM_PT_work_schedules(Panel): ) +class BIM_PT_animation_tools(Panel): + bl_label = "Animation Tools" + bl_idname = "BIM_PT_animation_tools" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_work_schedules" + + @classmethod + def poll(cls, context): + props = context.scene.BIMWorkScheduleProperties + if props.active_work_schedule_id: + return True + return False + + def draw(self, context): + self.props = context.scene.BIMWorkScheduleProperties + self.animation_props = context.scene.BIMAnimationProperties + row = self.layout.row(align=True) + row.alignment = "RIGHT" + row.prop( + self.props, "should_show_visualisation_ui", text="Animation Settings", icon="SETTINGS" + ) + row.prop(self.props, "should_show_snapshot_ui", text="Snapshot Settings", icon="SETTINGS") + if self.props.should_show_visualisation_ui: + self.draw_visualisation_ui() + if self.props.should_show_snapshot_ui: + self.draw_snapshot_ui() + self.draw_processing_options() + + + def draw_processing_options(self): + row = self.layout.row(align=True) + row.alignment = "LEFT" + row.label(text="Processing Tools") + row = self.layout.row() + row.alignment = "RIGHT" + row.operator("bim.clear_previous_animation", text="Clear Previous Animation", icon="TRACKING_CLEAR_FORWARDS") + row.operator("bim.add_animation_camera", text="Add Camera", icon="CAMERA_DATA") + + def draw_visualisation_ui(self): + row = self.layout.row(align=True) + row.label(text="Start Date/ Date Range:", icon="CAMERA_DATA") + row = self.layout.row(align=True) + row.alignment = "RIGHT" + op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Start Date", icon="REW") + op.target_prop = "BIMWorkScheduleProperties.visualisation_start" + op = row.operator("bim.datepicker", text=self.props.visualisation_finish or "Finish Date", icon="FF") + op.target_prop = "BIMWorkScheduleProperties.visualisation_finish" + op = row.operator("bim.guess_date_range", text="Guess", icon="FILE_REFRESH") + op.work_schedule = self.props.active_work_schedule_id + + row = self.layout.row(align=True) + row.label(text="Speed Settings") + row = self.layout.row(align=True) + row.alignment = "RIGHT" + row.prop(self.props, "speed_types", text="") + if self.props.speed_types == "FRAME_SPEED": + row.prop(self.props, "speed_animation_frames", text="") + row.prop(self.props, "speed_real_duration", text="") + elif self.props.speed_types == "DURATION_SPEED": + row.prop(self.props, "speed_animation_duration", text="") + row.label(text="->") + row.prop(self.props, "speed_real_duration", text="") + elif self.props.speed_types == "MULTIPLIER_SPEED": + row.prop(self.props, "speed_multiplier", text="") + row = self.layout.row(align=True) + row.label(text="Display Settings") + row = self.layout.row() + row.alignment = "RIGHT" + if not self.animation_props.is_editing: + op = row.operator( + "bim.enable_editing_task_animation_colors", text="Customize Object Colors", icon="SEQUENCE_COLOR_04" + ) + else: + op = row.operator( + "bim.disable_editing_task_animation_colors", text="Hide Object Colors", icon="SEQUENCE_COLOR_01" + ) + + row.prop(self.animation_props, "should_show_task_bar_options", text="Task Bar", icon="NLA_PUSHDOWN") + if self.animation_props.should_show_task_bar_options: + row = self.layout.row() + row.label(text="Task Bar Options", icon="NLA_PUSHDOWN") + row.alignment = "LEFT" + row = self.layout.row(align=True) + row.prop(self.props, "should_show_task_bar_selection", text="Enable Selection", icon="NLA_PUSHDOWN") + row.operator("bim.add_task_bars", text="Generate bars", icon="NLA_PUSHDOWN") + + grid = self.layout.grid_flow(columns=2, even_columns=True) + # Column1 + col = grid.column() + + row = col.row(align=True) + row.prop(self.animation_props, "color_progress") + + row = col.row(align=True) + row.prop(self.animation_props, "color_full") + + if self.animation_props.is_editing: + self.draw_visualisation_settings_ui() + + row = self.layout.row(align=True) + op = row.operator("bim.visualise_work_schedule_date_range", text="Create Animation", icon="OUTLINER_OB_CAMERA") + op.work_schedule = self.props.active_work_schedule_id + + def draw_snapshot_ui(self): + row = self.layout.row(align=True) + row.label(text="Date of Snapshot:", icon="CAMERA_STEREO") + row = self.layout.row(align=True) + row.alignment = "RIGHT" + op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Date", icon="PROP_PROJECTED") + op.target_prop = "BIMWorkScheduleProperties.visualisation_start" + row = self.layout.row(align=True) + row.alignment = "RIGHT" + op = row.operator("bim.visualise_work_schedule_date", text="Create SnapShot", icon="CAMERA_STEREO") + op.work_schedule = self.props.active_work_schedule_id + + def draw_visualisation_settings_ui(self): + grid = self.layout.grid_flow(columns=2, even_columns=True) + col = grid.column() + row1 = col.row(align=True) + row1.label(text="INPUT COLORS", icon="COLLECTION_COLOR_01") + row1 = col.row() + row1.template_list( + "BIM_UL_animation_colors", + "", + self.animation_props, + "task_colors_components_inputs", + self.animation_props, + "active_color_component_inputs_index", + ) + col = grid.column() + row1 = col.row(align=True) + row1.label(text="OUTPUT COLORS", icon="COLLECTION_COLOR_04") + row1 = col.row() + row1.template_list( + "BIM_UL_animation_colors", + "", + self.animation_props, + "task_colors_components_outputs", + self.animation_props, + "active_color_component_outputs_index", + ) + class BIM_PT_task_icom(Panel): bl_label = "Task ICOM" bl_idname = "BIM_PT_task_icom" @@ -813,10 +852,12 @@ class BIM_PT_work_calendars(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3" + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): if not SequenceData.is_loaded: @@ -841,7 +882,7 @@ class BIM_PT_work_calendars(Panel): if self.props.active_work_calendar_id == work_calendar_id: if self.props.editing_type == "ATTRIBUTES": row.operator("bim.edit_work_calendar", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_work_calendar", text="", icon="CANCEL") + row.operator("bim.disable_editing_work_calendar", text="Cancel", icon="CANCEL") elif self.props.active_work_calendar_id: row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = work_calendar_id else: @@ -879,7 +920,7 @@ class BIM_PT_work_calendars(Panel): row.label(text="{} - {}".format(work_time["Start"] or "*", work_time["Finish"] or "*")) if self.props.active_work_time_id == work_time["id"]: row.operator("bim.edit_work_time", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_work_time", text="", icon="CANCEL") + row.operator("bim.disable_editing_work_time", text="Cancel", icon="CANCEL") elif self.props.active_work_time_id: op = row.operator("bim.remove_work_time", text="", icon="X") op.work_time = work_time["id"] diff --git a/src/blenderbim/blenderbim/bim/module/system/__init__.py b/src/blenderbim/blenderbim/bim/module/system/__init__.py index 6cc772e135..a57b7fb5a8 100644 --- a/src/blenderbim/blenderbim/bim/module/system/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/system/__init__.py @@ -34,6 +34,7 @@ classes = ( operator.RemovePort, operator.RemoveSystem, operator.SelectSystemProducts, + operator.MEPConnectElements, operator.SetFlowDirection, operator.ShowPorts, operator.UnassignSystem, diff --git a/src/blenderbim/blenderbim/bim/module/system/data.py b/src/blenderbim/blenderbim/bim/module/system/data.py index 5624f3cd87..6c71e88dc9 100644 --- a/src/blenderbim/blenderbim/bim/module/system/data.py +++ b/src/blenderbim/blenderbim/bim/module/system/data.py @@ -91,8 +91,15 @@ class PortData: @classmethod def load(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + cls.element = element + is_port = cls.is_port() cls.data = { "total_ports": cls.total_ports(), + "located_ports_data": cls.located_ports_data(), + "is_port": is_port, + "port_connected_object": cls.port_connected_object() if is_port else None, + "port_relating_object": cls.port_relating_object() if is_port else None, } cls.is_loaded = True @@ -100,3 +107,36 @@ class PortData: def total_ports(cls): element = tool.Ifc.get_entity(bpy.context.active_object) return len(ifcopenshell.util.system.get_ports(element)) + + @classmethod + def is_port(cls): + return cls.element and cls.element.is_a("IfcDistributionPort") + + @classmethod + def port_relating_object(cls): + return tool.Ifc.get_object(tool.System.get_port_relating_element(cls.element)) + + @classmethod + def port_connected_object(cls): + connected_port = tool.System.get_connected_port(cls.element) + if not connected_port: + return + connected_element = tool.System.get_port_relating_element(connected_port) + return tool.Ifc.get_object(connected_element) + + @classmethod + def located_ports_data(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + ports = ifcopenshell.util.system.get_ports(element) + + data = [] + for port in ports: + port_obj = tool.Ifc.get_object(port) + connected_port = tool.System.get_connected_port(port) + if connected_port: + connected_element = tool.Ifc.get_object(tool.System.get_port_relating_element(connected_port)) + else: + connected_element = None + + data.append((port, port_obj, connected_element)) + return data diff --git a/src/blenderbim/blenderbim/bim/module/system/operator.py b/src/blenderbim/blenderbim/bim/module/system/operator.py index 828977060e..71336ec7fb 100644 --- a/src/blenderbim/blenderbim/bim/module/system/operator.py +++ b/src/blenderbim/blenderbim/bim/module/system/operator.py @@ -23,6 +23,7 @@ import blenderbim.core.system as core import blenderbim.bim.handler from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.system.data import PortData +from mathutils import Matrix class Operator: @@ -205,8 +206,50 @@ class DisconnectPort(bpy.types.Operator, Operator): bl_label = "Disconnect Ports" bl_options = {"REGISTER", "UNDO"} + element_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"}) + def _execute(self, context): - core.disconnect_port(tool.Ifc, port=tool.Ifc.get_entity(context.active_object)) + if self.element_id != 0: + element = tool.Ifc.get().by_id(self.element_id) + else: + element = tool.Ifc.get_entity(context.active_object) + core.disconnect_port(tool.Ifc, port=element) + + +class MEPConnectElements(bpy.types.Operator, Operator): + bl_idname = "bim.mep_connect_elements" + bl_label = "Connect MEP Elements" + bl_description = "Connects two selected elements if they have ports with matching location" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return len(context.selected_objects) == 2 + + def _execute(self, context): + obj1 = context.active_object + obj2 = next(o for o in context.selected_objects if o != obj1) + + el1 = tool.Ifc.get_entity(obj1) + el2 = tool.Ifc.get_entity(obj2) + + obj1_ports = [p for p in tool.System.get_ports(el1) if not tool.System.get_connected_port(p)] + obj2_ports = [p for p in tool.System.get_ports(el2) if not tool.System.get_connected_port(p)] + + if not obj1_ports or not obj2_ports: + self.report({"ERROR"}, "Couldn't find free ports to connect.") + return + + for port1 in obj1_ports: + port1_location = tool.Model.get_element_matrix(port1).translation + for port2 in obj2_ports: + port2_location = tool.Model.get_element_matrix(port2).translation + if tool.Cad.are_vectors_equal(port1_location, port2_location): + core.connect_port(tool.Ifc, port1, port2) + return {"FINISHED"} + + self.report({"ERROR"}, "Couldn't find any matching ports to connect.") + return {"CANCELLED"} class SetFlowDirection(bpy.types.Operator, Operator): diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py index c59b41c043..5b60c151e7 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -23,6 +23,14 @@ from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.system.data import SystemData, ObjectSystemData, PortData +FLOW_DIRECTION_TO_ICON = { + "SOURCE": "FORWARD", + "SINK": "BACK", + "SOURCEANDSINK": "ARROW_LEFTRIGHT", + "NOTDEFINED": "RESTRICT_INSTANCED_ON", +} + + class BIM_PT_systems(Panel): bl_label = "Systems" bl_idname = "BIM_PT_systems" @@ -115,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"]: @@ -156,11 +165,44 @@ class BIM_PT_ports(Panel): self.props = context.scene.BIMSystemProperties row = self.layout.row(align=True) - row.label(text=f"{PortData.data['total_ports']} Ports Found", icon="PLUGIN") + total_ports = PortData.data["total_ports"] + row.label(text=f"{total_ports} Ports Found", icon="PLUGIN") + row.operator("bim.mep_connect_elements", text="", icon="PLUGIN") row.operator("bim.show_ports", icon="HIDE_OFF", text="") row.operator("bim.hide_ports", icon="HIDE_ON", text="") row.operator("bim.add_port", icon="ADD", text="") + if total_ports == 0: + return + + row = self.layout.row(align=True) + row.label(text="Ports located on object and connected objects:") + row = self.layout.row(align=True) + cols = [row.column(align=True) for i in range(6)] + + for i, port_data in enumerate(PortData.data["located_ports_data"]): + port, port_obj, connected_obj = port_data + flow_direction_icon = FLOW_DIRECTION_TO_ICON[port.FlowDirection or "NOTDEFINED"] + if port_obj: + cols[0].label(text="", icon=flow_direction_icon) + cols[1].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port.id() + cols[2].label(text=port_obj.name) + else: + cols[0].label(text="", icon=flow_direction_icon) + cols[1].label(text="", icon="HIDE_ON") + cols[2].label(text="Port is hidden") + + if connected_obj: + cols[3].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port.id() + cols[4].operator( + "bim.select_entity", text="", icon="RESTRICT_SELECT_OFF" + ).ifc_id = connected_obj.BIMObjectProperties.ifc_definition_id + cols[5].label(text=f"{connected_obj.name}") + else: + cols[3].label(text="", icon="UNLINKED") + cols[4].label(text="", icon="BLANK1") + cols[5].label(text="Port is disconnected") + class BIM_PT_port(Panel): bl_label = "Port" @@ -184,36 +226,56 @@ class BIM_PT_port(Panel): def draw(self, context): self.props = context.scene.BIMSystemProperties - element = tool.Ifc.get_entity(context.active_object) - port_class = element.is_a() layout = self.layout row = layout.row(align=True) - row.label(text=port_class) + row.label(text="IfcDistributionPort") row.operator("bim.connect_port", icon="PLUGIN", text="") row.operator("bim.disconnect_port", icon="UNLINKED", text="") row.operator("bim.remove_port", icon="X", text="") - if port_class == "IfcDistributionPort": - current_flow_direction = str(element.FlowDirection) - row = layout.row(align=True) - row.label(text="Flow Direction:") - row.label(text=current_flow_direction) + if not PortData.is_loaded: + PortData.load() - # TODO: replace with enum property? - flow_directions = ( - ("SOURCE", "FORWARD"), - ("SINK", "BACK"), - ("SOURCEANDSINK", "ARROW_LEFTRIGHT"), - ("NOTDEFINED", "RESTRICT_INSTANCED_ON"), - ) + if not PortData.data["is_port"]: + return - row = layout.row(align=True) - row.label(text="Change Flow Direction:") - for flow_direction, icon in flow_directions: - row = layout.row() - row.operator("bim.set_flow_direction", icon=icon, text=flow_direction).direction = flow_direction - if flow_direction == current_flow_direction: - row.enabled = False + element = tool.Ifc.get_entity(context.active_object) + current_flow_direction = str(element.FlowDirection) + row = layout.row(align=True) + row.label(text="Flow Direction:") + row.label(text=current_flow_direction) + + # port located on + row = layout.row(align=True) + relating_object = PortData.data["port_relating_object"] + row.label(text="Port located on:") + row.label(text=relating_object.name) + row.operator( + "bim.select_entity", text="", icon="RESTRICT_SELECT_OFF" + ).ifc_id = relating_object.BIMObjectProperties.ifc_definition_id + + # object connected to the port + row = layout.row(align=True) + connected_object = PortData.data["port_connected_object"] + if connected_object: + row.label(text="Port connected to:") + row.label(text=connected_object.name) + row.operator( + "bim.select_entity", text="", icon="RESTRICT_SELECT_OFF" + ).ifc_id = connected_object.BIMObjectProperties.ifc_definition_id + else: + row.label(text="Port is not connected to any element") + + # TODO: replace with enum property? + row = layout.row(align=True) + row.label(text="Change Flow Direction:") + for flow_direction in FLOW_DIRECTION_TO_ICON.keys(): + row = layout.row() + row.operator( + "bim.set_flow_direction", icon=FLOW_DIRECTION_TO_ICON[flow_direction], text=flow_direction + ).direction = flow_direction + if flow_direction == current_flow_direction: + row.enabled = False class BIM_UL_systems(UIList): @@ -223,6 +285,7 @@ class BIM_UL_systems(UIList): "IfcDistributionSystem": "NETWORK_DRIVE", "IfcDistributionCircuit": "DRIVER", "IfcBuildingSystem": "MOD_BUILD", + "IfcBuiltSystem": "MOD_BUILD", "IfcZone": "CUBE", } if item: @@ -255,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/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index 151eaf0dd0..ea07090640 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -312,32 +312,36 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator): ) else: # NOTE: defaults dims are in meters / mm + # for now default names are hardcoded to mm if template == "FLOW_SEGMENT_RECTANGULAR": - default_x_dim = 0.4 / unit_scale - default_y_dim = 0.2 / unit_scale - profile_name = f"{ifc_class}-{default_x_dim*1000}x{default_x_dim*1000}" + default_x_dim = 0.4 + default_y_dim = 0.2 + profile_name = f"{ifc_class}-{default_x_dim*1000}x{default_y_dim*1000}" profile = ifc_file.create_entity( "IfcRectangleProfileDef", ProfileName=profile_name, ProfileType="AREA", - XDim=default_x_dim, - YDim=default_y_dim, + XDim=default_x_dim / unit_scale, + YDim=default_y_dim / unit_scale, ) elif template == "FLOW_SEGMENT_CIRCULAR": - default_diameter = 0.1 / unit_scale + default_diameter = 0.1 profile_name = f"{ifc_class}-{default_diameter*1000}" profile = ifc_file.create_entity( - "IfcCircleProfileDef", ProfileName=profile_name, ProfileType="AREA", Radius=default_diameter / 2 + "IfcCircleProfileDef", + ProfileName=profile_name, + ProfileType="AREA", + Radius=(default_diameter / 2) / unit_scale, ) elif template == "FLOW_SEGMENT_CIRCULAR_HOLLOW": - default_diameter = 0.15 / unit_scale - default_thickness = 0.005 / unit_scale + default_diameter = 0.15 + default_thickness = 0.005 profile_name = f"{ifc_class}-{default_diameter*1000}x{default_thickness*1000}" profile = ifc_file.create_entity( "IfcCircleHollowProfileDef", ProfileName=profile_name, ProfileType="AREA", - Radius=default_diameter / 2, + Radius=(default_diameter / 2) / unit_scale, WallThickness=default_thickness, ) diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py index 7dc0262881..34b587d58d 100644 --- a/src/blenderbim/blenderbim/bim/prop.py +++ b/src/blenderbim/blenderbim/bim/prop.py @@ -305,7 +305,7 @@ def get_tab(self, context): ("DRAWINGS", "Drawings and Documents", "", "DOCUMENTS", 3), ("SERVICES", "Services and Systems", "", "NETWORK_DRIVE", 4), ("STRUCTURE", "Structural Analysis", "", "EDITMODE_HLT", 5), - ("SCHEDULING", "Construction Scheduling", "", "NLA", 6), + ("SCHEDULING", "Costing and Scheduling", "", "NLA", 6), ("FM", "Facility Management", "", "PACKAGE", 7), ("QUALITY", "Quality and Coordination", "", "COMMUNITY", 8), ("BLENDER", "Blender Properties", "", "BLENDER", 9), diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 58958f4b20..b57deaa8b5 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -404,6 +404,21 @@ class BIM_PT_geometry(Panel): pass +class BIM_PT_tab_4D5D(Panel): + bl_label = "Costing and Scheduling" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_options = {"HIDE_HEADER"} + + @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_structural(Panel): bl_label = "Structural" bl_space_type = "PROPERTIES" 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/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index 6cc977c67c..683f7350d0 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -345,11 +345,10 @@ def update_drawing_name(ifc, drawing_tool, drawing=None, name=None): def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None): - context = drawing_tool.get_annotation_context( - target_view := drawing_tool.get_drawing_target_view(drawing), object_type - ) + target_view = drawing_tool.get_drawing_target_view(drawing) + context = drawing_tool.get_annotation_context(target_view, object_type) if not context: - return f"No annotation context Annotation/{target_view} for drawing" + context = drawing_tool.create_annotation_context(target_view, object_type) drawing_tool.show_decorations() obj = drawing_tool.create_annotation_object(drawing, object_type) diff --git a/src/blenderbim/blenderbim/core/georeference.py b/src/blenderbim/blenderbim/core/georeference.py index f3faded3b2..dcc9823904 100644 --- a/src/blenderbim/blenderbim/core/georeference.py +++ b/src/blenderbim/blenderbim/core/georeference.py @@ -83,4 +83,7 @@ def convert_global_to_local(georeference): def convert_angle_to_coord(georeference, type): vector_coordinates = georeference.angle2coords(georeference.get_angle(type), type) - georeference.set_vector_coordinates(vector_coordinates,type) \ No newline at end of file + georeference.set_vector_coordinates(vector_coordinates,type) + +def import_plot(georeference, filepath): + georeference.import_plot(filepath, georeference.get_map_conversion()) diff --git a/src/blenderbim/blenderbim/core/sequence.py b/src/blenderbim/blenderbim/core/sequence.py index 1a68ffd9ea..7af332cbf4 100644 --- a/src/blenderbim/blenderbim/core/sequence.py +++ b/src/blenderbim/blenderbim/core/sequence.py @@ -50,8 +50,9 @@ def enable_editing_work_plan_schedules(sequence, work_plan=None): sequence.enable_editing_work_plan_schedules(work_plan) -def add_work_schedule(ifc): - return ifc.run("sequence.add_work_schedule") +def add_work_schedule(ifc, sequence, name=None): + predefined_type, object_type = sequence.get_user_predefined_type() + return ifc.run("sequence.add_work_schedule", name=name, predefined_type=predefined_type, object_type=object_type) def remove_work_schedule(ifc, work_schedule=None): @@ -529,6 +530,7 @@ def disable_editing_task_animation_colors(sequence): def visualise_work_schedule_date_range(sequence, work_schedule=None): + sequence.clear_objects_animation(include_blender_objects=False) settings = sequence.get_animation_settings() if settings: product_frames = sequence.get_animation_product_frames(work_schedule, settings) @@ -579,3 +581,9 @@ def reorder_task_nesting(ifc, sequence, task, new_index): def create_baseline(ifc, sequence, work_schedule, name): ifc.run("sequence.create_baseline", work_schedule=work_schedule, name=name) + +def clear_previous_animation(sequence): + sequence.clear_objects_animation(include_blender_objects=False) + +def add_animation_camera(sequence): + sequence.add_animation_camera() \ No newline at end of file diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 44b14a3e3c..c19b93508e 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -183,7 +183,6 @@ class Cost: def get_cost_value_unit_component(cls): pass def get_direct_cost_item_products(cls): pass def get_highlighted_cost_item(cls): pass - def get_highlighted_cost_item(cls): pass def get_products(cls, related_object_type): pass def get_schedule_cost_items(cls, cost_schedule): pass def get_units(cls): pass @@ -648,6 +647,7 @@ class Search: @interface class Sequence: + def add_animation_camera(cls): pass def add_task_column(cls, column_type, name, data_type): pass def add_text_animation_handler(cls, settings): pass def animate_consumption(cls, obj, start_frame, product_frame, color, animation_type): pass @@ -660,6 +660,7 @@ class Sequence: def animate_operation(cls, obj, start_frame, product_frame, color): pass def animate_output(cls, obj, start_frame, product_frame): pass def clear_object_animation(cls, obj): pass + def clear_object_color(cls, obj): pass def clear_objects_animation(cls, include_blender_objects): pass def contract_all_tasks(cls): pass def contract_task(cls, task): pass @@ -676,6 +677,7 @@ class Sequence: def disable_editing_work_time(cls): pass def disable_selecting_deleted_task(cls): pass def disable_work_schedule(cls): pass + def display_object(cls, obj): pass def enable_editing_rel_sequence_attributes(cls, rel_sequence): pass def enable_editing_sequence_lag_time(cls, rel_sequence): pass def enable_editing_task_animation_colors(cls): pass @@ -719,6 +721,7 @@ class Sequence: def get_task_time_attributes(cls): pass def get_task_time(cls, task): pass def get_tasks_for_product(cls, product, work_schedule): pass + def get_user_predefined_type(cls): pass def get_work_calendar_attributes(cls): pass def get_work_plan_attributes(cls): pass def get_work_schedule_attributes(cls): pass 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/cad.py b/src/blenderbim/blenderbim/tool/cad.py index 9ed290e59a..6a28231b74 100644 --- a/src/blenderbim/blenderbim/tool/cad.py +++ b/src/blenderbim/blenderbim/tool/cad.py @@ -91,10 +91,14 @@ class Cad: tolerance = VTX_PRECISION if isinstance(x, (list, tuple)): for y in x: - if value > (y - tolerance) and value < (y + tolerance): + if (y + tolerance) > value > (y - tolerance): return True return False - return value > (x - tolerance) and value < (x + tolerance) + return (x + tolerance) > value > (x - tolerance) + + @classmethod + def are_vectors_equal(cls, v1: Vector, v2: Vector): + return cls.is_x((v2 - v1).length, 0) @classmethod def intersect_edges(cls, edge1, edge2): @@ -227,6 +231,48 @@ class Cad: res = [cls.is_point_on_edge(pt, edge) for edge in [edges[:2], edges[2:]]] return len([i for i in res if i]) + @classmethod + def get_edge_direction(cls, edge): + return (edge[1] - edge[0]).normalized() + + @classmethod + def are_edges_collinear(cls, edge1, edge2): + def is_point_on_line(p, edge): + a1, a2 = edge + # comparing slopes between PA1 and A2A1 + # using cross multiplication to avoid division by zero + return cls.is_x((p.y - a1.y) * (a2.x - a1.x), (a2.y - a1.y) * (p.x - a1.x)) + + edge1_dir = edge1[1] - edge1[0] + edge2_dir = edge2[1] - edge2[0] + + if cls.is_x(edge1_dir.cross(edge2_dir).length_squared, 0): # check they are parallel + if is_point_on_line(edge1[0], edge2) or is_point_on_line(edge1[1], edge2): + return True + return False + + @classmethod + def closest_points(cls, edge1, edge2): + """ + + closest end points between `edge1` and `edge2` assuming `edge1` and `edge2` are collinear. + + < returns two points, first one belongs to `edge1` and second to `edge2` + + """ + direction = (edge1[1] - edge1[0]).normalized() + + # Project points onto the line to get scalar values along the direction + points1_values = [(p, p.dot(direction)) for p in edge1] + points2_values = [(p, p.dot(direction)) for p in edge2] + + # Sort the projections for both edges + sorted_points1 = sorted(points1_values, key=lambda el: el[1]) + sorted_points2 = sorted(points2_values, key=lambda el: el[1]) + + # The closest points will be the last point of the first edge and the first point of the second edge + return sorted_points1[-1][0], sorted_points2[0][0] + @classmethod def find_intersecting_edges(cls, bm, pt, idx1, idx2): """ diff --git a/src/blenderbim/blenderbim/tool/cost.py b/src/blenderbim/blenderbim/tool/cost.py index aef65f093c..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 @@ -156,14 +156,18 @@ class Cost(blenderbim.core.tool.Cost): @classmethod def get_highlighted_cost_item(cls): props = bpy.context.scene.BIMCostProperties + if not props.active_cost_schedule_id: + return if props.active_cost_item_index < len(props.cost_items): return tool.Ifc.get().by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id) - return None + return @classmethod def load_cost_item_types(cls, cost_item=None): if not cost_item: - return + cost_item = cls.get_highlighted_cost_item() + if not cost_item: + return props = bpy.context.scene.BIMCostProperties props.cost_item_type_products.clear() # TODO implement process and resource types @@ -220,7 +224,7 @@ class Cost(blenderbim.core.tool.Cost): selected_quantitites = [] unit = "" for quantities in ifcopenshell.util.element.get_psets(product, qtos_only=True).values(): - for qto in (tool.Ifc.get().by_id(quantities["id"]).Quantities or []): + for qto in tool.Ifc.get().by_id(quantities["id"]).Quantities or []: for quantity in cost_item.CostQuantities: if quantity == qto: selected_quantitites.append(quantity) @@ -509,8 +513,9 @@ class Cost(blenderbim.core.tool.Cost): import subprocess import os import sys + if filepath: - path=filepath + path = filepath else: path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "build", "cost_schedules") @@ -518,6 +523,7 @@ class Cost(blenderbim.core.tool.Cost): os.makedirs(path) if format == "CSV": from ifc5d.ifc5Dspreadsheet import Ifc5DCsvWriter + writer = Ifc5DCsvWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule) writer.write() elif format == "ODS": @@ -528,7 +534,7 @@ class Cost(blenderbim.core.tool.Cost): elif format == "XLSX": from ifc5d.ifc5Dspreadsheet import Ifc5DXlsxWriter - writer = Ifc5DXlsxWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule ) + writer = Ifc5DXlsxWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule) writer.write() try: if path: @@ -539,7 +545,7 @@ class Cost(blenderbim.core.tool.Cost): elif sys.platform == "linux": subprocess.call(["xdg-open", path]) except: - return 'Could not open file location' + return "Could not open file location" @classmethod def get_units(cls): @@ -566,7 +572,6 @@ class Cost(blenderbim.core.tool.Cost): name = f"{unit.Prefix} {name}" return f"{unit.UnitType} / {name}" - @classmethod def get_cost_schedule(cls, cost_item): for rel in cost_item.HasAssignments or []: @@ -649,6 +654,8 @@ class Cost(blenderbim.core.tool.Cost): @classmethod def toggle_cost_item_parent_change(cls, cost_item=None): + if not cost_item: + return props = bpy.context.scene.BIMCostProperties if props.change_cost_item_parent: props.active_cost_item_id = cost_item.id() @@ -681,12 +688,14 @@ class Cost(blenderbim.core.tool.Cost): if product: cost_items = ifcopenshell.util.cost.get_cost_items_for_product(product) if pset: + def get_products_from_pset(pset): products = [] for rel in pset.DefinesOccurrence or []: if rel.is_a("IfcRelDefinesByProperties"): products.extend(rel.RelatedObjects) return products + products = get_products_from_pset(pset) for product in products or []: cost_items.extend(ifcopenshell.util.cost.get_cost_items_for_product(product)) @@ -705,4 +714,4 @@ class Cost(blenderbim.core.tool.Cost): currency = props.custom_currency return { "Currency": currency, - } \ No newline at end of file + } diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 73f24747d9..903794d4fa 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -122,11 +122,10 @@ class Drawing(blenderbim.core.tool.Drawing): # place the arrow # NOTE: may not work correctly in EDIT mode bbox = tool.Blender.get_object_bounding_box(stair) - float_is_zero = lambda f: 0.0001 >= f >= -0.0001 arrow.location = stair.matrix_world @ Vector( (bbox["min_x"], (bbox["max_y"] - bbox["min_y"]) / 2, bbox["max_z"]) ) - last_step_x = max(v.co.x for v in stair.data.vertices if float_is_zero(v.co.z - bbox["max_z"])) + last_step_x = max(v.co.x for v in stair.data.vertices if tool.Cad.is_x(v.co.z - bbox["max_z"], 0)) arrow.data.splines[0].points[0].co = Vector((0, 0, 0, 1)) arrow.data.splines[0].points[1].co = Vector((last_step_x, 0, 0, 1)) @@ -346,6 +345,27 @@ class Drawing(blenderbim.core.tool.Drawing): literals.append(literal_data) return literals + @classmethod + def create_annotation_context(cls, target_view, object_type=None): + # checking PLAN target view and annotation type that doesn't require 3d + if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and object_type not in ( + "FALL", + "SECTION_LEVEL", + "PLAN_LEVEL", + ): + parent = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan") + else: + parent = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model") + + return ifcopenshell.api.run( + "context.add_context", + tool.Ifc.get(), + context_type=parent.ContextType, + context_identifier="Annotation", + target_view=target_view, + parent=parent, + ) + @classmethod def get_annotation_context(cls, target_view, object_type=None): # checking PLAN target view and annotation type that doesn't require 3d @@ -451,7 +471,10 @@ class Drawing(blenderbim.core.tool.Drawing): elif target_view == "REFLECTED_PLAN_VIEW": if location_hint: z = tool.Ifc.get_object(tool.Ifc.get().by_id(location_hint)).matrix_world.translation.z - return mathutils.Matrix(((-1, 0, 0, x), (0, 1, 0, y), (0, 0, -1, z + 1.6), (0, 0, 0, 1))) + m = mathutils.Matrix() + m[2][2] = -1 + m.translation = (x, y, z + 1.6) + return m return mathutils.Matrix(((-1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0), (0, 0, 0, 1))) elif target_view == "ELEVATION_VIEW": if location_hint == "NORTH": @@ -678,6 +701,10 @@ class Drawing(blenderbim.core.tool.Drawing): ([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]) ) obj.matrix_world = mat + + if cls.get_drawing_target_view(drawing) == "REFLECTED_PLAN_VIEW": + obj.matrix_world[1][1] *= -1 + tool.Geometry.record_object_position(obj) tool.Collector.assign(obj) @@ -1484,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/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index 839f67ffb8..10f04a7f08 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -56,8 +56,13 @@ class Geometry(blenderbim.core.tool.Geometry): @classmethod def clear_scale(cls, obj): + # Note that clearing scale has no impact on cameras. if (obj.scale - Vector((1.0, 1.0, 1.0))).length > 1e-4: - if obj.data.users == 1: + if not obj.data: + obj.matrix_world[0][0] = 1 + obj.matrix_world[1][1] = 1 + obj.matrix_world[2][2] = 1 + elif obj.data.users == 1: context_override = {} context_override["object"] = context_override["active_object"] = obj context_override["selected_objects"] = context_override["selected_editable_objects"] = [obj] diff --git a/src/blenderbim/blenderbim/tool/georeference.py b/src/blenderbim/blenderbim/tool/georeference.py index 2dc6001b61..52c31de1a2 100644 --- a/src/blenderbim/blenderbim/tool/georeference.py +++ b/src/blenderbim/blenderbim/tool/georeference.py @@ -296,3 +296,37 @@ class Georeference(blenderbim.core.tool.Georeference): elif type == "rel_y": bpy.context.scene.BIMGeoreferenceProperties.y_axis_abscissa_output = str(x) bpy.context.scene.BIMGeoreferenceProperties.y_axis_ordinate_output = str(y) + + @classmethod + def import_plot(cls, filepath, map_conversion): + import bmesh + + def parse_csv(file_path): + import csv + + with open(file_path, "r") as f: + reader = csv.reader(f) # Assuming tab-delimited CSV + rows = [] + for row in reader: + if len(row) == 0: + continue + rows.append(row) + return rows + + rows = parse_csv(filepath) + vertices = [] + for row in rows: + coordinates = cls.enh2xyz([float(row[0]), float(row[1]), float(row[2])], map_conversion) + vertices.append(coordinates) + + mesh = bpy.data.meshes.new("mesh") + obj = bpy.data.objects.new("Plot Line", mesh) + bpy.context.scene.collection.objects.link(obj) + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + obj.data + bm = bmesh.new() + for vertex in vertices: + bm.verts.new(vertex) + bm.to_mesh(mesh) + bm.free() diff --git a/src/blenderbim/blenderbim/tool/loader.py b/src/blenderbim/blenderbim/tool/loader.py index 1b3ca755cc..5604f3f896 100644 --- a/src/blenderbim/blenderbim/tool/loader.py +++ b/src/blenderbim/blenderbim/tool/loader.py @@ -110,7 +110,7 @@ class Loader(blenderbim.core.tool.Loader): "IfcNormalisedRatioMeasure" ): diffuse_color_value = surface_style["DiffuseColour"].wrappedValue - diffuse_color = [v * diffuse_color_value for v in surface_style["SurfaceColor"][:3]] + [1] + diffuse_color = [v * diffuse_color_value for v in surface_style["SurfaceColour"][:3]] + [1] surface_style["DiffuseColour"] = ("IfcNormalisedRatioMeasure", diffuse_color) else: surface_style["DiffuseColour"] = None diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 705ca56710..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 @@ -768,6 +769,11 @@ class Model(blenderbim.core.tool.Model): return tool.Ifc.run("geometry.edit_object_placement", product=element, matrix=matrix, is_si=True) + @classmethod + def get_element_matrix(cls, element): + placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) + return Matrix(placement) + @classmethod def reload_body_representation(cls, obj_or_objects): """Update body representation including all decomposed objects""" 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/blenderbim/tool/search.py b/src/blenderbim/blenderbim/tool/search.py index 5edeb24e55..ea85c88589 100644 --- a/src/blenderbim/blenderbim/tool/search.py +++ b/src/blenderbim/blenderbim/tool/search.py @@ -55,7 +55,8 @@ class Search(blenderbim.core.tool.Search): comparison, value = cls.get_comparison_and_value(ifc_filter) filter_group_query.append(f"location{comparison}{value}") if not has_instance_or_entity_filter: - filter_group_query.insert(0, "IfcElement") + filter_group_query.insert(0, "IfcProduct") + filter_group_query.insert(0, "IfcTypeProduct") query.append(", ".join(filter_group_query)) return " + ".join(query) diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index eef330f713..43582e2723 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -19,7 +19,6 @@ import bpy import re import os -import isodate import ifcopenshell import ifcopenshell.util.sequence import ifcopenshell.util.date @@ -36,7 +35,6 @@ from datetime import datetime import mathutils import pystache import webbrowser -from datetime import timedelta class Sequence(blenderbim.core.tool.Sequence): @@ -212,7 +210,9 @@ class Sequence(blenderbim.core.tool.Sequence): item.calendar = "" item.derived_calendar = calendar.Name or "Unnamed" if calendar else "" - if task.TaskTime and (task.TaskTime.ScheduleStart or task.TaskTime.ScheduleFinish or task.TaskTime.ScheduleDuration): + if task.TaskTime and ( + task.TaskTime.ScheduleStart or task.TaskTime.ScheduleFinish or task.TaskTime.ScheduleDuration + ): task_time = task.TaskTime item.start = ( canonicalise_time(ifcopenshell.util.date.ifc2datetime(task_time.ScheduleStart)) @@ -719,26 +719,12 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def setup_default_task_columns(cls): - items = [ - { - "column_type": "IfcTaskTime", - "name": "ScheduleStart", - }, - { - "column_type": "IfcTaskTime", - "name": "ScheduleFinish", - }, - { - "column_type": "IfcTaskTime", - "name": "ScheduleDuration", - }, - ] - props = bpy.context.scene.BIMWorkScheduleProperties props.columns.clear() - for item in items: + default_columns = ["ScheduleStart", "ScheduleFinish", "ScheduleDuration"] + for item in default_columns: new = props.columns.add() - new.name = f"{item['column_type']}.{item['name']}" + new.name = f"IfcTaskTime.{item}" new.data_type = "string" @classmethod @@ -811,10 +797,10 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def update_visualisation_date(cls, start_date, finish_date): def canonicalise_time(time): - if not time: - return "-" return time.strftime("%d/%m/%y") + if not (start_date and finish_date): + return props = bpy.context.scene.BIMWorkScheduleProperties props.visualisation_start = canonicalise_time(start_date) props.visualisation_finish = canonicalise_time(finish_date) @@ -1227,21 +1213,23 @@ class Sequence(blenderbim.core.tool.Sequence): start, finish, props.speed_animation_frames, - isodate.parse_duration(props.speed_real_duration), + ifcopenshell.util.date.parse_duration(props.speed_real_duration), ) elif props.speed_types == "DURATION_SPEED": + animation_duration = ifcopenshell.util.date.parse_duration(props.speed_animation_duration) + real_duration = ifcopenshell.util.date.parse_duration(props.speed_real_duration) return calculate_using_duration( start, finish, fps, - isodate.parse_duration(props.speed_animation_duration), - isodate.parse_duration(props.speed_real_duration), + animation_duration, + real_duration, ) elif props.speed_types == "MULTIPLIER_SPEED": return calculate_using_multiplier( start, finish, - fps, + 1, props.speed_multiplier, ) @@ -1312,15 +1300,24 @@ class Sequence(blenderbim.core.tool.Sequence): if obj.animation_data: obj.animation_data_clear() + @classmethod + def clear_object_color(cls, obj): + obj.color = (1.0, 1.0, 1.0, 1.0) + + @classmethod + def display_object(cls, obj): + if not obj.visible_get(): + obj.hide_viewport = False + obj.hide_render = False + @classmethod def clear_objects_animation(cls, include_blender_objects=True): for obj in bpy.data.objects: if not include_blender_objects and not obj.BIMObjectProperties.ifc_definition_id: continue cls.clear_object_animation(obj) - if not obj.visible_get(): - obj.hide_viewport = False - obj.hide_render = False + cls.clear_object_color(obj) + cls.display_object(obj) @classmethod def animate_objects(cls, settings, frames, clear_previous=True, animation_type=""): @@ -1515,13 +1512,13 @@ class Sequence(blenderbim.core.tool.Sequence): compare_start = schedule_start compare_finish = schedule_finish task_name = task.Name or "Unnamed" - task_name = task_name.replace('\n', "") + task_name = task_name.replace("\n", "") data = { "pID": task.id(), "pName": task_name, "pCaption": task_name, "pStart": schedule_start, - "pEnd": schedule_finish , + "pEnd": schedule_finish, "pPlanStart": compare_start, "pPlanEnd": compare_finish, "pMile": 1 if task.IsMilestone else 0, @@ -1607,4 +1604,28 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def is_sort_reversed(cls): - return bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed \ No newline at end of file + return bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed + + @classmethod + def get_user_predefined_type(cls): + predefined_type = bpy.context.scene.BIMWorkScheduleProperties.work_schedule_predefined_types + object_type = None + if predefined_type == "USERDEFINED": + object_type = bpy.context.scene.BIMWorkScheduleProperties.object_type + return predefined_type, object_type + + @classmethod + def add_animation_camera(cls): + bpy.ops.object.camera_add() + camera = bpy.context.active_object + camera.data.lens = 26 + camera.name = "4D Camera" + camera.location = mathutils.Vector((15, 0, 15)) + camera.rotation_euler = mathutils.Euler((1.2, 0, 1.5), "XYZ") + for obj in bpy.context.scene.objects: + obj.select_set(False) + for obj in bpy.context.visible_objects: + if not (obj.hide_get() or obj.hide_render) and obj.type != "LIGHT": + obj.select_set(True) + bpy.context.scene.camera = camera + bpy.ops.view3d.camera_to_view_selected() diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py index e6aa4ecabc..3bb15d5cb0 100644 --- a/src/blenderbim/blenderbim/tool/system.py +++ b/src/blenderbim/blenderbim/tool/system.py @@ -21,9 +21,35 @@ import ifcopenshell.util.system import blenderbim.core.tool import blenderbim.tool as tool from blenderbim.bim import import_ifc +import re +from mathutils import Matrix class System(blenderbim.core.tool.System): + @classmethod + def add_ports(cls, obj, add_start_port=True, add_end_port=True): + def add_port(mep_element, matrix): + port = tool.Ifc.run("system.add_port", element=mep_element) + port.FlowDirection = "NOTDEFINED" + port.PredefinedType = tool.System.get_port_predefined_type(mep_element) + tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=matrix, is_si=True) + return port + + # make sure obj.dimensions and .matrix_world has valid data + bpy.context.view_layer.update() + # need to make sure .ObjectPlacement is also updated when we're going to add ports + if tool.Ifc.is_moved(obj): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + + mep_element = tool.Ifc.get_entity(obj) + length = obj.dimensions.z + ports = [] + if add_start_port: + ports.append(add_port(mep_element, obj.matrix_world @ Matrix())) + if add_end_port: + ports.append(add_port(mep_element, obj.matrix_world @ Matrix.Translation((0, 0, length)))) + return ports + @classmethod def create_empty_at_cursor_with_element_orientation(cls, element): element_obj = tool.Ifc.get_object(element) @@ -66,7 +92,19 @@ class System(blenderbim.core.tool.System): @classmethod def get_port_relating_element(cls, port): - return port.Nests[0].RelatingObject + if tool.Ifc.get_schema() == "IFC2X3": + element = port.ContainedIn[0].RelatedElement + else: + element = port.Nests[0].RelatingObject + return element + + @classmethod + def get_port_predefined_type(cls, mep_element): + split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x) + class_name = "".join(split_camel_case(mep_element.is_a())[1:-1]).upper() + if class_name == "CONVEYOR": + return "NOTDEFINED" + return class_name @classmethod def import_system_attributes(cls, system): 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/blenderbim/test/bim/feature/sequence.feature b/src/blenderbim/test/bim/feature/sequence.feature index 1015ef660e..fba026a0e8 100644 --- a/src/blenderbim/test/bim/feature/sequence.feature +++ b/src/blenderbim/test/bim/feature/sequence.feature @@ -326,7 +326,7 @@ Scenario: See the current frame date as text And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" - And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" When I am on frame "1" Then the object "Timeline" has a body of "2021-01-01" @@ -357,7 +357,7 @@ Scenario: Animate the construction of a wall And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" - And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" When I am on frame "1" Then "scene.objects.get('IfcWall/Cube').hide_viewport" is "True" @@ -393,7 +393,7 @@ Scenario: Animate the demolition of a wall And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" - And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" When I am on frame "1" Then "scene.objects.get('IfcWall/Cube').hide_viewport" is "False" @@ -432,7 +432,7 @@ Scenario: Animate the operation of a wall And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" - And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" When I am on frame "1" Then "scene.objects.get('IfcWall/Cube').color[:]" is "[1.0, 1.0, 1.0, 1]" @@ -472,7 +472,7 @@ Scenario: Animate the movement of a wall And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" - And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" When I am on frame "1" Then "scene.objects.get('IfcWall/FromObject').color[:]" is "[1.0, 1.0, 1.0, 1]" @@ -516,7 +516,7 @@ Scenario: Animate the consumption of a wall And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" - And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" When I am on frame "1" Then "scene.objects.get('IfcWall/Cube').color[:]" is "[1.0, 1.0, 1.0, 1]" @@ -532,6 +532,39 @@ Scenario: Animate the consumption of a wall Then "scene.objects.get('IfcWall/Cube').hide_render" is "True" + +Scenario: Clear Previous Animation + Given an empty IFC project + And I press "bim.add_work_schedule" + And the variable "work_schedule" is "IfcStore.get_file().by_type('IfcWorkSchedule')[0].id()" + And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})" + And I press "bim.add_summary_task(work_schedule={work_schedule})" + And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()" + And I press "bim.enable_editing_task_attributes(task={task})" + And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "CONSTRUCTION" + And I press "bim.edit_task" + And I press "bim.enable_editing_task_time(task={task})" + And I set "scene.BIMWorkScheduleProperties.task_time_attributes.get('ScheduleStart').string_value" to "2021-01-02" + And I set "scene.BIMWorkScheduleProperties.task_time_attributes.get('ScheduleFinish').string_value" to "2021-01-06" + And I press "bim.edit_task_time" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And the object "IfcWall/Cube" is selected + And I press "bim.assign_product(task={task})" + And I set "scene.BIMWorkScheduleProperties.visualisation_start" to "01/01/21" + And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" + And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" + And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" + And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" + And I press "bim.clear_previous_animation" + When I am on frame "3" + Then "scene.objects.get('IfcWall/Cube').hide_viewport" is "False" + And "scene.objects.get('IfcWall/Cube').hide_render" is "False" + And "scene.objects.get('IfcWall/Cube').color[:]" is "[1.0, 1.0, 1.0, 1]" + Scenario: Generate Gantt Chart Given an empty IFC project And I press "bim.add_work_schedule" @@ -808,4 +841,13 @@ Scenario: Duplicate Task and edit sequence Relationship And I press "bim.assign_successor(task={nested_task_two})" And I press "bim.duplicate_task(task={task})" When I press "bim.enable_editing_task_sequence(task={nested_task_one})" - Then nothing happens \ No newline at end of file + Then nothing happens + +Scenario: Add Animation Camera + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And I press "bim.add_animation_camera" + Then "scene.objects.get('4D Camera').name" is "4D Camera" 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/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp b/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp index e606304b63..7672eb381f 100644 --- a/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp +++ b/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp @@ -428,6 +428,8 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model) for (int i = 1; i <= n; ++i) { gp_XYZ p = tessellater.Value(i).XYZ(); + auto p_local = p; + trsf.Transforms(p); int current = addVertex(iit->ItemId(), surface_style_id, p); @@ -455,11 +457,10 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model) } d3 = d1.XYZ() + d2.XYZ(); d4 = d1.XYZ() - d2.XYZ(); - p2 = p - d3.XYZ() / 10.; - p3 = p - d4.XYZ() / 10.; + p2 = p_local - d3.XYZ() / 10.; + p3 = p_local - d4.XYZ() / 10.; trsf.Transforms(p2); trsf.Transforms(p3); - trsf.Transforms(p); int left = addVertex(iit->ItemId(), surface_style_id, p2); int right = addVertex(iit->ItemId(), surface_style_id, p3); 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/geometry/add_profile_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py index d2eddcdeb0..fe028bc657 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py @@ -63,7 +63,11 @@ class Usecase: def apply_clippings(self, first_operand): while self.settings["clippings"]: clipping = self.settings["clippings"].pop() - if clipping["operand_type"] == "IfcHalfSpaceSolid": + if isinstance(clipping, ifcopenshell.entity_instance): + new = ifcopenshell.util.element.copy(self.file, clipping) + new.FirstOperand = first_operand + first_operand = new + elif clipping["operand_type"] == "IfcHalfSpaceSolid": matrix = clipping["matrix"] second_operand = self.file.createIfcHalfSpaceSolid( self.file.createIfcPlane( @@ -81,7 +85,7 @@ class Usecase: ), False, ) - first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand) + first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand) return first_operand def convert_si_to_unit(self, co): diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py index c146c8cdf3..799e1b252f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py @@ -95,7 +95,11 @@ class Usecase: def apply_clippings(self, first_operand): while self.settings["clippings"]: clipping = self.settings["clippings"].pop() - if clipping["operand_type"] == "IfcHalfSpaceSolid": + if isinstance(clipping, ifcopenshell.entity_instance): + new = ifcopenshell.util.element.copy(self.file, clipping) + new.FirstOperand = first_operand + first_operand = new + elif clipping["operand_type"] == "IfcHalfSpaceSolid": matrix = clipping["matrix"] second_operand = self.file.createIfcHalfSpaceSolid( self.file.createIfcPlane( @@ -113,7 +117,7 @@ class Usecase: ), False, ) - first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand) + first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand) return first_operand def convert_si_to_unit(self, co): diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py index 983d6fb9c0..e228730974 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py @@ -22,7 +22,7 @@ import ifcopenshell.util.unit class Usecase: - def __init__(self, file, element=None, context=None, p1=None, p2=None, elevation=None, height=None, thickness=None): + def __init__(self, file, element=None, context=None, p1=None, p2=None, elevation=None, height=None, thickness=None, is_si=True): self.file = file self.settings = { "element": element, @@ -32,15 +32,25 @@ class Usecase: "elevation": elevation, "height": height, "thickness": thickness, + "is_si": is_si } def execute(self): self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) - self.settings["p1"] = np.array(self.settings["p1"]) - self.settings["p2"] = np.array(self.settings["p2"]) + self.settings["p1"] = np.array(self.settings["p1"]).astype(float) + self.settings["p2"] = np.array(self.settings["p2"]).astype(float) length = float(np.linalg.norm(self.settings["p2"] - self.settings["p1"])) + + if not self.settings["is_si"]: + length=self.convert_unit_to_si(length) + self.settings["height"]=self.convert_unit_to_si(self.settings["height"]) + self.settings["thickness"]=self.convert_unit_to_si(self.settings["thickness"]) + self.settings["p1"][0] = self.convert_unit_to_si(self.settings["p1"][0]) + self.settings["p1"][1] = self.convert_unit_to_si(self.settings["p1"][1]) + self.settings["elevation"] = self.convert_unit_to_si(self.settings["elevation"]) + representation = ifcopenshell.api.run( "geometry.add_wall_representation", self.file, @@ -55,7 +65,7 @@ class Usecase: [ [v[0], -v[1], 0, self.settings["p1"][0]], [v[1], v[0], 0, self.settings["p1"][1]], - [0, 0, 1, self.convert_si_to_unit(self.settings["elevation"])], + [0, 0, 1, self.settings["elevation"]], [0, 0, 0, 1], ] ) @@ -64,7 +74,5 @@ class Usecase: ) return representation - def convert_si_to_unit(self, co): - if isinstance(co, (tuple, list)): - return [self.convert_si_to_unit(o) for o in co] - return co / self.settings["unit_scale"] + def convert_unit_to_si(self, co): + return co * self.settings["unit_scale"] 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/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index f825445428..aeb880550b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -172,6 +172,7 @@ class Usecase: should_run_listeners=False, related_object=element, relating_type=new_type, + should_map_representations=False, ) ifcopenshell.api.owner.settings.restore() diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py index c3577a43e0..0ec81a87d7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py @@ -65,13 +65,29 @@ class Usecase: representations = self.settings["product"].Representation.Representations or [] else: representations = [] - + + # remove object placements object_placement = self.settings["product"].ObjectPlacement - if object_placement and self.file.get_total_inverses(object_placement) == 1: - self.settings["product"].ObjectPlacement = None # remove the inverse for remove_deep2 to work - ifcopenshell.util.element.remove_deep2(self.file, object_placement) + if object_placement: + if self.file.get_total_inverses(object_placement) == 1: + self.settings["product"].ObjectPlacement = None # remove the inverse for remove_deep2 to work + ifcopenshell.util.element.remove_deep2(self.file, object_placement) + elif self.settings["product"].is_a("IfcTypeProduct"): representations = [rm.MappedRepresentation for rm in self.settings["product"].RepresentationMaps or []] + + # remove psets + psets = self.settings["product"].HasPropertySets or [] + for pset in psets: + if self.file.get_total_inverses(pset) != 1: + continue + ifcopenshell.api.run( + "pset.remove_pset", + self.file, + product=self.settings["product"], + pset=pset, + ) + for representation in representations: ifcopenshell.api.run( "geometry.unassign_representation", diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py index ec63d53b3d..a9c775d338 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py @@ -27,6 +27,7 @@ class Usecase: file, name="Unnamed", predefined_type="NOTDEFINED", + object_type=None, start_time=None, work_plan=None, ): @@ -77,6 +78,7 @@ class Usecase: self.settings = { "name": name, "predefined_type": predefined_type, + "object_type": object_type, "start_time": start_time or datetime.now(), "work_plan": work_plan, } @@ -98,7 +100,8 @@ class Usecase: work_schedule.StartTime = ifcopenshell.util.date.datetime2ifc( self.settings["start_time"], "IfcDateTime" ) - + if self.settings["object_type"]: + work_schedule.ObjectType = self.settings["object_type"] if self.settings["work_plan"]: ifcopenshell.api.run( "aggregate.assign_object", diff --git a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py index 33bc25ab72..324e6c548d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py +++ b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py @@ -22,7 +22,7 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, related_object=None, relating_type=None): + def __init__(self, file, related_object=None, relating_type=None, should_map_representations=True): """Assigns a type to an occurrence of an object IFC supports the concept of occurrences and types. An occurrence is an @@ -87,6 +87,11 @@ class Usecase: :type related_object: ifcopenshell.entity_instance.entity_instance :param relating_type: The IfcElementType type. :type relating_type: ifcopenshell.entity_instance.entity_instance + :param should_map_representations: If a type has a representation map, + IFC requires all occurrences to map those representations. Some IFC + vendors might disobey this, or you might want to handle it + yourself. In this scenario, you may set this to False. + :type should_map_representations: bool :return: The IfcRelDefinesByType relationship :rtype: ifcopenshell.entity_instance.entity_instance @@ -164,6 +169,7 @@ class Usecase: self.settings = { "related_object": related_object, "relating_type": relating_type, + "should_map_representations": should_map_representations, } def execute(self): @@ -207,14 +213,15 @@ class Usecase: } ) - if getattr(self.settings["relating_type"], "RepresentationMaps", None): - ifcopenshell.api.run( - "type.map_type_representations", - self.file, - related_object=self.settings["related_object"], - relating_type=self.settings["relating_type"], - ) - self.map_material_usages() + if self.settings["should_map_representations"]: + if getattr(self.settings["relating_type"], "RepresentationMaps", None): + ifcopenshell.api.run( + "type.map_type_representations", + self.file, + related_object=self.settings["related_object"], + relating_type=self.settings["relating_type"], + ) + self.map_material_usages() return types def map_material_usages(self): diff --git a/src/ifcopenshell-python/ifcopenshell/draw.py b/src/ifcopenshell-python/ifcopenshell/draw.py index 3e1a25d149..df4169ddd8 100644 --- a/src/ifcopenshell-python/ifcopenshell/draw.py +++ b/src/ifcopenshell-python/ifcopenshell/draw.py @@ -60,6 +60,7 @@ class draw_settings: merge_cells: bool = False include_projection: bool = True prefilter: bool = True + include_curves: bool = False def main(settings, files, iterators=None, merge_projection=True, progress_function=DO_NOTHING): @@ -68,6 +69,7 @@ def main(settings, files, iterators=None, merge_projection=True, progress_functi # this is required for serialization APPLY_DEFAULT_MATERIALS=True, DISABLE_TRIANGULATION=True, + INCLUDE_CURVES=settings.include_curves, # when not doing booleans, proper solids from shells isn't a requirement SEW_SHELLS=settings.subtract_before_hlr, ) 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..2f08dc4049 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -294,7 +294,9 @@ class FacetTransformer(lark.Transformer): return False def compare(self, element_value, comparison, value): - if isinstance(value, str): + if isinstance(element_value, (list, tuple)): + return any(self.compare(ev, comparison, value) for ev in element_value) + elif isinstance(value, str): if isinstance(element_value, int): value = int(value) elif isinstance(element_value, float): @@ -520,6 +522,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/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index fdcd02815c..6cf52d0581 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -19,7 +19,7 @@ import collections import ifcopenshell import ifcopenshell.api -from math import cos, sin, pi +from math import cos, sin, pi, tan, radians from mathutils import Vector, Matrix from itertools import chain @@ -539,7 +539,7 @@ class ShapeBuilder: "Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcPositiveLengthMeasure.htm#8.11.2.71.3-Formal-representation" ) - if profile_or_curve.is_a() not in ("IfcArbitraryClosedProfileDef", "IfcArbitraryProfileDefWithVoids"): + if not profile_or_curve.is_a("IfcProfileDef"): profile_or_curve = self.profile(profile_or_curve) if position_y_axis: @@ -579,6 +579,8 @@ class ShapeBuilder: representation_type = "AdvancedSweptSolid" elif "IfcExtrudedAreaSolid" in item_types: representation_type = "SweptSolid" + elif items[0].is_a("IfcTessellatedItem"): + representation_type = "Tessellation" elif items[0].is_a("IfcCurve") and items[0].Dim == 3: representation_type = "Curve3D" else: @@ -746,8 +748,10 @@ class ShapeBuilder: ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments) return (points, segments, ifc_curve) - - def create_z_profile_lips_curve(self, FirstFlangeWidth, SecondFlangeWidth, Depth, Girth, WallThickness, FilletRadius): + + def create_z_profile_lips_curve( + self, FirstFlangeWidth, SecondFlangeWidth, Depth, Girth, WallThickness, FilletRadius + ): x1 = FirstFlangeWidth x2 = SecondFlangeWidth y = Depth / 2 @@ -770,20 +774,21 @@ class ShapeBuilder: (-x1+t, -y+t), (-t/2, -y+t) ) - # fmt: on # option for no additional thickness in outer radius: # points, segments, ifc_curve = create_curve_from_coords( # coords, fillets = (0, 1, 4, 5, 6, 7, 10, 11), fillet_radius=r, closed=True, ifc_file=ifc_file # ) - points, segments, ifc_curve = self.get_simple_2dcurve_data(coords, + points, segments, ifc_curve = self.get_simple_2dcurve_data( + coords, fillets = (0, 1, 4, 5, 6, 7, 10, 11), fillet_radius=(r+t, r+t, r, r, r+t, r+t, r, r), closed=True, create_ifc_curve=True) + # fmt: on return ifc_curve - + def create_transition_arc_ifc(self, width, height, create_ifc_curve=False): # create an arc in the rectangle with specified width and height # if it's not possible to make a complete arc @@ -814,4 +819,114 @@ class ShapeBuilder: points, segments, transition_arc = self.get_simple_2dcurve_data( curve_coords, fillets, fillet_radius, closed=False, create_ifc_curve=create_ifc_curve ) - return points, segments, transition_arc \ No newline at end of file + return points, segments, transition_arc + + def polygonal_face_set(self, points, faces): + """ + > `points` - list of points + + > `faces` - list of faces consisted of point indices (points indices starting from 0) + + < IfcPolygonalFaceSet + """ + + ifc_points = self.file.createIfcCartesianPointList3D(points) + ifc_faces = [] + for face in faces: + face = [i + 1 for i in face] + ifc_faces.append(self.file.createIfcIndexedPolygonalFace(face)) + + face_set = self.file.createIfcPolygonalFaceSet(Coordinates=ifc_points, Faces=ifc_faces) + + return face_set + + def mep_transition_shape(self, start_segment, end_segment, start_length, end_length, angle=30.0): + """ + returns tuple of Model/Body/MODEL_VIEW IfcRepresentation and transition shape data + """ + # good default values from angle = 30/60 deg + # 30 degree angle will result in 75 degrees on the transition (= 90 - α/2) - https://i.imgur.com/tcoYDWu.png + + # TODO: get rid of reliance on profiles + def get_profile(element): + material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) + if material and material.is_a("IfcMaterialProfileSet") and len(material.MaterialProfiles) == 1: + return material.MaterialProfiles[0].Profile + + start_profile = get_profile(start_segment) + end_profile = get_profile(end_segment) + + # TODO: support more profiles + if not start_profile.is_a("IfcRectangleProfileDef") or not end_profile.is_a("IfcRectangleProfileDef"): + # Non rectangular profiles are not yet supported + return None, None + + start_half_dim = V(start_profile.XDim / 2, start_profile.YDim / 2, start_length) + end_half_dim = V(end_profile.XDim / 2, end_profile.YDim / 2, end_length) + + transition_items = [] + end_extrusion_offset = V(0, 0, start_length) + + def get_transition_legth(start_half_dim, end_half_dim, angle): + diff = start_half_dim.xy - end_half_dim.xy + diff = Vector([abs(i) for i in diff]) + c = diff.x * tan(radians(90 - angle / 2)) + a = diff.y + b = (c**2 - a**2) ** 0.5 + return b + + transition_length = get_transition_legth(start_half_dim, end_half_dim, angle) + faces = [] + if transition_length != 0: + end_extrusion_offset.z += transition_length + + faces += [(3, 4, 7, 0), (11, 8, 15, 12), (3, 11, 12, 4), (7, 15, 8, 0)] + + # NOTE: clockwise order for correct face orientation + faces += [ + # start extrusion + (0, 1, 2, 3), + (8, 11, 10, 9), + (0, 8, 9, 1), + (1, 9, 10, 2), + (2, 10, 11, 3), + # end extrusion + (4, 5, 6, 7), + (12, 15, 14, 13), + (4, 12, 13, 5), + (5, 13, 14, 6), + (6, 14, 15, 7), + ] + points = [ + start_half_dim * V(-1, -1, 1), + start_half_dim * V(-1, -1, 0), + start_half_dim * V(1, -1, 0), + start_half_dim * V(1, -1, 1), + end_half_dim * V(1, -1, 0) + end_extrusion_offset, + end_half_dim * V(1, -1, 1) + end_extrusion_offset, + end_half_dim * V(-1, -1, 1) + end_extrusion_offset, + end_half_dim * V(-1, -1, 0) + end_extrusion_offset, + start_half_dim * V(-1, 1, 1), + start_half_dim * V(-1, 1, 0), + start_half_dim * V(1, 1, 0), + start_half_dim * V(1, 1, 1), + end_half_dim * V(1, 1, 0) + end_extrusion_offset, + end_half_dim * V(1, 1, 1) + end_extrusion_offset, + end_half_dim * V(-1, 1, 1) + end_extrusion_offset, + end_half_dim * V(-1, 1, 0) + end_extrusion_offset, + ] + + face_set = self.polygonal_face_set(points, faces) + transition_items.append(face_set) + + body = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW") + representation = self.get_representation(body, transition_items, "Tesselation") + transition_data = { + "start_length": start_length, + "end_length": end_length, + "angle": angle, + "transition_length": transition_length, + "full_transition_length": start_length + transition_length + end_length, + } + + return representation, transition_data 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/api/root/test_remove_product.py b/src/ifcopenshell-python/test/api/root/test_remove_product.py index d45b306e76..eb8a0d6631 100644 --- a/src/ifcopenshell-python/test/api/root/test_remove_product.py +++ b/src/ifcopenshell-python/test/api/root/test_remove_product.py @@ -54,6 +54,24 @@ class TestRemoveProduct(test.bootstrap.IFC4): ifcopenshell.api.run("root.remove_product", self.file, product=element1) assert len(self.file.by_type("IfcObjectPlacement")) == 0 + def test_removing_element_type_psets(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foo_Bar") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Bar"}) + + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element2.HasPropertySets = (pset,) + + # make sure it won't remove the pset if it's connected elsewhere + ifcopenshell.api.run("root.remove_product", self.file, product=element2) + assert len(self.file.by_type("IfcPropertySet")) == 1 + assert len(self.file.by_type("IfcPropertySingleValue")) == 1 + + # if it's the product is the only inverse for pset, it should remove the pset + ifcopenshell.api.run("root.remove_product", self.file, product=element) + assert len(self.file.by_type("IfcPropertySet")) == 0 + assert len(self.file.by_type("IfcPropertySingleValue")) == 0 + def test_removing_all_representations_of_an_element(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") ifcopenshell.api.run("unit.assign_unit", self.file) 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") diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 0ff7227b47..67749417bd 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -137,6 +137,9 @@ class TestFilterElements(test.bootstrap.IFC4): ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Baz": 123}) assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz=123") == {element} ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Bay": 123.3}) + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Pset_WallCommon") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Status": ["New"]}) + assert subject.filter_elements(self.file, "IfcWall, Pset_WallCommon.Status=New") == {element} def test_selecting_by_classification(self): project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index 0cac09d14d..ee9e952269 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -21,6 +21,8 @@ import sys import math import logging import datetime +import ifcopenshell +import ifcopenshell.util.element cwd = os.path.dirname(os.path.realpath(__file__)) @@ -163,6 +165,7 @@ class Json(Reporter): return self.results def report_specification(self, specification): + applicability = [a.to_string("applicability") for a in specification.applicability] requirements = [] for requirement in specification.requirements: requirements.append( @@ -182,12 +185,23 @@ class Json(Reporter): "total": total, "percentage": percentage, "required": specification.minOccurs != 0, + "applicability": applicability, "requirements": requirements, } def report_failed_entities(self, requirement): return [ - {"reason": requirement.failed_reasons[i], "element": str(e)} + { + "reason": requirement.failed_reasons[i], + "element": str(e), + "class": e.is_a(), + "predefined_type": ifcopenshell.util.element.get_predefined_type(e), + "name": getattr(e, "Name", None), + "description": getattr(e, "Description", None), + "id": e.id(), + "global_id": getattr(e, "GlobalId", None), + "tag": getattr(e, "Tag", None), + } for i, e in enumerate(requirement.failed_entities) ] diff --git a/src/serializers/SvgSerializer.cpp b/src/serializers/SvgSerializer.cpp index 43e2c8b941..f1962e6570 100644 --- a/src/serializers/SvgSerializer.cpp +++ b/src/serializers/SvgSerializer.cpp @@ -761,7 +761,14 @@ void SvgSerializer::write(const geometry_data& data) { // SVG has a coordinate system with the origin in the *upper*-left corner // therefore we mirror the shape along the XZ-plane. gp_Trsf trsf_mirror; - trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY())); + if (!mirror_y_) { + trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY())); + } + if (mirror_x_) { + gp_Trsf mirror_x; + mirror_x.SetMirror(gp_Ax2(gp::Origin(), gp::DX())); + trsf_mirror.PreMultiply(mirror_x); + } BRepBuilderAPI_Transform make_transform_mirror(compound_unmirrored, trsf_mirror, true); make_transform_mirror.Build(); // (When determinant < 0, copy is implied and the input is not mutated.) @@ -1667,6 +1674,13 @@ std::array, 3> SvgSerializer::resize() { cy = ymin * sc; } + if (mirror_y_) { + cy = - size_->second - cy; + } + if (mirror_x_) { + cx = - size_->first - cx; + } + m = {{ {{sc,0,-cx}},{{0,sc,-cy}},{{0,0,1}} }}; float_item_list::const_iterator it; @@ -1704,7 +1718,14 @@ void SvgSerializer::draw_hlr(const gp_Pln& pln, const drawing_key& drawing_name) TopoDS_Shape hlr_compound; if (drawing_name.first == nullptr) { gp_Trsf trsf_mirror; - trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY())); + if (!mirror_y_) { + trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY())); + } + if (mirror_x_) { + gp_Trsf mirror_x; + mirror_x.SetMirror(gp_Ax2(gp::Origin(), gp::DX())); + trsf_mirror.PreMultiply(mirror_x); + } BRepBuilderAPI_Transform make_transform_mirror(hlr_compound_unmirrored, trsf_mirror, true); make_transform_mirror.Build(); hlr_compound = make_transform_mirror.Shape(); diff --git a/src/serializers/SvgSerializer.h b/src/serializers/SvgSerializer.h index 1424c1420e..6cd806737c 100644 --- a/src/serializers/SvgSerializer.h +++ b/src/serializers/SvgSerializer.h @@ -309,11 +309,16 @@ namespace { } } else { gp_Pnt2d tmp; - for (int i = 0; i < 4; ++i) { + // 0,1,2,3 -> interp over bounding box edges (i%4, (i+1)%4) + // 4,5 -> interp over bounding box diagonals (i%4, (i+2)%4) + // @todo use boolean_utils.h points_on_planar_face_generator? + // ... or skip faces with inner bounds all together ? + // ... ? + for (int i = 0; i < 6; ++i) { // @todo proper edge intersection for (int j = 0; j < 16; ++j) { - const gp_Pnt2d& a = *loop[i]; - const gp_Pnt2d& b = *loop[(i + 1) % 4]; + const gp_Pnt2d& a = *loop[i % 4]; + const gp_Pnt2d& b = *loop[(i + (i >= 4 ? 2 : 1)) % 4]; interp(a, b, j / 16.0, tmp); if (fclass->Perform(tmp) == TopAbs_OUT) { return false; @@ -516,6 +521,8 @@ protected: bool emit_building_storeys_; bool no_css_; bool unify_inputs_; + bool mirror_y_; + bool mirror_x_; int profile_threshold_; @@ -567,6 +574,8 @@ public: , polygonal_(false) , emit_building_storeys_(true) , no_css_(false) + , mirror_y_(false) + , mirror_x_(false) , unify_inputs_(false) , profile_threshold_(-1) , file(0) @@ -708,6 +717,22 @@ public: return profile_threshold_; } + void setMirrorY(bool b) { + mirror_y_ = b; + } + + bool getMirrorY() const { + return mirror_y_; + } + + void setMirrorX(bool b) { + mirror_x_ = b; + } + + bool getMirrorX() const { + return mirror_x_; + } + protected: std::string writeMetadata(const drawing_meta& m); };