From 2128eda87bf701d7a6c02cc93e1a1c065b2a489f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 22 Aug 2023 18:09:04 +0500 Subject: [PATCH 01/86] Save blenderbim last commit hash to the ifc header Now it's possible to identify blenderbim version from .ifc file which is useful for debugging Example: FILE_NAME('test.ifc','2023-08-22T18:07:07+05:00',(),(),'IfcOpenShell v0.7.0-fc50bdd3a','BlenderBIM 0.0.999999-64bc7af','Nobody'); --- src/blenderbim/blenderbim/bim/export_ifc.py | 7 +++++-- src/blenderbim/blenderbim/bim/ui.py | 15 +++------------ src/blenderbim/blenderbim/tool/blender.py | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index 50412c2354..5813e8081a 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -82,7 +82,7 @@ class IfcExporter: ) self.file.wrapped_data.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version) self.file.wrapped_data.header.file_name.originating_system = "{} {}".format( - self.get_application_name(), self.get_application_version() + self.get_application_name(), tool.Blender.get_blenderbim_version() ) def sync_all_objects(self): @@ -162,7 +162,7 @@ class IfcExporter: return "BlenderBIM" def get_application_version(self): - return ".".join( + version = ".".join( [ str(x) for x in [ @@ -172,6 +172,9 @@ class IfcExporter: ][0] ] ) + if blenderbim.bim.last_commit_hash != "8888888": + version += f"-{blenderbim.bim.last_commit_hash[:7]}" + return version class IfcExportSettings: diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 03c8514ee1..7a568e85fa 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -464,6 +464,7 @@ class BIM_PT_tab_quality_control(Panel): def draw(self, context): pass + class BIM_PT_tab_clash_detection(Panel): bl_label = "Clash Detection" bl_space_type = "PROPERTIES" @@ -477,6 +478,7 @@ class BIM_PT_tab_clash_detection(Panel): def draw(self, context): pass + class BIM_PT_tab_sandbox(Panel): bl_label = "Sandbox" bl_space_type = "PROPERTIES" @@ -658,24 +660,13 @@ class UIData: @classmethod def version(cls): - return ".".join( - [ - str(x) - for x in [ - addon.bl_info.get("version", (-1, -1, -1)) - for addon in addon_utils.modules() - if addon.bl_info["name"] == "BlenderBIM" - ][0] - ] - ) + return tool.Blender.get_blenderbim_version() def draw_statusbar(self, context): if not UIData.is_loaded: UIData.load() text = f"BlenderBIM Add-on v{UIData.data['version']}" - if blenderbim.bim.last_commit_hash != "8888888": - text += f"-{blenderbim.bim.last_commit_hash[:7]}" self.layout.label(text=text) diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index f3702bb801..0ce77b6d1a 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -20,8 +20,10 @@ import bpy import json import ifcopenshell.api import blenderbim.tool as tool +import blenderbim.bim from mathutils import Vector from pathlib import Path +import addon_utils VIEWPORT_ATTRIBUTES = [ @@ -588,3 +590,19 @@ class Blender: child_obj = tool.Blender.get_object_from_guid(child_guid) if child_obj: yield child_obj + + @classmethod + def get_blenderbim_version(cls): + version = ".".join( + [ + str(x) + for x in [ + addon.bl_info.get("version", (-1, -1, -1)) + for addon in addon_utils.modules() + if addon.bl_info["name"] == "BlenderBIM" + ][0] + ] + ) + if blenderbim.bim.last_commit_hash != "8888888": + version += f"-{blenderbim.bim.last_commit_hash[:7]}" + return version From 0406166d8ab954e5342b116fecf97bad44b52c33 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Tue, 22 Aug 2023 19:21:26 +0100 Subject: [PATCH 02/86] fix updating an objective's metric --- .../ifcopenshell/api/constraint/add_metric.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py index 8619b32acb..2941eb8308 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py @@ -58,4 +58,5 @@ class Usecase: if self.settings["objective"]: benchmark_values = list(self.settings["objective"].BenchmarkValues or []) benchmark_values.append(metric) + self.settings["objective"].BenchmarkValues = benchmark_values return metric From 2699278ba3d9265c45703a4ab5b7f2d1e64b048b Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Tue, 22 Aug 2023 19:34:47 +0100 Subject: [PATCH 03/86] api usecase to add a reference path to a metric objective --- .../api/constraint/add_metric_reference.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py new file mode 100644 index 0000000000..072c71ecb0 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py @@ -0,0 +1,45 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell + +class Usecase: + def __init__(self, file, metric=None, reference_path=None): + """ + Adds a chain of references to a metric. The reference path is a string of the form "attribute.attribute.attribute" + Used to reference a value of an attribute of an instance through a metric objective entity. + """ + self.file = file + self.settings = {"metric": metric, "reference_path": reference_path} + + def execute(self): + if self.settings["reference_path"]: + attributes = self.settings["reference_path"].split(".") + references_created = [] + for i in range(len(attributes)): + if i == 0: + reference = self.file.create_entity("IfcReference") + reference.AttributeIdentifier = attributes[i] + self.settings["metric"].ReferencePath = reference + references_created.append(reference) + else: + reference = self.file.create_entity("IfcReference") + reference.AttributeIdentifier = attributes[i] + references_created[i-1].InnerReference = reference + references_created.append(reference) + return references_created \ No newline at end of file From 0ec337748fda6f8ee602a060be5b7212c7f4975a Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Tue, 22 Aug 2023 19:36:01 +0100 Subject: [PATCH 04/86] improve purge of remove_metric usecase --- .../ifcopenshell/api/constraint/remove_metric.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py index e5dfa02dda..229f198920 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py @@ -43,6 +43,10 @@ class Usecase: self.settings = {"metric": metric} def execute(self): + if self.settings["metric"].ReferencePath: + reference = self.settings["metric"].ReferencePath + self.delete_reference(reference) + self.file.remove(self.settings["metric"]) for rel in self.file.by_type("IfcRelAssociatesConstraint"): if not rel.RelatingConstraint: @@ -50,3 +54,8 @@ class Usecase: for resource_rel in self.file.by_type("IfcResourceConstraintRelationship"): if not resource_rel.RelatingConstraint: self.file.remove(resource_rel) + + def delete_reference(self, reference): + if reference.InnerReference: + self.delete_reference(reference.InnerReference) + self.file.remove(reference) From eae6983ca53e81e2150019117faa915b378007d5 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Tue, 22 Aug 2023 19:54:37 +0100 Subject: [PATCH 05/86] Fix calculating resource work based from parent's productivity and improve UI to show this --- .../blenderbim/bim/module/resource/data.py | 23 ++++ .../bim/module/resource/operator.py | 7 ++ .../blenderbim/bim/module/resource/prop.py | 10 +- .../blenderbim/bim/module/resource/ui.py | 101 ++++++++++-------- .../ifcopenshell/util/resource.py | 16 +-- 5 files changed, 107 insertions(+), 50 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/resource/data.py b/src/blenderbim/blenderbim/bim/module/resource/data.py index f9abe1729f..21f4250027 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/data.py +++ b/src/blenderbim/blenderbim/bim/module/resource/data.py @@ -58,6 +58,7 @@ class ResourceData: } if resource.is_a() in ["IfcLaborResource", "IfcConstructionEquipmentResource"]: results[resource.id()]["Productivity"] = {} + results[resource.id()]["InheritedProductivity"] = {} productivity = cls.get_productivity(resource) if productivity: results[resource.id()]["Productivity"] = { @@ -65,12 +66,34 @@ class ResourceData: "TimeConsumed": ifcopenshell.util.resource.get_unit_consumed(productivity), "QuantityProducedName": ifcopenshell.util.resource.get_quantity_produced_name(productivity), } + inherited_productivity = cls.get_parent_productivity(resource) + if inherited_productivity: + results[resource.id()]["InheritedProductivity"] = { + "QuantityProduced": ifcopenshell.util.resource.get_quantity_produced(inherited_productivity), + "TimeConsumed": ifcopenshell.util.resource.get_unit_consumed(inherited_productivity), + "QuantityProducedName": ifcopenshell.util.resource.get_quantity_produced_name( + inherited_productivity + ), + } + if resource.Usage: + results[resource.id()]["ScheduleWork"] = ( + ifcopenshell.util.date.readable_ifc_duration(resource.Usage.ScheduleWork) + if resource.Usage.ScheduleWork + else "Calculate", + ) + results[resource.id()]["ScheduleUsage"] = ( + resource.Usage.ScheduleUsage if resource.Usage.ScheduleUsage else "" + ) return results @classmethod def get_productivity(cls, resource): return ifcopenshell.util.resource.get_productivity(resource, should_inherit=False) + @classmethod + def get_parent_productivity(cls, resource): + return ifcopenshell.util.resource.get_parent_productivity(resource) + @classmethod def cost_values(cls): results = [] diff --git a/src/blenderbim/blenderbim/bim/module/resource/operator.py b/src/blenderbim/blenderbim/bim/module/resource/operator.py index 9d9d0aeb07..22fc5a0f07 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/operator.py +++ b/src/blenderbim/blenderbim/bim/module/resource/operator.py @@ -198,6 +198,13 @@ class CalculateResourceWork(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} resource: bpy.props.IntProperty() + @classmethod + def poll(cls, context): + active_resource = tool.Resource.get_highlighted_resource() + if active_resource: + if tool.Resource.get_productivity(active_resource, should_inherit=True): + return True + def _execute(self, context): core.calculate_resource_work(tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(self.resource)) diff --git a/src/blenderbim/blenderbim/bim/module/resource/prop.py b/src/blenderbim/blenderbim/bim/module/resource/prop.py index 951ea9286c..67b486d0dc 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/prop.py +++ b/src/blenderbim/blenderbim/bim/module/resource/prop.py @@ -22,6 +22,8 @@ import ifcopenshell.util.resource from blenderbim.bim.ifc import IfcStore import blenderbim.tool as tool import blenderbim.bim.module.pset.data +from blenderbim.bim.module.resource.data import refresh +from blenderbim.bim.module.sequence.data import refresh as refresh_sequence from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -56,6 +58,8 @@ def updateResourceName(self, context): if props.active_resource_id == self.ifc_definition_id: attribute = props.resource_attributes.get("Name") attribute.string_value = self.name + refresh() + tool.Sequence.refresh_task_resources() def get_quantity_types(self, context): @@ -72,7 +76,7 @@ def get_quantity_types(self, context): def update_active_resource_index(self, context): blenderbim.bim.module.pset.data.refresh() - if self.should_show_productivity: + if self.should_show_resource_tools: tool.Resource.load_productivity_data() @@ -91,7 +95,9 @@ def updateResourceUsage(self, context): ) resource.Usage.ScheduleUsage = self.schedule_usage blenderbim.bim.module.pset.data.refresh() + refresh() tool.Resource.load_resource_properties() + tool.Sequence.refresh_task_resources() class ISODuration(PropertyGroup): @@ -144,7 +150,7 @@ class BIMResourceProperties(PropertyGroup): quantity_types: EnumProperty(items=get_quantity_types, name="Quantity Types") is_editing_quantity: BoolProperty(name="Is Editing Quantity") quantity_attributes: CollectionProperty(name="Quantity Attributes", type=Attribute) - should_show_productivity: BoolProperty(name="Edit Productivity", update=update_active_resource_index) + should_show_resource_tools: BoolProperty(name="Edit Productivity", update=update_active_resource_index) class BIMResourceProductivity(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 090aa0f8c4..10a22625c3 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -64,11 +64,8 @@ class BIM_PT_resources(Panel): self.props, "active_resource_index", ) - row = self.layout.row(align=True) - row.alignment = "RIGHT" - row.prop(self.props, "should_show_productivity", icon="RECOVER_LAST") - if self.props.should_show_productivity: - self.draw_productivity_ui(context) + self.draw_productivity_ui(context) + if self.props.active_resource_id and self.props.editing_resource_type == "ATTRIBUTES": self.draw_editable_resource_attributes_ui() elif self.props.active_resource_id and self.props.editing_resource_type == "QUANTITY": @@ -79,43 +76,66 @@ class BIM_PT_resources(Panel): self.draw_editable_resource_time_attributes_ui() def draw_productivity_ui(self, context): - total_resources = len(self.tprops.resources) - if not total_resources or self.props.active_resource_index >= total_resources: - return - - ifc_definition_id = self.tprops.resources[self.props.active_resource_index].ifc_definition_id - resource = ResourceData.data["resources"][ifc_definition_id] - - if not resource["type"] in ["IfcConstructionEquipmentResource", "IfcLaborResource"]: - row = self.layout.row(align=True) - row.label(text="Resource type cannot have productivity data", icon="ERROR") - return - - self.productivity_props = context.scene.BIMResourceProductivity - - if resource["Productivity"]: - produtivitiy_rate_message = "Current Rate: {}/{}".format( - resource["Productivity"]["QuantityProduced"], resource["Productivity"]["TimeConsumed"] - ) - row = self.layout.row() - row.alignment = "LEFT" - row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA") - else: - row = self.layout.row(align=True) - row.alignment = "LEFT" - produtivitiy_rate_message = "No productivity data found" - row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA") row = self.layout.row(align=True) row.alignment = "RIGHT" - row.prop(self.productivity_props, "quantity_produced", text="Quantity Produced") - row.prop(self.productivity_props, "quantity_produced_name", text="Quantity Name") - row = self.layout.row() - row.alignment = "RIGHT" - self.draw_duration_property(self.productivity_props.quantity_consumed, row) - row = self.layout.row() - row.alignment = "RIGHT" - row.operator("bim.edit_productivity_data", text="Apply", icon="CHECKMARK") + row.prop(self.props, "should_show_resource_tools", icon="RECOVER_LAST") + if self.props.should_show_resource_tools: + total_resources = len(self.tprops.resources) + if not total_resources or self.props.active_resource_index >= total_resources: + return + + ifc_definition_id = self.tprops.resources[self.props.active_resource_index].ifc_definition_id + resource = ResourceData.data["resources"][ifc_definition_id] + + if not resource["type"] in ["IfcConstructionEquipmentResource", "IfcLaborResource"]: + row = self.layout.row(align=True) + row.label(text="Resource type cannot have productivity data", icon="ERROR") + else: + productivity = resource["Productivity"] + parent_productivity = resource["InheritedProductivity"] + + row = self.layout.row() + row.operator( + "bim.calculate_resource_work", text="Calculate Work", icon="TEMP" + ).resource = ifc_definition_id + + row = self.layout.row() + row.label(text="Productivity") + row = self.layout.row() + if productivity: + produtivitiy_rate_message = "Current Productivity Rate: {} {} / {}".format( + productivity["QuantityProduced"], + productivity["QuantityProducedName"], + productivity["TimeConsumed"], + ) + row.alignment = "LEFT" + row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA") + elif parent_productivity: + produtivitiy_rate_message = "Inherited Productivity Rate: {} {} / {}*".format( + parent_productivity["QuantityProduced"], + parent_productivity["QuantityProducedName"], + parent_productivity["TimeConsumed"], + ) + row.alignment = "LEFT" + row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA") + else: + row = self.layout.row(align=True) + row.alignment = "LEFT" + produtivitiy_rate_message = "No productivity data found" + row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA") + + productivity_props = context.scene.BIMResourceProductivity + row = self.layout.row(align=True) + row.alignment = "RIGHT" + row.prop(productivity_props, "quantity_produced", text="Quantity Produced") + row.prop(productivity_props, "quantity_produced_name", text="Quantity Name") + row = self.layout.row() + row.alignment = "RIGHT" + self.draw_duration_property(productivity_props.quantity_consumed, row) + row = self.layout.row() + row.alignment = "RIGHT" + row.operator("bim.edit_productivity_data", text="Apply", icon="CHECKMARK") def draw_resource_operators(self): row = self.layout.row(align=True) @@ -153,9 +173,6 @@ class BIM_PT_resources(Panel): if not self.props.active_resource_id: if resource["type"] in ["IfcLaborResource", "IfcConstructionEquipmentResource"]: - if resource["Productivity"]: - op = row.operator("bim.calculate_resource_work", text="", icon="TEMP") - op.resource = ifc_definition_id row.operator("bim.enable_editing_resource_time", text="", icon="TIME").resource = ifc_definition_id op = row.operator("bim.enable_editing_resource_base_quantity", text="", icon="PROPERTIES") op.resource = ifc_definition_id diff --git a/src/ifcopenshell-python/ifcopenshell/util/resource.py b/src/ifcopenshell-python/ifcopenshell/util/resource.py index 016a653209..477a4e2af6 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/resource.py +++ b/src/ifcopenshell-python/ifcopenshell/util/resource.py @@ -23,14 +23,18 @@ import ifcopenshell.util.date def get_productivity(resource, should_inherit=True): productivity = ifcopenshell.util.element.get_psets(resource).get("EPset_Productivity", None) if should_inherit and not productivity: - # Proposal for Schema - If instance doesn't have any productivity, inherit it's parent's productivity - if not resource.Nests: - return None - else: - parent_resource = resource.Nests[0].RelatingObject - productivity = ifcopenshell.util.element.get_psets(parent_resource).get("EPset_Productivity", None) + #Note: This is not part of the Schema - but it makes sense to inherit from parent + productivity = get_parent_productivity(resource) return productivity +def get_parent_productivity(resource): + if not resource.Nests: + return + else: + parent_resource = resource.Nests[0].RelatingObject + productivity = ifcopenshell.util.element.get_psets(parent_resource).get("EPset_Productivity", None) + return productivity + def get_unit_consumed(productivity): duration = productivity.get("BaseQuantityConsumed", None) From 0b2007011ef31c5915593b2d2b37118eee6892c7 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Tue, 22 Aug 2023 20:01:19 +0100 Subject: [PATCH 06/86] Feature to constrain a resource work or number of used resources --- .../bim/module/resource/__init__.py | 2 + .../blenderbim/bim/module/resource/data.py | 1 + .../bim/module/resource/operator.py | 26 +++++++ .../blenderbim/bim/module/resource/ui.py | 43 ++++++++++++ src/blenderbim/blenderbim/core/resource.py | 32 +++++++++ src/blenderbim/blenderbim/core/tool.py | 7 ++ src/blenderbim/blenderbim/tool/resource.py | 70 ++++++++++++++++++- .../test/bim/feature/resource.feature | 4 +- 8 files changed, 182 insertions(+), 3 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/resource/__init__.py b/src/blenderbim/blenderbim/bim/module/resource/__init__.py index c91d7b7591..095ee1c427 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/resource/__init__.py @@ -50,6 +50,8 @@ classes = ( operator.CalculateResourceWork, operator.ImportResources, operator.EditProductivityData, + operator.ConstrainResourceWork, + operator.RemoveUsageConstraint, prop.Resource, prop.BIMResourceProperties, prop.BIMResourceTreeProperties, diff --git a/src/blenderbim/blenderbim/bim/module/resource/data.py b/src/blenderbim/blenderbim/bim/module/resource/data.py index 21f4250027..b854002ce4 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/data.py +++ b/src/blenderbim/blenderbim/bim/module/resource/data.py @@ -56,6 +56,7 @@ class ResourceData: "type": resource.is_a(), "BaseQuantity": base_quantity, } + results[resource.id()]["Benchmarks"] = tool.Resource.get_resource_benchmarks(resource) if resource.is_a() in ["IfcLaborResource", "IfcConstructionEquipmentResource"]: results[resource.id()]["Productivity"] = {} results[resource.id()]["InheritedProductivity"] = {} diff --git a/src/blenderbim/blenderbim/bim/module/resource/operator.py b/src/blenderbim/blenderbim/bim/module/resource/operator.py index 22fc5a0f07..c160ef6d60 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/operator.py +++ b/src/blenderbim/blenderbim/bim/module/resource/operator.py @@ -363,3 +363,29 @@ class EditProductivityData(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.edit_productivity_pset(tool.Ifc, tool.Resource) + + +class ConstrainResourceWork(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_usage_constraint" + bl_label = "Constrain Resource Work" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + attribute: bpy.props.StringProperty() + + def _execute(self, context): + core.add_usage_constraint( + tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(self.resource), reference_path=self.attribute + ) + + +class RemoveUsageConstraint(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.remove_usage_constraint" + bl_label = "Remove Usage Constraint" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + attribute: bpy.props.StringProperty() + + def _execute(self, context): + core.remove_usage_constraint( + tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(self.resource), reference_path=self.attribute + ) diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 10a22625c3..f4b325571a 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -92,6 +92,49 @@ class BIM_PT_resources(Panel): row = self.layout.row(align=True) row.label(text="Resource type cannot have productivity data", icon="ERROR") else: + is_usage_locked = False + is_work_locked = False + for constraint in resource["Benchmarks"] or []: + for metric in constraint["metrics"] or []: + if ( + metric["ConstraintGrade"] == "HARD" + and metric["reference"] + and metric["reference"] == "Usage.ScheduleUsage" + ): + is_usage_locked = True + elif ( + metric["ConstraintGrade"] == "HARD" + and metric["reference"] + and metric["reference"] == "Usage.ScheduleWork" + ): + is_work_locked = True + row = self.layout.row() + row.label(text="Resource Work") + schedule_usage = "Schedule Usage: {}".format(resource.get("ScheduleUsage")) + schedule_work = "Schedule Work: {}".format(resource.get("ScheduleWork")) + row = self.layout.row() + row.alignment = "LEFT" + row.label(text=schedule_usage, icon="ARMATURE_DATA") + row2 = self.layout.row() + row2.alignment = "LEFT" + row2.label(text=schedule_work, icon="ARMATURE_DATA") + if not is_usage_locked: + op = row.operator("bim.add_usage_constraint", text="", icon="UNLOCKED") + op.resource = ifc_definition_id + op.attribute = "Usage.ScheduleUsage" + else: + op = row.operator("bim.remove_usage_constraint", text="", icon="LOCKED") + op.resource = ifc_definition_id + op.attribute = "Usage.ScheduleUsage" + if not is_work_locked: + op = row2.operator("bim.add_usage_constraint", text="", icon="UNLOCKED") + op.resource = ifc_definition_id + op.attribute = "Usage.ScheduleWork" + else: + op = row2.operator("bim.remove_usage_constraint", text="", icon="LOCKED") + op.resource = ifc_definition_id + op.attribute = "Usage.ScheduleWork" + productivity = resource["Productivity"] parent_productivity = resource["InheritedProductivity"] diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py index 0beafa9b9b..cc660e8685 100644 --- a/src/blenderbim/blenderbim/core/resource.py +++ b/src/blenderbim/blenderbim/core/resource.py @@ -182,3 +182,35 @@ def edit_productivity_pset(ifc, resource_tool): else: pset = ifc.run("pset.add_pset", product=resource, name="EPset_Productivity") ifc.run("pset.edit_pset", pset=pset, properties=resource_tool.get_productivity_attributes()) + + +def add_usage_constraint(ifc, resource_tool, resource=None, reference_path=None): + metric = resource_tool.has_usage_metric(resource) + if metric: + return print("Must remove existing metric first") + + objective = ifc.run("constraint.add_objective") + ifc.run("constraint.edit_objective", objective=objective, attributes={"ObjectiveQualifier": "PARAMETER"}) + metric = ifc.run("constraint.add_metric", objective=objective) + ifc.run( + "constraint.edit_metric", + metric=metric, + attributes={ + "ConstraintGrade": "HARD", + "Benchmark": "EQUALTO", + }, + ) + ifc.run("constraint.add_metric_reference", metric=metric, reference_path=reference_path) + ifc.run("constraint.assign_constraint", product=resource, constraint=objective) + + +def remove_usage_constraint(ifc, resource_tool, resource, reference_path): + constraints = resource_tool.get_constraints(resource) + for constraint in constraints: + metrics = resource_tool.get_metrics(constraint) + for metric in metrics: + reference = resource_tool.get_metric_reference(metric, is_deep=True) + if reference == reference_path: + ifc.run("constraint.remove_metric", metric=metric) + ifc.run("constraint.unassign_constraint", product=resource, constraint=constraint) + ifc.run("constraint.remove_constraint", constraint=constraint) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 797f556e25..452c84d70f 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -617,6 +617,13 @@ class Resource: def load_resource_properties(cls): pass def load_resource_time_attributes(cls, resource_time): pass def load_resources(cls): pass + def get_constraints(cls, resource): pass + def get_metrics(cls, constraint): pass + def get_metric_reference(cls, metric, is_deep): pass + def get_resource_benchmarks(cls, resource): pass + def has_metric_constraint(cls, resource, attribute): pass + def has_usage_metric(cls, resource): pass + @interface class Root: diff --git a/src/blenderbim/blenderbim/tool/resource.py b/src/blenderbim/blenderbim/tool/resource.py index ea41bfbe6c..f8b92074f1 100644 --- a/src/blenderbim/blenderbim/tool/resource.py +++ b/src/blenderbim/blenderbim/tool/resource.py @@ -380,4 +380,72 @@ class Resource(blenderbim.core.tool.Resource): "pset.edit_pset", pset=pset, properties=attributes, - ) \ No newline at end of file + ) + + @classmethod + def get_constraints(cls, resource): + constraints = [] + for rel in resource.HasAssociations or []: + if rel.is_a("IfcRelAssociatesConstraint"): + constraints.append(rel.RelatingConstraint) + return constraints + + @classmethod + def get_metrics(cls, constraint): + metrics = [] + for metric in constraint.BenchmarkValues or []: + metrics.append(metric) + return metrics + + @classmethod + def get_metric_reference(cls, metric, is_deep=True): + def get_reference_Attribute(ref, path): + if ref: + if is_deep: + if not path: + path = ref.AttributeIdentifier + else: + path += ".{}".format(ref.AttributeIdentifier) if ref.AttributeIdentifier else "" + return get_reference_Attribute(ref.InnerReference, path) + else: + return ref.AttributeIdentifier + return path + + reference = metric.ReferencePath + return get_reference_Attribute(reference, "") + + @classmethod + def get_resource_benchmarks(cls, resource): + constraints = [] + for constraint in cls.get_constraints(resource) or []: + metrics = [] + for metric in cls.get_metrics(constraint) or []: + metrics.append( + { + "reference": cls.get_metric_reference(metric), + "Benchmark": metric.Benchmark, + "ConstraintGrade": metric.ConstraintGrade, + } + ) + constraints.append({"ObjectiveQualifier": constraint.ObjectiveQualifier, "metrics": metrics}) + return constraints + + @classmethod + def has_metric_constraint(cls, resource, attribute): + constraints = tool.Resource.get_constraints(resource) + metrics = [] + for constraint in constraints: + for metric in tool.Resource.get_metrics(constraint) or []: + is_same_reference = bool( + tool.Resource.get_metric_reference(metric, is_deep=False) == attribute + or tool.Resource.get_metric_reference(metric, is_deep=True) == attribute + ) + if is_same_reference: + metrics.append(metric) + if metrics: + return metrics[0] + return None + + @classmethod + def has_usage_metric(cls, resource): + return cls.has_metric_constraint(resource, "Usage") diff --git a/src/blenderbim/test/bim/feature/resource.feature b/src/blenderbim/test/bim/feature/resource.feature index 836639f16f..71af9ae05a 100644 --- a/src/blenderbim/test/bim/feature/resource.feature +++ b/src/blenderbim/test/bim/feature/resource.feature @@ -249,7 +249,7 @@ Scenario: Add Productivity data When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I set "scene.BIMResourceProperties.active_resource_index" to "1" - And I set "scene.BIMResourceProperties.should_show_productivity" to "True" + And I set "scene.BIMResourceProperties.should_show_resource_tools" to "True" And I set "scene.BIMResourceProductivity.quantity_produced" to "5.00" And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea" And I set "scene.BIMResourceProductivity.quantity_consumed[0].hours" to "5" @@ -286,7 +286,7 @@ Scenario: Calculate Resource Work When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I set "scene.BIMResourceProperties.active_resource_index" to "1" - And I set "scene.BIMResourceProperties.should_show_productivity" to "True" + And I set "scene.BIMResourceProperties.should_show_resource_tools" to "True" And I set "scene.BIMResourceProductivity.quantity_produced" to "5.00" And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea" And I set "scene.BIMResourceProductivity.quantity_consumed[0].hours" to "5" From 7a3ad6c3a1b7750c6cd7292463513d6fbb88e2ef Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Tue, 22 Aug 2023 20:03:09 +0100 Subject: [PATCH 07/86] forgotten bits and bobs + run black --- src/blenderbim/blenderbim/core/tool.py | 1 + src/blenderbim/blenderbim/tool/sequence.py | 25 ++++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 452c84d70f..4b75600581 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -766,6 +766,7 @@ class Sequence: def show_snapshot(cls, product_states): pass def update_task_ICOM(cls, task): pass def update_visualisation_date(cls, start_date, finish_date): pass + def refresh_task_resources(cls): pass @interface diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index ebd84b656c..0706103f2b 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -198,8 +198,12 @@ class Sequence(blenderbim.core.tool.Sequence): item.name = task.Name or "Unnamed" item.identification = task.Identification or "XXX" if props.highlighted_task_id: - item.is_predecessor = props.highlighted_task_id in [rel.RelatedProcess.id() for rel in task.IsPredecessorTo] - item.is_successor = props.highlighted_task_id in [rel.RelatingProcess.id() for rel in task.IsSuccessorFrom] + item.is_predecessor = props.highlighted_task_id in [ + rel.RelatedProcess.id() for rel in task.IsPredecessorTo + ] + item.is_successor = props.highlighted_task_id in [ + rel.RelatingProcess.id() for rel in task.IsSuccessorFrom + ] calendar = ifcopenshell.util.sequence.derive_calendar(task) if task.HasAssignments: for rel in task.HasAssignments: @@ -466,9 +470,11 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def get_highlighted_task(cls): - props = bpy.context.scene.BIMWorkScheduleProperties - task_props = bpy.context.scene.BIMTaskTreeProperties - return tool.Ifc.get().by_id(task_props.tasks[props.active_task_index].ifc_definition_id) + tasks = bpy.context.scene.BIMTaskTreeProperties.tasks + if len(tasks) and len(tasks) > bpy.context.scene.BIMWorkScheduleProperties.active_task_index: + return tool.Ifc.get().by_id( + tasks[bpy.context.scene.BIMWorkScheduleProperties.active_task_index].ifc_definition_id + ) @classmethod def get_direct_nested_tasks(cls, task): @@ -1667,4 +1673,11 @@ class Sequence(blenderbim.core.tool.Sequence): resources = cls.get_task_resources(task) cls.load_task_inputs(inputs) cls.load_task_outputs(outputs) - cls.load_task_resources(resources) \ No newline at end of file + cls.load_task_resources(resources) + + @classmethod + def refresh_task_resources(cls): + task = cls.get_highlighted_task() + if not task: + return + cls.load_task_resources(cls.get_task_resources(task)) From d53641ce5e00a358f0af3c2ca8a77c22042a8147 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Fri, 18 Aug 2023 16:32:53 -0700 Subject: [PATCH 08/86] Check Brick library location exists --- src/blenderbim/blenderbim/bim/module/brick/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/data.py b/src/blenderbim/blenderbim/bim/module/brick/data.py index b08e25280c..ee3bd341e4 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/data.py +++ b/src/blenderbim/blenderbim/bim/module/brick/data.py @@ -146,7 +146,7 @@ class BrickschemaReferencesData: for library in ifc.by_type("IfcLibraryInformation"): if tool.Ifc.get_schema() == "IFC2X3": results.append((str(library.id()), library.Name or "Unnamed", "")) - elif ".ttl" in library.Location: + elif library.Location and ".ttl" in library.Location: results.append((str(library.id()), library.Name or "Unnamed", "")) return results From 52f3392ecea60628f4cab3993e00a5c1b81a99dc Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Fri, 18 Aug 2023 17:14:58 -0700 Subject: [PATCH 09/86] Remove Brick data debugging printouts --- src/blenderbim/blenderbim/bim/module/brick/data.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/data.py b/src/blenderbim/blenderbim/bim/module/brick/data.py index ee3bd341e4..6b2c68f2ae 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/data.py +++ b/src/blenderbim/blenderbim/bim/module/brick/data.py @@ -84,7 +84,6 @@ class BrickschemaData: predicate_uri = row.get("predicate") predicate_name = predicate_uri.toPython().split("#")[-1] object_uri = row.get("object") - print("DEBUG: object is ", object_uri) if isinstance(object_uri, BNode): object_name = "[]" else: @@ -109,7 +108,6 @@ class BrickschemaData: object2_name = object2.toPython().split("#")[-1] except: object2_name = str(object2) - print("DEBUG: ", predicate_name, ":", predicate2_name) results.append( { "predicate_uri": None, @@ -120,7 +118,6 @@ class BrickschemaData: "is_globalid": predicate2_name == "globalID", } ) - print("") return results From b8ca287883d666fc8d1a3e4140d2f2a1ce04ee1e Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Fri, 18 Aug 2023 17:25:29 -0700 Subject: [PATCH 10/86] Fix up convert_ifc_to_brick Remove line separation between code, but add comments Add full URI to Brick relations Fix typo where "system_uris" was written as "space_uris" Remove unnecessary get of "distribution_elements = brick.get_convertable_brick_elements()" again --- src/blenderbim/blenderbim/core/brick.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index 167b8ae4d8..5c8cd6ee58 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -91,6 +91,7 @@ def add_brick_relation(brick, brick_uri=None, predicate=None, object=None): def convert_ifc_to_brick(brick, namespace=None, library=None): + # convert spaces to brick spaces = brick.get_convertable_brick_spaces() space_uris = {} for space in spaces: @@ -98,39 +99,35 @@ def convert_ifc_to_brick(brick, namespace=None, library=None): space_uris[space] = brick_uri if library: brick.run_assign_brick_reference(element=space, library=library, brick_uri=brick_uri) - for space in spaces: parent = brick.get_parent_space(space) if parent: - brick.add_relation(space_uris[parent], "hasPart", space_uris[space]) - + brick.add_relation(space_uris[parent], "https://brickschema.org/schema/Brick#hasPart", space_uris[space]) + # convert systems to brick systems = brick.get_convertable_brick_systems() system_uris = {} for system in systems: brick_uri = brick.add_brick_from_element(system, namespace, brick.get_brick_class(system)) - space_uris[space] = brick_uri + system_uris[system] = brick_uri if library: brick.run_assign_brick_reference(element=system, library=library, brick_uri=brick_uri) - + # convert services to brick distribution_elements = brick.get_convertable_brick_elements() equipment_uris = {} for element in distribution_elements: brick_uri = brick.add_brick_from_element(element, namespace, brick.get_brick_class(element)) equipment_uris[element] = brick_uri space = brick.get_element_container(element) - brick.add_relation(brick_uri, "hasLocation", space_uris[space]) + brick.add_relation(brick_uri, "https://brickschema.org/schema/Brick#hasLocation", space_uris[space]) systems = brick.get_element_systems(element) for system in systems: - brick.add_relation(system_uris[system], "hasPart", brick_uri) + brick.add_relation(system_uris[system], "https://brickschema.org/schema/Brick#hasPart", brick_uri) if library: brick.run_assign_brick_reference(element=element, library=library, brick_uri=brick_uri) - - distribution_elements = brick.get_convertable_brick_elements() for element in distribution_elements: feeds = brick.get_element_feeds(element) for downstream_equipment in feeds: - brick.add_relation(equipment_uris[element], "feeds", equipment_uris[downstream_equipment]) - + brick.add_relation(equipment_uris[element], "https://brickschema.org/schema/Brick#feeds", equipment_uris[downstream_equipment]) brick.run_refresh_brick_viewer() brick.run_refresh_brick_viewer(split_screen=True) From 499eb5ea1433d1661912a42a2a4f66973b39030f Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Sat, 19 Aug 2023 14:27:40 -0700 Subject: [PATCH 11/86] Rework Brick UI into toggle panels --- .../blenderbim/bim/module/brick/__init__.py | 6 +- .../blenderbim/bim/module/brick/prop.py | 1 - .../blenderbim/bim/module/brick/ui.py | 140 +++++++++++++----- 3 files changed, 104 insertions(+), 43 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/__init__.py b/src/blenderbim/blenderbim/bim/module/brick/__init__.py index ac53d3eb41..3908ee88a7 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/brick/__init__.py @@ -39,8 +39,12 @@ classes = ( prop.Brick, prop.BIMBrickProperties, ui.BIM_PT_brickschema, - ui.BIM_PT_ifc_brickschema_references, + ui.BIM_PT_brickschema_project_info, + ui.BIM_PT_brickschema_namespaces, + ui.BIM_PT_brickschema_create_entity, + ui.BIM_PT_brickschema_viewport, ui.BIM_UL_bricks, + ui.BIM_PT_ifc_brickschema_references, ) diff --git a/src/blenderbim/blenderbim/bim/module/brick/prop.py b/src/blenderbim/blenderbim/bim/module/brick/prop.py index fed515b044..01bffbfd61 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/prop.py +++ b/src/blenderbim/blenderbim/bim/module/brick/prop.py @@ -93,7 +93,6 @@ class BIMBrickProperties(PropertyGroup): 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) new_brick_namespace_alias: StringProperty(name="New Brick Namespace Alias") new_brick_namespace_uri: StringProperty(name="New Brick Namespace URI") # create brick entity diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index a9b19d526d..b54d2dd0f7 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -34,8 +34,19 @@ class BIM_PT_brickschema(Panel): def draw(self, context): if not BrickschemaData.is_loaded: BrickschemaData.load() - self.props = context.scene.BIMBrickProperties + +class BIM_PT_brickschema_project_info(Panel): + bl_label = "Project Info" + bl_idname = "BIM_PT_brickschema_project_info" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_brickschema" + + def draw(self, context): + self.props = context.scene.BIMBrickProperties + if not BrickschemaData.data["is_loaded"]: row = self.layout.row(align=True) row.operator("bim.new_brick_file", text="Create Project") @@ -61,44 +72,91 @@ class BIM_PT_brickschema(Panel): op.should_save_as = True row.operator("bim.close_brick_project", text="", icon="CANCEL") - 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) - row.label(text="Active Namespace:") +class BIM_PT_brickschema_namespaces(Panel): + bl_label = "Namespaces" + bl_idname = "BIM_PT_brickschema_namespaces" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_brickschema" - row = box.row(align=True) - prop_with_search(row, self.props, "namespace", text="") + @classmethod + def poll(cls, context): + return BrickStore.graph != None - row = box.row(align=True) - row.label(text="Bind New Namespace:") - - row = box.row(align=True) - row.prop(data=self.props, property="new_brick_namespace_alias", text="") - col = row.column() - col.alignment = "CENTER" - col.scale_x = 1.1 - col.label(text=":") - row.prop(data=self.props, property="new_brick_namespace_uri", text="") - row.operator("bim.add_brick_namespace", text="", icon="ADD") + def draw(self, context): + self.props = context.scene.BIMBrickProperties row = self.layout.row(align=True) - row.label(text="Create Entity:") + row.label(text="Active Namespace:") row = self.layout.row(align=True) - row.prop(data=self.props, property="brick_entity_create_type", text="") + prop_with_search(row, self.props, "namespace", text="") + row = self.layout.row(align=True) + row.label(text="Bind New Namespace:") + + row = self.layout.row(align=True) + row.prop(data=self.props, property="new_brick_namespace_alias", text="") + col = row.column() + col.alignment = "CENTER" + col.scale_x = 1.1 + col.label(text=":") + row.prop(data=self.props, property="new_brick_namespace_uri", text="") + row.operator("bim.add_brick_namespace", text="", icon="ADD") + + +class BIM_PT_brickschema_create_entity(Panel): + bl_label = "Create Entity" + bl_idname = "BIM_PT_brickschema_create_entity" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_brickschema" + + @classmethod + def poll(cls, context): + return BrickStore.graph != None + + def draw(self, context): + self.props = context.scene.BIMBrickProperties # hide this if selected entity already has a reference + + row = self.layout.row(align=True) + row.prop(data=self.props, property="brick_entity_create_type", text="Class") + + row = self.layout.row(align=True) + prop_with_search(row, self.props, "brick_entity_class", text="Type") + row = self.layout.row(align=True) active = tool.Ifc.get_entity(context.active_object) if active and context.selected_objects: + row.label(text="Label:") row.label(text=active.Name if active.Name else "Unnamed") else: - 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.prop(data=self.props, property="new_brick_label", text="Label") + + row = self.layout.row(align=True) + row.operator("bim.add_brick", text="Create Entity") + + +class BIM_PT_brickschema_viewport(Panel): + bl_label = "Viewport" + bl_idname = "BIM_PT_brickschema_viewport" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_brickschema" + + @classmethod + def poll(cls, context): + return BrickStore.graph != None + + def draw(self, context): + self.props = context.scene.BIMBrickProperties row = self.layout.row(align=True) col = row.column() @@ -196,6 +254,21 @@ class BIM_PT_brickschema(Panel): op.global_id = relation["object_name"] +class BIM_UL_bricks(UIList): + split_screen = False + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + label = item.label if item.label else item.name + if item.total_items: + op = row.operator("bim.view_brick_class", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False) + op.brick_class = item.name + op.split_screen = self.split_screen + label = label + " (" + str(item.total_items) + ")" + row.label(text=label) + + class BIM_PT_ifc_brickschema_references(Panel): bl_label = "Brickschema References" bl_idname = "BIM_PT_ifc_brickschema_references" @@ -245,19 +318,4 @@ class BIM_PT_ifc_brickschema_references(Panel): row.label(text=reference["identification"], icon="ASSET_MANAGER") row.label(text=reference["name"]) row.operator("bim.unassign_library_reference", text="", icon="X").reference = reference["id"] - row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT").item = reference["identification"] - - -class BIM_UL_bricks(UIList): - split_screen = False - - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - if item: - row = layout.row(align=True) - label = item.label if item.label else item.name - if item.total_items: - op = row.operator("bim.view_brick_class", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False) - op.brick_class = item.name - op.split_screen = self.split_screen - label = label + " (" + str(item.total_items) + ")" - row.label(text=label) + row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT").item = reference["identification"] \ No newline at end of file From 0a3a2db4ae2a9dcbb408162bd32c39ffcc39ef74 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Sat, 19 Aug 2023 14:34:03 -0700 Subject: [PATCH 12/86] Simplify buttons for add Brick relations manually --- .../blenderbim/bim/module/brick/operator.py | 2 +- src/blenderbim/blenderbim/bim/module/brick/prop.py | 1 - src/blenderbim/blenderbim/bim/module/brick/ui.py | 13 ------------- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 88a25b90b4..18b3e7bd4b 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -159,7 +159,7 @@ class AddBrickRelation(bpy.types.Operator, Operator): elif props.split_screen_toggled: object = props.split_screen_bricks[props.split_screen_active_brick_index].uri else: - object = props.new_brick_relation_namespace + props.new_brick_relation_object + object = props.namespace + props.new_brick_relation_object core.add_brick_relation( tool.Brick, brick_uri=brick.uri, diff --git a/src/blenderbim/blenderbim/bim/module/brick/prop.py b/src/blenderbim/blenderbim/bim/module/brick/prop.py index 01bffbfd61..ff96823a64 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/prop.py +++ b/src/blenderbim/blenderbim/bim/module/brick/prop.py @@ -103,7 +103,6 @@ class BIMBrickProperties(PropertyGroup): brick_create_relations_toggled: BoolProperty(name="Brick Create Relations Toggled", default=False) brick_edit_relations_toggled: BoolProperty(name="Brick Edit Relations Toggled", default=False) new_brick_relation_type: EnumProperty(name="New Brick Relation Type", items=get_brick_relations) - new_brick_relation_namespace: EnumProperty(name="New Brick Relation Namespace", items=get_namespaces) new_brick_relation_object: StringProperty(name="New Brick Relation Object") add_relation_failed: BoolProperty(name="Add Relation Failed", default=False) # create relations split screen diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index b54d2dd0f7..c175ecb867 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -206,10 +206,6 @@ class BIM_PT_brickschema_viewport(Panel): row.operator("bim.remove_brick", text="", icon="X") if self.props.brick_create_relations_toggled: - row = self.layout.row(align=True) - row.label(text="Create Relation:") - - if self.props.brick_create_relations_toggled and self.props.new_brick_relation_type == "http://www.w3.org/2000/01/rdf-schema#label": row = self.layout.row(align=True) prop_with_search(row, self.props, "new_brick_relation_type", text="") row.prop(data=self.props, property="new_brick_relation_object", text="") @@ -225,15 +221,6 @@ class BIM_PT_brickschema_viewport(Panel): row.label(text=split_screen_selection.label if split_screen_selection.label else split_screen_selection.name) row.operator("bim.add_brick_relation", text="", icon="ADD") - elif self.props.brick_create_relations_toggled: - row = self.layout.row(align=True) - prop_with_search(row, self.props, "new_brick_relation_namespace", text="") - - row = self.layout.row(align=True) - 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") From d6655a5c1470d33d5b54fed221139c1954549092 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Sat, 19 Aug 2023 14:36:15 -0700 Subject: [PATCH 13/86] Move Brick "set last saved" to BrickStore --- src/blenderbim/blenderbim/tool/brick.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 45b79b04e6..a84220ca99 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -337,7 +337,7 @@ 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.set_last_saved() BrickStore.load_sub_roots() BrickStore.load_namespaces() BrickStore.load_entity_classes() @@ -413,7 +413,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() + BrickStore.set_last_saved() @classmethod def add_namespace(cls, alias, uri): @@ -427,11 +427,7 @@ 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: @@ -590,3 +586,9 @@ class BrickStore: for i in range(0, total_changesets): BrickStore.graph.redo() BrickStore.history.append(total_changesets) + + @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}" \ No newline at end of file From a8046a6d699e26c7201e8154d8380cd2ebca8e19 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Sat, 19 Aug 2023 14:41:18 -0700 Subject: [PATCH 14/86] Restore Brick select ifcGlobalID functionality --- src/blenderbim/blenderbim/bim/module/brick/data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/data.py b/src/blenderbim/blenderbim/bim/module/brick/data.py index 6b2c68f2ae..ff6e5af5fb 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/data.py +++ b/src/blenderbim/blenderbim/bim/module/brick/data.py @@ -98,7 +98,7 @@ class BrickschemaData: "object_uri": object_uri, "object_name": object_name, "is_uri": isinstance(object_uri, URIRef), - "is_globalid": predicate_uri == "globalID", + "is_globalid": predicate_name == "ifcGlobalID", } ) if isinstance(object_uri, BNode): @@ -115,7 +115,7 @@ class BrickschemaData: "object_uri": object2, "object_name": object2_name, "is_uri": isinstance(object2, URIRef), - "is_globalid": predicate2_name == "globalID", + "is_globalid": predicate2_name == "ifcGlobalID", } ) return results From 9740582a84766759825ede215d03c8390bcbee6c Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Sat, 19 Aug 2023 15:10:33 -0700 Subject: [PATCH 15/86] Brick UI relations list uses "label" if there is one --- .../blenderbim/bim/module/brick/data.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/data.py b/src/blenderbim/blenderbim/bim/module/brick/data.py index ff6e5af5fb..e5c31a70b6 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/data.py +++ b/src/blenderbim/blenderbim/bim/module/brick/data.py @@ -67,13 +67,17 @@ class BrickschemaData: PREFIX brick: PREFIX rdfs: PREFIX rdf: - SELECT DISTINCT ?predicate ?object ?sp ?sv WHERE { - <{uri}> ?predicate ?object . - OPTIONAL { - { ?predicate rdfs:range brick:TimeseriesReference . } - UNION - { ?predicate a brick:EntityProperty . } - ?object ?sp ?sv } + SELECT DISTINCT ?predicate ?object ?label ?sp ?sv WHERE { + <{uri}> ?predicate ?object . + OPTIONAL { + ?object rdfs:label ?label . + } + OPTIONAL { + { ?predicate rdfs:range brick:TimeseriesReference . } + UNION + { ?predicate a brick:EntityProperty . } + ?object ?sp ?sv . + } } GROUP BY ?object """.replace( @@ -84,13 +88,15 @@ class BrickschemaData: predicate_uri = row.get("predicate") predicate_name = predicate_uri.toPython().split("#")[-1] object_uri = row.get("object") - if isinstance(object_uri, BNode): - object_name = "[]" - else: - try: - object_name = object_uri.toPython().split("#")[-1] - except: - object_name = str(object_uri) + object_name = row.get("label") + if not object_name: + if isinstance(object_uri, BNode): + object_name = "[]" + else: + try: + object_name = object_uri.toPython().split("#")[-1] + except: + object_name = str(object_uri) results.append( { "predicate_uri": predicate_uri, From 20d6eae50aecfaa04081b82951596c54d4041861 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Sat, 19 Aug 2023 15:35:23 -0700 Subject: [PATCH 16/86] Export/assign Brick library reference uses "label" if there is one --- src/blenderbim/blenderbim/tool/brick.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index a84220ca99..d2e7d1a319 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -141,10 +141,22 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def export_brick_attributes(cls, brick_uri): + query = BrickStore.graph.query( + """ + PREFIX rdfs: + SELECT ?label { + <{brick_uri}> rdfs:label ?label . + } + LIMIT 1 + """.replace("{brick_uri}", brick_uri)) + for row in query: + name = row.get("label") + if not name: + name = brick_uri.split("#")[-1] if tool.Ifc.get_schema() == "IFC2X3": - return {"ItemReference": brick_uri, "Name": brick_uri.split("#")[-1]} + return {"ItemReference": brick_uri, "Name": name} else: - return {"Identification": brick_uri, "Name": brick_uri.split("#")[-1]} + return {"Identification": brick_uri, "Name": name} @classmethod def get_active_brick_class(cls, split_screen=False): From 991c42e8163e84b846ff5ba41d238bb9d028a870 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Sat, 19 Aug 2023 22:00:55 -0700 Subject: [PATCH 17/86] Update Brick References Panel --- .../blenderbim/bim/module/brick/operator.py | 3 +++ .../blenderbim/bim/module/brick/ui.py | 20 ++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 18b3e7bd4b..9cb9c81c63 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -105,6 +105,7 @@ class ConvertBrickProject(bpy.types.Operator, Operator): bl_idname = "bim.convert_brick_project" bl_label = "Convert Brick Project" bl_options = {"REGISTER", "UNDO"} + bl_description = "Create an Ifc library for this Brick project" def _execute(self, context): core.convert_brick_project(tool.Ifc, tool.Brick) @@ -114,6 +115,7 @@ class AssignBrickReference(bpy.types.Operator, Operator): bl_idname = "bim.assign_brick_reference" bl_label = "Assign Brick Reference" bl_options = {"REGISTER", "UNDO"} + bl_description = "Assign the selected Ifc entity to the selected Brick entity" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -172,6 +174,7 @@ class ConvertIfcToBrick(bpy.types.Operator, Operator): bl_idname = "bim.convert_ifc_to_brick" bl_label = "Convert IFC To Brick" bl_options = {"REGISTER", "UNDO"} + bl_description = "Convert Ifc entities and relations to Brick entities and relations" def _execute(self, context): props = context.scene.BIMBrickProperties diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index c175ecb867..ecb8c39ac7 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -279,18 +279,24 @@ class BIM_PT_ifc_brickschema_references(Panel): row.label(text="No Brickschema Project Loaded") return - if not BrickschemaReferencesData.data["libraries"]: + if not BrickschemaReferencesData.data["libraries"] and BrickStore.path: row = self.layout.row(align=True) - if BrickStore.path: - row.label(text="No IFC Libraries") - row.operator("bim.convert_brick_project", text="", icon="ADD") - else: - row.label(text="No IFC Libraries. Save the Brick project to create a new library.", icon="ERROR") + row.label(text="No IFC Libraries") + row.operator("bim.convert_brick_project", text="", icon="ADD") + return + + if not BrickschemaReferencesData.data["libraries"] and not BrickStore.path: + row = self.layout.row(align=True) + row.label(text="No IFC Libraries: save the Brick project to create a new library", icon="ERROR") + + row = self.layout.row(align=True) + row.label(text="Libraries must have location pointing to a \".ttl\" file", icon="INFO") return row = self.layout.row(align=True) prop_with_search(row, self.props, "libraries") - row.operator("bim.convert_brick_project", text="", icon="ADD") + if BrickStore.path: + row.operator("bim.convert_brick_project", text="", icon="ADD") row = self.layout.row(align=True) row.operator("bim.assign_brick_reference", icon="ADD") From 3f18ad0bc5e7cd0f7079a09bba8c3c8bdcb4386e Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Sun, 20 Aug 2023 12:16:39 -0700 Subject: [PATCH 18/86] Use "split" in Brick relations list to better align layout --- src/blenderbim/blenderbim/bim/module/brick/ui.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index ecb8c39ac7..f039df8264 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -159,8 +159,7 @@ class BIM_PT_brickschema_viewport(Panel): self.props = context.scene.BIMBrickProperties row = self.layout.row(align=True) - col = row.column() - col.alignment = "RIGHT" + row.column().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") @@ -199,8 +198,7 @@ class BIM_PT_brickschema_viewport(Panel): if BrickschemaData.data["active_relations"]: row = self.layout.row(align=True) - col = row.column() - col.alignment = "RIGHT" + row.column().alignment = "RIGHT" row.prop(data=self.props, property="brick_create_relations_toggled", text="", icon="PLUGIN") row.prop(data=self.props, property="brick_edit_relations_toggled", text="", icon="TOOL_SETTINGS") row.operator("bim.remove_brick", text="", icon="X") @@ -226,9 +224,12 @@ class BIM_PT_brickschema_viewport(Panel): row.label(text="Failed to find this entity!", icon="ERROR") for relation in BrickschemaData.data["active_relations"]: - row = self.layout.row(align=True) + split = self.layout.split(factor=0.85, align=True) + row = split.row(align=True) row.label(text=relation["predicate_name"]) row.label(text=relation["object_name"]) + row = split.row(align=True) + row.column().alignment = "RIGHT" if self.props.brick_edit_relations_toggled and relation["predicate_uri"] and relation["predicate_name"] != "type": op = row.operator("bim.remove_brick_relation", text="", icon="UNLINKED") op.predicate = relation["predicate_uri"] From 49f825ba82ab34961a6147a45c0fa9e38fb0af08 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Sun, 20 Aug 2023 12:47:52 -0700 Subject: [PATCH 19/86] Refactor+polish Brick UI --- .../blenderbim/bim/module/brick/ui.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index f039df8264..770987a1bb 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -123,7 +123,7 @@ class BIM_PT_brickschema_create_entity(Panel): def draw(self, context): self.props = context.scene.BIMBrickProperties - # hide this if selected entity already has a reference + # TO DO: hide this if selected entity already has a reference, or something similar row = self.layout.row(align=True) row.prop(data=self.props, property="brick_entity_create_type", text="Class") @@ -203,13 +203,7 @@ class BIM_PT_brickschema_viewport(Panel): row.prop(data=self.props, property="brick_edit_relations_toggled", text="", icon="TOOL_SETTINGS") row.operator("bim.remove_brick", text="", icon="X") - if self.props.brick_create_relations_toggled: - row = self.layout.row(align=True) - 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") - - elif self.props.brick_create_relations_toggled and self.props.split_screen_toggled: + if self.props.brick_create_relations_toggled and self.props.split_screen_toggled: row = self.layout.row(align=True) split_screen_selection = self.props.split_screen_bricks[self.props.split_screen_active_brick_index] if split_screen_selection.total_items: @@ -219,6 +213,12 @@ class BIM_PT_brickschema_viewport(Panel): row.label(text=split_screen_selection.label if split_screen_selection.label else split_screen_selection.name) row.operator("bim.add_brick_relation", text="", icon="ADD") + elif self.props.brick_create_relations_toggled: + row = self.layout.row(align=True) + 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") @@ -227,6 +227,7 @@ class BIM_PT_brickschema_viewport(Panel): split = self.layout.split(factor=0.85, align=True) row = split.row(align=True) row.label(text=relation["predicate_name"]) + row.separator() row.label(text=relation["object_name"]) row = split.row(align=True) row.column().alignment = "RIGHT" @@ -247,15 +248,17 @@ class BIM_UL_bricks(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: - row = layout.row(align=True) + split = layout.split(factor=0.85, align=True) + row = split.row() label = item.label if item.label else item.name if item.total_items: op = row.operator("bim.view_brick_class", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False) op.brick_class = item.name op.split_screen = self.split_screen - label = label + " (" + str(item.total_items) + ")" row.label(text=label) - + if item.total_items: + row = split.row() + row.label(text=str(item.total_items)) class BIM_PT_ifc_brickschema_references(Panel): bl_label = "Brickschema References" @@ -286,7 +289,7 @@ class BIM_PT_ifc_brickschema_references(Panel): row.operator("bim.convert_brick_project", text="", icon="ADD") return - if not BrickschemaReferencesData.data["libraries"] and not BrickStore.path: + elif not BrickschemaReferencesData.data["libraries"]: row = self.layout.row(align=True) row.label(text="No IFC Libraries: save the Brick project to create a new library", icon="ERROR") From 2f99dd8e988739b9f821bd7b6cc74e9c4b4e46af Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Sun, 20 Aug 2023 12:56:55 -0700 Subject: [PATCH 20/86] Catch Assign_Brick_Reference with no selections --- src/blenderbim/blenderbim/bim/module/brick/operator.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 9cb9c81c63..4194514355 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -118,7 +118,15 @@ class AssignBrickReference(bpy.types.Operator, Operator): bl_description = "Assign the selected Ifc entity to the selected Brick entity" def _execute(self, context): + if not context.active_object: + self.report({'ERROR'}, f'No Ifc selected') + return props = context.scene.BIMBrickProperties + try: + props.bricks[props.active_brick_index] + except: + self.report({'ERROR'}, f'No Brick selected') + return core.assign_brick_reference( tool.Ifc, tool.Brick, From 17b04ad697f8143105533de48ea64a694da74aaa Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Sun, 20 Aug 2023 13:01:58 -0700 Subject: [PATCH 21/86] Update Brick feature tests --- src/blenderbim/test/bim/feature/brick.feature | 250 +++++++++++------- 1 file changed, 158 insertions(+), 92 deletions(-) diff --git a/src/blenderbim/test/bim/feature/brick.feature b/src/blenderbim/test/bim/feature/brick.feature index d7a86cab5e..8d92c51956 100644 --- a/src/blenderbim/test/bim/feature/brick.feature +++ b/src/blenderbim/test/bim/feature/brick.feature @@ -1,6 +1,11 @@ @brick Feature: Brick +Scenario: Create Brick project + Given an empty Blender session + When I press "bim.new_brick_file" + Then nothing happens + Scenario: Load Brick project Given an empty Blender session When I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" @@ -9,20 +14,20 @@ Scenario: Load Brick project Scenario: View Brick class Given an empty Blender session And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - When I press "bim.view_brick_class(brick_class='Equipment')" + When I press "bim.view_brick_class(brick_class='Building')" Then nothing happens Scenario: View Brick item Given an empty Blender session And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - When I press "bim.view_brick_item(item='https://brickschema.org/schema/Brick#Chiller')" + When I press "bim.view_brick_item(item='https://brickschema.org/schema/Brick#Building')" Then nothing happens -Scenario: Rewind brick class +Scenario: Rewind Brick class Given an empty Blender session And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - And I press "bim.view_brick_class(brick_class='Equipment')" - When I press "bim.rewind_brick_class()" + And I press "bim.view_brick_class(brick_class='Building')" + When I press "bim.rewind_brick_class" Then nothing happens Scenario: Close Brick project @@ -31,82 +36,150 @@ Scenario: Close Brick project When I press "bim.close_brick_project" Then nothing happens +Scenario: Close Brick project then create Brick project + Given an empty Blender session + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I press "bim.close_brick_project" + When I press "bim.new_brick_file" + Then nothing happens + +Scenario: Add Brick - vanilla Brick with no IFC + Given an empty Blender session + And I press "bim.new_brick_file" + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + And I set "scene.BIMBrickProperties.new_brick_label" to "abc123" + When I press "bim.add_brick" + Then nothing happens + +Scenario: Add Brick - from geometry without a Brick IFC library + Given an empty IFC project + And I press "bim.new_brick_file" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" + And I press "bim.assign_class" + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And the object "IfcChiller/Cube" is selected + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + When I press "bim.add_brick" + Then nothing happens + +Scenario: Add Brick - from geometry with a Brick IFC library + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" + And I press "bim.assign_class" + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I press "bim.convert_brick_project" + And the object "IfcChiller/Cube" is selected + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + When I press "bim.add_brick" + Then nothing happens + +Scenario: Refresh Brick viewer + Given an empty Blender session + And I press "bim.new_brick_file" + When I press "bim.refresh_brick_viewer" + Then nothing happens + +Scenario: Add Brick relation - vanilla Brick with no IFC + Given an empty Blender session + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + And I set "scene.BIMBrickProperties.brick_entity_create_type" to "Location" + And I set "scene.BIMBrickProperties.new_brick_label" to "abc123" + And I set "scene.BIMBrickProperties.brick_entity_class" to "Room" + And I press "bim.add_brick" + And I press "bim.view_brick_class(brick_class='Lighting_Zone')" + And I set "scene.BIMBrickProperties.active_brick_index" to "0" + And I set "scene.BIMRootProperties.brick_create_relations_toggled" to "True" + And I set "scene.BIMRootProperties.new_brick_relation_namespace" to "https://example.org/digitaltwin#" + And I set "scene.BIMRootProperties.new_brick_relation_type" to "hasPart" + And I set "scene.BIMRootProperties.new_brick_relation_object" to "xyz789" + When I press "bim.add_brick_relation()" + Then nothing happens + +Scenario: Add Brick relation - vanilla Brick with no IFC and with split screen + Given an empty Blender session + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + And I set "scene.BIMBrickProperties.brick_entity_create_type" to "Location" + And I set "scene.BIMBrickProperties.new_brick_label" to "abc123" + And I set "scene.BIMBrickProperties.brick_entity_class" to "Room" + And I press "bim.add_brick" + And I press "bim.view_brick_class(brick_class='Lighting_Zone')" + And I set "scene.BIMBrickProperties.active_brick_index" to "0" + And I set "scene.BIMRootProperties.split_screen_toggled" to "True" + And I press "bim.view_brick_class(brick_class='Room')" + And I set "scene.BIMBrickProperties.split_screen_active_brick_index" to "0" + And I set "scene.BIMRootProperties.brick_create_relations_toggled" to "True" + And I set "scene.BIMRootProperties.new_brick_relation_type" to "hasPart" + When I press "bim.add_brick_relation" + Then nothing happens + +Scenario: Remove Brick - vanilla Brick + Given an empty Blender session + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I press "bim.view_brick_class(brick_class='Lighting_Zone')" + And I set "scene.BIMBrickProperties.active_brick_index" to "0" + When I press "bim.remove_brick" + Then nothing happens + +Scenario: Remove Brick - with a Brick IFC library reference + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" + And I press "bim.assign_class" + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And the object "IfcChiller/Cube" is selected + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + And I press "bim.add_brick" + And I press "bim.view_brick_class(brick_class='Chiller')" + And I set "scene.BIMBrickProperties.active_brick_index" to "0" + When I press "bim.remove_brick" + Then nothing happens + +Scenario: Change viewer list root + Given an empty Blender session + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I set "scene.BIMBrickProperties.set_list_root_toggled" to "True" + When I set "scene.BIMBrickProperties.brick_list_root" to "Location" + Then nothing happens + +Scenario: Change viewer list root - split screen + Given an empty Blender session + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I set "scene.BIMBrickProperties.set_list_root_toggled" to "True" + And I set "scene.BIMRootProperties.split_screen_toggled" to "True" + When I set "scene.BIMBrickProperties.split_screen_brick_list_root" to "Location" + Then nothing happens + +Scenario: Toggle split screen + Given an empty Blender session + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + When I set "scene.BIMRootProperties.split_screen_toggled" to "True" + Then nothing happens + +Scenario: Set active namespace + Given an empty Blender session + When I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + Then nothing happens + +Scenario: Bind new namespace + Given an empty Blender session + And I set "scene.BIMBrickProperties.new_brick_namespace_alias" to "digitaltwin2" + And I set "scene.BIMBrickProperties.new_brick_namespace_uri" to "https://example.org/digitaltwin2#" + When I press "bim.add_brick_namespace" + Then nothing happens + Scenario: Convert brick project Given an empty IFC project And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" When I press "bim.convert_brick_project" Then nothing happens -Scenario: Assign brick reference - Given an empty IFC project - And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - And I set "scene.BIMBrickProperties.brick_equipment_class" to "https://brickschema.org/schema/Brick#Chiller" - And I press "bim.add_brick" - And I press "bim.view_brick_class(brick_class='Chiller')" - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" - And I press "bim.assign_class" - And the object "IfcChiller/Cube" is selected - And I press "bim.convert_brick_project" - When I press "bim.assign_brick_reference" - Then nothing happens - -Scenario: Add brick - vanilla brick with no IFC - Given an empty Blender session - And I press "bim.new_brick_file" - And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - When I press "bim.add_brick" - Then nothing happens - -Scenario: Add brick - without a brick IFC library - Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" - And I press "bim.assign_class" - And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - And the object "IfcChiller/Cube" is selected - And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - When I press "bim.add_brick" - Then nothing happens - -Scenario: Add brick - with a brick IFC library - Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" - And I press "bim.assign_class" - And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - And I press "bim.convert_brick_project" - And the object "IfcChiller/Cube" is selected - And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - When I press "bim.add_brick" - Then nothing happens - -Scenario: Add brick feed - Given an empty IFC project - And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - And I press "bim.convert_brick_project" - And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_class" to "IfcUnitaryEquipment" - And I set "scene.BIMRootProperties.ifc_predefined_type" to "AIRHANDLER" - And I press "bim.assign_class" - And I press "bim.add_brick" - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_class" to "IfcAirTerminalBox" - And I set "scene.BIMRootProperties.ifc_predefined_type" to "VARIABLEFLOWPRESSUREDEPENDANT" - And I press "bim.assign_class" - And I press "bim.add_brick" - And the object "IfcUnitaryEquipment/Cube" is selected - And additionally the object "IfcAirTerminalBox/Cube" is selected - When I press "bim.add_brick_feed" - Then nothing happens - Scenario: Convert IFC to brick Given an empty IFC project And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" @@ -120,26 +193,19 @@ Scenario: Convert IFC to brick When I press "bim.convert_ifc_to_brick" Then nothing happens -Scenario: New brick file - Given an empty Blender session - When I press "bim.new_brick_file" - Then nothing happens - -Scenario: Refresh brick viewer - Given an empty Blender session - And I press "bim.new_brick_file" - When I press "bim.refresh_brick_viewer" - Then nothing happens - -Scenario: Remove brick - without a brick IFC library +Scenario: Assign brick reference Given an empty IFC project + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + And I set "scene.BIMBrickProperties.brick_entity_class" to "https://brickschema.org/schema/Brick#Chiller" + And I press "bim.add_brick" + And I press "bim.view_brick_class(brick_class='Chiller')" + And I set "scene.BIMBrickProperties.active_brick_index" to "0" And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" And I press "bim.assign_class" - And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And the object "IfcChiller/Cube" is selected - And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - And I press "bim.add_brick" - When I press "bim.remove_brick" - Then nothing happens + And I press "bim.convert_brick_project" + When I press "bim.assign_brick_reference" + Then nothing happens \ No newline at end of file From 40d9713326fa2382d3451566c76d817a8bee156a Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Wed, 23 Aug 2023 02:11:18 +0100 Subject: [PATCH 22/86] consolidate constraint utilities into ifcopenshell.util.constraint and refactor --- .../blenderbim/bim/module/resource/data.py | 18 +++++- src/blenderbim/blenderbim/core/tool.py | 1 - .../ifcopenshell/util/constraint.py | 62 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/util/constraint.py diff --git a/src/blenderbim/blenderbim/bim/module/resource/data.py b/src/blenderbim/blenderbim/bim/module/resource/data.py index b854002ce4..269535125a 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/data.py +++ b/src/blenderbim/blenderbim/bim/module/resource/data.py @@ -56,7 +56,7 @@ class ResourceData: "type": resource.is_a(), "BaseQuantity": base_quantity, } - results[resource.id()]["Benchmarks"] = tool.Resource.get_resource_benchmarks(resource) + results[resource.id()]["Benchmarks"] = cls.get_resource_benchmarks(resource) if resource.is_a() in ["IfcLaborResource", "IfcConstructionEquipmentResource"]: results[resource.id()]["Productivity"] = {} results[resource.id()]["InheritedProductivity"] = {} @@ -87,6 +87,22 @@ class ResourceData: ) return results + @classmethod + def get_resource_benchmarks(cls, resource): + constraints = [] + for constraint in ifcopenshell.util.constraint.get_constraints(resource) or []: + metrics = [] + for metric in ifcopenshell.util.constraint.get_metrics(constraint) or []: + metrics.append( + { + "reference": ifcopenshell.util.constraint.get_metric_reference(metric), + "Benchmark": metric.Benchmark, + "ConstraintGrade": metric.ConstraintGrade, + } + ) + constraints.append({"ObjectiveQualifier": constraint.ObjectiveQualifier, "metrics": metrics}) + return constraints + @classmethod def get_productivity(cls, resource): return ifcopenshell.util.resource.get_productivity(resource, should_inherit=False) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 4b75600581..484b13f340 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -620,7 +620,6 @@ class Resource: def get_constraints(cls, resource): pass def get_metrics(cls, constraint): pass def get_metric_reference(cls, metric, is_deep): pass - def get_resource_benchmarks(cls, resource): pass def has_metric_constraint(cls, resource, attribute): pass def has_usage_metric(cls, resource): pass diff --git a/src/ifcopenshell-python/ifcopenshell/util/constraint.py b/src/ifcopenshell-python/ifcopenshell/util/constraint.py new file mode 100644 index 0000000000..66397bb58c --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/util/constraint.py @@ -0,0 +1,62 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2023 Dion Moult, Yassine Oualid +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . +# +# + +def get_constraints(product): + constraints = [] + if product.HasAssociations: + for rel in product.HasAssociations: + if rel.is_a("IfcRelAssociatesConstraint"): + constraints.append(rel.RelatingConstraint) + return constraints + +def get_metrics(constraint): + metrics = [] + for metric in constraint.BenchmarkValues or []: + metrics.append(metric) + return metrics + +def get_metric_reference(metric, is_deep=True): + def get_reference_Attribute(ref, path): + if ref: + if is_deep: + if not path: + path = ref.AttributeIdentifier + else: + path += ".{}".format(ref.AttributeIdentifier) if ref.AttributeIdentifier else "" + return get_reference_Attribute(ref.InnerReference, path) + else: + return ref.AttributeIdentifier + return path + + reference = metric.ReferencePath + return get_reference_Attribute(reference, "") + +def has_metric_constraints(resource, attribute): + metrics = [] + for constraint in get_constraints(resource) or []: + for metric in get_metrics(constraint) or []: + if bool( + get_metric_reference(metric, is_deep=False) == attribute + or get_metric_reference(metric, is_deep=True) == attribute + ): + metrics.append(metric) + if metrics: + return metrics + return None \ No newline at end of file From 839e35f1da91433838201c1737b549bb5f7aca81 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 23 Aug 2023 11:15:21 +1000 Subject: [PATCH 23/86] Fix #3627. Error where using filters incorrectly applied includes / excludes. --- src/blenderbim/blenderbim/tool/drawing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 008e1667a8..9c98e756c0 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -1529,10 +1529,10 @@ class Drawing(blenderbim.core.tool.Drawing): tool.Ifc.get_object(drawing), [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSpace")] ) if include: - elements = set(ifcopenshell.util.selector.filter_elements(ifc_file, include, elements=elements)) + elements = set(ifcopenshell.util.selector.filter_elements(ifc_file, include, elements=elements.copy())) exclude = pset.get("Exclude", None) if exclude: - elements -= set(ifcopenshell.util.selector.filter_elements(ifc_file, exclude, elements=elements)) + elements -= set(ifcopenshell.util.selector.filter_elements(ifc_file, exclude, elements=elements.copy())) return elements @classmethod From 66f5081ada29e557fd2ade3a7a103ac19a5c8cc4 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Wed, 23 Aug 2023 02:19:32 +0100 Subject: [PATCH 24/86] implement calculating task duration based on resource usage change for fixed person-hours and prevent constrained attributes from updating --- .../blenderbim/bim/module/resource/prop.py | 24 +++---- .../blenderbim/bim/module/resource/ui.py | 4 +- src/blenderbim/blenderbim/tool/resource.py | 68 ++++--------------- .../api/resource/calculate_resource_work.py | 3 + .../api/resource/edit_resource_time.py | 15 +++- 5 files changed, 44 insertions(+), 70 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/resource/prop.py b/src/blenderbim/blenderbim/bim/module/resource/prop.py index 67b486d0dc..1d3491a3c2 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/prop.py +++ b/src/blenderbim/blenderbim/bim/module/resource/prop.py @@ -1,5 +1,5 @@ # BlenderBIM Add-on - OpenBIM Blender Add-on -# Copyright (C) 2020, 2021 Dion Moult +# Copyright (C) 2021, 2022, 2023 Dion Moult, Yassine Oualid # # This file is part of BlenderBIM Add-on. # @@ -22,8 +22,8 @@ import ifcopenshell.util.resource from blenderbim.bim.ifc import IfcStore import blenderbim.tool as tool import blenderbim.bim.module.pset.data -from blenderbim.bim.module.resource.data import refresh -from blenderbim.bim.module.sequence.data import refresh as refresh_sequence +import blenderbim.bim.module.resource.data +import blenderbim.bim.module.sequence.data from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -58,7 +58,7 @@ def updateResourceName(self, context): if props.active_resource_id == self.ifc_definition_id: attribute = props.resource_attributes.get("Name") attribute.string_value = self.name - refresh() + blenderbim.bim.module.resource.data.refresh() tool.Sequence.refresh_task_resources() @@ -88,17 +88,15 @@ def updateResourceUsage(self, context): if self.schedule_usage == "": return resource = tool.Ifc.get().by_id(self.ifc_definition_id) - if not resource.Usage: - tool.Ifc.run( - "resource.add_resource_time", - resource=resource, - ) - resource.Usage.ScheduleUsage = self.schedule_usage - blenderbim.bim.module.pset.data.refresh() - refresh() + tool.Resource.run_edit_resource_time(resource, attributes={ + "ScheduleUsage": self.schedule_usage + }) tool.Resource.load_resource_properties() + tool.Sequence.load_task_properties() + blenderbim.bim.module.resource.data.refresh() + blenderbim.bim.module.sequence.data.refresh() tool.Sequence.refresh_task_resources() - + blenderbim.bim.module.pset.data.refresh() class ISODuration(PropertyGroup): name: StringProperty(name="Name") diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index f4b325571a..963b807ae6 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -110,11 +110,11 @@ class BIM_PT_resources(Panel): is_work_locked = True row = self.layout.row() row.label(text="Resource Work") - schedule_usage = "Schedule Usage: {}".format(resource.get("ScheduleUsage")) schedule_work = "Schedule Work: {}".format(resource.get("ScheduleWork")) row = self.layout.row() row.alignment = "LEFT" - row.label(text=schedule_usage, icon="ARMATURE_DATA") + row.label(text="Schedule Usage:") + row.prop(self.tprops.resources[self.props.active_resource_index], "schedule_usage", text="") row2 = self.layout.row() row2.alignment = "LEFT" row2.label(text=schedule_work, icon="ARMATURE_DATA") diff --git a/src/blenderbim/blenderbim/tool/resource.py b/src/blenderbim/blenderbim/tool/resource.py index f8b92074f1..a130d01d7a 100644 --- a/src/blenderbim/blenderbim/tool/resource.py +++ b/src/blenderbim/blenderbim/tool/resource.py @@ -32,7 +32,7 @@ import ifcopenshell.util.date as ifcdateutils import ifcopenshell.util.cost import ifcopenshell.util.resource import blenderbim.bim.schema - +import ifcopenshell.util.constraint class Resource(blenderbim.core.tool.Resource): @classmethod @@ -384,68 +384,30 @@ class Resource(blenderbim.core.tool.Resource): @classmethod def get_constraints(cls, resource): - constraints = [] - for rel in resource.HasAssociations or []: - if rel.is_a("IfcRelAssociatesConstraint"): - constraints.append(rel.RelatingConstraint) - return constraints + return ifcopenshell.util.constraint.get_constraints(product=resource) @classmethod def get_metrics(cls, constraint): - metrics = [] - for metric in constraint.BenchmarkValues or []: - metrics.append(metric) - return metrics + return ifcopenshell.util.constraint.get_metrics(constraint) @classmethod def get_metric_reference(cls, metric, is_deep=True): - def get_reference_Attribute(ref, path): - if ref: - if is_deep: - if not path: - path = ref.AttributeIdentifier - else: - path += ".{}".format(ref.AttributeIdentifier) if ref.AttributeIdentifier else "" - return get_reference_Attribute(ref.InnerReference, path) - else: - return ref.AttributeIdentifier - return path - - reference = metric.ReferencePath - return get_reference_Attribute(reference, "") - - @classmethod - def get_resource_benchmarks(cls, resource): - constraints = [] - for constraint in cls.get_constraints(resource) or []: - metrics = [] - for metric in cls.get_metrics(constraint) or []: - metrics.append( - { - "reference": cls.get_metric_reference(metric), - "Benchmark": metric.Benchmark, - "ConstraintGrade": metric.ConstraintGrade, - } - ) - constraints.append({"ObjectiveQualifier": constraint.ObjectiveQualifier, "metrics": metrics}) - return constraints + return ifcopenshell.util.constraint.get_metric_reference(metric, is_deep=is_deep) @classmethod def has_metric_constraint(cls, resource, attribute): - constraints = tool.Resource.get_constraints(resource) - metrics = [] - for constraint in constraints: - for metric in tool.Resource.get_metrics(constraint) or []: - is_same_reference = bool( - tool.Resource.get_metric_reference(metric, is_deep=False) == attribute - or tool.Resource.get_metric_reference(metric, is_deep=True) == attribute - ) - if is_same_reference: - metrics.append(metric) - if metrics: - return metrics[0] - return None + metrics = ifcopenshell.util.constraint.has_metric_constraints(resource, attribute) + return metrics[0] if metrics else None @classmethod def has_usage_metric(cls, resource): return cls.has_metric_constraint(resource, "Usage") + + @classmethod + def run_edit_resource_time(cls, resource, attributes): + if not resource.Usage: + tool.Ifc.run( + "resource.add_resource_time", + resource=resource, + ) + tool.Ifc.run("resource.edit_resource_time", resource_time=resource.Usage, attributes=attributes) \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py index c1d09b3ea0..cc6717fd5f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py @@ -60,6 +60,9 @@ class Usecase: self.settings = {"resource": resource} def execute(self): + metrics= ifcopenshell.util.constraint.has_metric_constraints(self.settings["resource"], "Usage.ScheduleWork") + if metrics and metrics[0].ConstraintGrade == "HARD" and metrics[0].Benchmark == "EQUALTO": + return amount_worked = ifcopenshell.util.resource.get_resource_required_work( self.settings["resource"] ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py index 451fb62658..29317007fb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py @@ -17,8 +17,7 @@ # along with IfcOpenShell. If not, see . import datetime -import ifcopenshell.util.date - +import ifcopenshell class Usecase: def __init__(self, file, resource_time=None, attributes=None): @@ -77,6 +76,9 @@ class Usecase: del self.settings["attributes"]["ActualFinish"] for name, value in self.settings["attributes"].items(): + metrics = ifcopenshell.util.constraint.has_metric_constraints(self.resource, "Usage." + name) + if metrics and self.is_hard_constraint(metrics[0]): + continue if value: if "Start" in name or "Finish" in name or name == "StatusTime": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime") @@ -87,6 +89,15 @@ class Usecase: ): value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration") setattr(self.settings["resource_time"], name, value) + if name == "ScheduleUsage" and ifcopenshell.util.constraint.has_metric_constraints(self.resource, "Usage.ScheduleWork"): + for rel in self.resource.HasAssignments or []: + if not rel.is_a("IfcRelAssignsToProcess"): + continue + task = rel.RelatingProcess + ifcopenshell.api.run("sequence.calculate_task_duration", self.file, task=task) + + def is_hard_constraint(self, metric): + return bool(metric.ConstraintGrade == "HARD" and metric.Benchmark == "EQUALTO") def get_resource(self): return [ From 422f772c40b490190346b527e0595c450ce1c8a9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 23 Aug 2023 11:07:38 +0500 Subject: [PATCH 25/86] Fixed bug with remembering materials assigned to faces The problem was that bim.update_representation was assigning representation styles without taking into account that some styles may not be actually used in the mesh, so it was always assigning first style (blender material) to the first IFCPOLYGONALFACESET even though it might be using for example the 3rd style. More - https://community.osarch.org/discussion/1636/materials-not-remembering-their-assigment --- .../blenderbim/bim/module/geometry/operator.py | 2 +- src/blenderbim/blenderbim/tool/geometry.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index c4ee14f969..d40edddf47 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -329,7 +329,7 @@ class UpdateRepresentation(bpy.types.Operator, Operator): "style.assign_representation_styles", self.file, shape_representation=new_representation, - styles=tool.Geometry.get_styles(obj), + styles=tool.Geometry.get_styles(obj, only_assigned_to_faces=True), should_use_presentation_style_assignment=context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, ) tool.Geometry.record_object_materials(obj) diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index fe57aa6a76..dbe96048c0 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -396,8 +396,16 @@ class Geometry(blenderbim.core.tool.Geometry): return f"{representation.ContextOfItems.id()}/{representation.id()}" @classmethod - def get_styles(cls, obj): - return [tool.Style.get_style(s.material) for s in obj.material_slots if s.material] + def get_styles(cls, obj, only_assigned_to_faces=False): + styles = [tool.Style.get_style(s.material) for s in obj.material_slots if s.material] + if not only_assigned_to_faces: + return styles + + usage_count = [0] * len(obj.material_slots) + for poly in obj.data.polygons: + usage_count[poly.material_index] += 1 + styles = [style for style, usage in zip(styles, usage_count, strict=True) if usage > 0] + return styles # TODO: multiple Literals? @classmethod From 132fef1b7694dfa1b8d8256d4bee27039fc9aa0d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 23 Aug 2023 17:08:28 +1000 Subject: [PATCH 26/86] New Status panel for managing statuses in the costing / scheduling tab. --- src/blenderbim/blenderbim/bim/__init__.py | 5 +- .../blenderbim/bim/module/cost/ui.py | 3 +- .../blenderbim/bim/module/csv/prop.py | 26 +++--- .../blenderbim/bim/module/resource/ui.py | 3 +- .../bim/module/sequence/__init__.py | 9 ++ .../bim/module/sequence/operator.py | 89 ++++++++++++++++++- .../blenderbim/bim/module/sequence/prop.py | 10 +++ .../blenderbim/bim/module/sequence/ui.py | 40 ++++++++- src/blenderbim/blenderbim/bim/ui.py | 47 +++++++++- 9 files changed, 208 insertions(+), 24 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 3d8bf6b965..de25096754 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -146,7 +146,10 @@ classes = [ # Structural analysis ui.BIM_PT_tab_structural, # Construction scheduling - ui.BIM_PT_tab_4D5D, + ui.BIM_PT_tab_status, + ui.BIM_PT_tab_resources, + ui.BIM_PT_tab_cost, + ui.BIM_PT_tab_sequence, # Facility management ui.BIM_PT_tab_handover, ui.BIM_PT_tab_operations, diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 0d00b12a71..ae7f17da8c 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -29,7 +29,8 @@ class BIM_PT_cost_schedules(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_tab_4D5D" + bl_parent_id = "BIM_PT_tab_cost" + bl_options = {"HIDE_HEADER"} @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/csv/prop.py b/src/blenderbim/blenderbim/bim/module/csv/prop.py index 96fc51e822..ba89b6aed7 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/prop.py +++ b/src/blenderbim/blenderbim/bim/module/csv/prop.py @@ -38,24 +38,24 @@ class CsvAttribute(PropertyGroup): sort: EnumProperty(items=[("NONE", "None", ""), ("ASC", "Ascending", ""), ("DESC", "Descending", "")]) group: EnumProperty( items=[ - ("NONE", "None", ""), - ("GROUP", "Group", "All rows where this value is identical will be merged."), - ("CONCAT", "Concatenation", "Concatenate values if values vary within a group."), - ("VARIES", "Varies", "Show a custom value if values vary within a group."), - ("SUM", "Sum", "Sums the total value of rows in a group."), - ("AVERAGE", "Average", "Averages the total value of rows in a group."), - ("MIN", "Min", "Gets the minimum value of rows in a group."), - ("MAX", "Max", "Gets the maximum value of rows in a group."), + ("NONE", "None", "Don't group any rows"), + ("GROUP", "Group", "All rows where this value is identical will be merged"), + ("CONCAT", "Concatenation", "Concatenate values if values vary within a group"), + ("VARIES", "Varies", "Show a custom value if values vary within a group"), + ("SUM", "Sum", "Sums the total value of rows in a group"), + ("AVERAGE", "Average", "Averages the total value of rows in a group"), + ("MIN", "Min", "Gets the minimum value of rows in a group"), + ("MAX", "Max", "Gets the maximum value of rows in a group"), ] ) varies_value: StringProperty(default="Varies", name="Varies Value") summary: EnumProperty( items=[ - ("NONE", "None", ""), - ("SUM", "Sum", "Sums the total value of all rows."), - ("AVERAGE", "Average", "Averages the total value of all rows."), - ("MIN", "Min", "Gets the minimum value of all rows."), - ("MAX", "Max", "Gets the maximum value of all rows."), + ("NONE", "None", "Don't provide a summary row"), + ("SUM", "Sum", "Sums the total value of all rows"), + ("AVERAGE", "Average", "Averages the total value of all rows"), + ("MIN", "Min", "Gets the minimum value of all rows"), + ("MAX", "Max", "Gets the maximum value of all rows"), ] ) diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 963b807ae6..65015ba254 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -28,7 +28,8 @@ class BIM_PT_resources(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_tab_4D5D" + bl_parent_id = "BIM_PT_tab_resources" + bl_options = {"HIDE_HEADER"} @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 23779b4616..c2f79dc7f3 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -118,12 +118,18 @@ classes = ( operator.UnassignWorkSchedule, operator.VisualiseWorkScheduleDate, operator.VisualiseWorkScheduleDateRange, + operator.EnableStatusFilters, + operator.DisableStatusFilters, + operator.ActivateStatusFilters, + operator.SelectStatusFilter, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, prop.TaskResource, prop.TaskProduct, prop.ISODuration, + prop.IFCStatus, + prop.BIMStatusProperties, prop.BIMWorkScheduleProperties, prop.BIMTaskTreeProperties, prop.BIMTaskTypeColor, @@ -133,6 +139,7 @@ classes = ( prop.BIMWorkCalendarProperties, prop.DatePickerProperties, prop.BIMDateTextProperties, + ui.BIM_PT_status, ui.BIM_PT_work_plans, ui.BIM_PT_work_schedules, ui.BIM_PT_work_calendars, @@ -165,6 +172,7 @@ def menu_func_import(self, context): def register(): + bpy.types.Scene.BIMStatusProperties = bpy.props.PointerProperty(type=prop.BIMStatusProperties) bpy.types.Scene.BIMWorkPlanProperties = bpy.props.PointerProperty(type=prop.BIMWorkPlanProperties) bpy.types.Scene.BIMWorkScheduleProperties = bpy.props.PointerProperty(type=prop.BIMWorkScheduleProperties) bpy.types.Scene.BIMTaskTreeProperties = bpy.props.PointerProperty(type=prop.BIMTaskTreeProperties) @@ -177,6 +185,7 @@ def register(): def unregister(): + del bpy.types.Scene.BIMStatusProperties del bpy.types.Scene.BIMWorkPlanProperties del bpy.types.Scene.BIMWorkScheduleProperties del bpy.types.Scene.BIMTaskTreeProperties diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 08b48c979a..e4ac5efa44 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -27,11 +27,98 @@ import webbrowser import blenderbim.core.sequence as core import blenderbim.tool as tool import blenderbim.bim.module.sequence.helper as helper +import ifcopenshell.util.sequence +import ifcopenshell.util.selector from datetime import datetime from dateutil import parser, relativedelta from blenderbim.bim.ifc import IfcStore from bpy_extras.io_utils import ImportHelper -import ifcopenshell.util.sequence + + +class EnableStatusFilters(bpy.types.Operator): + bl_idname = "bim.enable_status_filters" + bl_label = "Enable Status Filters" + + def execute(self, context): + props = context.scene.BIMStatusProperties + props.is_enabled = True + + props.statuses.clear() + + statuses = set() + for element in tool.Ifc.get().by_type("IfcPropertyEnumeratedValue"): + if element.Name == "Status": + pset = element.PartOfPset[0] + if pset.Name.startswith("Pset_") and pset.Name.endswith("Common"): + statuses.update(element.EnumerationValues) + elif pset.Name == "EPset_Status": # Our secret sauce + statuses.update(element.EnumerationValues) + elif element.Name == "UserDefinedStatus": + statuses.add(element.NominalValue) + + statuses = ["No Status"] + sorted([s.wrappedValue for s in statuses]) + + for status in statuses: + new = props.statuses.add() + new.name = status + return {"FINISHED"} + + +class DisableStatusFilters(bpy.types.Operator): + bl_idname = "bim.disable_status_filters" + bl_label = "Disable Status Filters" + + def execute(self, context): + props = context.scene.BIMStatusProperties + props.is_enabled = False + return {"FINISHED"} + + +class ActivateStatusFilters(bpy.types.Operator): + bl_idname = "bim.activate_status_filters" + bl_label = "Activate Status Filters" + + def execute(self, context): + props = context.scene.BIMStatusProperties + + query = [] + visible_statuses = {s.name for s in props.statuses if s.is_visible} + for name in visible_statuses: + if name == "No Status": + q = f"IfcProduct, /Pset_.*Common/.Status=NULL, EPset_Status.Status=NULL" + else: + q = f"IfcProduct, /Pset_.*Common/.Status={name} + IfcProduct, EPset_Status.Status={name}" + query.append(q) + query = " + ".join(query) + + if not query: + return {"FINISHED"} + + visible_elements = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) + + for obj in bpy.context.view_layer.objects: + element = tool.Ifc.get_entity(obj) + if not element or not element.is_a("IfcProduct"): + continue + obj.hide_set(element not in visible_elements) + return {"FINISHED"} + + +class SelectStatusFilter(bpy.types.Operator): + bl_idname = "bim.select_status_filter" + bl_label = "Select Status Filter" + name: bpy.props.StringProperty() + + def execute(self, context): + props = context.scene.BIMStatusProperties + query = f"IfcProduct, /Pset_.*Common/.Status={self.name} + IfcProduct, EPset_Status.Status={self.name}" + if self.name == "No Status": + query = f"IfcProduct, /Pset_.*Common/.Status=NULL, EPset_Status.Status=NULL" + for element in ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query): + obj = tool.Ifc.get_object(element) + if obj: + obj.select_set(True) + return {"FINISHED"} class AddWorkPlan(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 711e783f9f..2de4293a0d 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -381,6 +381,16 @@ class ISODuration(PropertyGroup): seconds: IntProperty(name="Seconds", default=0) +class IFCStatus(PropertyGroup): + name: StringProperty(name="Name") + is_visible: BoolProperty(name="Is Visible", default=True) + + +class BIMStatusProperties(PropertyGroup): + is_enabled: BoolProperty(name="Is Enabled") + statuses: CollectionProperty(name="Statuses", type=IFCStatus) + + class BIMWorkScheduleProperties(PropertyGroup): work_schedule_predefined_types: EnumProperty( items=get_schedule_predefined_types, name="Predefined Type", default=None diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 5339fcae58..61f8a22311 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -30,6 +30,38 @@ from blenderbim.bim.module.sequence.data import ( ) +class BIM_PT_status(Panel): + bl_label = "Status" + bl_idname = "BIM_PT_status" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_tab_status" + bl_options = {"HIDE_HEADER"} + + @classmethod + def poll(cls, context): + return IfcStore.get_file() + + def draw(self, context): + self.props = context.scene.BIMStatusProperties + + if not self.props.is_enabled: + row = self.layout.row() + row.operator("bim.enable_status_filters", icon="GREASEPENCIL") + return + + row = self.layout.row(align=True) + row.operator("bim.activate_status_filters", icon="TIME") + row.operator("bim.disable_status_filters", icon="CANCEL", text="") + + for status in self.props.statuses: + row = self.layout.row() + row.label(text=status.name) + row.prop(status, "is_visible", text="", emboss=False, icon="HIDE_OFF" if status.is_visible else "HIDE_ON") + row.operator("bim.select_status_filter", icon="RESTRICT_SELECT_OFF", text="").name = status.name + + class BIM_PT_work_plans(Panel): bl_label = "Work Plans" bl_idname = "BIM_PT_work_plans" @@ -37,7 +69,7 @@ class BIM_PT_work_plans(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_tab_4D5D" + bl_parent_id = "BIM_PT_tab_sequence" @classmethod def poll(cls, context): @@ -107,7 +139,7 @@ class BIM_PT_work_schedules(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_tab_4D5D" + bl_parent_id = "BIM_PT_tab_sequence" @classmethod def poll(cls, context): @@ -550,7 +582,7 @@ class BIM_PT_animation_Color_Scheme(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_tab_4D5D" + bl_parent_id = "BIM_PT_tab_sequence" @classmethod def poll(cls, context): @@ -888,7 +920,7 @@ class BIM_PT_work_calendars(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_tab_4D5D" + bl_parent_id = "BIM_PT_tab_sequence" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 7a568e85fa..8526bffa49 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -408,12 +408,53 @@ class BIM_PT_geometry(Panel): pass -class BIM_PT_tab_4D5D(Panel): - bl_label = "Costing and Scheduling" +class BIM_PT_tab_status(Panel): + bl_label = "Status" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() + + def draw(self, context): + pass + + +class BIM_PT_tab_resources(Panel): + bl_label = "Resources" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() + + def draw(self, context): + pass + + +class BIM_PT_tab_cost(Panel): + bl_label = "Cost" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() + + def draw(self, context): + pass + + +class BIM_PT_tab_sequence(Panel): + bl_label = "Construction Scheduling" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_options = {"HIDE_HEADER"} @classmethod def poll(cls, context): From 899056f9294a077124b36aa8ae7bc9f8216acb0d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 23 Aug 2023 17:09:09 +1000 Subject: [PATCH 27/86] Due to popular demand, custom EPset_Status to be used for "userdefined" status values as well as for things which don't have a status. --- .../blenderbim/bim/data/pset/EPset_Status.ifc | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/blenderbim/blenderbim/bim/data/pset/EPset_Status.ifc diff --git a/src/blenderbim/blenderbim/bim/data/pset/EPset_Status.ifc b/src/blenderbim/blenderbim/bim/data/pset/EPset_Status.ifc new file mode 100644 index 0000000000..8b48798f75 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/data/pset/EPset_Status.ifc @@ -0,0 +1,13 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION((),'2;1'); +FILE_NAME('EPset_Status.ifc','2020-01-01T00:00:00',(),(),'EPset_Status','EPset_Status',$); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPROPERTYSETTEMPLATE('3JoyTt$cz4fhYuQvJeTMhs',$,'EPset_Status','',.PSET_TYPEDRIVENOVERRIDE.,'IfcProduct',(#2,#4)); +#2= IFCSIMPLEPROPERTYTEMPLATE('2HBOdOfz5E3hZ7IBLCq$$D',$,'Status','Status of the element, predominately used in renovation or retrofitting projects. The status can be assigned to as "New" - element designed as new addition, "Existing" - element exists and remains, "Demolish" - element existed but is to be demolished, "Temporary" - element will exists only temporary (like a temporary support structure).',.P_ENUMERATEDVALUE.,'IfcLabel',$,#3,$,$,$,.READWRITE.); +#3= IFCPROPERTYENUMERATION('EPEnum_ElementStatus',(IFCLABEL('NEW'),IFCLABEL('EXISTING'),IFCLABEL('DEMOLISH'),IFCLABEL('TEMPORARY'),IFCLABEL('OTHER'),IFCLABEL('NOTKNOWN'),IFCLABEL('UNSET')),$); +#4=IFCSIMPLEPROPERTYTEMPLATE('3ttXxqmlz62BlxjzEPpqvH',$,'UserDefinedStatus','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); +ENDSEC; +END-ISO-10303-21; From a3fd169bb6edf10ac1a3c3abc516f3490adbc6b5 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 23 Aug 2023 17:09:51 +1000 Subject: [PATCH 28/86] IfcPatch now uses the new facet selector syntax. --- src/ifcopenshell-python/docs/ifcpatch.rst | 4 ++-- src/ifcpatch/ifcpatch/recipes/ExtractElements.py | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ifcopenshell-python/docs/ifcpatch.rst b/src/ifcopenshell-python/docs/ifcpatch.rst index 7a296e4853..2923e2227c 100644 --- a/src/ifcopenshell-python/docs/ifcpatch.rst +++ b/src/ifcopenshell-python/docs/ifcpatch.rst @@ -47,7 +47,7 @@ In this example, we'll extract out all `IfcWall` elements. :: - $ ifcpatch -i input.ifc -o output.ifc -r ExtractElements -a ".IfcWall" + $ ifcpatch -i input.ifc -o output.ifc -r ExtractElements -a "IfcWall" $ cat output.ifc Here is a minimal example of how to use IfcPatch as a library: @@ -60,7 +60,7 @@ Here is a minimal example of how to use IfcPatch as a library: "input": "input.ifc", "file": ifcopenshell.open("input.ifc"), "recipe": "ExtractElements", - "arguments": [".IfcWall"], + "arguments": ["IfcWall"], }) ifcpatch.write(output, "output.ifc") diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py index f21d2fd31d..fe1ced106e 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py @@ -37,13 +37,13 @@ class Patcher: .. code:: python # Extract all walls - ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": [".IfcWall"]}) + ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": ["IfcWall"]}) # Extract all slabs - ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": [".IfcSlab"]}) + ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": ["IfcSlab"]}) # Extract all walls and slabs - ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": [".IfcWall|.IfcSlab"]}) + ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": ["IfcWall, IfcSlab"]}) """ self.src = src self.file = file @@ -59,8 +59,7 @@ class Patcher: self.owner_history = self.new.add(owner_history) break self.add_element(self.file.by_type("IfcProject")[0]) - selector = ifcopenshell.util.selector.Selector() - for element in selector.parse(self.file, self.query): + for element in ifcopenshell.util.selector.filter_elements(self.file, self.query): self.add_element(element) self.create_spatial_tree() self.file = self.new From 6f9da163fbfd5fcb377bcaa803df75948b3febe7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 23 Aug 2023 12:30:13 +0500 Subject: [PATCH 29/86] Small fix for 422f772c4 --- src/blenderbim/blenderbim/tool/geometry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index dbe96048c0..3b78dfb8d4 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -402,6 +402,8 @@ class Geometry(blenderbim.core.tool.Geometry): return styles usage_count = [0] * len(obj.material_slots) + if not usage_count: # if there are no materials, polygons will still use index 0 + return [] for poly in obj.data.polygons: usage_count[poly.material_index] += 1 styles = [style for style, usage in zip(styles, usage_count, strict=True) if usage > 0] From e242244ec0a603e591fc63e553b6cef358a41cac Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 23 Aug 2023 17:46:47 +1000 Subject: [PATCH 30/86] The copy property tool now works on enumerated properties. Hooray for copying over statuses! --- src/blenderbim/blenderbim/bim/module/pset/operator.py | 8 +++++++- src/blenderbim/blenderbim/bim/module/pset/ui.py | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py index 293d46b4f9..adbb9cfbcd 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/operator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py @@ -455,7 +455,13 @@ class CopyPropertyToSelection(bpy.types.Operator, Operator): else: is_pset = context.active_object.PsetProperties.active_pset_type == "PSET" pset_name = context.active_object.PsetProperties.active_pset_name - prop_value = context.active_object.PsetProperties.properties.get(self.name).metadata.get_value() + prop = context.active_object.PsetProperties.properties.get(self.name) + if prop.value_type == "IfcPropertySingleValue": + prop_value = prop.metadata.get_value() + elif prop.value_type == "IfcPropertyEnumeratedValue": + value_name = prop.metadata.get_value_name() + prop_value = [e[value_name] for e in prop.enumerated_value.enumerated_values if e.is_selected] + for obj in tool.Blender.get_selected_objects(): core.copy_property_to_selection( tool.Ifc, diff --git a/src/blenderbim/blenderbim/bim/module/pset/ui.py b/src/blenderbim/blenderbim/bim/module/pset/ui.py index 91b1d4751e..b3296530d4 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/ui.py +++ b/src/blenderbim/blenderbim/bim/module/pset/ui.py @@ -71,6 +71,9 @@ def draw_enumerated_property(prop, layout, copy_operator=None): grid = layout.column_flow(columns=3) for e in prop.enumerated_value.enumerated_values: grid.prop(e, "is_selected", text=str(e[value_name])) + if copy_operator: + op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN") + op.name = prop.metadata.name def get_active_pset_obj_name(context, obj_type): From efa734c06e19148b71ca70677246f7f5bda1c21e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 23 Aug 2023 22:42:21 +1000 Subject: [PATCH 31/86] Fix #3628. Minor issue where regex queries didn't load properly. --- src/blenderbim/blenderbim/tool/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/tool/search.py b/src/blenderbim/blenderbim/tool/search.py index 49c5b978ef..9eeb17eff2 100644 --- a/src/blenderbim/blenderbim/tool/search.py +++ b/src/blenderbim/blenderbim/tool/search.py @@ -184,7 +184,7 @@ class ImportFilterQueryTransformer(lark.Transformer): elif args[0].data == "quoted_string": return args[0].children[0].value[1:-1].replace('\\"', '"') elif args[0].data == "regex_string": - return args[0].children[0].value + return "/" + args[0].children[0].value + "/" elif args[0].data == "special": if args[0].children[0].data == "null": return "NULL" From 5cf25c45f96b3372b0bbfd931a34929777ff9406 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Wed, 23 Aug 2023 13:59:31 +0100 Subject: [PATCH 32/86] feature to select a resource in resource tree from task ICOM list --- .../bim/module/resource/__init__.py | 51 ++++++++++--------- .../bim/module/resource/operator.py | 12 +++++ src/blenderbim/blenderbim/core/resource.py | 3 ++ src/blenderbim/blenderbim/core/tool.py | 11 ++-- src/blenderbim/blenderbim/tool/resource.py | 26 +++++++++- 5 files changed, 72 insertions(+), 31 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/resource/__init__.py b/src/blenderbim/blenderbim/bim/module/resource/__init__.py index 095ee1c427..8f1a8fd2cd 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/resource/__init__.py @@ -20,38 +20,39 @@ import bpy from . import ui, prop, operator classes = ( - operator.DisableResourceEditingUI, - operator.DisableEditingResource, - operator.EnableEditingResource, - operator.LoadResources, operator.AddResource, operator.AddResourceQuantity, - operator.EditResource, - operator.RemoveResource, - operator.RemoveResourceQuantity, - operator.LoadResourceProperties, - operator.ExpandResource, - operator.ContractResource, operator.AssignResource, - operator.UnassignResource, - operator.EnableEditingResourceTime, - operator.EnableEditingResourceQuantity, - operator.EnableEditingResourceBaseQuantity, - operator.EnableEditingResourceCosts, - operator.EnableEditingResourceCostValueFormula, - operator.EnableEditingResourceCostValue, - operator.EditResourceTime, - operator.EditResourceQuantity, + operator.CalculateResourceWork, + operator.ConstrainResourceWork, + operator.ContractResource, + operator.DisableEditingResource, + operator.DisableEditingResourceCostValue, + operator.DisableEditingResourceQuantity, + operator.DisableEditingResourceTime, + operator.DisableResourceEditingUI, + operator.EditProductivityData, + operator.EditResource, operator.EditResourceCostValue, operator.EditResourceCostValueFormula, - operator.DisableEditingResourceTime, - operator.DisableEditingResourceQuantity, - operator.DisableEditingResourceCostValue, - operator.CalculateResourceWork, + operator.EditResourceQuantity, + operator.EditResourceTime, + operator.EnableEditingResource, + operator.EnableEditingResourceBaseQuantity, + operator.EnableEditingResourceCosts, + operator.EnableEditingResourceCostValue, + operator.EnableEditingResourceCostValueFormula, + operator.EnableEditingResourceQuantity, + operator.EnableEditingResourceTime, + operator.ExpandResource, + operator.GoToResource, operator.ImportResources, - operator.EditProductivityData, - operator.ConstrainResourceWork, + operator.LoadResourceProperties, + operator.LoadResources, + operator.RemoveResource, + operator.RemoveResourceQuantity, operator.RemoveUsageConstraint, + operator.UnassignResource, prop.Resource, prop.BIMResourceProperties, prop.BIMResourceTreeProperties, diff --git a/src/blenderbim/blenderbim/bim/module/resource/operator.py b/src/blenderbim/blenderbim/bim/module/resource/operator.py index c160ef6d60..40cb07c444 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/operator.py +++ b/src/blenderbim/blenderbim/bim/module/resource/operator.py @@ -389,3 +389,15 @@ class RemoveUsageConstraint(bpy.types.Operator, tool.Ifc.Operator): core.remove_usage_constraint( tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(self.resource), reference_path=self.attribute ) + + +class GoToResource(bpy.types.Operator): + bl_idname = "bim.go_to_resource" + bl_label = "Go To Resource" + bl_description = "Selects the resource in the Resource Panel" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + + def execute(self, context): + core.go_to_resource(tool.Resource, resource=tool.Ifc.get().by_id(self.resource)) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py index cc660e8685..4de2bbeb11 100644 --- a/src/blenderbim/blenderbim/core/resource.py +++ b/src/blenderbim/blenderbim/core/resource.py @@ -214,3 +214,6 @@ def remove_usage_constraint(ifc, resource_tool, resource, reference_path): ifc.run("constraint.remove_metric", metric=metric) ifc.run("constraint.unassign_constraint", product=resource, constraint=constraint) ifc.run("constraint.remove_constraint", constraint=constraint) + +def go_to_resource(resource_tool, resource): + resource_tool.go_to_resource(resource) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 484b13f340..52b9fd1e5c 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -601,7 +601,10 @@ class Resource: def enable_editing_resource_time(cls, resource): pass def enable_editing_resource(cls, resource): pass def expand_resource(cls, resource): pass + def get_constraints(cls, resource): pass def get_highlighted_resource(cls): pass + def get_metric_reference(cls, metric, is_deep): pass + def get_metrics(cls, constraint): pass def get_productivity_attributes(cls): pass def get_productivity(cls, resource, should_inherit): pass def get_resource_attributes(cls): pass @@ -610,6 +613,9 @@ class Resource: def get_resource_quantity_attributes(cls): pass def get_resource_time_attributes(cls): pass def get_resource_time(cls, resource): pass + def go_to_resource(cls, resource): pass + def has_metric_constraint(cls, resource, attribute): pass + def has_usage_metric(cls, resource): pass def import_resources(cls, file_path): pass def load_cost_value_attributes(cls, cost_value): pass def load_productivity_data(cls): pass @@ -617,11 +623,6 @@ class Resource: def load_resource_properties(cls): pass def load_resource_time_attributes(cls, resource_time): pass def load_resources(cls): pass - def get_constraints(cls, resource): pass - def get_metrics(cls, constraint): pass - def get_metric_reference(cls, metric, is_deep): pass - def has_metric_constraint(cls, resource, attribute): pass - def has_usage_metric(cls, resource): pass @interface diff --git a/src/blenderbim/blenderbim/tool/resource.py b/src/blenderbim/blenderbim/tool/resource.py index a130d01d7a..6ee7f1fa77 100644 --- a/src/blenderbim/blenderbim/tool/resource.py +++ b/src/blenderbim/blenderbim/tool/resource.py @@ -279,6 +279,8 @@ class Resource(blenderbim.core.tool.Resource): def expand_resource(cls, resource): props = bpy.context.scene.BIMResourceProperties contracted_resources = json.loads(props.contracted_resources) + if not resource.id() in contracted_resources: + return contracted_resources.remove(resource.id()) props.contracted_resources = json.dumps(contracted_resources) @@ -410,4 +412,26 @@ class Resource(blenderbim.core.tool.Resource): "resource.add_resource_time", resource=resource, ) - tool.Ifc.run("resource.edit_resource_time", resource_time=resource.Usage, attributes=attributes) \ No newline at end of file + tool.Ifc.run("resource.edit_resource_time", resource_time=resource.Usage, attributes=attributes) + + @classmethod + def go_to_resource(cls, resource): + def get_ancestors_ids(resource): + ids = [] + for rel in resource.Nests or []: + ids.append(rel.RelatingObject.id()) + ids.extend(get_ancestors_ids(rel.RelatingObject)) + return ids + + ancestors = get_ancestors_ids(resource) + contracted_resources = json.loads(bpy.context.scene.BIMResourceProperties.contracted_resources) + for ancestor in ancestors: + if ancestor in contracted_resources: + contracted_resources.remove(ancestor) + bpy.context.scene.BIMResourceProperties.contracted_resources = json.dumps(contracted_resources) + cls.load_resources() + cls.load_resource_properties() + + resource_props = bpy.context.scene.BIMResourceTreeProperties + expanded_resources = [item.ifc_definition_id for item in resource_props.resources] + bpy.context.scene.BIMResourceProperties.active_resource_index = expanded_resources.index(resource.id()) From 1b83fcc1c79933ac4e4c9594e2a77c5ed43b1bb6 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Wed, 23 Aug 2023 14:02:53 +0100 Subject: [PATCH 33/86] when editing sequence relationships, enabling selecting successor and predecessor task --- .../blenderbim/bim/module/sequence/__init__.py | 2 +- .../blenderbim/bim/module/sequence/operator.py | 6 +++--- src/blenderbim/blenderbim/bim/module/sequence/ui.py | 9 +++++---- src/blenderbim/blenderbim/core/sequence.py | 6 +++--- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index c2f79dc7f3..bb89b8152f 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -82,7 +82,7 @@ classes = ( operator.ExportP6, operator.GenerateGanttChart, operator.GuessDateRange, - operator.HighlightTask, + operator.GoToTask, operator.ImportCSV, operator.ImportMSP, operator.ImportP6, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index e4ac5efa44..ddcfc309e4 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1410,14 +1410,14 @@ class LoadProductTasks(bpy.types.Operator): return {"FINISHED"} -class HighlightTask(bpy.types.Operator): - bl_idname = "bim.highlight_task" +class GoToTask(bpy.types.Operator): + bl_idname = "bim.go_to_task" bl_label = "Highlight Task" bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): - r = core.highlight_task(tool.Sequence, task=tool.Ifc.get().by_id(self.task)) + r = core.go_to_task(tool.Sequence, task=tool.Ifc.get().by_id(self.task)) if isinstance(r, str): self.report({"WARNING"}, r) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 61f8a22311..f28794e481 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -351,6 +351,7 @@ class BIM_PT_work_schedules(Panel): def draw_editable_sequence_ui(self, sequence, process_type): task = SequenceData.data["tasks"][sequence[process_type]] row = self.layout.row(align=True) + row.operator("bim.go_to_task", text="", icon="RESTRICT_SELECT_OFF").task = task["id"] row.label(text=task["Identification"] or "XXX") row.label(text=task["Name"] or "Unnamed") row.label(text=sequence["SequenceType"] or "N/A") @@ -765,9 +766,9 @@ class BIM_UL_task_resources(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: row = layout.row(align=True) + row.operator("bim.go_to_resource", text="", icon="STYLUS_PRESSURE").resource = item.ifc_definition_id row.prop(item, "name", emboss=False, text="") - row.label(text=str(item.schedule_usage)) - + row.prop(item, "schedule_usage", emboss=False, text="") class BIM_UL_animation_colors(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): @@ -790,7 +791,7 @@ class BIM_UL_product_input_tasks(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: row = layout.row(align=True) - op = row.operator("bim.highlight_task", text="", icon="STYLUS_PRESSURE") + op = row.operator("bim.go_to_task", text="", icon="STYLUS_PRESSURE") op.task = item.ifc_definition_id row.split(factor=0.8) row.label(text=item.name) @@ -800,7 +801,7 @@ class BIM_UL_product_output_tasks(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: row = layout.row(align=True) - op = row.operator("bim.highlight_task", text="", icon="STYLUS_PRESSURE") + op = row.operator("bim.go_to_task", text="", icon="STYLUS_PRESSURE") op.task = item.ifc_definition_id row.split(factor=0.8) row.label(text=item.name) diff --git a/src/blenderbim/blenderbim/core/sequence.py b/src/blenderbim/blenderbim/core/sequence.py index eb3bdbc8c9..e0258d39a1 100644 --- a/src/blenderbim/blenderbim/core/sequence.py +++ b/src/blenderbim/blenderbim/core/sequence.py @@ -465,11 +465,11 @@ def load_animation_color_scheme(sequence, scheme): sequence.load_animation_color_scheme(scheme) -def highlight_task(sequence, task=None): +def go_to_task(sequence, task=None): work_schedule = sequence.get_work_schedule(task) is_work_schedule_active = sequence.is_work_schedule_active(work_schedule) if is_work_schedule_active: - sequence.highlight_task(task) + sequence.go_to_task(task) else: return "Work schedule is not active" @@ -485,7 +485,7 @@ def highlight_product_related_task(sequence, spatial, product_type=None): work_schedule = sequence.get_work_schedule(task) is_work_schedule_active = sequence.is_work_schedule_active(work_schedule) if is_work_schedule_active: - sequence.highlight_task(task) + sequence.go_to_task(task) def guess_date_range(sequence, work_schedule=None): From 3690998dbca518f776db6b229d56596758fbcb84 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 23 Aug 2023 23:02:54 +1000 Subject: [PATCH 34/86] Fix #3620. Bug where manually inserted windows should preseve the existing Z value. --- .../blenderbim/bim/module/model/opening.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index ebbc764c79..9c93accf58 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -70,7 +70,10 @@ class FilledOpeningGenerator: ) if target is None: - target = bpy.context.scene.cursor.location + should_set_z_level = True + target = bpy.context.scene.cursor.location.copy() + else: + should_set_z_level = False # Sometimes, the voided_obj may be an aggregate, which won't have any representation. if voided_obj.data: @@ -86,12 +89,17 @@ class FilledOpeningGenerator: axis = tool.Model.get_wall_axis(voided_obj, layers=layers)["base"] new_matrix = voided_obj.matrix_world.copy() - new_matrix.translation = tool.Cad.point_on_edge(target, axis) + point_on_axis = tool.Cad.point_on_edge(target, axis) + new_matrix.translation.x = point_on_axis.x + new_matrix.translation.y = point_on_axis.y - if filling.is_a("IfcDoor"): - new_matrix.translation.z = voided_obj.matrix_world.translation.z + if should_set_z_level: + if filling.is_a("IfcDoor"): + new_matrix.translation.z = voided_obj.matrix_world.translation.z + else: + new_matrix.translation.z = voided_obj.matrix_world.translation.z + props.rl2 else: - new_matrix.translation.z = voided_obj.matrix_world.translation.z + props.rl2 + new_matrix.translation.z = filling_obj.matrix_world.copy().translation.z filling_obj.matrix_world = new_matrix bpy.context.view_layer.update() From 7ab07ed149d289b2f955d69554304746cebe309b Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Wed, 23 Aug 2023 14:04:09 +0100 Subject: [PATCH 35/86] refactor highligh_task code --- src/blenderbim/blenderbim/tool/sequence.py | 34 ++++++++++++---------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index 0706103f2b..f996917870 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -774,25 +774,27 @@ class Sequence(blenderbim.core.tool.Sequence): ) @classmethod - def highlight_task(cls, task): - def expand_ancestors(task): + def go_to_task(cls, task): + def get_ancestor_ids(task): + ids = [] for rel in task.Nests or []: - parent_task = rel.RelatingObject if rel.RelatingObject.is_a("IfcTask") else None - contracted_tasks = json.loads(bpy.context.scene.BIMWorkScheduleProperties.contracted_tasks) - if parent_task and parent_task.id() in contracted_tasks: - contracted_tasks.remove(parent_task.id()) - bpy.context.scene.BIMWorkScheduleProperties.contracted_tasks = json.dumps(contracted_tasks) - expand_ancestors(parent_task) - work_schedule = cls.get_active_work_schedule() - cls.load_task_tree(work_schedule) - cls.load_task_properties() + ids.append(rel.RelatingObject.id()) + ids.extend(get_ancestor_ids(rel.RelatingObject)) + return ids + + contracted_tasks = json.loads(bpy.context.scene.BIMWorkScheduleProperties.contracted_tasks) + for ancestor_id in get_ancestor_ids(task): + if ancestor_id in contracted_tasks: + contracted_tasks.remove(ancestor_id) + bpy.context.scene.BIMWorkScheduleProperties.contracted_tasks = json.dumps(contracted_tasks) + + work_schedule = cls.get_active_work_schedule() + cls.load_task_tree(work_schedule) + cls.load_task_properties() task_props = bpy.context.scene.BIMTaskTreeProperties - displayed_tasks = [item.ifc_definition_id for item in task_props.tasks] - if not task.id() in displayed_tasks: - expand_ancestors(task) - task_index = displayed_tasks.index(task.id()) or 0 - bpy.context.scene.BIMWorkScheduleProperties.active_task_index = task_index + expanded_tasks = [item.ifc_definition_id for item in task_props.tasks] + bpy.context.scene.BIMWorkScheduleProperties.active_task_index = expanded_tasks.index(task.id()) or 0 @classmethod def guess_date_range(cls, work_schedule): From aad5d280fde3751471e6fa249a623787bccb95bd Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Wed, 23 Aug 2023 14:13:16 +0100 Subject: [PATCH 36/86] you can now also update resource numbers (usage) from the task ICOM resource list --- .../blenderbim/bim/module/sequence/prop.py | 26 ++++++++++++++++--- src/blenderbim/blenderbim/core/tool.py | 2 +- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 2de4293a0d..b806fa445c 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -24,7 +24,8 @@ 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 -from blenderbim.bim.module.sequence.data import SequenceData, AnimationColorSchemeData +from blenderbim.bim.module.sequence.data import SequenceData, AnimationColorSchemeData, refresh as refresh_sequence_data +import blenderbim.bim.module.resource.data import blenderbim.bim.module.pset.data from blenderbim.bim.prop import StrProperty, Attribute from dateutil import parser @@ -325,6 +326,25 @@ def get_saved_color_schemes(self, context): return AnimationColorSchemeData.data["saved_color_schemes"] +def updateAssignedResourceName(self, context): + pass + +def updateAssignedResourceUsage(self, context): + if not self.schedule_usage: + return + resource = tool.Ifc.get().by_id(self.ifc_definition_id) + if resource.Usage and resource.Usage.ScheduleUsage == self.schedule_usage: + return + tool.Resource.run_edit_resource_time(resource, attributes={ + "ScheduleUsage": self.schedule_usage + }) + tool.Sequence.load_task_properties() + tool.Resource.load_resource_properties() + tool.Sequence.refresh_task_resources() + blenderbim.bim.module.resource.data.refresh() + blenderbim.bim.module.sequence.data.refresh() + blenderbim.bim.module.pset.data.refresh() + class Task(PropertyGroup): name: StringProperty(name="Name", update=updateTaskName) identification: StringProperty(name="Identification", update=updateTaskIdentification) @@ -352,9 +372,9 @@ class WorkPlan(PropertyGroup): class TaskResource(PropertyGroup): - name: StringProperty(name="Name") + name: StringProperty(name="Name", update=updateAssignedResourceName) ifc_definition_id: IntProperty(name="IFC Definition ID") - schedule_usage: FloatProperty(name="Schedule Usage") + schedule_usage: FloatProperty(name="Schedule Usage", update=updateAssignedResourceUsage) class TaskProduct(PropertyGroup): diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 52b9fd1e5c..64d78f87bd 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -735,7 +735,7 @@ class Sequence: def get_work_time_attributes(cls): pass def guess_date_range(cls, work_schedule): pass def has_task_assignments(cls, product, cost_schedule=None): pass - def highlight_task(cls, task): pass + def go_to_task(cls, task): pass def is_filter_by_active_schedule(cls): pass def is_work_schedule_active(cls, work_schedule): pass def load_animation_color_scheme(cls, scheme): pass From 2bc22e150caa4005b59c541cbf8e26b22c22d341 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 23 Aug 2023 11:32:04 +0500 Subject: [PATCH 37/86] added tests for shape builder transition length calculator --- .../ifcopenshell/util/shape_builder.py | 257 +++++++++--------- .../test/util/test_shape_builder.py | 136 +++++++++ 2 files changed, 268 insertions(+), 125 deletions(-) create mode 100644 src/ifcopenshell-python/test/util/test_shape_builder.py diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 87a1b3652a..b21fbc4389 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -928,131 +928,7 @@ class ShapeBuilder: start_offset = V(0, 0, start_length) end_extrusion_offset = start_offset.copy() - si_conversion = ifcopenshell.util.unit.calculate_unit_scale(self.file) - - # TODO: move to separate shape_builder method - # so we could check transition length without creating representation - def get_transition_length(start_half_dim, end_half_dim, angle, profile_offset=None, verbose=True): - """get the final transition length for two profiles dimensions, angle and XY offset between them, - - the difference from `calculate_transition` - `get_transition_length` is making sure - that length will fit both sides of the transition - """ - print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None - - # offsets tend to have bunch of float point garbage - # that can result in errors when we're calculating value for square root below - offset = V(0, 0) if profile_offset is None else round_vector_to_precision(profile_offset, si_conversion) - diff = start_half_dim.xy - end_half_dim.xy - diff = Vector([abs(i) for i in diff]) - - # TODO: move to separate shape_builder method - # so it could be tested later separately - def calculate_transition( - start_half_dim, end_half_dim, diff, offset, end_profile=False, angle=None, length=None - ): - """will return transition length based on the profile dimension differences and offset. - - If `length` is provided will return transition angle""" - - if end_profile: - diff, offset = diff.yx, offset.yx - - same_dimensions = is_x(diff.length, 0) - - a = diff.x + offset.x - b = diff.x - offset.x - if length is None: - if not same_dimensions: - if diff.x == 0: - return 0 - - t = tan(radians(angle)) - h = (a + b + sqrt(a**2 + 4 * a * b * t**2 + 2 * a * b + b**2)) / (2 * t) - length = sqrt(h**2 - offset.y**2) - - # TODO: move somewhere to tests? - if verbose: - A = (end_half_dim if end_profile else start_half_dim) * V(1, 0, 0) - end_profile_offset = offset.to_3d() + V(0, 0, length) - D = (start_half_dim if end_profile else end_half_dim) * V(1, 0, 0) - B, C = -A, -D - C += end_profile_offset - D += end_profile_offset - tested_angle = degrees((A - D).angle(B - C)) - print(f"II. length = {length}, requested angle = {angle}, tested angle = {tested_angle}") - else: - if is_x(offset.x, 0): - angle = 90 # NOTE: for now we just hardcode the good value for that case - h = start_half_dim.x / tan(radians(angle / 2)) - length = sqrt(h**2 - offset.y**2) - - if verbose: # TODO: move to tests - O = V(0, 0, 0) - A = V(-start_half_dim.x, 0, length) + offset.to_3d() - B = A * V(-1, 1, 1) - tested_angle = degrees((A - O).angle(B - O)) - print(f"I. length = {length}, requested angle = {angle}, tested angle = {tested_angle}") - else: - h = offset.x / tan(radians(angle)) - length = sqrt(h**2 - offset.y**2) - - if verbose: # TODO: move to tests - A = V(-start_half_dim.x, 0, 0) - H = A + V(0, 0, length) - D = H + offset.to_3d() - tested_angle = degrees((H - A).angle(D - A)) - print( - f"III. length = {length}, requested angle = {angle}, tested angle = {tested_angle}" - ) - - return length - - elif angle is None: - # TODO: write some tests here too - if not same_dimensions: - if length == 0: - return 0 - - h = sqrt(length**2 + offset.y**2) - t = -h * (a + b) / (a * b - h**2) - angle = degrees(atan(t)) - else: - h = sqrt(length**2 + offset.y**2) - if is_x(offset.x, 0): - angle = degrees(2 * atan(start_half_dim.x / h)) - else: - angle = degrees(atan(offset.x / length)) - return angle - - print(f"offset = {profile_offset} / {offset}") - print(f"diff = {diff}") - - calculation_arguments = (start_half_dim, end_half_dim, diff, offset) - - def check_transition(end_profile=False): - length = calculate_transition(*calculation_arguments, angle=angle, end_profile=end_profile) - other_side_angle = calculate_transition( - *calculation_arguments, length=length, end_profile=not end_profile - ) - - # NOTE: for now we just hardcode the good value for that case - same_dimensions = is_x(diff.length, 0) - if same_dimensions and is_x(offset.y if not end_profile else offset.x, 0): - requested_angle = 90 - else: - requested_angle = angle - - print(f"other_side_angle = {other_side_angle}, requested_angle = {requested_angle}") - # need to make sure that the worst angle (maximum angle) - # for this transition angle is `requested_angle` - if other_side_angle < requested_angle or is_x(other_side_angle, requested_angle): - print(f"final length = {length}, angle = {requested_angle}, other side angle = {other_side_angle}") - return length - - return check_transition() or check_transition(True) - - transition_length = get_transition_length(start_half_dim, end_half_dim, angle, profile_offset) + transition_length = self.mep_transition_length(start_half_dim, end_half_dim, angle, profile_offset) if transition_length is None: return None, None @@ -1199,3 +1075,134 @@ class ShapeBuilder: } return representation, transition_data + + # TODO: move to separate shape_builder method + # so we could check transition length without creating representation + def mep_transition_length(self, start_half_dim, end_half_dim, angle, profile_offset=None, verbose=True): + """get the final transition length for two profiles dimensions, angle and XY offset between them, + + the difference from `calculate_transition` - `get_transition_length` is making sure + that length will fit both sides of the transition + """ + print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None + + # offsets tend to have bunch of float point garbage + # that can result in errors when we're calculating value for square root below + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(self.file) + offset = V(0, 0) if profile_offset is None else round_vector_to_precision(profile_offset, si_conversion) + diff = start_half_dim.xy - end_half_dim.xy + diff = Vector([abs(i) for i in diff]) + + print(f"offset = {profile_offset} / {offset}") + print(f"diff = {diff}") + + calculation_arguments = { + "start_half_dim": start_half_dim, + "end_half_dim": end_half_dim, + "diff": diff, + "offset": offset, + "verbose": verbose, + } + + def check_transition(end_profile=False): + length = self.mep_transition_calculate(**calculation_arguments, angle=angle, end_profile=end_profile) + other_side_angle = self.mep_transition_calculate( + **calculation_arguments, length=length, end_profile=not end_profile + ) + + # NOTE: for now we just hardcode the good value for that case + same_dimensions = is_x(diff.length, 0) + if same_dimensions and is_x(offset.y if not end_profile else offset.x, 0): + requested_angle = 90 + else: + requested_angle = angle + + print(f"other_side_angle = {other_side_angle}, requested_angle = {requested_angle}") + # need to make sure that the worst angle (maximum angle) + # for this transition angle is `requested_angle` + if other_side_angle < requested_angle or is_x(other_side_angle, requested_angle): + print(f"final length = {length}, angle = {requested_angle}, other side angle = {other_side_angle}") + return length + + return check_transition() or check_transition(True) + + def mep_transition_calculate( + self, start_half_dim, end_half_dim, offset, diff=None, end_profile=False, angle=None, length=None, verbose=True + ): + """will return transition length based on the profile dimension differences and offset. + + If `length` is provided will return transition angle""" + + print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None + + if diff is None: + diff = start_half_dim.xy - end_half_dim.xy + diff = Vector([abs(i) for i in diff]) + + if end_profile: + diff, offset = diff.yx, offset.yx + + same_dimensions = is_x(diff.length, 0) + + a = diff.x + offset.x + b = diff.x - offset.x + if length is None: + if not same_dimensions: + if diff.x == 0: + return 0 + + t = tan(radians(angle)) + h = (a + b + sqrt(a**2 + 4 * a * b * t**2 + 2 * a * b + b**2)) / (2 * t) + length = sqrt(h**2 - offset.y**2) + + if verbose: + A = (end_half_dim if end_profile else start_half_dim) * V(1, 0, 0) + end_profile_offset = offset.to_3d() + V(0, 0, length) + D = (start_half_dim if end_profile else end_half_dim) * V(1, 0, 0) + B, C = -A, -D + C += end_profile_offset + D += end_profile_offset + tested_angle = degrees((A - D).angle(B - C)) + print(f"A. length = {length}, requested angle = {angle}, tested angle = {tested_angle}") + else: + if is_x(offset.x, 0): + angle = 90 # NOTE: for now we just hardcode the good value for that case + h = start_half_dim.x / tan(radians(angle / 2)) + length = sqrt(h**2 - offset.y**2) + + if verbose: + O = V(0, 0, 0) + A = V(-start_half_dim.x, 0, length) + offset.to_3d() + B = A * V(-1, 1, 1) + tested_angle = degrees((A - O).angle(B - O)) + print(f"B. length = {length}, requested angle = {angle}, tested angle = {tested_angle}") + else: + h = offset.x / tan(radians(angle)) + length = sqrt(h**2 - offset.y**2) + + if verbose: + A = V(-start_half_dim.x, 0, 0) + H = A + V(0, 0, length) + H.y += offset.y + D = H.copy() + D.x += offset.x + tested_angle = degrees((H - A).angle(D - A)) + print(f"C. length = {length}, requested angle = {angle}, tested angle = {tested_angle}") + + return length + + elif angle is None: + if not same_dimensions: + if length == 0: + return 0 + + h = sqrt(length**2 + offset.y**2) + t = -h * (a + b) / (a * b - h**2) + angle = degrees(atan(t)) + else: + h = sqrt(length**2 + offset.y**2) + if is_x(offset.x, 0): + angle = degrees(2 * atan(start_half_dim.x / h)) + else: + angle = degrees(atan(offset.x / h)) + return angle diff --git a/src/ifcopenshell-python/test/util/test_shape_builder.py b/src/ifcopenshell-python/test/util/test_shape_builder.py new file mode 100644 index 0000000000..23b2d92f90 --- /dev/null +++ b/src/ifcopenshell-python/test/util/test_shape_builder.py @@ -0,0 +1,136 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2023 Dion Moult , @Andrej730 +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +from ifcopenshell.util.shape_builder import ShapeBuilder, V, is_x +from math import degrees, radians, tan +from mathutils import Vector + + +class TestCalculateTransitions(test.bootstrap.IFC4): + def calculate_and_test(self, params, length): + end_profile = params["end_profile"] + start_half_dim = params["start_half_dim"] + end_half_dim = params["end_half_dim"] + offset = params["offset"] + offset = offset if not end_profile else offset.yx + angle = params["angle"] + + calculated_length = self.builder.mep_transition_calculate(**params) + assert is_x(calculated_length, length) + + # angle confirmation methods: + # A - between two profiles of different dimensions + # B - between two profiles of same dimensions, no offset by x + # C - between two profiles of same dimensions, has offset by x + same_dimensions = is_x((start_half_dim.xy - end_half_dim.xy).length, 0) + if not same_dimensions: + confirmation_method = "A" + else: + confirmation_method = "B" if is_x(offset.x, 0) else "C" + + if confirmation_method == "A": + A = (end_half_dim if end_profile else start_half_dim) * V(1, 0, 0) + end_profile_offset = offset.to_3d() + V(0, 0, length) + D = (start_half_dim if end_profile else end_half_dim) * V(1, 0, 0) + B, C = -A, -D + C += end_profile_offset + D += end_profile_offset + tested_angle = degrees((A - D).angle(B - C)) + assert is_x(tested_angle, angle) + + elif confirmation_method == "B": + O = V(0, 0, 0) + A = V(-start_half_dim.x, 0, length) + offset.to_3d() + B = A * V(-1, 1, 1) + tested_angle = degrees((A - O).angle(B - O)) + assert is_x(tested_angle, angle) + + elif confirmation_method == "C": + A = V(-start_half_dim.x, 0, 0) + H = A + V(0, 0, length) + H.y += offset.y + D = H.copy() + D.x += offset.x + tested_angle = degrees((H - A).angle(D - A)) + assert is_x(tested_angle, angle) + + angle = self.builder.mep_transition_calculate(**params | {"angle": None, "length": calculated_length}) + assert is_x(angle, angle) + + def test_mep_transition_same_dims_no_offset(self): + self.builder = ShapeBuilder(self.file) + params = { + "start_half_dim": V(100, 50, 0), + "end_half_dim": V(100, 50, 0), + "offset": V(0, 0), + "end_profile": False, + "angle": 90, + "verbose": True, + } + self.calculate_and_test(params, 100) + + def test_mep_transition_same_dims_has_x_offset(self): + self.builder = ShapeBuilder(self.file) + params = { + "start_half_dim": V(100, 50, 0), + "end_half_dim": V(100, 50, 0), + "offset": V(50, 50), + "end_profile": False, + "angle": 30, + "verbose": True, + } + self.calculate_and_test(params, 70.71068) + + def test_mep_transition_same_dims_has_y_offset(self): + self.builder = ShapeBuilder(self.file) + params = { + "start_half_dim": V(100, 50, 0), + "end_half_dim": V(100, 50, 0), + "offset": V(0, 50), + "end_profile": False, + "angle": 90, + "verbose": True, + } + self.calculate_and_test(params, 86.60254) + + def test_mep_transition_diff_dims_no_offset(self): + self.builder = ShapeBuilder(self.file) + params = { + "start_half_dim": V(100, 50, 0), + "end_half_dim": V(50, 100, 0), + "offset": V(0, 0), + "end_profile": False, + "angle": 30, + "verbose": True, + } + self.calculate_and_test(params, 186.60254) + + def test_mep_transition_diff_dims_has_x_y_offset(self): + self.builder = ShapeBuilder(self.file) + params = { + "start_half_dim": V(100, 50, 0), + "end_half_dim": V(50, 100, 0), + "offset": V(50, 50), + "end_profile": False, + "angle": 30, + "verbose": True, + } + self.calculate_and_test(params, 165.83124) From 1a142ad5eb714cbb99696093342ffc2570689e07 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 23 Aug 2023 15:06:05 +0500 Subject: [PATCH 38/86] fixed bug in mep transitions it wasn't taking into account that offset can be too big to sustain the angle --- .../ifcopenshell/util/shape_builder.py | 37 ++++++++++++++----- .../test/util/test_shape_builder.py | 32 +++++++++++++++- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index b21fbc4389..d0667cad9a 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -1106,13 +1106,16 @@ class ShapeBuilder: def check_transition(end_profile=False): length = self.mep_transition_calculate(**calculation_arguments, angle=angle, end_profile=end_profile) + if length is None: + return + other_side_angle = self.mep_transition_calculate( **calculation_arguments, length=length, end_profile=not end_profile ) # NOTE: for now we just hardcode the good value for that case - same_dimensions = is_x(diff.length, 0) - if same_dimensions and is_x(offset.y if not end_profile else offset.x, 0): + same_dimension = is_x(diff.y if not end_profile else diff.x, 0) + if same_dimension and is_x(offset.y if not end_profile else offset.x, 0): requested_angle = 90 else: requested_angle = angle @@ -1142,17 +1145,24 @@ class ShapeBuilder: if end_profile: diff, offset = diff.yx, offset.yx - same_dimensions = is_x(diff.length, 0) - + same_dimension = is_x(diff.x, 0) a = diff.x + offset.x b = diff.x - offset.x if length is None: - if not same_dimensions: - if diff.x == 0: - return 0 - + if not same_dimension: t = tan(radians(angle)) - h = (a + b + sqrt(a**2 + 4 * a * b * t**2 + 2 * a * b + b**2)) / (2 * t) + h0 = a**2 + 4 * a * b * t**2 + 2 * a * b + b**2 + # TODO: we might need to specify the exact failing cases in the future + if h0 < 0: + print( + f"B. Coulndn't calculate transition length for angle = {angle}, offset = {offset}, diff = {diff}" + ) + return None + + h = (a + b + sqrt(h0)) / (2 * t) + if h < abs(offset.y) or is_x(h, offset.y): + print(f"B. angle = {angle} requires h = {h} which is not possible with y offset = {offset.y}") + return None length = sqrt(h**2 - offset.y**2) if verbose: @@ -1168,6 +1178,9 @@ class ShapeBuilder: if is_x(offset.x, 0): angle = 90 # NOTE: for now we just hardcode the good value for that case h = start_half_dim.x / tan(radians(angle / 2)) + if h < abs(offset.y) or is_x(h, offset.y): + print(f"B. angle = {angle} requires h = {h} which is not possible with y offset = {offset.y}") + return None length = sqrt(h**2 - offset.y**2) if verbose: @@ -1178,6 +1191,9 @@ class ShapeBuilder: print(f"B. length = {length}, requested angle = {angle}, tested angle = {tested_angle}") else: h = offset.x / tan(radians(angle)) + if h < abs(offset.y) or is_x(h, offset.y): + print(f"C. angle = {angle} requires h = {h} which is not possible with y offset = {offset.y}") + return None length = sqrt(h**2 - offset.y**2) if verbose: @@ -1192,13 +1208,14 @@ class ShapeBuilder: return length elif angle is None: - if not same_dimensions: + if not same_dimension: if length == 0: return 0 h = sqrt(length**2 + offset.y**2) t = -h * (a + b) / (a * b - h**2) angle = degrees(atan(t)) + else: h = sqrt(length**2 + offset.y**2) if is_x(offset.x, 0): diff --git a/src/ifcopenshell-python/test/util/test_shape_builder.py b/src/ifcopenshell-python/test/util/test_shape_builder.py index 23b2d92f90..c3b1b29fc5 100644 --- a/src/ifcopenshell-python/test/util/test_shape_builder.py +++ b/src/ifcopenshell-python/test/util/test_shape_builder.py @@ -34,14 +34,19 @@ class TestCalculateTransitions(test.bootstrap.IFC4): angle = params["angle"] calculated_length = self.builder.mep_transition_calculate(**params) + if length is None: + assert calculated_length is None + return + assert is_x(calculated_length, length) # angle confirmation methods: # A - between two profiles of different dimensions # B - between two profiles of same dimensions, no offset by x # C - between two profiles of same dimensions, has offset by x - same_dimensions = is_x((start_half_dim.xy - end_half_dim.xy).length, 0) - if not same_dimensions: + diff = start_half_dim.xy - end_half_dim.xy + same_dimension = is_x(diff.x if not end_profile else diff.y, 0) + if not same_dimension: confirmation_method = "A" else: confirmation_method = "B" if is_x(offset.x, 0) else "C" @@ -134,3 +139,26 @@ class TestCalculateTransitions(test.bootstrap.IFC4): "verbose": True, } self.calculate_and_test(params, 165.83124) + + def test_mep_transition_y_offset_too_big(self): + self.builder = ShapeBuilder(self.file) + + # method A + params = { + "start_half_dim": V(100, 50, 0), + "end_half_dim": V(50, 100, 0), + # offset.y > h - 190 > 186.6 + "offset": V(0, 190), + "end_profile": False, + "angle": 30, + "verbose": True, + } + self.calculate_and_test(params, None) + + # method B + params["end_half_dim"] = V(100, 100, 0) + self.calculate_and_test(params, None) + + # method C + params["offset"].x = 10 + self.calculate_and_test(params, None) From a78769c8ffee63603ab006466b534f4b5e7b1daa Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 23 Aug 2023 16:09:08 +0500 Subject: [PATCH 39/86] a bit faster renaming ifc elements when renaming blender objects Previously when you renamed blender object it was requiring the "Ifc.../" prefix so it would also change the name for ifc element. So when you wanted to rename something you'd only select and change the name part without prefix. Now you can just type the name right in after using F2 which seems a bit quicker by a few less clicks. Before - https://imgur.com/a/Rn7a17W After - https://imgur.com/a/a3lmxte --- src/blenderbim/blenderbim/bim/handler.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 27e9f34c7a..2d1cfad4c5 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -73,17 +73,27 @@ def name_callback(obj, data): refresh_ui_data() return - if not obj.BIMObjectProperties.ifc_definition_id or "/" not in obj.name: + if not obj.BIMObjectProperties.ifc_definition_id: return + element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) + if "/" in obj.name: + object_name = obj.name + element_name = obj.name.split("/", 1)[1] + else: + element_name = obj.name + object_name = element.is_a() + f"/{element_name}" + obj.name = object_name # NOTE: doesn't trigger infinite recursion + if element.is_a("IfcGridAxis"): - element.AxisTag = obj.name.split("/")[1] + element.AxisTag = object_name.split("/")[1] refresh_ui_data() + if not element.is_a("IfcRoot"): return + element.Name = element_name if obj.BIMObjectProperties.collection: - obj.BIMObjectProperties.collection.name = obj.name - element.Name = "/".join(obj.name.split("/")[1:]) + obj.BIMObjectProperties.collection.name = object_name refresh_ui_data() From 2ccc9d886e3ca91ad000f65b8bfdda1328e414ae Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 23 Aug 2023 17:29:42 +0500 Subject: [PATCH 40/86] =?UTF-8?q?test=20to=20make=20sure=20nothing=20happe?= =?UTF-8?q?ned=20to=20ifcsverchok=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/blenderbim/test/bim/feature/misc.feature | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/blenderbim/test/bim/feature/misc.feature b/src/blenderbim/test/bim/feature/misc.feature index 6f08795603..282ec0d16b 100644 --- a/src/blenderbim/test/bim/feature/misc.feature +++ b/src/blenderbim/test/bim/feature/misc.feature @@ -48,3 +48,10 @@ Scenario: Split along edge When I press "bim.split_along_edge" Then the object "IfcWall/Cube" is an "IfcWall" And the object "IfcWall/Cube.001" is an "IfcWall" + +Scenario: Enabling and disabling IFC Sverchok + Given an empty IFC project + And I press "preferences.addon_enable(module="sverchok")" + And I press "preferences.addon_enable(module="ifcsverchok")" + And I press "preferences.addon_disable(module="sverchok")" + And I press "preferences.addon_disable(module="ifcsverchok")" \ No newline at end of file From 12b5a888618a23b312d57477418c4387c30689cb Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 23 Aug 2023 18:21:43 +0500 Subject: [PATCH 41/86] Added small UI to show realizing elements for the connections Example - https://i.imgur.com/JLDcnbK.png --- .../blenderbim/bim/module/geometry/data.py | 20 +++++++++++++++++-- .../blenderbim/bim/module/geometry/ui.py | 14 +++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/data.py b/src/blenderbim/blenderbim/bim/module/geometry/data.py index 8b1849b200..6a3ece41c9 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/data.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/data.py @@ -114,7 +114,13 @@ class ConnectionsData: else: related_element = rel.RelatedElement - if element.is_a("IfcRelConnectsPathElements"): + realizing_elements = [] + realizing_elements_connection_type = "" + if rel.is_a("IfcRelConnectsWithRealizingElements"): + realizing_elements.extend(rel.RealizingElements) + realizing_elements_connection_type = rel.ConnectionType + + if rel.is_a("IfcRelConnectsPathElements"): related_element_connection_type = rel.RelatedConnectionType else: related_element_connection_type = "" @@ -125,6 +131,8 @@ class ConnectionsData: "is_relating": True, "Name": related_element.Name or "Unnamed", "ConnectionType": related_element_connection_type, + "realizing_elements": realizing_elements, + "realizing_elements_connection_type": realizing_elements_connection_type } ) @@ -134,7 +142,13 @@ class ConnectionsData: else: relating_element = rel.RelatingElement - if element.is_a("IfcRelConnectsPathElements"): + realizing_elements = [] + realizing_elements_connection_type = "" + if rel.is_a("IfcRelConnectsWithRealizingElements"): + realizing_elements.extend(rel.RealizingElements) + realizing_elements_connection_type = rel.ConnectionType + + if rel.is_a("IfcRelConnectsPathElements"): relating_element_connection_type = rel.RelatingConnectionType else: relating_element_connection_type = "" @@ -145,6 +159,8 @@ class ConnectionsData: "is_relating": False, "Name": relating_element.Name or "Unnamed", "ConnectionType": relating_element_connection_type, + "realizing_elements": realizing_elements, + "realizing_elements_connection_type": realizing_elements_connection_type } ) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/ui.py b/src/blenderbim/blenderbim/bim/module/geometry/ui.py index ee18b60875..69520f7a51 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/ui.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/ui.py @@ -146,6 +146,20 @@ class BIM_PT_connections(Panel): op = row.operator("bim.remove_connection", icon="X", text="") op.connection = connection["id"] + if connection["realizing_elements"]: + row = self.layout.row(align=True) + connection_type = connection["realizing_elements_connection_type"] + connection_type = f" ({connection_type})" if connection_type else "" + row.label(text=f"Realizing elements{connection_type}:") + + for element in connection["realizing_elements"]: + row = self.layout.row(align=True) + obj = tool.Ifc.get_object(element) + row.label(text=obj.name) + row.operator( + "bim.select_entity", text="", icon="RESTRICT_SELECT_OFF" + ).ifc_id = element.id() + class BIM_PT_mesh(Panel): bl_label = "Representation Utilities" From 262c6d1fcb9e05474d3f3133540b66c22370a331 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 23 Aug 2023 18:39:19 +0500 Subject: [PATCH 42/86] added UI to display connections for realizing elements Example - https://imgur.com/a/1ZZlTwp --- .../blenderbim/bim/module/geometry/data.py | 23 +++++++++++-- .../blenderbim/bim/module/geometry/ui.py | 34 ++++++++++++++++--- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/data.py b/src/blenderbim/blenderbim/bim/module/geometry/data.py index 6a3ece41c9..22575778d3 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/data.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/data.py @@ -97,7 +97,7 @@ class ConnectionsData: @classmethod def load(cls): - cls.data = {"connections": cls.connections()} + cls.data = {"connections": cls.connections(), "is_connection_realization": cls.is_connection_realization()} cls.is_loaded = True @classmethod @@ -132,7 +132,7 @@ class ConnectionsData: "Name": related_element.Name or "Unnamed", "ConnectionType": related_element_connection_type, "realizing_elements": realizing_elements, - "realizing_elements_connection_type": realizing_elements_connection_type + "realizing_elements_connection_type": realizing_elements_connection_type, } ) @@ -160,12 +160,29 @@ class ConnectionsData: "Name": relating_element.Name or "Unnamed", "ConnectionType": relating_element_connection_type, "realizing_elements": realizing_elements, - "realizing_elements_connection_type": realizing_elements_connection_type + "realizing_elements_connection_type": realizing_elements_connection_type, } ) return results + @classmethod + def is_connection_realization(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + connections = element.IsConnectionRealization + if not connections: + return + + results = [] + for rel in connections: + data = { + "realizing_elements_connection_type": rel.ConnectionType, + "connected_from": rel.RelatingElement, + "connected_to": rel.RelatedElement, + } + results.append(data) + return results + class DerivedPlacementsData: data = {} diff --git a/src/blenderbim/blenderbim/bim/module/geometry/ui.py b/src/blenderbim/blenderbim/bim/module/geometry/ui.py index 69520f7a51..92a8167443 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/ui.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/ui.py @@ -134,7 +134,7 @@ class BIM_PT_connections(Panel): layout = self.layout props = context.active_object.BIMObjectProperties - if not ConnectionsData.data["connections"]: + if not ConnectionsData.data["connections"] and not ConnectionsData.data["is_connection_realization"]: layout.label(text="No connections found") for connection in ConnectionsData.data["connections"]: @@ -151,14 +151,38 @@ class BIM_PT_connections(Panel): connection_type = connection["realizing_elements_connection_type"] connection_type = f" ({connection_type})" if connection_type else "" row.label(text=f"Realizing elements{connection_type}:") - + for element in connection["realizing_elements"]: row = self.layout.row(align=True) obj = tool.Ifc.get_object(element) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = element.id() row.label(text=obj.name) - row.operator( - "bim.select_entity", text="", icon="RESTRICT_SELECT_OFF" - ).ifc_id = element.id() + + # display connections where element is connection realization + connections = ConnectionsData.data["is_connection_realization"] + if not connections: + return + + row = self.layout.row(align=True) + row.label(text="Element is connections realization:") + for connection in connections: + # NOTE: not displayed yet + connection_type = connection["realizing_elements_connection_type"] + connection_type = f" ({connection_type})" if connection_type else "" + + row = self.layout.row(align=True) + + connected_from = connection["connected_from"] + obj = tool.Ifc.get_object(connected_from) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = connected_from.id() + row.label(text=obj.name) + + row.label(text="", icon="FORWARD") + + connected_to = connection["connected_to"] + obj = tool.Ifc.get_object(connected_to) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = connected_to.id() + row.label(text=obj.name) class BIM_PT_mesh(Panel): From 3e13eb8942313b75875bc9ffdea90a3ec84a64d7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 23 Aug 2023 18:43:27 +0500 Subject: [PATCH 43/86] bump ifcsverchok release in readme --- src/ifcsverchok/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcsverchok/README.md b/src/ifcsverchok/README.md index 3f3ad6d44d..cdc605f17b 100644 --- a/src/ifcsverchok/README.md +++ b/src/ifcsverchok/README.md @@ -5,7 +5,7 @@ N.B.! IfcSverchok nodes are WIP. You can experience Blender crashes while using ## Packaged installation[](https://blenderbim.org/docs-python/ifcsverchok/installation.html#packaged-installation "Permalink to this headline") -IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. [Download IfcSverchok here](https://blenderbim.org/builds/ifcsverchok-230704.zip). +IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. [Download IfcSverchok here](https://blenderbim.org/builds/ifcsverchok-230823.zip). Like all Blender add-ons, they can be installed using `Edit > Preferences > Addons > Install > Choose Downloaded ZIP > Enable Add-on Checkbox`. You can enable add-ons permanently by using `Save User Settings` from the Addons menu. From 5d8539071503406a5830c59ce1f073c0f9732570 Mon Sep 17 00:00:00 2001 From: Jesusbill Date: Wed, 23 Aug 2023 17:01:25 +0200 Subject: [PATCH 44/86] Fix duplicate id in EP_set_Drawing template --- .../bim/data/pset/EPset_Drawing.ifc | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc b/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc index ce61846389..5a2227b90e 100644 --- a/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc +++ b/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc @@ -5,27 +5,27 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset FILE_SCHEMA(('IFC4')); ENDSEC; DATA; -#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21)); +#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22)); #2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); -#4=IFCSIMPLEPROPERTYTEMPLATE('2T$a4OFsv2LeD5JeBKEV4f',$,'IsNTS','Whether or not the scale is intended to be significant',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); -#5=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); -#6=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); -#7=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); -#8=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); -#9=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#10=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#11=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#12=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#13=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#14=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#15=IFCSIMPLEPROPERTYTEMPLATE('2sHDBuW7P4TROy$hL2w7ct',$,'Patterns','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#16=IFCSIMPLEPROPERTYTEMPLATE('1$xfo9EVb26QLqmPll2_RK',$,'MetricPrecision','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); -#17=IFCSIMPLEPROPERTYTEMPLATE('38uAtrp9nD_901NO42zd$7',$,'ImperialPrecision','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); -#18=IFCSIMPLEPROPERTYTEMPLATE('1MX0uffTL6TOvtvEJpxFmk',$,'DecimalPlaces','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.); -#19=IFCSIMPLEPROPERTYTEMPLATE('0joEq0Rd10cxweEh0NeHT6',$,'JoinCriteria','Comma separated selection keys which determine what cut objects are to be joined.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#20=IFCSIMPLEPROPERTYTEMPLATE('0nYMT3OSj5gArVniCWZRtv',$,'ShadingStyles','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#21=IFCSIMPLEPROPERTYTEMPLATE('3VWG22eZXBdQwdKlzMeVQH',$,'CurrentShadingStyle','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); +#5=IFCSIMPLEPROPERTYTEMPLATE('2T$a4OFsv2LeD5JeBKEV4f',$,'IsNTS','Whether or not the scale is intended to be significant',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#6=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#14=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#15=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#16=IFCSIMPLEPROPERTYTEMPLATE('2sHDBuW7P4TROy$hL2w7ct',$,'Patterns','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#17=IFCSIMPLEPROPERTYTEMPLATE('1$xfo9EVb26QLqmPll2_RK',$,'MetricPrecision','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); +#18=IFCSIMPLEPROPERTYTEMPLATE('38uAtrp9nD_901NO42zd$7',$,'ImperialPrecision','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); +#19=IFCSIMPLEPROPERTYTEMPLATE('1MX0uffTL6TOvtvEJpxFmk',$,'DecimalPlaces','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.); +#20=IFCSIMPLEPROPERTYTEMPLATE('0joEq0Rd10cxweEh0NeHT6',$,'JoinCriteria','Comma separated selection keys which determine what cut objects are to be joined.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#21=IFCSIMPLEPROPERTYTEMPLATE('0nYMT3OSj5gArVniCWZRtv',$,'ShadingStyles','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#22=IFCSIMPLEPROPERTYTEMPLATE('3VWG22eZXBdQwdKlzMeVQH',$,'CurrentShadingStyle','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); ENDSEC; END-ISO-10303-21; From 4e31a87c20166e623c906fdf09bc3ade834d4236 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Wed, 23 Aug 2023 21:15:59 +0100 Subject: [PATCH 45/86] clean up Resource UI using column layout --- .../blenderbim/bim/module/resource/ui.py | 104 +++++++++--------- 1 file changed, 55 insertions(+), 49 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 65015ba254..92374d782a 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -80,7 +80,7 @@ class BIM_PT_resources(Panel): row = self.layout.row(align=True) row.alignment = "RIGHT" - row.prop(self.props, "should_show_resource_tools", icon="RECOVER_LAST") + row.prop(self.props, "should_show_resource_tools", text="Resource Tools",icon="RECOVER_LAST") if self.props.should_show_resource_tools: total_resources = len(self.tprops.resources) if not total_resources or self.props.active_resource_index >= total_resources: @@ -109,43 +109,44 @@ class BIM_PT_resources(Panel): and metric["reference"] == "Usage.ScheduleWork" ): is_work_locked = True - row = self.layout.row() - row.label(text="Resource Work") - schedule_work = "Schedule Work: {}".format(resource.get("ScheduleWork")) - row = self.layout.row() - row.alignment = "LEFT" - row.label(text="Schedule Usage:") - row.prop(self.tprops.resources[self.props.active_resource_index], "schedule_usage", text="") - row2 = self.layout.row() - row2.alignment = "LEFT" - row2.label(text=schedule_work, icon="ARMATURE_DATA") - if not is_usage_locked: - op = row.operator("bim.add_usage_constraint", text="", icon="UNLOCKED") - op.resource = ifc_definition_id - op.attribute = "Usage.ScheduleUsage" - else: - op = row.operator("bim.remove_usage_constraint", text="", icon="LOCKED") - op.resource = ifc_definition_id - op.attribute = "Usage.ScheduleUsage" - if not is_work_locked: - op = row2.operator("bim.add_usage_constraint", text="", icon="UNLOCKED") - op.resource = ifc_definition_id - op.attribute = "Usage.ScheduleWork" - else: - op = row2.operator("bim.remove_usage_constraint", text="", icon="LOCKED") - op.resource = ifc_definition_id - op.attribute = "Usage.ScheduleWork" + grid = self.layout.grid_flow(columns=3, even_columns=False, even_rows=False, align=False) + + col1 = grid.column(align=True) + col2 = grid.column(align=False) + col3 = grid.column(align=True) + col1.ui_units_x = 1 + col2.ui_units_x = 1 + col3.ui_units_x = 2 + + row1_col1 = col1.row() + row1_col1.label(text="Schedule Work") + row1col2 = col2.row() + row1col2.label(text=resource.get("ScheduleWork", "-"), icon="TIME") + + row1col3 = col3.row() + row1col3.operator("bim.calculate_resource_work", text="", icon="TEMP").resource = ifc_definition_id + op = row1col3.operator( + "bim.add_usage_constraint" if not is_work_locked else "bim.remove_usage_constraint", + text="", + icon="LOCKED" if is_work_locked else "UNLOCKED", + ) + op.resource = ifc_definition_id + op.attribute = "Usage.ScheduleWork" + row2_col1 = col1.row() + row2_col1.label(text="Schedule Usage") + row2col2 = col2.row() + row2col2.prop(self.tprops.resources[self.props.active_resource_index], "schedule_usage", text="") + row2col3 = col3.row() + op = row2col3.operator( + "bim.add_usage_constraint" if not is_usage_locked else "bim.remove_usage_constraint", + text="", + icon="LOCKED" if is_usage_locked else "UNLOCKED", + ) + op.resource = ifc_definition_id + op.attribute = "Usage.ScheduleUsage" productivity = resource["Productivity"] parent_productivity = resource["InheritedProductivity"] - - row = self.layout.row() - row.operator( - "bim.calculate_resource_work", text="Calculate Work", icon="TEMP" - ).resource = ifc_definition_id - - row = self.layout.row() - row.label(text="Productivity") row = self.layout.row() if productivity: produtivitiy_rate_message = "Current Productivity Rate: {} {} / {}".format( @@ -162,24 +163,30 @@ class BIM_PT_resources(Panel): parent_productivity["TimeConsumed"], ) row.alignment = "LEFT" - row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA") + row.label(text="Productivity: {}".format(produtivitiy_rate_message), icon="ARMATURE_DATA") else: row = self.layout.row(align=True) row.alignment = "LEFT" produtivitiy_rate_message = "No productivity data found" - row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA") - + row.label(text="Productivity: {}".format(produtivitiy_rate_message), icon="ARMATURE_DATA") productivity_props = context.scene.BIMResourceProductivity - row = self.layout.row(align=True) - row.alignment = "RIGHT" - row.prop(productivity_props, "quantity_produced", text="Quantity Produced") - row.prop(productivity_props, "quantity_produced_name", text="Quantity Name") - row = self.layout.row() - row.alignment = "RIGHT" - self.draw_duration_property(productivity_props.quantity_consumed, row) - row = self.layout.row() - row.alignment = "RIGHT" - row.operator("bim.edit_productivity_data", text="Apply", icon="CHECKMARK") + grid = self.layout.grid_flow(columns=2, even_columns=False, even_rows=False, align=False) + col1 = grid.column(align=False) + col2 = grid.column(align=False) + col1.ui_units_x = 1 + col2.ui_units_x = 3 + row1_col1 = col1.row() + row1_col1.label(text="Quantity") + row1_col2 = col2.row() + row1_col2.prop(productivity_props, "quantity_produced", text="") + row1_col2.prop(productivity_props, "quantity_produced_name", text="") + row2_col1 = col1.row() + row2_col1.label(text="Time") + row2_col2 = col2.row() + self.draw_duration_property(productivity_props.quantity_consumed, row2_col2) + row3_col2 = col2.row() + row3_col2.alignment = "RIGHT" + row3_col2.operator("bim.edit_productivity_data", text="", icon="CHECKMARK") def draw_resource_operators(self): row = self.layout.row(align=True) @@ -311,7 +318,6 @@ class BIM_PT_resources(Panel): def draw_duration_property(self, duration_props, layout): for duration_prop in duration_props: if duration_prop.name == "BaseQuantityConsumed": - layout.label(text=duration_prop.name) layout.prop(duration_prop, "years", text="Y") layout.prop(duration_prop, "months", text="M") layout.prop(duration_prop, "days", text="D") From 52ea4ff44d37d7775004ed07130ff98a9b579393 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Wed, 23 Aug 2023 21:19:16 +0100 Subject: [PATCH 46/86] Calculates the number of resources based on required person-hours. --- .../api/resource/calculate_resource_usage.py | 71 +++++++++++++++++++ .../ifcopenshell/util/resource.py | 6 ++ 2 files changed, 77 insertions(+) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py new file mode 100644 index 0000000000..1137b23cf3 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py @@ -0,0 +1,71 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import math +import ifcopenshell.api +import ifcopenshell.util.date +import ifcopenshell.util.element +import ifcopenshell.util.resource + + +class Usecase: + def __init__(self, file, resource=None): + """Calculates the number of resources required to perform scheduled work on a task. + """ + self.file = file + self.settings = {"resource": resource} + + def execute(self): + metrics = ifcopenshell.util.constraint.has_metric_constraints( + self.settings["resource"], "Usage.ScheduleUsage" + ) + if ( + metrics + and metrics[0].ConstraintGrade == "HARD" + and metrics[0].Benchmark == "EQUALTO" + ): + return + if ( + not self.settings["resource"].Usage + or not self.settings["resource"].Usage.ScheduleWork + ): + return + + task = ifcopenshell.util.resource.get_task_assignments( + self.settings["resource"] + ) + if not task or not task.TaskTime: + return + + if not task.TaskTime.DurationType or task.TaskTime.DurationType == "WORKTIME": + hours_per_day = 8 + else: + hours_per_day = 24 + + task_duration = ifcopenshell.util.date.ifc2datetime( + task.TaskTime.ScheduleDuration + ) + seconds = task_duration.days * hours_per_day * 60 * 60 + seconds += task_duration.seconds + + person_hours = ifcopenshell.util.date.ifc2datetime( + self.settings["resource"].Usage.ScheduleWork + ) + + required_resources = person_hours.total_seconds() / seconds + self.settings["resource"].Usage.ScheduleUsage = float(required_resources) diff --git a/src/ifcopenshell-python/ifcopenshell/util/resource.py b/src/ifcopenshell-python/ifcopenshell/util/resource.py index 477a4e2af6..f4ce388dc5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/resource.py +++ b/src/ifcopenshell-python/ifcopenshell/util/resource.py @@ -84,6 +84,12 @@ def get_parametric_resource_products(resource): products.append(rel2.RelatingProduct) return products +def get_task_assignments(resource): + for rel in resource.HasAssignments or []: + if not rel.is_a("IfcRelAssignsToProcess"): + continue + return rel.RelatingProcess + def get_resource_required_work(resource): productivity = get_productivity(resource) From 5adc29b0b3963bb3831bfe17d34c6870a92d3c2a Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Wed, 23 Aug 2023 21:21:03 +0100 Subject: [PATCH 47/86] Feature to calculate required resources --- .../bim/module/resource/__init__.py | 1 + .../blenderbim/bim/module/resource/data.py | 4 ++-- .../bim/module/resource/operator.py | 20 +++++++++++++++++++ .../blenderbim/bim/module/resource/prop.py | 5 ++--- .../blenderbim/bim/module/resource/ui.py | 2 +- src/blenderbim/blenderbim/core/resource.py | 5 +++++ src/blenderbim/blenderbim/tool/resource.py | 9 +++++++++ src/blenderbim/blenderbim/tool/sequence.py | 6 ++++++ 8 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/resource/__init__.py b/src/blenderbim/blenderbim/bim/module/resource/__init__.py index 8f1a8fd2cd..a1a840b172 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/resource/__init__.py @@ -41,6 +41,7 @@ classes = ( operator.EnableEditingResourceBaseQuantity, operator.EnableEditingResourceCosts, operator.EnableEditingResourceCostValue, + operator.CalculateResourceUsage, operator.EnableEditingResourceCostValueFormula, operator.EnableEditingResourceQuantity, operator.EnableEditingResourceTime, diff --git a/src/blenderbim/blenderbim/bim/module/resource/data.py b/src/blenderbim/blenderbim/bim/module/resource/data.py index 269535125a..18262ba5d1 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/data.py +++ b/src/blenderbim/blenderbim/bim/module/resource/data.py @@ -80,10 +80,10 @@ class ResourceData: results[resource.id()]["ScheduleWork"] = ( ifcopenshell.util.date.readable_ifc_duration(resource.Usage.ScheduleWork) if resource.Usage.ScheduleWork - else "Calculate", + else None ) results[resource.id()]["ScheduleUsage"] = ( - resource.Usage.ScheduleUsage if resource.Usage.ScheduleUsage else "" + resource.Usage.ScheduleUsage if resource.Usage.ScheduleUsage else None ) return results diff --git a/src/blenderbim/blenderbim/bim/module/resource/operator.py b/src/blenderbim/blenderbim/bim/module/resource/operator.py index 40cb07c444..702e172067 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/operator.py +++ b/src/blenderbim/blenderbim/bim/module/resource/operator.py @@ -401,3 +401,23 @@ class GoToResource(bpy.types.Operator): def execute(self, context): core.go_to_resource(tool.Resource, resource=tool.Ifc.get().by_id(self.resource)) return {"FINISHED"} + + +class CalculateResourceUsage(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.calculate_resource_usage" + bl_label = "Calculate Resource Usage" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + + @classmethod + def poll(cls, context): + active_resource = tool.Resource.get_highlighted_resource() + if active_resource: + if active_resource.Usage and active_resource.Usage.ScheduleWork: + task = tool.Resource.get_task_assignments(active_resource) + if task and tool.Sequence.has_duration(task): + return True + return False + + def _execute(self, context): + core.calculate_resource_usage(tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(self.resource)) diff --git a/src/blenderbim/blenderbim/bim/module/resource/prop.py b/src/blenderbim/blenderbim/bim/module/resource/prop.py index 1d3491a3c2..615e1e8b60 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/prop.py +++ b/src/blenderbim/blenderbim/bim/module/resource/prop.py @@ -88,9 +88,7 @@ def updateResourceUsage(self, context): if self.schedule_usage == "": return resource = tool.Ifc.get().by_id(self.ifc_definition_id) - tool.Resource.run_edit_resource_time(resource, attributes={ - "ScheduleUsage": self.schedule_usage - }) + tool.Resource.run_edit_resource_time(resource, attributes={"ScheduleUsage": self.schedule_usage}) tool.Resource.load_resource_properties() tool.Sequence.load_task_properties() blenderbim.bim.module.resource.data.refresh() @@ -98,6 +96,7 @@ def updateResourceUsage(self, context): tool.Sequence.refresh_task_resources() blenderbim.bim.module.pset.data.refresh() + class ISODuration(PropertyGroup): name: StringProperty(name="Name") years: IntProperty(name="Years", default=0) diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 92374d782a..55293cc196 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -77,7 +77,6 @@ class BIM_PT_resources(Panel): self.draw_editable_resource_time_attributes_ui() def draw_productivity_ui(self, context): - row = self.layout.row(align=True) row.alignment = "RIGHT" row.prop(self.props, "should_show_resource_tools", text="Resource Tools",icon="RECOVER_LAST") @@ -137,6 +136,7 @@ class BIM_PT_resources(Panel): row2col2 = col2.row() row2col2.prop(self.tprops.resources[self.props.active_resource_index], "schedule_usage", text="") row2col3 = col3.row() + row2col3.operator("bim.calculate_resource_usage", text="", icon="TEMP").resource = ifc_definition_id op = row2col3.operator( "bim.add_usage_constraint" if not is_usage_locked else "bim.remove_usage_constraint", text="", diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py index 4de2bbeb11..b265d9326b 100644 --- a/src/blenderbim/blenderbim/core/resource.py +++ b/src/blenderbim/blenderbim/core/resource.py @@ -217,3 +217,8 @@ def remove_usage_constraint(ifc, resource_tool, resource, reference_path): def go_to_resource(resource_tool, resource): resource_tool.go_to_resource(resource) + +def calculate_resource_usage(ifc, resource_tool, resource): + ifc.run("resource.calculate_resource_usage", resource=resource) + resource_tool.load_resources() + resource_tool.load_resource_properties() \ No newline at end of file diff --git a/src/blenderbim/blenderbim/tool/resource.py b/src/blenderbim/blenderbim/tool/resource.py index 6ee7f1fa77..2b820dcbc3 100644 --- a/src/blenderbim/blenderbim/tool/resource.py +++ b/src/blenderbim/blenderbim/tool/resource.py @@ -435,3 +435,12 @@ class Resource(blenderbim.core.tool.Resource): resource_props = bpy.context.scene.BIMResourceTreeProperties expanded_resources = [item.ifc_definition_id for item in resource_props.resources] bpy.context.scene.BIMResourceProperties.active_resource_index = expanded_resources.index(resource.id()) + + + @classmethod + def run_calculate_resource_usage(cls, resource): + tool.Ifc.run("resource.calculate_resource_usage", resource=resource) + + @classmethod + def get_task_assignments(cls, resource): + return ifcopenshell.util.resource.get_task_assignments(resource) diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index f996917870..2491b245d0 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -1683,3 +1683,9 @@ class Sequence(blenderbim.core.tool.Sequence): if not task: return cls.load_task_resources(cls.get_task_resources(task)) + + @classmethod + def has_duration(cls, task): + if task.TaskTime and task.TaskTime.ScheduleDuration: + return True + return False From 002985a44672fbbe472d44549ed49f7f9777c347 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Wed, 23 Aug 2023 21:21:19 +0100 Subject: [PATCH 48/86] run black --- .../api/resource/calculate_resource_work.py | 10 ++++++-- .../api/resource/edit_resource_time.py | 24 ++++++++++++------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py index cc6717fd5f..c8638e846c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py @@ -60,8 +60,14 @@ class Usecase: self.settings = {"resource": resource} def execute(self): - metrics= ifcopenshell.util.constraint.has_metric_constraints(self.settings["resource"], "Usage.ScheduleWork") - if metrics and metrics[0].ConstraintGrade == "HARD" and metrics[0].Benchmark == "EQUALTO": + metrics = ifcopenshell.util.constraint.has_metric_constraints( + self.settings["resource"], "Usage.ScheduleWork" + ) + if ( + metrics + and metrics[0].ConstraintGrade == "HARD" + and metrics[0].Benchmark == "EQUALTO" + ): return amount_worked = ifcopenshell.util.resource.get_resource_required_work( self.settings["resource"] diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py index 29317007fb..22d800a5c6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py @@ -19,6 +19,7 @@ import datetime import ifcopenshell + class Usecase: def __init__(self, file, resource_time=None, attributes=None): """Edits the attributes of an IfcResourceTime @@ -76,8 +77,10 @@ class Usecase: del self.settings["attributes"]["ActualFinish"] for name, value in self.settings["attributes"].items(): - metrics = ifcopenshell.util.constraint.has_metric_constraints(self.resource, "Usage." + name) - if metrics and self.is_hard_constraint(metrics[0]): + metrics = ifcopenshell.util.constraint.has_metric_constraints( + self.resource, "Usage." + name + ) + if metrics and self.is_hard_constraint(metrics[0]): continue if value: if "Start" in name or "Finish" in name or name == "StatusTime": @@ -89,12 +92,17 @@ class Usecase: ): value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration") setattr(self.settings["resource_time"], name, value) - if name == "ScheduleUsage" and ifcopenshell.util.constraint.has_metric_constraints(self.resource, "Usage.ScheduleWork"): - for rel in self.resource.HasAssignments or []: - if not rel.is_a("IfcRelAssignsToProcess"): - continue - task = rel.RelatingProcess - ifcopenshell.api.run("sequence.calculate_task_duration", self.file, task=task) + if ( + name == "ScheduleUsage" + and ifcopenshell.util.constraint.has_metric_constraints( + self.resource, "Usage.ScheduleWork" + ) + ): + task = ifcopenshell.util.resource.get_task_assignments(self.resource) + if task: + ifcopenshell.api.run( + "sequence.calculate_task_duration", self.file, task=task + ) def is_hard_constraint(self, metric): return bool(metric.ConstraintGrade == "HARD" and metric.Benchmark == "EQUALTO") From 968cceb725cced1f79b8d0c5f2e3280cbaa1aafc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 24 Aug 2023 11:50:33 +1000 Subject: [PATCH 49/86] Fix #3627. Bug where exclude didn't take into account entities are "OR" not "AND". Whoops. --- src/blenderbim/blenderbim/tool/drawing.py | 8 ++++---- src/ifcopenshell-python/ifcopenshell/util/selector.py | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 9c98e756c0..8b4bc48f6d 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -1504,7 +1504,7 @@ class Drawing(blenderbim.core.tool.Drawing): elements = cls.get_elements_in_camera_view(tool.Ifc.get_object(drawing), bpy.data.objects) include = pset.get("Include", None) if include: - elements = set(ifcopenshell.util.selector.filter_elements(ifc_file, include, elements=elements)) + elements = ifcopenshell.util.selector.filter_elements(ifc_file, include, elements=elements) else: if tool.Ifc.get_schema() == "IFC2X3": base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement")) @@ -1516,7 +1516,7 @@ class Drawing(blenderbim.core.tool.Drawing): exclude = pset.get("Exclude", None) if exclude: - elements -= set(ifcopenshell.util.selector.filter_elements(ifc_file, exclude, elements=elements)) + elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) elements -= set(ifc_file.by_type("IfcOpeningElement")) return elements @@ -1529,10 +1529,10 @@ class Drawing(blenderbim.core.tool.Drawing): tool.Ifc.get_object(drawing), [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSpace")] ) if include: - elements = set(ifcopenshell.util.selector.filter_elements(ifc_file, include, elements=elements.copy())) + elements = ifcopenshell.util.selector.filter_elements(ifc_file, include, elements=elements) exclude = pset.get("Exclude", None) if exclude: - elements -= set(ifcopenshell.util.selector.filter_elements(ifc_file, exclude, elements=elements.copy())) + elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) return elements @classmethod diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 1f62223b7b..f2dda2a9e5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -119,7 +119,9 @@ def get_element_value(element, query): return Selector.get_element_value(element, filter_query["keys"], filter_query["is_regex"]) -def filter_elements(ifc_file, query, elements=None): +def filter_elements(ifc_file, query, elements=None, edit_in_place=False): + if elements and not edit_in_place: + elements = elements.copy() transformer = FacetTransformer(ifc_file, elements) transformer.transform(filter_elements_grammar.parse(query)) return transformer.get_results() From 99574d62556709e02c954a4108669791a9730748 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 24 Aug 2023 13:24:08 +1000 Subject: [PATCH 50/86] Loading IFCs now prioritise 2D bodies over 3D non-bodies. This helps prevent loading and seeing a bunch of Box / Clearance representations. --- src/blenderbim/blenderbim/bim/import_ifc.py | 51 +++++++++++---------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index a6ce741550..1ed67ac988 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -227,16 +227,16 @@ class IfcImporter: self.settings.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance) self.settings.set_angular_tolerance(self.ifc_import_settings.angular_tolerance) self.settings.set(self.settings.STRICT_TOLERANCE, True) - self.settings_curve = ifcopenshell.geom.settings() - self.settings_curve.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance) - self.settings_curve.set_angular_tolerance(self.ifc_import_settings.angular_tolerance) - self.settings_curve.set(self.settings_curve.STRICT_TOLERANCE, True) - self.settings_curve.set(self.settings_curve.INCLUDE_CURVES, True) + self.settings_body_2d = ifcopenshell.geom.settings() + self.settings_body_2d.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance) + self.settings_body_2d.set_angular_tolerance(self.ifc_import_settings.angular_tolerance) + self.settings_body_2d.set(self.settings_body_2d.STRICT_TOLERANCE, True) + self.settings_body_2d.set(self.settings_body_2d.INCLUDE_CURVES, True) self.settings_native = ifcopenshell.geom.settings() self.settings_native.set(self.settings_native.INCLUDE_CURVES, True) - self.settings_2d = ifcopenshell.geom.settings() - self.settings_2d.set(self.settings_2d.INCLUDE_CURVES, True) - self.settings_2d.set(self.settings_2d.STRICT_TOLERANCE, True) + self.settings_plan_2d = ifcopenshell.geom.settings() + self.settings_plan_2d.set(self.settings_plan_2d.INCLUDE_CURVES, True) + self.settings_plan_2d.set(self.settings_plan_2d.STRICT_TOLERANCE, True) self.project = None self.has_existing_project = False self.collections = {} @@ -348,13 +348,14 @@ class IfcImporter: if c.ContextIdentifier in ["Body", "Facetation"] ] # Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly - self.body_contexts.extend( - [ - c.id() - for c in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False) - if c.ContextType == "Model" - ] - ) + if not self.body_contexts: + self.body_contexts.extend( + [ + c.id() + for c in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False) + if c.ContextType == "Model" + ] + ) if self.body_contexts: self.settings.set_context_ids(self.body_contexts) # Annotation ContextType is to accommodate broken Revit files @@ -365,7 +366,7 @@ class IfcImporter: if c.ContextType in ["Plan", "Annotation"] or c.ContextIdentifier == "Annotation" ] if self.plan_contexts: - self.settings_2d.set_context_ids(self.plan_contexts) + self.settings_plan_2d.set_context_ids(self.plan_contexts) def process_element_filter(self): offset = self.ifc_import_settings.element_offset @@ -645,7 +646,7 @@ class IfcImporter: self.ifc_import_settings.logger.error("An invalid grid was found %s", grid) continue if grid.Representation: - shape = ifcopenshell.geom.create_shape(self.settings_2d, grid) + shape = ifcopenshell.geom.create_shape(self.settings_plan_2d, grid) grid_obj = self.create_product(grid, shape) if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import: grid_obj.lock_location = (True, True, True) @@ -664,7 +665,7 @@ class IfcImporter: def create_grid_axes(self, axes, grid_collection, grid_obj): for axis in axes: - shape = ifcopenshell.geom.create_shape(self.settings_2d, axis.AxisCurve) + shape = ifcopenshell.geom.create_shape(self.settings_plan_2d, axis.AxisCurve) mesh = self.create_mesh(axis, shape) obj = bpy.data.objects.new(f"IfcGridAxis/{axis.AxisTag}", mesh) if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import: @@ -698,7 +699,7 @@ class IfcImporter: shape = ifcopenshell.geom.create_shape(self.settings, representation) except: try: - shape = ifcopenshell.geom.create_shape(self.settings_2d, representation) + shape = ifcopenshell.geom.create_shape(self.settings_plan_2d, representation) except: self.ifc_import_settings.logger.error("Failed to generate shape for %s", element) if shape: @@ -764,9 +765,9 @@ class IfcImporter: if self.ifc_import_settings.should_load_geometry: products = self.create_products(elements) elements -= products - products = self.create_products(elements, settings=self.settings_curve) + products = self.create_products(elements, settings=self.settings_body_2d) elements -= products - products = self.create_products(elements, settings=self.settings_2d) + products = self.create_products(elements, settings=self.settings_plan_2d) elements -= products products = self.create_pointclouds(elements) elements -= products @@ -901,10 +902,10 @@ class IfcImporter: self.structural_collection.children.link(self.structural_connection_collection) self.project["blender"].children.link(self.structural_collection) - self.create_products(self.file.by_type("IfcStructuralCurveMember"), settings=self.settings_2d) - self.create_products(self.file.by_type("IfcStructuralCurveConnection"), settings=self.settings_2d) - self.create_products(self.file.by_type("IfcStructuralSurfaceMember"), settings=self.settings_2d) - self.create_products(self.file.by_type("IfcStructuralSurfaceConnection"), settings=self.settings_2d) + self.create_products(self.file.by_type("IfcStructuralCurveMember"), settings=self.settings_plan_2d) + self.create_products(self.file.by_type("IfcStructuralCurveConnection"), settings=self.settings_plan_2d) + self.create_products(self.file.by_type("IfcStructuralSurfaceMember"), settings=self.settings_plan_2d) + self.create_products(self.file.by_type("IfcStructuralSurfaceConnection"), settings=self.settings_plan_2d) self.create_structural_point_connections() def create_structural_point_connections(self): From 13a75e7ad7183d4071c98783fe637ae6df54ad4b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 24 Aug 2023 13:56:43 +1000 Subject: [PATCH 51/86] You can now reorder columns in IfcCSV --- .../blenderbim/bim/module/csv/__init__.py | 13 +++++++------ .../blenderbim/bim/module/csv/operator.py | 18 ++++++++++++++++++ src/blenderbim/blenderbim/bim/module/csv/ui.py | 10 ++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/csv/__init__.py b/src/blenderbim/blenderbim/bim/module/csv/__init__.py index 1f1502b695..b8cc7d0079 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/csv/__init__.py @@ -21,13 +21,14 @@ from . import ui, prop, operator classes = ( operator.AddCsvAttribute, - operator.RemoveCsvAttribute, - operator.RemoveAllCsvAttributes, - operator.ExportIfcCsv, - operator.ImportIfcCsv, - operator.SelectCsvIfcFile, - operator.ImportCsvAttributes, operator.ExportCsvAttributes, + operator.ExportIfcCsv, + operator.ImportCsvAttributes, + operator.ImportIfcCsv, + operator.RemoveAllCsvAttributes, + operator.RemoveCsvAttribute, + operator.ReorderCsvAttribute, + operator.SelectCsvIfcFile, prop.CsvAttribute, prop.CsvProperties, ui.BIM_PT_ifccsv, diff --git a/src/blenderbim/blenderbim/bim/module/csv/operator.py b/src/blenderbim/blenderbim/bim/module/csv/operator.py index ee3e312e29..c1b63e4dd3 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/operator.py +++ b/src/blenderbim/blenderbim/bim/module/csv/operator.py @@ -62,6 +62,24 @@ class RemoveAllCsvAttributes(bpy.types.Operator): return {"FINISHED"} +class ReorderCsvAttribute(bpy.types.Operator): + bl_idname = "bim.reorder_csv_attribute" + bl_label = "Reorder CSV Attribute" + bl_options = {"REGISTER", "UNDO"} + old_index: bpy.props.IntProperty() + new_index: bpy.props.IntProperty() + + def execute(self, context): + old = context.scene.CsvProperties.csv_attributes[self.old_index] + new = context.scene.CsvProperties.csv_attributes[self.new_index] + props = ["name", "header", "sort", "group", "varies_value", "summary"] + for prop in props: + value = getattr(new, prop) + setattr(new, prop, getattr(old, prop)) + setattr(old, prop, value) + return {"FINISHED"} + + class ImportCsvAttributes(bpy.types.Operator): bl_idname = "bim.import_csv_attributes" bl_label = "Load CSV Settings" diff --git a/src/blenderbim/blenderbim/bim/module/csv/ui.py b/src/blenderbim/blenderbim/bim/module/csv/ui.py index d4f4951155..c1e5c93222 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/ui.py +++ b/src/blenderbim/blenderbim/bim/module/csv/ui.py @@ -87,6 +87,7 @@ class BIM_PT_ifccsv(Panel): row.prop(props, "should_show_group", icon="OUTLINER_COLLECTION", text="") row.prop(props, "should_show_summary", icon="SYNTAX_ON", text="") + total = len(props.csv_attributes) for index, attribute in enumerate(props.csv_attributes): row = layout.row(align=True) row.prop(attribute, "name", text="") @@ -99,6 +100,15 @@ class BIM_PT_ifccsv(Panel): row.prop(attribute, "varies_value", text="") if props.should_show_summary: row.prop(attribute, "summary", text="") + if total > 1: + if index != 0: + op = row.operator(f"bim.reorder_csv_attribute", icon="TRIA_UP", text="") + op.old_index = index + op.new_index = index - 1 + if index + 1 != total: + op = row.operator(f"bim.reorder_csv_attribute", icon="TRIA_DOWN", text="") + op.old_index = index + op.new_index = index + 1 row.operator("bim.remove_csv_attribute", icon="X", text="").index = index row = layout.row(align=True) From 0a0f977b5c89d098a753305d64402b749c2a175e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 24 Aug 2023 17:01:25 +1000 Subject: [PATCH 52/86] Fix #3632. Make search and CSV UIs share the same Blender props for DRY. --- src/blenderbim/blenderbim/bim/helper.py | 6 ++++-- src/blenderbim/blenderbim/bim/module/csv/prop.py | 12 ------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index 8b6ffe2a6b..aa2100b0c5 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -277,6 +277,8 @@ def draw_filter(layout, props, data, module): if not data.is_loaded: data.load() + sprops = bpy.context.scene.BIMSearchProperties + if tool.Ifc.get(): row = layout.row(align=True) row.label(text=f"{len(data.data['saved_searches'])} Saved Searches") @@ -293,9 +295,9 @@ def draw_filter(layout, props, data, module): box = layout.box() row = box.row(align=True) - row.prop(props, "facet", text="") + row.prop(sprops, "facet", text="") op = row.operator("bim.add_filter", text="Add Filter", icon="ADD") - op.type = props.facet + op.type = sprops.facet op.index = i op.module = module diff --git a/src/blenderbim/blenderbim/bim/module/csv/prop.py b/src/blenderbim/blenderbim/bim/module/csv/prop.py index ba89b6aed7..31a334e389 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/prop.py +++ b/src/blenderbim/blenderbim/bim/module/csv/prop.py @@ -64,18 +64,6 @@ class CsvProperties(PropertyGroup): csv_ifc_file: StringProperty(default="", name="IFC File") ifc_selector: StringProperty(default="", name="IFC Selector") filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") - facet: EnumProperty( - items=[ - ("entity", "Class", "", "FILE_3D", 0), - ("attribute", "Attribute", "", "COPY_ID", 1), - ("property", "Property", "", "PROPERTIES", 2), - ("material", "Material", "", "MATERIAL", 3), - ("classification", "Classification", "", "OUTLINER", 4), - ("location", "Location", "", "PACKAGE", 5), - ("type", "Type", "", "FILE_VOLUME", 6), - ("instance", "GlobalId", "", "GRIP", 7), - ], - ) csv_attributes: CollectionProperty(name="CSV Attributes", type=CsvAttribute) should_preserve_existing: BoolProperty(default=False, name="Preserve Existing") include_global_id: BoolProperty(default=True, name="Include GlobalId") From 39540e57942832accdc324da2260fc279da71fd1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 24 Aug 2023 14:07:21 +0500 Subject: [PATCH 53/86] A button to jump back from boolean to the original object Example - https://imgur.com/a/A3dVbFM --- src/blenderbim/blenderbim/bim/module/model/opening.py | 9 ++++++++- src/blenderbim/blenderbim/bim/module/void/ui.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index 9c93accf58..714f88f73f 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -506,10 +506,14 @@ class AddBoolean(Operator, tool.Ifc.Operator): bl_idname = "bim.add_boolean" bl_label = "Add Boolean" bl_options = {"REGISTER", "UNDO"} + bl_description = "Applies a boolean to the selected IFC object using the other selected blender object as a void" @classmethod def poll(cls, context): - return len(context.selected_objects) == 2 + if not len(context.selected_objects) == 2: + cls.poll_message_set("Exactly 2 objects need to be selected.") + return False + return True def _execute(self, context): props = context.scene.BIMModelProperties @@ -654,6 +658,7 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): + upstream_obj = None for obj in context.selected_objects: if ( not obj.data @@ -681,6 +686,8 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper): should_sync_changes_first=False, ) bpy.data.objects.remove(obj) + + tool.Blender.set_active_object(upstream_obj) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/void/ui.py b/src/blenderbim/blenderbim/bim/module/void/ui.py index 2347e63c4c..de0b978650 100644 --- a/src/blenderbim/blenderbim/bim/module/void/ui.py +++ b/src/blenderbim/blenderbim/bim/module/void/ui.py @@ -115,8 +115,17 @@ class BIM_PT_booleans(Panel): row.operator("bim.add_boolean", text="Apply Boolean", icon="ADD") show_boolean_button = row.row(align=True) show_boolean_button.operator("bim.show_booleans", text="", icon="HIDE_OFF") - show_boolean_button.enabled = BooleansData.data['total_booleans'] > 0 + show_boolean_button.enabled = BooleansData.data["total_booleans"] > 0 row.operator("bim.hide_booleans", text="", icon="HIDE_ON") + elif context.active_object.data.BIMMeshProperties.ifc_boolean_id: + upsteam_obj = context.active_object.data.BIMMeshProperties.obj + upstream_obj_ifc_id = upsteam_obj.BIMObjectProperties.ifc_definition_id + + row = layout.row(align=True) + row.label(text="Used as a boolean operand with:") + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = upstream_obj_ifc_id + row.label(text=upsteam_obj.name) + row = layout.row() row.operator("bim.remove_booleans", text="Remove Boolean", icon="X") From 5a4bc0909b8a13ee08c21c9e876f3b77ba417abd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 24 Aug 2023 15:20:36 +0500 Subject: [PATCH 54/86] Update currently selected ifc class and type on selecting object Had this idea for awhile now and heard it multiple times from other people. Please give feedback if this feature gets in the way instead of helping. Example - https://imgur.com/a/K4hz2FB --- src/blenderbim/blenderbim/bim/handler.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 2d1cfad4c5..502718bb1d 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -124,6 +124,14 @@ def update_bim_tool_props(): representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if not representation: return + + props = bpy.context.scene.BIMModelProperties + if element.is_a("IfcElementType") or element.is_a("IfcElement"): + element_type = ifcopenshell.util.element.get_type(element) + if element_type: + props.ifc_class = element_type.is_a() + props.relating_type_id = str(element_type.id() +) extrusion = tool.Model.get_extrusion(representation) if not extrusion: return @@ -134,7 +142,6 @@ def update_bim_tool_props(): return x_angle si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - props = bpy.context.scene.BIMModelProperties if not AuthoringData.is_loaded: AuthoringData.load() From 13309cb8c402fdeecce7d1066a132e1708e66d65 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 24 Aug 2023 15:41:50 +0500 Subject: [PATCH 55/86] Fixed #3624 - tools props were updating only for BIM Tool It wasn't taking into account WallTool, BeamTool and all other tools we have. --- src/blenderbim/blenderbim/bim/handler.py | 11 ++++++----- .../blenderbim/bim/module/model/workspace.py | 3 +++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 502718bb1d..3e4636396a 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -27,6 +27,7 @@ from bpy.app.handlers import persistent from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.owner.prop import get_user_person, get_user_organisation from blenderbim.bim.module.model.data import AuthoringData +from blenderbim.bim.module.model.workspace import LIST_OF_TOOLS from mathutils import Vector from math import cos, degrees @@ -116,7 +117,7 @@ def update_bim_tool_props(): return mode = bpy.context.mode current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode) - if not current_tool or current_tool.idname != "bim.bim_tool": + if not current_tool or current_tool.idname not in LIST_OF_TOOLS: return element = tool.Ifc.get_entity(obj) if not element: @@ -124,14 +125,14 @@ def update_bim_tool_props(): representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if not representation: return - + props = bpy.context.scene.BIMModelProperties if element.is_a("IfcElementType") or element.is_a("IfcElement"): element_type = ifcopenshell.util.element.get_type(element) if element_type: - props.ifc_class = element_type.is_a() - props.relating_type_id = str(element_type.id() -) + if current_tool.idname == "bim.bim_tool": + props.ifc_class = element_type.is_a() + props.relating_type_id = str(element_type.id()) extrusion = tool.Model.get_extrusion(representation) if not extrusion: return diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 8d2b66ce4d..61815d6ede 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -761,3 +761,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.edit_openings() else: bpy.ops.bim.show_openings() + + +LIST_OF_TOOLS = [cls.bl_idname for cls in (BimTool.__subclasses__() + [BimTool])] From d4e2d3d2dd9650f62495005b4a2642f37de460fb Mon Sep 17 00:00:00 2001 From: Christoph Mellueh <=> Date: Thu, 24 Aug 2023 12:23:22 +0200 Subject: [PATCH 56/86] rename File and unify with ci-ifctester-pypi --- .github/workflows/ci-bcf-pypi.yml | 57 +++++++++++++++++++++++++++++++ .github/workflows/ci-bcf.yml | 37 -------------------- 2 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/ci-bcf-pypi.yml delete mode 100644 .github/workflows/ci-bcf.yml diff --git a/.github/workflows/ci-bcf-pypi.yml b/.github/workflows/ci-bcf-pypi.yml new file mode 100644 index 0000000000..ae0a3a8900 --- /dev/null +++ b/.github/workflows/ci-bcf-pypi.yml @@ -0,0 +1,57 @@ +name: ci-bcf-pypi + +on: + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # * * * * * + - cron: "0 0 18 * *" + push: + paths: + - '.github/workflows/ci-bcf-pypi.yml' + workflow_dispatch: + +env: + major: 0 + minor: 0 + name: ifcopenshell + +jobs: + activate: + runs-on: ubuntu-latest + if: | + github.repository == 'IfcOpenShell/IfcOpenShell' + steps: + - name: Set env + run: echo ok go + + build: + needs: activate + name: ${{ matrix.config.name }}-${{ matrix.pyver }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v2 # https://github.com/actions/checkout + - uses: actions/setup-python@v2 # https://github.com/actions/setup-python + with: + python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax + architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified + - run: echo ${{ env.DATE }} + - name: Get current date + id: date + run: echo "::set-output name=date::$(date +'%y%m%d')" + - name: Compile + run: | + pip install build + cd src/bcf && + python -m build + - name: Publish a Python distribution to PyPI + uses: ortega2247/pypi-upload-action@master + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + packages_dir: src/bcf/dist diff --git a/.github/workflows/ci-bcf.yml b/.github/workflows/ci-bcf.yml deleted file mode 100644 index 2d82036975..0000000000 --- a/.github/workflows/ci-bcf.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: ci-bcf - -on: - push: - -jobs: - activate: - runs-on: ubuntu-latest - if: | - github.repository == 'IfcOpenShell/IfcOpenShell' && - contains(github.event.head_commit.message, '[bcf release]') - steps: - - run: echo ok go - upload: - needs: activate - name: Upload BCF package to Pypi - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: '3.10' - - name: Build package - run: | - cd src/bcf - pip install build - python -m build - - name: Test - run: | - cd src/bcf - make test - - name: Publish package - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_TOKEN }} - packages_dir: src/bcf/dist From b708100c03e38943b5af0e3c00bb4eb3fcfc1f3e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 24 Aug 2023 22:12:30 +1000 Subject: [PATCH 57/86] See #3581. Fix failing IOS tests. --- .../ifcopenshell/api/document/remove_information.py | 2 -- .../test/api/document/test_remove_information.py | 1 - .../test/api/resource/test_calculate_resource_work.py | 1 + src/ifcopenshell-python/test/util/test_pset.py | 3 ++- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py b/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py index 64fba5975c..7869a1c317 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py @@ -50,9 +50,7 @@ class Usecase: 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) 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 d03a7341ad..ec2adaf9fb 100644 --- a/src/ifcopenshell-python/test/api/document/test_remove_information.py +++ b/src/ifcopenshell-python/test/api/document/test_remove_information.py @@ -54,7 +54,6 @@ class TestRemoveInformation(test.bootstrap.IFC4): project = self.file.createIfcProject() information = ifcopenshell.api.run("document.add_information", self.file, parent=None) information2 = ifcopenshell.api.run("document.add_information", self.file, parent=information) - ifcopenshell.api.run("document.add_reference", self.file, information=information) ifcopenshell.api.run("document.add_reference", self.file, information=information2) ifcopenshell.api.run("document.remove_information", self.file, information=information) assert len(self.file.by_type("IfcDocumentInformation")) == 0 diff --git a/src/ifcopenshell-python/test/api/resource/test_calculate_resource_work.py b/src/ifcopenshell-python/test/api/resource/test_calculate_resource_work.py index 040a9ba53f..83a9d9303d 100644 --- a/src/ifcopenshell-python/test/api/resource/test_calculate_resource_work.py +++ b/src/ifcopenshell-python/test/api/resource/test_calculate_resource_work.py @@ -18,6 +18,7 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.constraint class TestCalculateResourceWork(test.bootstrap.IFC4): diff --git a/src/ifcopenshell-python/test/util/test_pset.py b/src/ifcopenshell-python/test/util/test_pset.py index 9fefd61396..a953193b97 100644 --- a/src/ifcopenshell-python/test/util/test_pset.py +++ b/src/ifcopenshell-python/test/util/test_pset.py @@ -46,5 +46,6 @@ class TestPsetQto: names = self.pset_qto.get_applicable_names("IfcWall") assert "Pset_WallCommon" in names names = self.pset_qto.get_applicable_names("IfcWallType") - assert len(names) == 5 + assert len(names) == 6 assert "Pset_WallCommon" in names + assert "Qto_WallBaseQuantities" in names # Backported fix for IFC4 From 8f6932de64494b90e4bc9e705459bb5d7340dd9d Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Thu, 24 Aug 2023 14:16:59 +0200 Subject: [PATCH 58/86] Prevent user from editing the mesh of a circle based extrusion --- src/blenderbim/blenderbim/bim/module/geometry/operator.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index d40edddf47..c7955a0d08 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -1372,6 +1372,10 @@ class OverrideModeSetEdit(bpy.types.Operator): is_profile = True if usage_type == "PROFILE": operator = lambda: bpy.ops.bim.hotkey(hotkey="A_E") + elif "IfcCircleProfileDef" in tool.Geometry.get_ifc_representation_class(element, representation): + self.report({"INFO"}, "Can't edit Circle Profile Extrusion") + obj.select_set(False) + continue elif ( tool.Geometry.is_profile_based(obj.data) or usage_type == "LAYER3" @@ -1418,8 +1422,7 @@ class OverrideModeSetEdit(bpy.types.Operator): else: obj.select_set(False) continue - - if not context.selected_objects or len(context.selected_objects) != len(selected_objs): + if len(selected_objs) > 1 and (not context.selected_objects or len(context.selected_objects) != len(selected_objs)): # We are trying to edit at least one non-mesh-like object : Display a hint to the user self.report({"INFO"}, "Only mesh-compatible representations may be edited concurrently in edit mode.") From fc7459c07fa4f2b0183af4694732bb11559e7562 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Thu, 24 Aug 2023 13:43:25 +0100 Subject: [PATCH 59/86] Resource Features : - Adding/removing and edit productivity data is now easier (+ cleaner UI) - Pressing calculate schedule work on a parent resource will default to calculating its nested resources schedule work. - Show derived Schedule Work for a parent resource - Improve apperance of Resource Tree Structure --- .../bim/module/resource/__init__.py | 1 + .../blenderbim/bim/module/resource/data.py | 14 +++ .../bim/module/resource/operator.py | 17 ++++ .../blenderbim/bim/module/resource/ui.py | 92 ++++++++++++------- src/blenderbim/blenderbim/core/resource.py | 28 +++--- src/blenderbim/blenderbim/tool/resource.py | 19 ++-- src/blenderbim/blenderbim/tool/sequence.py | 2 +- .../test/bim/feature/resource.feature | 2 + .../api/resource/calculate_resource_work.py | 21 +++-- .../ifcopenshell/util/date.py | 2 +- .../ifcopenshell/util/resource.py | 3 + .../ifcopenshell/util/sequence.py | 2 +- 12 files changed, 137 insertions(+), 66 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/resource/__init__.py b/src/blenderbim/blenderbim/bim/module/resource/__init__.py index a1a840b172..8512a370dd 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/resource/__init__.py @@ -22,6 +22,7 @@ from . import ui, prop, operator classes = ( operator.AddResource, operator.AddResourceQuantity, + operator.AddProductivityData, operator.AssignResource, operator.CalculateResourceWork, operator.ConstrainResourceWork, diff --git a/src/blenderbim/blenderbim/bim/module/resource/data.py b/src/blenderbim/blenderbim/bim/module/resource/data.py index 18262ba5d1..146bb7ce00 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/data.py +++ b/src/blenderbim/blenderbim/bim/module/resource/data.py @@ -63,6 +63,7 @@ class ResourceData: productivity = cls.get_productivity(resource) if productivity: results[resource.id()]["Productivity"] = { + "id": productivity.get("id"), "QuantityProduced": ifcopenshell.util.resource.get_quantity_produced(productivity), "TimeConsumed": ifcopenshell.util.resource.get_unit_consumed(productivity), "QuantityProducedName": ifcopenshell.util.resource.get_quantity_produced_name(productivity), @@ -85,8 +86,21 @@ class ResourceData: results[resource.id()]["ScheduleUsage"] = ( resource.Usage.ScheduleUsage if resource.Usage.ScheduleUsage else None ) + if resource.IsNestedBy: + results[resource.id()]["DerivedScheduleWork"] = cls.sum_person_hours(resource) return results + @classmethod + def sum_person_hours(cls, resource): + sum = 0 + nested_resources = ifcopenshell.util.resource.get_nested_resources(resource) + for nested_resource in nested_resources or []: + if not nested_resource.Usage: + continue + duration = ifcopenshell.util.date.ifc2datetime(nested_resource.Usage.ScheduleWork) + sum += duration.total_seconds() / 3600 + return round(float(sum), 2) if sum else 0 + @classmethod def get_resource_benchmarks(cls, resource): constraints = [] diff --git a/src/blenderbim/blenderbim/bim/module/resource/operator.py b/src/blenderbim/blenderbim/bim/module/resource/operator.py index 702e172067..b3eaea14b2 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/operator.py +++ b/src/blenderbim/blenderbim/bim/module/resource/operator.py @@ -18,6 +18,7 @@ import bpy from bpy_extras.io_utils import ImportHelper +from blenderbim.bim.module.resource.ui import draw_productivity_ui import blenderbim.core.resource as core import blenderbim.tool as tool @@ -355,6 +356,16 @@ class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): core.import_resources(tool.Resource, file_path=self.filepath) +class AddProductivityData(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_productivity_data" + bl_description = "Apply" + bl_label = "Add Productivity" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + tool.Ifc.run("pset.add_pset", product=tool.Resource.get_highlighted_resource(), name="EPset_Productivity") + + class EditProductivityData(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.edit_productivity_data" bl_description = "Apply" @@ -364,6 +375,12 @@ class EditProductivityData(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.edit_productivity_pset(tool.Ifc, tool.Resource) + def draw(self, context): + draw_productivity_ui(self, context) + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width=600) + class ConstrainResourceWork(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_usage_constraint" diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 55293cc196..9e7495db0f 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -65,21 +65,22 @@ class BIM_PT_resources(Panel): self.props, "active_resource_index", ) - self.draw_productivity_ui(context) - if self.props.active_resource_id and self.props.editing_resource_type == "ATTRIBUTES": - self.draw_editable_resource_attributes_ui() - elif self.props.active_resource_id and self.props.editing_resource_type == "QUANTITY": - self.draw_editable_resource_quantity_ui() - elif self.props.active_resource_id and self.props.editing_resource_type == "COSTS": - self.draw_editable_resource_costs_ui() - elif self.props.active_resource_id and self.props.editing_resource_type == "USAGE": - self.draw_editable_resource_time_attributes_ui() + if self.props.active_resource_id: + if self.props.editing_resource_type == "ATTRIBUTES": + self.draw_editable_resource_attributes_ui() + elif self.props.active_resource_id and self.props.editing_resource_type == "QUANTITY": + self.draw_editable_resource_quantity_ui() + elif self.props.active_resource_id and self.props.editing_resource_type == "COSTS": + self.draw_editable_resource_costs_ui() + elif self.props.active_resource_id and self.props.editing_resource_type == "USAGE": + self.draw_editable_resource_time_attributes_ui() + self.draw_productivity_ui(context) def draw_productivity_ui(self, context): row = self.layout.row(align=True) row.alignment = "RIGHT" - row.prop(self.props, "should_show_resource_tools", text="Resource Tools",icon="RECOVER_LAST") + row.prop(self.props, "should_show_resource_tools", text="Resource Tools", icon="RECOVER_LAST") if self.props.should_show_resource_tools: total_resources = len(self.tprops.resources) if not total_resources or self.props.active_resource_index >= total_resources: @@ -120,7 +121,12 @@ class BIM_PT_resources(Panel): row1_col1 = col1.row() row1_col1.label(text="Schedule Work") row1col2 = col2.row() - row1col2.label(text=resource.get("ScheduleWork", "-"), icon="TIME") + schedule_work = resource.get("ScheduleWork", None) + derived_schedule_work = resource.get("DerivedScheduleWork", None) + row1col2.label( + text="{}".format(schedule_work) if schedule_work else "*{}".format(derived_schedule_work), + icon="TIME", + ) row1col3 = col3.row() row1col3.operator("bim.calculate_resource_work", text="", icon="TEMP").resource = ifc_definition_id @@ -156,6 +162,12 @@ class BIM_PT_resources(Panel): ) row.alignment = "LEFT" row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA") + row.operator("bim.edit_productivity_data", text="", icon="GREASEPENCIL") + op = row.operator("bim.remove_pset", text="", icon="X") + op.pset_id = productivity["id"] + op.obj_type = "Resource" + op.obj = "" + elif parent_productivity: produtivitiy_rate_message = "Inherited Productivity Rate: {} {} / {}*".format( parent_productivity["QuantityProduced"], @@ -163,30 +175,14 @@ class BIM_PT_resources(Panel): parent_productivity["TimeConsumed"], ) row.alignment = "LEFT" - row.label(text="Productivity: {}".format(produtivitiy_rate_message), icon="ARMATURE_DATA") + row.label(text="{}".format(produtivitiy_rate_message), icon="ARMATURE_DATA") + row.operator("bim.add_productivity_data", text="", icon="ADD") else: row = self.layout.row(align=True) row.alignment = "LEFT" produtivitiy_rate_message = "No productivity data found" - row.label(text="Productivity: {}".format(produtivitiy_rate_message), icon="ARMATURE_DATA") - productivity_props = context.scene.BIMResourceProductivity - grid = self.layout.grid_flow(columns=2, even_columns=False, even_rows=False, align=False) - col1 = grid.column(align=False) - col2 = grid.column(align=False) - col1.ui_units_x = 1 - col2.ui_units_x = 3 - row1_col1 = col1.row() - row1_col1.label(text="Quantity") - row1_col2 = col2.row() - row1_col2.prop(productivity_props, "quantity_produced", text="") - row1_col2.prop(productivity_props, "quantity_produced_name", text="") - row2_col1 = col1.row() - row2_col1.label(text="Time") - row2_col2 = col2.row() - self.draw_duration_property(productivity_props.quantity_consumed, row2_col2) - row3_col2 = col2.row() - row3_col2.alignment = "RIGHT" - row3_col2.operator("bim.edit_productivity_data", text="", icon="CHECKMARK") + row.label(text="{}".format(produtivitiy_rate_message), icon="ARMATURE_DATA") + row.operator("bim.add_productivity_data", text="", icon="ADD") def draw_resource_operators(self): row = self.layout.row(align=True) @@ -231,6 +227,12 @@ class BIM_PT_resources(Panel): op.resource = ifc_definition_id row.operator("bim.enable_editing_resource", text="", icon="GREASEPENCIL").resource = ifc_definition_id row.operator("bim.remove_resource", text="", icon="X").resource = ifc_definition_id + else: + if self.props.editing_resource_type == "ATTRIBUTES": + row.operator("bim.edit_resource", text="", icon="CHECKMARK") + elif self.props.editing_resource_type == "USAGE": + row.operator("bim.edit_resource_time", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_resource", text="", icon="CANCEL") def draw_editable_resource_attributes_ui(self): blenderbim.bim.helper.draw_attributes(self.props.resource_attributes, self.layout) @@ -354,7 +356,7 @@ class BIM_UL_resources(UIList): else: row.label(text="", icon="DOT") row.prop(item, "name", emboss=False, text="", icon=icon_map[resource["type"]]) - row.prop(item, "schedule_usage", text="", emboss=False) + row.prop(item, "schedule_usage", text="", emboss=False) if item.schedule_usage else None if context.active_object and not props.active_resource_id: row = layout.row(align=True) if item.ifc_definition_id in ResourceData.data["active_resource_ids"]: @@ -370,3 +372,29 @@ class BIM_UL_resources(UIList): elif props.editing_resource_type == "USAGE": row.operator("bim.edit_resource_time", text="", icon="CHECKMARK") row.operator("bim.disable_editing_resource", text="", icon="CANCEL") + + +def draw_productivity_ui(self, context): + def draw_duration_property(duration_props, layout): + for duration_prop in duration_props: + if duration_prop.name == "BaseQuantityConsumed": + layout.prop(duration_prop, "years", text="Y") + layout.prop(duration_prop, "months", text="M") + layout.prop(duration_prop, "days", text="D") + layout.prop(duration_prop, "hours", text="H") + layout.prop(duration_prop, "minutes", text="Min") + layout.prop(duration_prop, "seconds", text="S") + + productivity_props = context.scene.BIMResourceProductivity + grid = self.layout.grid_flow(columns=2, even_columns=False, even_rows=False, align=False) + col1 = grid.column(align=False) + col2 = grid.column(align=False) + row1_col1 = col1.row() + row1_col1.label(text="Quantity") + row1_col2 = col2.row() + row1_col2.prop(productivity_props, "quantity_produced", text="") + row1_col2.prop(productivity_props, "quantity_produced_name", text="") + row2_col1 = col1.row() + row2_col1.label(text="Time") + row2_col2 = col2.row() + draw_duration_property(productivity_props.quantity_consumed, row2_col2) diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py index b265d9326b..6b1ee21719 100644 --- a/src/blenderbim/blenderbim/core/resource.py +++ b/src/blenderbim/blenderbim/core/resource.py @@ -26,8 +26,7 @@ def load_resources(resource): def add_resource(tool_ifc, resource_tool, ifc_class, parent_resource=None): tool_ifc.run("resource.add_resource", ifc_class=ifc_class, parent_resource=parent_resource) - resource_tool.load_resources() - resource_tool.load_resource_properties() + load_resources(resource_tool) def load_resource_properties(resource_tool, resource=None): @@ -56,8 +55,7 @@ def edit_resource(ifc, resource_tool, resource): def remove_resource(ifc, resource_tool, resource=None): ifc.run("resource.remove_resource", resource=resource) - resource_tool.load_resources() - resource_tool.load_resource_properties() + load_resources(resource_tool) def enable_editing_resource_time(ifc_tool, resource_tool, resource): @@ -79,9 +77,13 @@ def disable_editing_resource_time(resource_tool): def calculate_resource_work(ifc, resource_tool, resource): - ifc.run("resource.calculate_resource_work", resource=resource) - resource_tool.load_resources() - resource_tool.load_resource_properties() + if resource_tool.get_task_assignments(resource): + ifc.run("resource.calculate_resource_work", resource=resource) + else: + nested_resources = resource_tool.get_nested_resources(resource) + for nested_resource in nested_resources or []: + ifc.run("resource.calculate_resource_work", resource=nested_resource) + load_resources(resource_tool) def enable_editing_resource_costs(resource_tool, resource): @@ -142,20 +144,17 @@ def edit_resource_quantity(resource_tool, ifc, physical_quantity=None): def import_resources(resource_tool, file_path): resource_tool.import_resources(file_path) - resource_tool.load_resources() - resource_tool.load_resource_properties() + load_resources(resource_tool) def expand_resource(resource_tool, resource): resource_tool.expand_resource(resource) - resource_tool.load_resources() - resource_tool.load_resource_properties() + load_resources(resource_tool) def contract_resource(resource_tool, resource): resource_tool.contract_resource(resource) - resource_tool.load_resources() - resource_tool.load_resource_properties() + load_resources(resource_tool) def assign_resource(ifc, spatial, resource=None, products=None): @@ -220,5 +219,4 @@ def go_to_resource(resource_tool, resource): def calculate_resource_usage(ifc, resource_tool, resource): ifc.run("resource.calculate_resource_usage", resource=resource) - resource_tool.load_resources() - resource_tool.load_resource_properties() \ No newline at end of file + load_resources(resource_tool) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/tool/resource.py b/src/blenderbim/blenderbim/tool/resource.py index 2b820dcbc3..13d51f8d05 100644 --- a/src/blenderbim/blenderbim/tool/resource.py +++ b/src/blenderbim/blenderbim/tool/resource.py @@ -55,12 +55,13 @@ class Resource(blenderbim.core.tool.Resource): tprops = bpy.context.scene.BIMResourceTreeProperties tprops.resources.clear() contracted_resources = json.loads(props.contracted_resources) - + props.is_resource_update_enabled = False for resource in tool.Ifc.get().by_type("IfcResource"): if not resource.HasContext: continue create_new_resource_li(resource, 0) cls.load_productivity_data() + props.is_resource_update_enabled = True props.is_editing = True @classmethod @@ -71,7 +72,7 @@ class Resource(blenderbim.core.tool.Resource): for item in tprops.resources: resource = tool.Ifc.get().by_id(item.ifc_definition_id) item.name = resource.Name if resource.Name else "Unnamed" - item.schedule_usage = resource.Usage.ScheduleUsage or 1 if resource.Usage else 0 + item.schedule_usage = resource.Usage.ScheduleUsage if (resource.Usage and resource.Usage.ScheduleUsage) else 0 props.is_resource_update_enabled = True @classmethod @@ -374,13 +375,11 @@ class Resource(blenderbim.core.tool.Resource): @classmethod def edit_productivity_pset(cls, resource, attributes): productivity = cls.get_productivity(resource) - if productivity: - pset = tool.Ifc.get().by_id(productivity["id"]) - else: - pset = tool.Ifc.run("pset.add_pset", product=resource, name="EPset_Productivity") - tool.Ifc.run( + if not productivity: + return + return tool.Ifc.run( "pset.edit_pset", - pset=pset, + pset= tool.Ifc.get().by_id(productivity["id"]), properties=attributes, ) @@ -444,3 +443,7 @@ class Resource(blenderbim.core.tool.Resource): @classmethod def get_task_assignments(cls, resource): return ifcopenshell.util.resource.get_task_assignments(resource) + + @classmethod + def get_nested_resources(cls, resource): + return ifcopenshell.util.resource.get_nested_resources(resource) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index 2491b245d0..e7f924b72f 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -416,7 +416,7 @@ class Sequence(blenderbim.core.tool.Sequence): new = props.task_resources.add() new.ifc_definition_id = resource.id() new.name = resource.Name or "Unnamed" - new.schedule_usage = resource.Usage.ScheduleUsage or 1 if resource.Usage else 0 + new.schedule_usage = resource.Usage.ScheduleUsage or 0 if resource.Usage else 0 @classmethod def load_resources(cls): diff --git a/src/blenderbim/test/bim/feature/resource.feature b/src/blenderbim/test/bim/feature/resource.feature index 71af9ae05a..3e01cfa926 100644 --- a/src/blenderbim/test/bim/feature/resource.feature +++ b/src/blenderbim/test/bim/feature/resource.feature @@ -249,6 +249,7 @@ Scenario: Add Productivity data When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I set "scene.BIMResourceProperties.active_resource_index" to "1" + And I press "bim.add_productivity_data" And I set "scene.BIMResourceProperties.should_show_resource_tools" to "True" And I set "scene.BIMResourceProductivity.quantity_produced" to "5.00" And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea" @@ -286,6 +287,7 @@ Scenario: Calculate Resource Work When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I set "scene.BIMResourceProperties.active_resource_index" to "1" + And I press "bim.add_productivity_data" And I set "scene.BIMResourceProperties.should_show_resource_tools" to "True" And I set "scene.BIMResourceProductivity.quantity_produced" to "5.00" And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea" diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py index c8638e846c..ac978573e0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py @@ -60,14 +60,7 @@ class Usecase: self.settings = {"resource": resource} def execute(self): - metrics = ifcopenshell.util.constraint.has_metric_constraints( - self.settings["resource"], "Usage.ScheduleWork" - ) - if ( - metrics - and metrics[0].ConstraintGrade == "HARD" - and metrics[0].Benchmark == "EQUALTO" - ): + if self.has_hard_constraint(): return amount_worked = ifcopenshell.util.resource.get_resource_required_work( self.settings["resource"] @@ -81,3 +74,15 @@ class Usecase: resource=self.settings["resource"], ) self.settings["resource"].Usage.ScheduleWork = amount_worked + + def has_hard_constraint(self): + metrics = ifcopenshell.util.constraint.has_metric_constraints( + self.settings["resource"], "Usage.ScheduleWork" + ) + if ( + metrics + and metrics[0].ConstraintGrade == "HARD" + and metrics[0].Benchmark == "EQUALTO" + ): + return True + return False diff --git a/src/ifcopenshell-python/ifcopenshell/util/date.py b/src/ifcopenshell-python/ifcopenshell/util/date.py index 8f7c45c054..27e6df4af0 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/date.py +++ b/src/ifcopenshell-python/ifcopenshell/util/date.py @@ -98,7 +98,7 @@ def readable_ifc_duration(string): final_string += f"{months} m " if months else "" final_string += f"{weeks} w " if weeks else "" final_string += f"{days} d " if days else "" - final_string += f"{hours} h " if hours else "" + final_string += f"{round(float(hours),2)} h " if hours else "" final_string += f"{minutes} m " if minutes else "" final_string += f"{seconds} s " if seconds else "" diff --git a/src/ifcopenshell-python/ifcopenshell/util/resource.py b/src/ifcopenshell-python/ifcopenshell/util/resource.py index f4ce388dc5..ad75887361 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/resource.py +++ b/src/ifcopenshell-python/ifcopenshell/util/resource.py @@ -112,3 +112,6 @@ def get_resource_required_work(resource): required_work = total_quantity_to_produce * productivity_ratio iso_string = f"P{required_work}D" return iso_string + +def get_nested_resources(resource): + return [object for rel in resource.IsNestedBy or [] for object in rel.RelatedObjects] \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py index 27db7330d5..02ec945a63 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py @@ -248,7 +248,7 @@ def get_task_work_schedule(task): def get_nested_tasks(task): - return [object for rel in task.IsNestedBy for object in rel.RelatedObjects] + return [object for rel in task.IsNestedBy or [] for object in rel.RelatedObjects] def get_parent_task(task): From 1c7e6cf1b97b772d12ee8650cbb7a90a6a981738 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Thu, 24 Aug 2023 13:58:18 +0100 Subject: [PATCH 60/86] fix bug where nested resources in task ICOM panel didn't show --- src/blenderbim/blenderbim/bim/module/resource/data.py | 2 +- src/blenderbim/blenderbim/bim/module/resource/ui.py | 2 +- src/blenderbim/blenderbim/tool/sequence.py | 9 --------- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/resource/data.py b/src/blenderbim/blenderbim/bim/module/resource/data.py index 146bb7ce00..e70b29fb7a 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/data.py +++ b/src/blenderbim/blenderbim/bim/module/resource/data.py @@ -95,7 +95,7 @@ class ResourceData: sum = 0 nested_resources = ifcopenshell.util.resource.get_nested_resources(resource) for nested_resource in nested_resources or []: - if not nested_resource.Usage: + if not nested_resource.Usage or not nested_resource.Usage.ScheduleWork: continue duration = ifcopenshell.util.date.ifc2datetime(nested_resource.Usage.ScheduleWork) sum += duration.total_seconds() / 3600 diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 9e7495db0f..7304bd25e6 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -124,7 +124,7 @@ class BIM_PT_resources(Panel): schedule_work = resource.get("ScheduleWork", None) derived_schedule_work = resource.get("DerivedScheduleWork", None) row1col2.label( - text="{}".format(schedule_work) if schedule_work else "*{}".format(derived_schedule_work), + text="{}".format(schedule_work) if schedule_work else "{} h*".format(derived_schedule_work), icon="TIME", ) diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index e7f924b72f..97340431a0 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -489,15 +489,6 @@ class Sequence(blenderbim.core.tool.Sequence): is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_outputs return ifcopenshell.util.sequence.get_task_outputs(task, is_deep) - @classmethod - def get_task_resources(cls, task): - resources = [] - for rel in task.OperatesOn: - for object in rel.RelatedObjects: - if object.is_a("IfcResource"): - resources.append(object) - return resources - @classmethod def enable_editing_work_calendar_times(cls, work_calendar): props = bpy.context.scene.BIMWorkCalendarProperties From 8f25a31f19a829c1c7ef0a1c1a16a54c90e2900c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 24 Aug 2023 23:03:36 +1000 Subject: [PATCH 61/86] Fix more failing tests. --- src/blenderbim/blenderbim/core/tool.py | 1 + src/blenderbim/blenderbim/tool/drawing.py | 2 +- src/blenderbim/scripts/setup_pytest.py | 16 ++++++++-------- src/blenderbim/test/core/test_drawing.py | 23 ++++++++++++++++++++--- src/blenderbim/test/tool/test_drawing.py | 19 ++++++++++++++----- src/blenderbim/test/tool/test_root.py | 7 ++++--- 6 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 64d78f87bd..731543b61f 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -241,6 +241,7 @@ class Drawing: def activate_drawing(cls, camera): pass def add_literal_to_annotation(cls, obj, Literal='Literal', Path='RIGHT', BoxAlignment='bottom-left'): pass def copy_representation(cls, source, dest): pass + def create_annotation_context(cls, target_view, object_type=None): pass def create_annotation_object(cls, drawing, object_type): pass def create_camera(cls, name, matrix): pass def create_svg_schedule(cls, schedule): pass diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 8b4bc48f6d..aa3af91100 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -475,7 +475,7 @@ class Drawing(blenderbim.core.tool.Drawing): 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))) + 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": return mathutils.Matrix(((-1, 0, 0, x), (0, 0, 1, y), (0, 1, 0, z), (0, 0, 0, 1))) diff --git a/src/blenderbim/scripts/setup_pytest.py b/src/blenderbim/scripts/setup_pytest.py index 2e2f6eea51..8bfad2dfb0 100644 --- a/src/blenderbim/scripts/setup_pytest.py +++ b/src/blenderbim/scripts/setup_pytest.py @@ -32,16 +32,16 @@ subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pip"]) sys_paths = [p for p in sys.path if "site-packages" in p] if sys_paths: print("Detected installation directory:", sys_paths[-1]) - subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pytest"]) - subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pytest-bdd"]) - subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pytest-blender"]) - subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pygments"]) + subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "--upgrade", "pytest"]) + subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "--upgrade", "pytest-bdd"]) + subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "--upgrade", "pytest-blender"]) + subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "--upgrade", "pygments"]) else: print("Could not detect installation directory. Good luck.") - subprocess.call([py_exec, "-m", "pip", "install", "pytest"]) - subprocess.call([py_exec, "-m", "pip", "install", "pytest-bdd"]) - subprocess.call([py_exec, "-m", "pip", "install", "pytest-blender"]) - subprocess.call([py_exec, "-m", "pip", "install", "pygments"]) + subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pytest"]) + subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pytest-bdd"]) + subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pytest-blender"]) + subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pygments"]) try: import pytest diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py index ba839a1869..cb5e76f248 100644 --- a/src/blenderbim/test/core/test_drawing.py +++ b/src/blenderbim/test/core/test_drawing.py @@ -490,7 +490,7 @@ class TestUpdateDrawingName: ) ifc.resolve_uri("relative_layout_uri").should_be_called().will_return("absolute_layout_uri") drawing.does_file_exist("absolute_layout_uri").should_be_called().will_return(True) - drawing.update_embedded_svg_location("absolute_layout_uri", "old_location", "new_location").should_be_called() + drawing.update_embedded_svg_location("absolute_layout_uri", "reference_with_old_location", "new_uri").should_be_called() drawing.is_editing_sheets().should_be_called().will_return(True) drawing.import_sheets().should_be_called() @@ -500,9 +500,9 @@ class TestUpdateDrawingName: class TestAddAnnotation: def test_run(self, ifc, collector, drawing): - drawing.show_decorations().should_be_called() drawing.get_drawing_target_view("drawing").should_be_called().will_return("target_view") drawing.get_annotation_context("target_view", "object_type").should_be_called().will_return("context") + drawing.show_decorations().should_be_called() drawing.create_annotation_object("drawing", "object_type").should_be_called().will_return("obj") ifc.get_entity("obj").should_be_called().will_return(None) drawing.get_ifc_representation_class("object_type").should_be_called().will_return("ifc_representation_class") @@ -520,7 +520,24 @@ class TestAddAnnotation: drawing.enable_editing("obj").should_be_called() subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type") - def test_do_not_add_without_an_annotation_context(self, ifc, collector, drawing): + def test_create_a_missing_annotation_context_on_the_fly(self, ifc, collector, drawing): drawing.get_drawing_target_view("drawing").should_be_called().will_return("target_view") drawing.get_annotation_context("target_view", "object_type").should_be_called().will_return(None) + drawing.create_annotation_context("target_view", "object_type").should_be_called().will_return("context") + drawing.show_decorations().should_be_called() + drawing.create_annotation_object("drawing", "object_type").should_be_called().will_return("obj") + ifc.get_entity("obj").should_be_called().will_return(None) + drawing.get_ifc_representation_class("object_type").should_be_called().will_return("ifc_representation_class") + drawing.run_root_assign_class( + obj="obj", + ifc_class="IfcAnnotation", + predefined_type="object_type", + should_add_representation=True, + context="context", + ifc_representation_class="ifc_representation_class", + ).should_be_called().will_return("element") + drawing.get_drawing_group("drawing").should_be_called().will_return("group") + ifc.run("group.assign_group", group="group", products=["element"]).should_be_called() + collector.assign("obj").should_be_called() + drawing.enable_editing("obj").should_be_called() subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type") diff --git a/src/blenderbim/test/tool/test_drawing.py b/src/blenderbim/test/tool/test_drawing.py index 86eecc143e..f7fe0c4743 100644 --- a/src/blenderbim/test/tool/test_drawing.py +++ b/src/blenderbim/test/tool/test_drawing.py @@ -376,7 +376,7 @@ class TestGenerateDrawingMatrix(NewFile): def test_creating_an_rcp_at_the_origin(self): assert subject.generate_drawing_matrix("REFLECTED_PLAN_VIEW", None) == mathutils.Matrix( - ((-1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0), (0, 0, 0, 1)) + ((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0), (0, 0, 0, 1)) ) def test_creating_an_rcp_at_the_cursor_at_a_storey(self): @@ -389,7 +389,7 @@ class TestGenerateDrawingMatrix(NewFile): obj.matrix_world[2][3] = 3 bpy.context.scene.cursor.location = (1.0, 2.0, 0.0) assert subject.generate_drawing_matrix("REFLECTED_PLAN_VIEW", element.id()) == mathutils.Matrix( - ((-1, 0, 0, 1), (0, 1, 0, 2), (0, 0, -1, 3 + 1.6), (0, 0, 0, 1)) + ((1, 0, 0, 1), (0, 1, 0, 2), (0, 0, -1, 3 + 1.6), (0, 0, 0, 1)) ) def test_creating_a_north_elevation_at_the_cursor(self): @@ -485,9 +485,12 @@ class TestImportDrawings(NewFile): ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"TargetView": "PLAN_VIEW"}) subject.import_drawings() props = bpy.context.scene.DocProperties - assert props.drawings[0].ifc_definition_id == drawing.id() - assert props.drawings[0].name == "FOOBAR" - assert props.drawings[0].target_view == "PLAN_VIEW" + for d in props.drawings: + d.is_expanded = True + subject.import_drawings() + assert props.drawings[1].target_view == "PLAN_VIEW" + assert props.drawings[2].ifc_definition_id == drawing.id() + assert props.drawings[2].name == "FOOBAR" class TestImportSchedules(NewFile): @@ -668,6 +671,7 @@ class TestDrawingMaintainingSheetPosition(NewFile): return drawing_data def test_run(self): + props = bpy.context.scene.DocProperties bpy.ops.bim.create_project() ifc = tool.Ifc.get() sheet_path = Path.cwd() / "layouts" / "A00 - UNTITLED.svg" @@ -676,9 +680,14 @@ class TestDrawingMaintainingSheetPosition(NewFile): bpy.ops.bim.add_sheet() bpy.ops.bim.load_drawings() + for d in props.drawings: + d.is_expanded = True bpy.ops.bim.add_drawing() drawing = ifc.by_type("IfcAnnotation")[0] + for i, d in enumerate(props.drawings): + if d.ifc_definition_id == drawing.id(): + props.active_drawing_index = i bpy.ops.bim.activate_drawing(drawing=drawing.id()) bpy.ops.bim.create_drawing() bpy.ops.bim.add_drawing_to_sheet() diff --git a/src/blenderbim/test/tool/test_root.py b/src/blenderbim/test/tool/test_root.py index 5f9ac6c063..29ad103472 100644 --- a/src/blenderbim/test/tool/test_root.py +++ b/src/blenderbim/test/tool/test_root.py @@ -160,11 +160,12 @@ class TestSetObjectName(NewFile): obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) element = ifc.createIfcWall() subject.set_object_name(obj, element) - assert obj.name == "IfcWall/Object" + assert obj.name == "IfcWall/Unnamed" - def test_existing_ifc_prefixes_are_not_repeated(self): + def test_existing_blender_names_are_ignored(self): ifc = ifcopenshell.file() obj = bpy.data.objects.new("IfcSlab/Object", bpy.data.meshes.new("Mesh")) element = ifc.createIfcWall() + element.Name = "Foobar" subject.set_object_name(obj, element) - assert obj.name == "IfcWall/Object" + assert obj.name == "IfcWall/Foobar" From 2b1e17681faa12faec4d366acdd8d466d3872a9e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 24 Aug 2023 18:14:29 +0500 Subject: [PATCH 62/86] Fixed bug with segments incorrectly adjusted after adding transition If transition was really long comparing to segments length then DumbProfileJoiner.join_E would start change their their length in unexpected way - ATSTART when you'd still expect ATEND and vice versa. Also fixed the bug when segments length was incorrectly calculated - it wasn't calculating it world space when it makes more sense here to calculate it by local segments Z axis. --- .../blenderbim/bim/module/model/mep.py | 37 +++++++++++++++---- .../blenderbim/bim/module/model/profile.py | 14 +++++-- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index a0282a9a24..5d203ad817 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -688,7 +688,13 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator): ) if p not in (start_point, end_point) ] - entire_length = (first_segment_start - second_segment_end).length + + def get_segments_length(): + start_dir = (start_point - first_segment_start).normalized() + segments_vector = second_segment_end - first_segment_start + return segments_vector.dot(start_dir) + + entire_length = get_segments_length() # can't rely on (end_point-start_point) here because # transition might change the segments length and therefore direction will be changed @@ -717,28 +723,40 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator): f"Failed to add transition - transition length is larger the segments and the distance between them.\n" + f"Transition length: {full_transition_length:.2f}m, segments length: {entire_length:.2f}m", ) - # TODO: handle the case without creating a representation in the first place? ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=rep) return {"CANCELLED"} + # calculate bunch of points to for adjustments middle_point = keep_only_z_axis((start_point + end_point) / 2 - start_point) + start_point start_segment_extend_point = middle_point - segments_dir * full_transition_length / 2 end_segment_extend_point = middle_point + segments_dir * full_transition_length / 2 + profile_offset_ws transition_dir = keep_only_z_axis(end_segment_extend_point - start_segment_extend_point).normalized() - DumbProfileJoiner().join_E(start_object, start_segment_extend_point) - DumbProfileJoiner().join_E(end_object, end_segment_extend_point) + # adjust the segments + end_object_rotation = end_object.matrix_world.to_quaternion() + end_object_z_basis = end_object_rotation.to_matrix().col[2] # z basis vector + if tool.Cad.is_x(start_object_z_basis.dot(transition_dir), 1): + start_connection = "ATEND" + else: + start_connection = "ATSTART" + if tool.Cad.is_x(end_object_z_basis.dot(transition_dir), 1): + end_connection = "ATSTART" + else: + end_connection = "ATEND" + DumbProfileJoiner().join_E(start_object, start_segment_extend_point, start_connection) + DumbProfileJoiner().join_E(end_object, end_segment_extend_point, end_connection) + + # find the compatible fitting type 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 if transition_type: # TODO: handle the case without creating a representation in the first place? ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=rep) - start_port_match = fitting_data["start_port_match"] if fitting_data else True + # create new fitting type if nothing is compatible if not transition_type: mesh = bpy.data.meshes.new("Transition") obj = bpy.data.objects.new("Transition", mesh) @@ -762,16 +780,21 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator): ) # NOTE: at this point we loose current blender objects selection + # create transition element 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 + # required since we'll base our `transition_obj_dir` on this transition_obj.matrix_world = start_object.matrix_world context.view_layer.update() + # depending on transition direction we may need to flip it or attach it's origin to end segment + # direction can be different depending on: + # - order of the current segments + # - order of the segments that were used with the same transition type before transition_obj_dir = tool.Cad.get_edge_direction(tool.Model.get_flow_segment_axis(transition_obj)) direction_match = tool.Cad.are_vectors_equal(transition_dir, transition_obj_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") diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index 10aa597da4..8bac8d9199 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -272,13 +272,21 @@ class DumbProfileJoiner: body = copy.deepcopy(axis1) self.recreate_profile(element1, profile1, axis, body) - def join_E(self, profile1, target): + def join_E(self, profile1, target, connection=None): + """`connection` = `ATEND` / `ATSTART` to explicitly define the reference point for the join. + + For example if profile 1m long and `target` is at (0, 0, 0.1) and `connection` = `None` + it will implicitly use `connection` = `ATSTART` resulting in profile object 0.9m long and moved to (0, 0, 0.1). + + But with `connection` = `ATEND` it will result in the profile object 0.1m long, locaiton unchanged. + """ element1 = tool.Ifc.get_entity(profile1) if not element1: return axis1 = self.get_profile_axis(profile1) - intersect, connection = mathutils.geometry.intersect_point_line(target, *axis1) - connection = "ATEND" if connection > 0.5 else "ATSTART" + intersect, connection_value = mathutils.geometry.intersect_point_line(target, *axis1) + if connection is None: + connection = "ATEND" if connection_value > 0.5 else "ATSTART" ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type=connection) From a30ad9b8865d38df7b64309138f714f91b214379 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 24 Aug 2023 18:16:02 +0500 Subject: [PATCH 63/86] Fixed bug in shape_builder.mep_transition_calculate it wasn't taking account that `h` can also end up negative --- .../ifcopenshell/util/shape_builder.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index d0667cad9a..aef9bca169 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -1160,10 +1160,11 @@ class ShapeBuilder: return None h = (a + b + sqrt(h0)) / (2 * t) - if h < abs(offset.y) or is_x(h, offset.y): + length_squared = h**2 - offset.y**2 + if length_squared <= 0: print(f"B. angle = {angle} requires h = {h} which is not possible with y offset = {offset.y}") return None - length = sqrt(h**2 - offset.y**2) + length = sqrt(length_squared) if verbose: A = (end_half_dim if end_profile else start_half_dim) * V(1, 0, 0) @@ -1178,10 +1179,11 @@ class ShapeBuilder: if is_x(offset.x, 0): angle = 90 # NOTE: for now we just hardcode the good value for that case h = start_half_dim.x / tan(radians(angle / 2)) - if h < abs(offset.y) or is_x(h, offset.y): + length_squared = h**2 - offset.y**2 + if length_squared <= 0: print(f"B. angle = {angle} requires h = {h} which is not possible with y offset = {offset.y}") return None - length = sqrt(h**2 - offset.y**2) + length = sqrt(length_squared) if verbose: O = V(0, 0, 0) @@ -1191,10 +1193,11 @@ class ShapeBuilder: print(f"B. length = {length}, requested angle = {angle}, tested angle = {tested_angle}") else: h = offset.x / tan(radians(angle)) - if h < abs(offset.y) or is_x(h, offset.y): + length_squared = h**2 - offset.y**2 + if length_squared <= 0: print(f"C. angle = {angle} requires h = {h} which is not possible with y offset = {offset.y}") return None - length = sqrt(h**2 - offset.y**2) + length = sqrt(length_squared) if verbose: A = V(-start_half_dim.x, 0, 0) From 6226aea3c35d33c2c24fb12db58111952d8ce7e7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 24 Aug 2023 18:18:31 +0500 Subject: [PATCH 64/86] Fixed bug adding ports to the transitions between profiles with an offset end port ended up located without the offset --- src/blenderbim/blenderbim/bim/module/model/mep.py | 2 +- src/blenderbim/blenderbim/tool/system.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index 5d203ad817..75681f9166 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -801,7 +801,7 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator): 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) + ports = tool.System.add_ports(transition_obj, offset_end_port=profile_offset_ws) 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") diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py index 3bb15d5cb0..b274ff2929 100644 --- a/src/blenderbim/blenderbim/tool/system.py +++ b/src/blenderbim/blenderbim/tool/system.py @@ -22,12 +22,12 @@ import blenderbim.core.tool import blenderbim.tool as tool from blenderbim.bim import import_ifc import re -from mathutils import Matrix +from mathutils import Matrix, Vector class System(blenderbim.core.tool.System): @classmethod - def add_ports(cls, obj, add_start_port=True, add_end_port=True): + def add_ports(cls, obj, add_start_port=True, add_end_port=True, offset_end_port=None): def add_port(mep_element, matrix): port = tool.Ifc.run("system.add_port", element=mep_element) port.FlowDirection = "NOTDEFINED" @@ -47,7 +47,10 @@ class System(blenderbim.core.tool.System): 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)))) + m = obj.matrix_world @ Matrix.Translation((0, 0, length)) + if offset_end_port: + m.translation += offset_end_port + ports.append(add_port(mep_element, m)) return ports @classmethod From d3bcb8c6b943ee65363a3b649c0f29f24a432526 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Thu, 24 Aug 2023 15:51:33 +0100 Subject: [PATCH 65/86] updating a duration will not auto update schedule usage --- .../bim/module/resource/operator.py | 4 +-- .../blenderbim/bim/module/resource/prop.py | 13 ++++++---- .../blenderbim/bim/module/resource/ui.py | 2 +- .../blenderbim/bim/module/sequence/prop.py | 2 ++ src/blenderbim/blenderbim/core/resource.py | 3 +-- src/blenderbim/blenderbim/core/sequence.py | 1 + src/blenderbim/blenderbim/core/tool.py | 3 +-- src/blenderbim/blenderbim/tool/resource.py | 14 +++++------ src/blenderbim/blenderbim/tool/sequence.py | 1 + .../api/resource/calculate_resource_usage.py | 9 +------ .../api/resource/calculate_resource_work.py | 14 +---------- .../api/resource/edit_resource_time.py | 9 +++---- .../api/sequence/edit_task_time.py | 11 ++++++++ .../ifcopenshell/util/constraint.py | 25 ++++++++++++++----- .../ifcopenshell/util/resource.py | 2 +- 15 files changed, 60 insertions(+), 53 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/resource/operator.py b/src/blenderbim/blenderbim/bim/module/resource/operator.py index b3eaea14b2..fbdd4374f7 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/operator.py +++ b/src/blenderbim/blenderbim/bim/module/resource/operator.py @@ -424,7 +424,6 @@ class CalculateResourceUsage(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.calculate_resource_usage" bl_label = "Calculate Resource Usage" bl_options = {"REGISTER", "UNDO"} - resource: bpy.props.IntProperty() @classmethod def poll(cls, context): @@ -434,7 +433,8 @@ class CalculateResourceUsage(bpy.types.Operator, tool.Ifc.Operator): task = tool.Resource.get_task_assignments(active_resource) if task and tool.Sequence.has_duration(task): return True + return False def _execute(self, context): - core.calculate_resource_usage(tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(self.resource)) + core.calculate_resource_usage(tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(tool.Resource.get_highlighted_resource())) diff --git a/src/blenderbim/blenderbim/bim/module/resource/prop.py b/src/blenderbim/blenderbim/bim/module/resource/prop.py index 615e1e8b60..9ea20d446c 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/prop.py +++ b/src/blenderbim/blenderbim/bim/module/resource/prop.py @@ -84,16 +84,19 @@ def updateResourceUsage(self, context): props = context.scene.BIMResourceProperties if not props.is_resource_update_enabled: return - - if self.schedule_usage == "": + if not self.schedule_usage: return resource = tool.Ifc.get().by_id(self.ifc_definition_id) - tool.Resource.run_edit_resource_time(resource, attributes={"ScheduleUsage": self.schedule_usage}) - tool.Resource.load_resource_properties() + if resource.Usage and resource.Usage.ScheduleUsage == self.schedule_usage: + return + tool.Resource.run_edit_resource_time(resource, attributes={ + "ScheduleUsage": self.schedule_usage + }) tool.Sequence.load_task_properties() + tool.Resource.load_resource_properties() + tool.Sequence.refresh_task_resources() blenderbim.bim.module.resource.data.refresh() blenderbim.bim.module.sequence.data.refresh() - tool.Sequence.refresh_task_resources() blenderbim.bim.module.pset.data.refresh() diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 7304bd25e6..29cb56aa07 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -142,7 +142,7 @@ class BIM_PT_resources(Panel): row2col2 = col2.row() row2col2.prop(self.tprops.resources[self.props.active_resource_index], "schedule_usage", text="") row2col3 = col3.row() - row2col3.operator("bim.calculate_resource_usage", text="", icon="TEMP").resource = ifc_definition_id + row2col3.operator("bim.calculate_resource_usage", text="", icon="TEMP") op = row2col3.operator( "bim.add_usage_constraint" if not is_usage_locked else "bim.remove_usage_constraint", text="", diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index b806fa445c..4ea2f667d8 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -225,7 +225,9 @@ def updateTaskDuration(self, context): 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() + blenderbim.core.sequence.load_task_properties(tool.Sequence) bpy.ops.bim.load_task_properties() + tool.Sequence.load_resources() def get_schedule_predefined_types(self, context): diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py index 6b1ee21719..c026139446 100644 --- a/src/blenderbim/blenderbim/core/resource.py +++ b/src/blenderbim/blenderbim/core/resource.py @@ -23,7 +23,6 @@ def load_resources(resource): resource.load_resources() resource.load_resource_properties() - def add_resource(tool_ifc, resource_tool, ifc_class, parent_resource=None): tool_ifc.run("resource.add_resource", ifc_class=ifc_class, parent_resource=parent_resource) load_resources(resource_tool) @@ -184,7 +183,7 @@ def edit_productivity_pset(ifc, resource_tool): def add_usage_constraint(ifc, resource_tool, resource=None, reference_path=None): - metric = resource_tool.has_usage_metric(resource) + metric = resource_tool.has_metric_constraint(resource, "Usage") if metric: return print("Must remove existing metric first") diff --git a/src/blenderbim/blenderbim/core/sequence.py b/src/blenderbim/blenderbim/core/sequence.py index e0258d39a1..2321796783 100644 --- a/src/blenderbim/blenderbim/core/sequence.py +++ b/src/blenderbim/blenderbim/core/sequence.py @@ -193,6 +193,7 @@ def edit_task_time(ifc, sequence, task_time=None): task = sequence.get_active_task() sequence.load_task_properties(task=task) sequence.disable_editing_task_time() + sequence.load_resources() def assign_predecessor(ifc, sequence, task=None): diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 731543b61f..30905d0928 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -615,8 +615,7 @@ class Resource: def get_resource_time_attributes(cls): pass def get_resource_time(cls, resource): pass def go_to_resource(cls, resource): pass - def has_metric_constraint(cls, resource, attribute): pass - def has_usage_metric(cls, resource): pass + def has_metric_constraint(cls, resource): pass def import_resources(cls, file_path): pass def load_cost_value_attributes(cls, cost_value): pass def load_productivity_data(cls): pass diff --git a/src/blenderbim/blenderbim/tool/resource.py b/src/blenderbim/blenderbim/tool/resource.py index 13d51f8d05..3c5698505d 100644 --- a/src/blenderbim/blenderbim/tool/resource.py +++ b/src/blenderbim/blenderbim/tool/resource.py @@ -397,12 +397,8 @@ class Resource(blenderbim.core.tool.Resource): @classmethod def has_metric_constraint(cls, resource, attribute): - metrics = ifcopenshell.util.constraint.has_metric_constraints(resource, attribute) - return metrics[0] if metrics else None - - @classmethod - def has_usage_metric(cls, resource): - return cls.has_metric_constraint(resource, "Usage") + metrics = ifcopenshell.util.constraint.get_metric_constraints(resource, attribute) + return True if metrics else False @classmethod def run_edit_resource_time(cls, resource, attributes): @@ -446,4 +442,8 @@ class Resource(blenderbim.core.tool.Resource): @classmethod def get_nested_resources(cls, resource): - return ifcopenshell.util.resource.get_nested_resources(resource) \ No newline at end of file + return ifcopenshell.util.resource.get_nested_resources(resource) + + @classmethod + def is_attribute_locked(cls, resource, attribute): + return ifcopenshell.util.constraint.is_attribute_locked(resource, attribute) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index 97340431a0..6fea1e3639 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -421,6 +421,7 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def load_resources(cls): blenderbim.core.resource.load_resources(tool.Resource) + cls.refresh_task_resources @classmethod def get_task_inputs(cls, task): diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py index 1137b23cf3..08602c9f0f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py @@ -31,14 +31,7 @@ class Usecase: self.settings = {"resource": resource} def execute(self): - metrics = ifcopenshell.util.constraint.has_metric_constraints( - self.settings["resource"], "Usage.ScheduleUsage" - ) - if ( - metrics - and metrics[0].ConstraintGrade == "HARD" - and metrics[0].Benchmark == "EQUALTO" - ): + if ifcopenshell.util.constraint.is_attribute_locked(self.settings["resource"], "Usage.ScheduleUsage"): return if ( not self.settings["resource"].Usage diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py index ac978573e0..a3df2a710b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py @@ -60,7 +60,7 @@ class Usecase: self.settings = {"resource": resource} def execute(self): - if self.has_hard_constraint(): + if ifcopenshell.util.constraint.is_attribute_locked(self.settings["resource"], "Usage.ScheduleWork"): return amount_worked = ifcopenshell.util.resource.get_resource_required_work( self.settings["resource"] @@ -74,15 +74,3 @@ class Usecase: resource=self.settings["resource"], ) self.settings["resource"].Usage.ScheduleWork = amount_worked - - def has_hard_constraint(self): - metrics = ifcopenshell.util.constraint.has_metric_constraints( - self.settings["resource"], "Usage.ScheduleWork" - ) - if ( - metrics - and metrics[0].ConstraintGrade == "HARD" - and metrics[0].Benchmark == "EQUALTO" - ): - return True - return False diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py index 22d800a5c6..6a4b2029d4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py @@ -77,10 +77,10 @@ class Usecase: del self.settings["attributes"]["ActualFinish"] for name, value in self.settings["attributes"].items(): - metrics = ifcopenshell.util.constraint.has_metric_constraints( + metrics = ifcopenshell.util.constraint.get_metric_constraints( self.resource, "Usage." + name ) - if metrics and self.is_hard_constraint(metrics[0]): + if metrics and ifcopenshell.util.constraint.is_hard_constraint(metrics[0]): continue if value: if "Start" in name or "Finish" in name or name == "StatusTime": @@ -94,7 +94,7 @@ class Usecase: setattr(self.settings["resource_time"], name, value) if ( name == "ScheduleUsage" - and ifcopenshell.util.constraint.has_metric_constraints( + and ifcopenshell.util.constraint.get_metric_constraints( self.resource, "Usage.ScheduleWork" ) ): @@ -104,9 +104,6 @@ class Usecase: "sequence.calculate_task_duration", self.file, task=task ) - def is_hard_constraint(self, metric): - return bool(metric.ConstraintGrade == "HARD" and metric.Benchmark == "EQUALTO") - def get_resource(self): return [ e diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py index 7ccb1056ce..4aed2fd050 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py @@ -125,6 +125,8 @@ class Usecase: or "ScheduleDuration" in self.settings["attributes"].keys() ): ifcopenshell.api.run("sequence.cascade_schedule", self.file, task=self.task) + if self.settings["task_time"].ScheduleDuration: + self.handle_resource_calculation() def calculate_finish(self): finish = ifcopenshell.util.sequence.get_start_or_finish_date( @@ -173,3 +175,12 @@ class Usecase: for e in self.file.get_inverse(self.settings["task_time"]) if e.is_a("IfcTask") ][0] + + def handle_resource_calculation(self): + resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_deep=False) + for resource in resources: + if ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleWork"): + ifcopenshell.api.run("resource.calculate_resource_usage", self.file, resource=resource) + #TODO: If the duration changes, this implies the productivity rate must change to accomModate the new Schedule Work to be calculated. + # elif ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleUsage"): + # ifcopenshell.api.run("resource.calculate_resource_work", self.file, resource=resource) diff --git a/src/ifcopenshell-python/ifcopenshell/util/constraint.py b/src/ifcopenshell-python/ifcopenshell/util/constraint.py index 66397bb58c..b2a38d6060 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/util/constraint.py @@ -20,10 +20,9 @@ def get_constraints(product): constraints = [] - if product.HasAssociations: - for rel in product.HasAssociations: - if rel.is_a("IfcRelAssociatesConstraint"): - constraints.append(rel.RelatingConstraint) + for rel in product.HasAssociations or []: + if rel.is_a("IfcRelAssociatesConstraint"): + constraints.append(rel.RelatingConstraint) return constraints def get_metrics(constraint): @@ -48,7 +47,7 @@ def get_metric_reference(metric, is_deep=True): reference = metric.ReferencePath return get_reference_Attribute(reference, "") -def has_metric_constraints(resource, attribute): +def get_metric_constraints(resource, attribute): metrics = [] for constraint in get_constraints(resource) or []: for metric in get_metrics(constraint) or []: @@ -59,4 +58,18 @@ def has_metric_constraints(resource, attribute): metrics.append(metric) if metrics: return metrics - return None \ No newline at end of file + return None + +def is_hard_constraint(metric): + if metric.ConstraintGrade == "HARD" and metric.Benchmark == "EQUALTO": + return True + +def is_attribute_locked(product, attribute): + is_locked = False + metrics = get_metric_constraints( + product, attribute + ) + for metric in metrics or []: + if is_hard_constraint(metric): + is_locked = True + return is_locked diff --git a/src/ifcopenshell-python/ifcopenshell/util/resource.py b/src/ifcopenshell-python/ifcopenshell/util/resource.py index ad75887361..f1e17dcc42 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/resource.py +++ b/src/ifcopenshell-python/ifcopenshell/util/resource.py @@ -114,4 +114,4 @@ def get_resource_required_work(resource): return iso_string def get_nested_resources(resource): - return [object for rel in resource.IsNestedBy or [] for object in rel.RelatedObjects] \ No newline at end of file + return [object for rel in resource.IsNestedBy or [] for object in rel.RelatedObjects] From 695dc6ec5ad7282889d5b41aa9f6551020795dd2 Mon Sep 17 00:00:00 2001 From: Gorgious Date: Thu, 24 Aug 2023 20:42:42 +0200 Subject: [PATCH 66/86] Fix #3636 : Check if representation class exists before testing if it's a circle extrusion representation --- src/blenderbim/blenderbim/bim/module/geometry/operator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index c7955a0d08..b50a82a4fd 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -1370,9 +1370,10 @@ class OverrideModeSetEdit(bpy.types.Operator): continue is_profile = True + representation_class = tool.Geometry.get_ifc_representation_class(element, representation) if usage_type == "PROFILE": operator = lambda: bpy.ops.bim.hotkey(hotkey="A_E") - elif "IfcCircleProfileDef" in tool.Geometry.get_ifc_representation_class(element, representation): + elif representation_class and "IfcCircleProfileDef" in representation_class: self.report({"INFO"}, "Can't edit Circle Profile Extrusion") obj.select_set(False) continue From 5b207e6f541fc5aca144015aabfa1f6461980f74 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Tue, 22 Aug 2023 16:01:33 -0700 Subject: [PATCH 67/86] Fix indentation for assign Brick reference --- src/blenderbim/blenderbim/tool/brick.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index d2e7d1a319..71035db82f 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -151,12 +151,12 @@ class Brick(blenderbim.core.tool.Brick): """.replace("{brick_uri}", brick_uri)) for row in query: name = row.get("label") - if not name: - name = brick_uri.split("#")[-1] - if tool.Ifc.get_schema() == "IFC2X3": - return {"ItemReference": brick_uri, "Name": name} - else: - return {"Identification": brick_uri, "Name": name} + if not name: + name = brick_uri.split("#")[-1] + if tool.Ifc.get_schema() == "IFC2X3": + return {"ItemReference": brick_uri, "Name": name} + else: + return {"Identification": brick_uri, "Name": name} @classmethod def get_active_brick_class(cls, split_screen=False): From ff63fd9f6ff79700d37594ac7d078882047e0045 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Tue, 22 Aug 2023 16:18:55 -0700 Subject: [PATCH 68/86] Rename 'add_brick_failed" to "add_brick_relation_failed" --- src/blenderbim/blenderbim/bim/module/brick/prop.py | 2 +- src/blenderbim/blenderbim/bim/module/brick/ui.py | 2 +- src/blenderbim/blenderbim/tool/brick.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/prop.py b/src/blenderbim/blenderbim/bim/module/brick/prop.py index ff96823a64..ee80cacf28 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/prop.py +++ b/src/blenderbim/blenderbim/bim/module/brick/prop.py @@ -104,7 +104,7 @@ class BIMBrickProperties(PropertyGroup): brick_edit_relations_toggled: BoolProperty(name="Brick Edit Relations Toggled", default=False) new_brick_relation_type: EnumProperty(name="New Brick Relation Type", items=get_brick_relations) new_brick_relation_object: StringProperty(name="New Brick Relation Object") - add_relation_failed: BoolProperty(name="Add Relation Failed", default=False) + add_brick_relation_failed: BoolProperty(name="Add Relation Failed", default=False) # create relations split screen split_screen_toggled: BoolProperty(name="Split Screen Toggled", default=False) split_screen_bricks: CollectionProperty(name="Split Screen Bricks", type=Brick) diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index 770987a1bb..65f2ec5640 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -219,7 +219,7 @@ class BIM_PT_brickschema_viewport(Panel): 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: + if self.props.brick_create_relations_toggled and self.props.add_brick_relation_failed: row = self.layout.row(align=True) row.label(text="Failed to find this entity!", icon="ERROR") diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 71035db82f..de62b55353 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -106,15 +106,15 @@ class Brick(blenderbim.core.tool.Brick): with BrickStore.new_changeset() as cs: cs.add((URIRef(brick_uri), URIRef(predicate), Literal(object))) bpy.context.scene.BIMBrickProperties.new_brick_relation_type = BrickStore.relationships[0][0] - bpy.context.scene.BIMBrickProperties.add_relation_failed = False + bpy.context.scene.BIMBrickProperties.add_brick_relation_failed = False return query = BrickStore.graph.query("ASK { <{object_uri}> a ?o . }".replace("{object_uri}", object)) if query: with BrickStore.new_changeset() as cs: cs.add((URIRef(brick_uri), URIRef(predicate), URIRef(object))) - bpy.context.scene.BIMBrickProperties.add_relation_failed = False + bpy.context.scene.BIMBrickProperties.add_brick_relation_failed = False else: - bpy.context.scene.BIMBrickProperties.add_relation_failed = True + bpy.context.scene.BIMBrickProperties.add_brick_relation_failed = True @classmethod def remove_relation(cls, brick_uri, predicate, object): From 33cf40debb861f523b4b562a09ad2f93323ddbc8 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Tue, 22 Aug 2023 16:20:07 -0700 Subject: [PATCH 69/86] Fix condition for hiding "view_brick_item" --- src/blenderbim/blenderbim/bim/module/brick/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index 65f2ec5640..68f2190498 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -235,7 +235,7 @@ class BIM_PT_brickschema_viewport(Panel): op = row.operator("bim.remove_brick_relation", text="", icon="UNLINKED") op.predicate = relation["predicate_uri"] op.object = relation["object_uri"] - if relation["is_uri"] and relation["object_name"] != self.props.active_brick_class: + if relation["is_uri"] and relation["object_uri"].toPython().split("#")[-1] != self.props.active_brick_class: op = row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT") op.item = relation["object_uri"] if relation["is_globalid"]: From 802bd74c52301903d77b1801bf0ea03f7a5ad2ad Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Tue, 22 Aug 2023 16:43:24 -0700 Subject: [PATCH 70/86] Refactor UI get_brick_relations --- src/blenderbim/blenderbim/bim/module/brick/prop.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/prop.py b/src/blenderbim/blenderbim/bim/module/brick/prop.py index ee80cacf28..2f7a3509f8 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/prop.py +++ b/src/blenderbim/blenderbim/bim/module/brick/prop.py @@ -58,13 +58,13 @@ def get_brick_roots(self, context): def get_brick_relations(self, context): - def is_label(relation): - return relation["predicate_name"] == "label" - if not list(filter(is_label, BrickschemaData.data["active_relations"])): - new_relations = BrickStore.relationships.copy() - new_relations.append(("http://www.w3.org/2000/01/rdf-schema#label", "label", "")) - return new_relations - return BrickStore.relationships + for relation in BrickschemaData.data["active_relations"]: + if relation["predicate_name"] == "label": + return BrickStore.relationships + new_relations = BrickStore.relationships.copy() + new_relations.append(("http://www.w3.org/2000/01/rdf-schema#label", "label", "")) + return new_relations + def update_view(self, context): From 78a2f65ec34288959a12c5c02c3bdc141fad6bec Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Tue, 22 Aug 2023 17:03:36 -0700 Subject: [PATCH 71/86] Fix brick.feature For one particular, I removed "And I set 'scene.BIMRootProperties.new_brick_relation_type' to 'hasPart'". Although it should really be "https://brickschema.org/schema/Brick#hasPart" to begin with, it was still failing the test. So removing it as a work around. --- src/blenderbim/test/bim/feature/brick.feature | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/blenderbim/test/bim/feature/brick.feature b/src/blenderbim/test/bim/feature/brick.feature index 8d92c51956..f48547609a 100644 --- a/src/blenderbim/test/bim/feature/brick.feature +++ b/src/blenderbim/test/bim/feature/brick.feature @@ -20,7 +20,7 @@ Scenario: View Brick class Scenario: View Brick item Given an empty Blender session And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - When I press "bim.view_brick_item(item='https://brickschema.org/schema/Brick#Building')" + When I press "bim.view_brick_item(item='https://example.org/digitaltwin#lighting_zone_1')" Then nothing happens Scenario: Rewind Brick class @@ -87,17 +87,11 @@ Scenario: Add Brick relation - vanilla Brick with no IFC Given an empty Blender session And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - And I set "scene.BIMBrickProperties.brick_entity_create_type" to "Location" - And I set "scene.BIMBrickProperties.new_brick_label" to "abc123" - And I set "scene.BIMBrickProperties.brick_entity_class" to "Room" - And I press "bim.add_brick" And I press "bim.view_brick_class(brick_class='Lighting_Zone')" And I set "scene.BIMBrickProperties.active_brick_index" to "0" - And I set "scene.BIMRootProperties.brick_create_relations_toggled" to "True" - And I set "scene.BIMRootProperties.new_brick_relation_namespace" to "https://example.org/digitaltwin#" - And I set "scene.BIMRootProperties.new_brick_relation_type" to "hasPart" - And I set "scene.BIMRootProperties.new_brick_relation_object" to "xyz789" - When I press "bim.add_brick_relation()" + And I set "scene.BIMBrickProperties.brick_create_relations_toggled" to "True" + And I set "scene.BIMBrickProperties.new_brick_relation_object" to "xyz789" + When I press "bim.add_brick_relation" Then nothing happens Scenario: Add Brick relation - vanilla Brick with no IFC and with split screen @@ -106,15 +100,14 @@ Scenario: Add Brick relation - vanilla Brick with no IFC and with split screen And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" And I set "scene.BIMBrickProperties.brick_entity_create_type" to "Location" And I set "scene.BIMBrickProperties.new_brick_label" to "abc123" - And I set "scene.BIMBrickProperties.brick_entity_class" to "Room" + And I set "scene.BIMBrickProperties.brick_entity_class" to "https://brickschema.org/schema/Brick#Room" And I press "bim.add_brick" And I press "bim.view_brick_class(brick_class='Lighting_Zone')" And I set "scene.BIMBrickProperties.active_brick_index" to "0" - And I set "scene.BIMRootProperties.split_screen_toggled" to "True" - And I press "bim.view_brick_class(brick_class='Room')" + And I set "scene.BIMBrickProperties.split_screen_toggled" to "True" + And I press "bim.view_brick_class(brick_class='Room', split_screen=True)" And I set "scene.BIMBrickProperties.split_screen_active_brick_index" to "0" - And I set "scene.BIMRootProperties.brick_create_relations_toggled" to "True" - And I set "scene.BIMRootProperties.new_brick_relation_type" to "hasPart" + And I set "scene.BIMBrickProperties.brick_create_relations_toggled" to "True" When I press "bim.add_brick_relation" Then nothing happens @@ -152,14 +145,14 @@ Scenario: Change viewer list root - split screen Given an empty Blender session And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And I set "scene.BIMBrickProperties.set_list_root_toggled" to "True" - And I set "scene.BIMRootProperties.split_screen_toggled" to "True" + And I set "scene.BIMBrickProperties.split_screen_toggled" to "True" When I set "scene.BIMBrickProperties.split_screen_brick_list_root" to "Location" Then nothing happens Scenario: Toggle split screen Given an empty Blender session And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - When I set "scene.BIMRootProperties.split_screen_toggled" to "True" + When I set "scene.BIMBrickProperties.split_screen_toggled" to "True" Then nothing happens Scenario: Set active namespace From 0d7e71a2690010d689d47e98bc4bd12a8c58ca27 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Wed, 23 Aug 2023 16:03:03 -0700 Subject: [PATCH 72/86] Fix Brick tool - Data is now stored regularly and parsed in props - "get_convertable_brick_spaces" returns a set - "export_brick_attributes" always returns a tuple of strings --- src/blenderbim/blenderbim/bim/module/brick/prop.py | 12 ++++++------ src/blenderbim/blenderbim/tool/brick.py | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/prop.py b/src/blenderbim/blenderbim/bim/module/brick/prop.py index 2f7a3509f8..a9f271e927 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/prop.py +++ b/src/blenderbim/blenderbim/bim/module/brick/prop.py @@ -45,12 +45,12 @@ def get_libraries(self, context): def get_namespaces(self, context): - return BrickStore.namespaces + return [(uri, f"{alias}: {uri}", "") for alias, uri in BrickStore.namespaces] def get_brick_entity_classes(self, context): entity = self.brick_entity_create_type - return BrickStore.entity_classes[entity] + return [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]] def get_brick_roots(self, context): @@ -58,12 +58,12 @@ def get_brick_roots(self, context): def get_brick_relations(self, context): + relations = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships] for relation in BrickschemaData.data["active_relations"]: if relation["predicate_name"] == "label": - return BrickStore.relationships - new_relations = BrickStore.relationships.copy() - new_relations.append(("http://www.w3.org/2000/01/rdf-schema#label", "label", "")) - return new_relations + return relations + relations.append(("http://www.w3.org/2000/01/rdf-schema#label", "label", "")) + return relations diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index de62b55353..fdfce170e3 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -105,7 +105,7 @@ class Brick(blenderbim.core.tool.Brick): if predicate == "http://www.w3.org/2000/01/rdf-schema#label": with BrickStore.new_changeset() as cs: cs.add((URIRef(brick_uri), URIRef(predicate), Literal(object))) - bpy.context.scene.BIMBrickProperties.new_brick_relation_type = BrickStore.relationships[0][0] + bpy.context.scene.BIMBrickProperties.new_brick_relation_type = BrickStore.relationships[0] bpy.context.scene.BIMBrickProperties.add_brick_relation_failed = False return query = BrickStore.graph.query("ASK { <{object_uri}> a ?o . }".replace("{object_uri}", object)) @@ -150,7 +150,7 @@ class Brick(blenderbim.core.tool.Brick): LIMIT 1 """.replace("{brick_uri}", brick_uri)) for row in query: - name = row.get("label") + name = str(row.get("label")) if not name: name = brick_uri.split("#")[-1] if tool.Ifc.get_schema() == "IFC2X3": @@ -216,8 +216,8 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def get_convertable_brick_spaces(cls): if tool.Ifc.get_schema() == "IFC2X3": - return tool.Ifc.get().by_type("IfcSpatialStructureElement") - return tool.Ifc.get().by_type("IfcSpatialElement") + return set(tool.Ifc.get().by_type("IfcSpatialStructureElement")) + return set(tool.Ifc.get().by_type("IfcSpatialElement")) @classmethod def get_convertable_brick_systems(cls): @@ -520,7 +520,7 @@ class BrickStore: ignore_namespace = True break if not ignore_namespace: - BrickStore.namespaces.append((uri, f"{alias}: {uri}", "")) + BrickStore.namespaces.append((alias, str(uri))) @classmethod def load_entity_classes(cls): @@ -542,7 +542,7 @@ class BrickStore: ) BrickStore.entity_classes[root_class] = [] for uri in sorted([x[0].toPython() for x in query]): - BrickStore.entity_classes[root_class].append((uri, uri.split("#")[-1], "")) + BrickStore.entity_classes[root_class].append(uri) @classmethod def load_relationships(cls): @@ -556,7 +556,7 @@ class BrickStore: """ ) for uri in sorted([x[0].toPython() for x in query]): - BrickStore.relationships.append((uri, uri.split("#")[-1], "")) + BrickStore.relationships.append(uri) @classmethod def set_history_size(cls, size): From 39fc60a4823de742c59e6f5315bc4996bec18a38 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Wed, 23 Aug 2023 16:55:28 -0700 Subject: [PATCH 73/86] Update core\tool.py for Brick --- src/blenderbim/blenderbim/core/tool.py | 34 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 30905d0928..f0f168a7ba 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -94,36 +94,46 @@ class Boundary: pass @interface class Brick: - def add_brick(cls, namespace, brick_class): pass - def add_brick_breadcrumb(cls): pass + def add_brick(cls, namespace, brick_class, label): pass + def add_brick_breadcrumb(cls, split_screen=False): pass def add_brick_from_element(cls, element, namespace, brick_class): pass def add_brickifc_project(cls, namespace): pass def add_brickifc_reference(cls, brick, element, project): pass - def add_feed(cls, source, destination): pass - def clear_brick_browser(cls): pass + def add_relation(cls, brick_uri, predicate, object): pass + def remove_relation(cls, brick_uri, predicate, object): pass + def clear_brick_browser(cls, split_screen=False): pass def clear_project(cls): pass def export_brick_attributes(cls, brick_uri): pass - def get_active_brick_class(cls): pass + def get_active_brick_class(cls, split_screen=False): pass def get_brick(cls, element): pass def get_brick_class(cls, element): pass def get_brick_path(cls): pass def get_brick_path_name(cls): pass def get_brickifc_project(cls): pass def get_convertable_brick_elements(cls): pass + def get_convertable_brick_spaces(cls): pass + def get_convertable_brick_systems(cls): pass + def get_parent_space(cls, space): pass + def get_element_container(cls, element): pass + def get_element_systems(cls, element): pass + def get_element_feeds(cls, element): pass def get_item_class(cls, item): pass def get_library_brick_reference(cls, library, brick_uri): pass def get_namespace(cls, uri): pass - def import_brick_classes(cls, brick_class): pass - def import_brick_items(cls, brick_class): pass + def import_brick_classes(cls, brick_class, split_screen=False): pass + def import_brick_items(cls, brick_class, split_screen=False): pass def load_brick_file(cls, filepath): pass def new_brick_file(cls): pass - def pop_brick_breadcrumb(cls): pass + def pop_brick_breadcrumb(cls, split_screen=False): pass def remove_brick(cls, brick_uri): pass def run_assign_brick_reference(cls, element=None, library=None, brick_uri=None): pass - def run_refresh_brick_viewer(cls): pass - def run_view_brick_class(cls, brick_class=None): pass - def select_browser_item(cls, item): pass - def set_active_brick_class(cls, brick_class): pass + def run_refresh_brick_viewer(cls, split_screen=False): pass + def run_view_brick_class(cls, brick_class=None, split_screen=False): pass + def select_browser_item(cls, item, split_screen=False): pass + def set_active_brick_class(cls, brick_class, split_screen=False): pass + def serialize_brick(cls): pass + def add_namespace(cls, alias, uri): pass + def clear_breadcrumbs(cls, split_screen=False): pass @interface From c3de72b71aa6fefdab887c54f1a5670248923655 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Wed, 23 Aug 2023 16:58:27 -0700 Subject: [PATCH 74/86] Fix Brick core - Refresh viewer now reloads both screens by default - Rename "add_namespace" to "add_brick_namespace" - Fix error where "view_brick_class" was run instead of "brick.run_view_brick_class" --- .../blenderbim/bim/module/brick/operator.py | 3 +- src/blenderbim/blenderbim/core/brick.py | 38 ++++++++----------- src/blenderbim/blenderbim/tool/brick.py | 4 +- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 4194514355..2c57e937e5 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -233,7 +233,6 @@ class RefreshBrickViewer(bpy.types.Operator, Operator): def _execute(self, context): core.refresh_brick_viewer(tool.Brick) - core.refresh_brick_viewer(tool.Brick, split_screen=True) class RemoveBrick(bpy.types.Operator, Operator): @@ -289,7 +288,7 @@ class AddBrickNamespace(bpy.types.Operator, Operator): props = context.scene.BIMBrickProperties alias = props.new_brick_namespace_alias uri = props.new_brick_namespace_uri - core.add_namespace(tool.Brick, alias=alias, uri=uri) + core.add_brick_namespace(tool.Brick, alias=alias, uri=uri) class RemoveBrickRelation(bpy.types.Operator, Operator): diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index 5c8cd6ee58..c16b341b1c 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -25,6 +25,14 @@ def load_brick_project(brick, filepath=None, brick_root=None): brick.set_active_brick_class(brick_root, split_screen=True) +def new_brick_file(brick, brick_root=None): + brick.new_brick_file() + brick.import_brick_classes(brick_root) + brick.import_brick_classes(brick_root, split_screen=True) + brick.set_active_brick_class(brick_root) + brick.set_active_brick_class(brick_root, split_screen=True) + + def view_brick_class(brick, brick_class=None, split_screen=False): brick.add_brick_breadcrumb(split_screen=split_screen) brick.clear_brick_browser(split_screen=split_screen) @@ -35,7 +43,7 @@ def view_brick_class(brick, brick_class=None, split_screen=False): def view_brick_item(brick, item=None, split_screen=False): brick_class = brick.get_item_class(item) - view_brick_class(brick, brick_class=brick_class, split_screen=split_screen) + brick.run_view_brick_class(brick_class=brick_class, split_screen=split_screen) brick.select_browser_item(item, split_screen=split_screen) @@ -79,15 +87,13 @@ def add_brick(ifc, brick, element=None, namespace=None, brick_class=None, librar if library: brick.run_assign_brick_reference(element=element, library=library, brick_uri=brick_uri) else: - brick_uri = brick.add_brick(namespace, brick_class, label) + brick.add_brick(namespace, brick_class, label) brick.run_refresh_brick_viewer() - brick.run_refresh_brick_viewer(split_screen=True) def add_brick_relation(brick, brick_uri=None, predicate=None, object=None): brick.add_relation(brick_uri, predicate, object) brick.run_refresh_brick_viewer() - brick.run_refresh_brick_viewer(split_screen=True) def convert_ifc_to_brick(brick, namespace=None, library=None): @@ -129,23 +135,13 @@ def convert_ifc_to_brick(brick, namespace=None, library=None): for downstream_equipment in feeds: brick.add_relation(equipment_uris[element], "https://brickschema.org/schema/Brick#feeds", equipment_uris[downstream_equipment]) brick.run_refresh_brick_viewer() - brick.run_refresh_brick_viewer(split_screen=True) -def new_brick_file(brick, brick_root=None): - brick.new_brick_file() - brick.import_brick_classes(brick_root) - brick.import_brick_classes(brick_root, split_screen=True) - brick.set_active_brick_class(brick_root) - brick.set_active_brick_class(brick_root, split_screen=True) - - -def refresh_brick_viewer(brick, split_screen=False): - if split_screen: - brick.run_view_brick_class(brick_class=brick.get_active_brick_class(split_screen=split_screen), split_screen=split_screen) - else: - brick.run_view_brick_class(brick_class=brick.get_active_brick_class(), split_screen=split_screen) - brick.pop_brick_breadcrumb(split_screen=split_screen) +def refresh_brick_viewer(brick): + brick.run_view_brick_class(brick_class=brick.get_active_brick_class()) + brick.pop_brick_breadcrumb() + brick.run_view_brick_class(brick_class=brick.get_active_brick_class(split_screen=True), split_screen=True) + brick.pop_brick_breadcrumb(split_screen=True) def remove_brick(ifc, brick, library=None, brick_uri=None): @@ -155,14 +151,13 @@ def remove_brick(ifc, brick, library=None, brick_uri=None): ifc.run("library.remove_reference", reference=reference) brick.remove_brick(brick_uri) brick.run_refresh_brick_viewer() - brick.run_refresh_brick_viewer(split_screen=True) def serialize_brick(brick): brick.serialize_brick() -def add_namespace(brick, alias=None, uri=None): +def add_brick_namespace(brick, alias=None, uri=None): brick.add_namespace(alias, uri) @@ -173,4 +168,3 @@ def set_brick_list_root(brick, brick_root=None, split_screen=False): def remove_brick_relation(brick, brick_uri=None, predicate=None, object=None): brick.remove_relation(brick_uri, predicate, object) - brick.run_refresh_brick_viewer() diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index fdfce170e3..6b2f7a1435 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -398,8 +398,8 @@ class Brick(blenderbim.core.tool.Brick): ) @classmethod - def run_refresh_brick_viewer(cls, split_screen=False): - return blenderbim.core.brick.refresh_brick_viewer(tool.Brick, split_screen) + def run_refresh_brick_viewer(cls): + return blenderbim.core.brick.refresh_brick_viewer(tool.Brick) @classmethod def run_view_brick_class(cls, brick_class=None, split_screen=False): From a0613cfee63062184ef2031a374108e0b73c87c3 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Wed, 23 Aug 2023 18:17:18 -0700 Subject: [PATCH 75/86] Undo indentation from commit (did not account for no name existing) --- src/blenderbim/blenderbim/tool/brick.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 6b2f7a1435..ed58180852 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -149,14 +149,15 @@ class Brick(blenderbim.core.tool.Brick): } LIMIT 1 """.replace("{brick_uri}", brick_uri)) + name = None for row in query: name = str(row.get("label")) - if not name: - name = brick_uri.split("#")[-1] - if tool.Ifc.get_schema() == "IFC2X3": - return {"ItemReference": brick_uri, "Name": name} - else: - return {"Identification": brick_uri, "Name": name} + if not name: + name = brick_uri.split("#")[-1] + if tool.Ifc.get_schema() == "IFC2X3": + return {"ItemReference": brick_uri, "Name": name} + else: + return {"Identification": brick_uri, "Name": name} @classmethod def get_active_brick_class(cls, split_screen=False): From dba43590a5f4f6374c44e0b2d94c9bfea5a2a3a0 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Wed, 23 Aug 2023 18:26:01 -0700 Subject: [PATCH 76/86] Fix bug where split screen selection from previous page is out of range on the next --- src/blenderbim/blenderbim/bim/module/brick/ui.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index 68f2190498..b09507ea83 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -205,8 +205,11 @@ class BIM_PT_brickschema_viewport(Panel): if self.props.brick_create_relations_toggled and self.props.split_screen_toggled: row = self.layout.row(align=True) - split_screen_selection = self.props.split_screen_bricks[self.props.split_screen_active_brick_index] - if split_screen_selection.total_items: + try: + split_screen_selection = self.props.split_screen_bricks[self.props.split_screen_active_brick_index] + except: + split_screen_selection = None + if not split_screen_selection or split_screen_selection.total_items: row.label(text="No selection", icon="INFO") else: prop_with_search(row, self.props, "new_brick_relation_type", text="") From e00ffb07addb259dfe6b83ec592f0ad56714df35 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 24 Aug 2023 16:37:08 -0700 Subject: [PATCH 77/86] Update tool/test_brick.py --- src/blenderbim/test/tool/test_brick.py | 259 +++++++++++++++++++++---- 1 file changed, 217 insertions(+), 42 deletions(-) diff --git a/src/blenderbim/test/tool/test_brick.py b/src/blenderbim/test/tool/test_brick.py index 14687ce694..8996796cff 100644 --- a/src/blenderbim/test/tool/test_brick.py +++ b/src/blenderbim/test/tool/test_brick.py @@ -19,6 +19,8 @@ import os import bpy import brickschema +import brickschema.persistent +from brickschema.namespaces import REF, A import ifcopenshell import blenderbim.core.tool import blenderbim.tool as tool @@ -36,17 +38,17 @@ class TestImplementsTool(NewFile): class TestAddBrick(NewFile): def test_run(self): - BrickStore.graph = brickschema.Graph() - result = subject.add_brick("https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment") + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + result = subject.add_brick("https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "label") assert "https://example.org/digitaltwin#" in result assert list( BrickStore.graph.triples( - (URIRef(result), RDF.type, URIRef("https://brickschema.org/schema/Brick#Equipment")) + (URIRef(result), A, URIRef("https://brickschema.org/schema/Brick#Equipment")) ) ) assert list( BrickStore.graph.triples( - (URIRef(result), URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("Unnamed")) + (URIRef(result), URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("label")) ) ) @@ -59,6 +61,13 @@ class TestAddBrickBreadcrumb(NewFile): subject.add_brick_breadcrumb() assert bpy.context.scene.BIMBrickProperties.brick_breadcrumbs[1].name == "brick_class" + def test_run_split_screen(self): + subject.set_active_brick_class("brick_class", split_screen=True) + subject.add_brick_breadcrumb(split_screen=True) + assert bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs[0].name == "brick_class" + subject.add_brick_breadcrumb(split_screen=True) + assert bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs[1].name == "brick_class" + class TestAddBrickFromElement(NewFile): def test_run(self): @@ -66,20 +75,39 @@ class TestAddBrickFromElement(NewFile): element = ifc.createIfcChiller() element.Name = "Chiller" element.GlobalId = ifcopenshell.guid.new() - BrickStore.graph = brickschema.Graph() + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") result = subject.add_brick_from_element( element, "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment" ) uri = f"http://example.org/digitaltwin#{element.GlobalId}" assert result == uri assert list( - BrickStore.graph.triples((URIRef(uri), RDF.type, URIRef("https://brickschema.org/schema/Brick#Equipment"))) + BrickStore.graph.triples((URIRef(uri), A, URIRef("https://brickschema.org/schema/Brick#Equipment"))) ) assert list( BrickStore.graph.triples( (URIRef(uri), URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("Chiller")) ) ) + + def test_run_no_element_name(self): + ifc = ifcopenshell.file() + element = ifc.createIfcChiller() + element.GlobalId = ifcopenshell.guid.new() + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + result = subject.add_brick_from_element( + element, "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment" + ) + uri = f"http://example.org/digitaltwin#{element.GlobalId}" + assert result == uri + assert list( + BrickStore.graph.triples((URIRef(uri), A, URIRef("https://brickschema.org/schema/Brick#Equipment"))) + ) + assert list( + BrickStore.graph.triples( + (URIRef(uri), URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("Unnamed")) + ) + ) class TestAddBrickifcProject(NewFile): @@ -88,66 +116,94 @@ class TestAddBrickifcProject(NewFile): tool.Ifc.set(ifc) project = ifc.createIfcProject(ifcopenshell.guid.new()) project.Name = "My Project" - BrickStore.graph = brickschema.Graph() + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") result = subject.add_brickifc_project("http://example.org/digitaltwin#") assert result == f"http://example.org/digitaltwin#{project.GlobalId}" brick = URIRef(result) assert list( - BrickStore.graph.triples((brick, RDF.type, URIRef("https://brickschema.org/extension/ifc#Project"))) + BrickStore.graph.triples((brick, A, REF.ifcProject)) + ) + assert list( + BrickStore.graph.triples( + (brick, REF.ifcProjectID, Literal(project.GlobalId)) + ) + ) + assert list( + BrickStore.graph.triples( + (brick, REF.ifcFileLocation, Literal(bpy.context.scene.BIMProperties.ifc_file)) + ) ) assert list( BrickStore.graph.triples( (brick, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("My Project")) ) ) - assert list( - BrickStore.graph.triples( - (brick, URIRef("https://brickschema.org/extension/ifc#projectID"), Literal(project.GlobalId)) - ) - ) - assert list( - BrickStore.graph.triples( - ( - brick, - URIRef("https://brickschema.org/extension/ifc#fileLocation"), - Literal(bpy.context.scene.BIMProperties.ifc_file), - ) - ) - ) class TestAddBrickifcReference(NewFile): def test_run(self): TestAddBrickifcProject().test_run() element = tool.Ifc.get().createIfcChiller(ifcopenshell.guid.new()) + element.Name = "Chiller" project = URIRef(f"http://example.org/digitaltwin#{tool.Ifc.get().by_type('IfcProject')[0].GlobalId}") subject.add_brickifc_reference("http://example.org/digitaltwin#foo", element, project) brick = URIRef("http://example.org/digitaltwin#foo") bnode = list( - BrickStore.graph.triples((brick, URIRef("https://brickschema.org/extension/ifc#hasIFCReference"), None)) - )[0][2] + BrickStore.graph.triples((brick, A, REF.IFCReference)) + ) assert list( BrickStore.graph.triples( - (bnode, URIRef("https://brickschema.org/extension/ifc#hasProjectReference"), project) + (bnode, REF.hasIfcProjectReference, URIRef(project)) ) ) assert list( BrickStore.graph.triples( - (bnode, URIRef("https://brickschema.org/extension/ifc#globalID"), Literal(element.GlobalId)) + (bnode, REF.ifcGlobalID, Literal(element.GlobalId)) + ) + ) + assert list( + BrickStore.graph.triples( + (bnode, REF.ifcName, Literal(element.Name)) ) ) -class TestAddFeed(NewFile): +class TestAddRelation(NewFile): def test_run(self): - BrickStore.graph = brickschema.Graph() - subject.add_feed("http://example.org/digitaltwin#source", "http://example.org/digitaltwin#destination") + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + source = subject.add_brick( + "http://example.org/digitaltwin#", + "https://brickschema.org/schema/Brick#Equipment", + "source" + ) + destination = subject.add_brick( + "http://example.org/digitaltwin#", + "https://brickschema.org/schema/Brick#Equipment", + "destination" + ) + subject.add_relation(source, "https://brickschema.org/schema/Brick#feeds", destination) assert list( BrickStore.graph.triples( ( - URIRef("http://example.org/digitaltwin#source"), + URIRef(source), URIRef("https://brickschema.org/schema/Brick#feeds"), - URIRef("http://example.org/digitaltwin#destination"), + URIRef(destination), + ) + ) + ) + + +class TestRemoveRelation(NewFile): + def test_run(self): + TestAddRelation().test_run() + source, relation, destination = list(BrickStore.graph.triples((None, URIRef("https://brickschema.org/schema/Brick#feeds"), None)))[0] + subject.remove_relation(source, relation, destination) + assert not list( + BrickStore.graph.triples( + ( + URIRef(source), + URIRef(relation), + URIRef(destination), ) ) ) @@ -158,31 +214,40 @@ class TestClearBrickBrowser(NewFile): bpy.context.scene.BIMBrickProperties.bricks.add() subject.clear_brick_browser() assert len(bpy.context.scene.BIMBrickProperties.bricks) == 0 + + def test_run_split_screen(self): + bpy.context.scene.BIMBrickProperties.split_screen_bricks.add() + subject.clear_brick_browser(split_screen=True) + assert len(bpy.context.scene.BIMBrickProperties.split_screen_bricks) == 0 class TestClearProject(NewFile): def test_run(self): BrickStore.graph = "graph" bpy.context.scene.BIMBrickProperties.active_brick_class == "brick_class" - bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.add().name = "foo" + bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class == "brick_class2" subject.clear_project() assert BrickStore.graph is None assert bpy.context.scene.BIMBrickProperties.active_brick_class == "" - assert len(bpy.context.scene.BIMBrickProperties.brick_breadcrumbs) == 0 + assert bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class == "" class TestExportBrickAttributes(NewFile): def test_run(self): - assert subject.export_brick_attributes("http://example.org/digitaltwin#floor") == { - "Identification": "http://example.org/digitaltwin#floor", - "Name": "floor", + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + brick = subject.add_brick("https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "name") + assert subject.export_brick_attributes(brick) == { + "Identification": brick, + "Name": "name", } def test_run_ifc2x3(self): + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + brick = subject.add_brick("https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "name") tool.Ifc.set(ifcopenshell.file(schema="IFC2X3")) - assert subject.export_brick_attributes("http://example.org/digitaltwin#floor") == { - "ItemReference": "http://example.org/digitaltwin#floor", - "Name": "floor", + assert subject.export_brick_attributes(brick) == { + "ItemReference": brick, + "Name": "name", } @@ -191,6 +256,10 @@ class TestGetActiveBrickClass(NewFile): subject.set_active_brick_class("brick_class") assert subject.get_active_brick_class() == "brick_class" + def test_run_split_screen(self): + subject.set_active_brick_class("brick_class", split_screen=True) + assert subject.get_active_brick_class(split_screen=True) == "brick_class" + class TestGetBrick(NewFile): def test_run(self): @@ -247,6 +316,50 @@ class TestGetConvertableBrickElements(NewFile): assert subject.get_convertable_brick_elements() == {element} +class TestGetConvertableBrickSpaces(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + element = ifc.createIfcBuildingStorey() + ifc.createIfcWall() + assert subject.get_convertable_brick_spaces() == {element} + + +class TestGetConvertableBrickSystems(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + element = ifc.createIfcSystem() + ifc.createIfcWall() + assert subject.get_convertable_brick_systems() == {element} + + +class TestGetParentSpace(NewFile): + def test_run(cls): + ifc = ifcopenshell.file() + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBuildingStorey") + subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace") + project = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", ifc, product=element, relating_object=project) + assert subject.get_parent_space(subelement) == element + assert subject.get_parent_space(element) is None + +class TestGetElementContainer(NewFile): + def test_nothing(cls): + pass + + +class TestGetElementSystems(NewFile): + def test_nothing(cls): + pass + + +class TestGetElementFeeds(NewFile): + def test_run(cls): + pass + + class TestGetItemClass(NewFile): def test_run(self): TestLoadBrickFile().test_run() @@ -291,6 +404,21 @@ class TestImportBrickClasses(NewFile): assert brick.total_items == 1 assert not brick.label + def test_run_split_sccreen(self): + TestLoadBrickFile().test_run() + subject.import_brick_classes("Class", split_screen=True) + assert len(bpy.context.scene.BIMBrickProperties.split_screen_bricks) == 2 + brick = bpy.context.scene.BIMBrickProperties.split_screen_bricks[0] + assert brick.name == "Building" + assert brick.uri == "https://brickschema.org/schema/Brick#Building" + assert brick.total_items == 1 + assert not brick.label + brick = bpy.context.scene.BIMBrickProperties.split_screen_bricks[1] + assert brick.name == "Location" + assert brick.uri == "https://brickschema.org/schema/Brick#Location" + assert brick.total_items == 1 + assert not brick.label + class TestImportBrickItems(NewFile): def test_run(self): @@ -303,6 +431,16 @@ class TestImportBrickItems(NewFile): assert brick.uri == "https://example.org/digitaltwin#bldg" assert brick.total_items == 0 + def test_run_split_screen(self): + TestLoadBrickFile().test_run() + subject.import_brick_items("Building", split_screen=True) + assert len(bpy.context.scene.BIMBrickProperties.split_screen_bricks) == 1 + brick = bpy.context.scene.BIMBrickProperties.split_screen_bricks[0] + assert brick.name == "bldg" + assert brick.label == "My Building" + assert brick.uri == "https://example.org/digitaltwin#bldg" + assert brick.total_items == 0 + class TestLoadBrickFile(NewFile): def test_run(self): @@ -315,6 +453,8 @@ class TestLoadBrickFile(NewFile): filepath = os.path.join(cwd, "..", "files", "spaces.ttl") subject.load_brick_file(filepath) assert BrickStore.graph + namespaces = [(ns[0], ns[1].toPython()) for ns in BrickStore.graph.namespaces()] + assert ("brick", "https://brickschema.org/schema/Brick#") in namespaces class TestNewBrickFile(NewFile): @@ -340,14 +480,33 @@ class TestPopBrickBreadcrumb(NewFile): assert len(bpy.context.scene.BIMBrickProperties.brick_breadcrumbs) == 1 assert bpy.context.scene.BIMBrickProperties.brick_breadcrumbs[0].name == "foo" + def test_run_split_screen(self): + bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs.add().name = "foo" + bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs.add().name = "bar" + assert subject.pop_brick_breadcrumb(split_screen=True) == "bar" + assert len(bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs) == 1 + assert bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs[0].name == "foo" + class TestRemoveBrick(NewFile): def test_run(self): - BrickStore.graph = brickschema.Graph() - result = subject.add_brick("http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment") + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + result = subject.add_brick("http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "label") subject.remove_brick(result) assert not list(BrickStore.graph.triples((URIRef(result), None, None))) + def test_run_with_bnode(self): + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + result = subject.add_brick("http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "label") + TestAddBrickifcProject().test_run() + element = tool.Ifc.get().createIfcChiller(ifcopenshell.guid.new()) + element.Name = "Chiller" + project = URIRef(f"http://example.org/digitaltwin#{tool.Ifc.get().by_type('IfcProject')[0].GlobalId}") + subject.add_brickifc_reference(result, element, project) + subject.remove_brick(result) + assert not list(BrickStore.graph.triples((URIRef(result), None, None))) + assert not list(BrickStore.graph.triples((None, REF.hasIfcProjectReference, None))) + class TestRunAssignBrickReference(NewFile): def test_nothing(self): @@ -359,7 +518,7 @@ class TestRunRefreshBrickViewer(NewFile): pass -class TestViewBrickClass(NewFile): +class TestRunViewBrickClass(NewFile): def test_nothing(self): pass @@ -369,6 +528,10 @@ class TestSelectBrowserItem(NewFile): subject.set_active_brick_class("brick_class") assert bpy.context.scene.BIMBrickProperties.active_brick_class == "brick_class" + def test_run(self): + subject.set_active_brick_class("brick_class", split_screen=True) + assert bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class == "brick_class" + class TestSetActiveBrickClass(NewFile): def test_run(self): @@ -376,3 +539,15 @@ class TestSetActiveBrickClass(NewFile): bpy.context.scene.BIMBrickProperties.bricks.add().name = "bar" subject.select_browser_item("namespace#bar") assert bpy.context.scene.BIMBrickProperties.active_brick_index == 1 + + +class TestSerializeBrick(NewFile): + def test_nothing(self): + pass + + +class TestAddNamespace(NewFile): + def test_run(self): + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + subject.add_namespace("digitaltwin", "http://example.org/digitaltwin") + assert ("digitaltwin", "http://example.org/digitaltwin") in BrickStore.namespaces \ No newline at end of file From 1344cbc192515e22b8aee4ac939d76cc5f24d43b Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 24 Aug 2023 16:37:16 -0700 Subject: [PATCH 78/86] Update core/test_brick.py --- src/blenderbim/test/core/test_brick.py | 207 ++++++++++++++++++------- 1 file changed, 150 insertions(+), 57 deletions(-) diff --git a/src/blenderbim/test/core/test_brick.py b/src/blenderbim/test/core/test_brick.py index 2b5eb70855..157a363682 100644 --- a/src/blenderbim/test/core/test_brick.py +++ b/src/blenderbim/test/core/test_brick.py @@ -23,46 +23,80 @@ from test.core.bootstrap import ifc, brick class TestLoadBrickProject: def test_run(self, brick): brick.load_brick_file("filepath").should_be_called() - brick.import_brick_classes("Class").should_be_called() - brick.set_active_brick_class("Class").should_be_called() - subject.load_brick_project(brick, filepath="filepath") + brick.import_brick_classes("brick_root").should_be_called() + brick.import_brick_classes("brick_root", split_screen=True).should_be_called() + brick.set_active_brick_class("brick_root").should_be_called() + brick.set_active_brick_class("brick_root", split_screen=True).should_be_called() + subject.load_brick_project(brick, filepath="filepath", brick_root="brick_root") + + +class TestNewBrickFile: + def test_run(self, brick): + brick.new_brick_file().should_be_called() + brick.import_brick_classes("brick_root").should_be_called() + brick.import_brick_classes("brick_root", split_screen=True).should_be_called() + brick.set_active_brick_class("brick_root").should_be_called() + brick.set_active_brick_class("brick_root", split_screen=True).should_be_called() + subject.new_brick_file(brick, brick_root="brick_root") class TestViewBrickClass: def test_run(self, brick): - brick.add_brick_breadcrumb().should_be_called() - brick.clear_brick_browser().should_be_called() - brick.import_brick_classes("brick_class").should_be_called() - brick.import_brick_items("brick_class").should_be_called() - brick.set_active_brick_class("brick_class").should_be_called() - subject.view_brick_class(brick, brick_class="brick_class") + brick.add_brick_breadcrumb(split_screen=False).should_be_called() + brick.clear_brick_browser(split_screen=False).should_be_called() + brick.import_brick_classes("brick_class", split_screen=False).should_be_called() + brick.import_brick_items("brick_class", split_screen=False).should_be_called() + brick.set_active_brick_class("brick_class", split_screen=False).should_be_called() + subject.view_brick_class(brick, brick_class="brick_class", split_screen=False) + + def test_split_screen(self, brick): + brick.add_brick_breadcrumb(split_screen=True).should_be_called() + brick.clear_brick_browser(split_screen=True).should_be_called() + brick.import_brick_classes("brick_class", split_screen=True).should_be_called() + brick.import_brick_items("brick_class", split_screen=True).should_be_called() + brick.set_active_brick_class("brick_class", split_screen=True).should_be_called() + subject.view_brick_class(brick, brick_class="brick_class", split_screen=True) class TestViewBrickItem: def test_run(self, brick): - brick.add_brick_breadcrumb().should_be_called() - brick.clear_brick_browser().should_be_called() brick.get_item_class("item").should_be_called().will_return("brick_class") - brick.import_brick_classes("brick_class").should_be_called() - brick.import_brick_items("brick_class").should_be_called() - brick.set_active_brick_class("brick_class").should_be_called() - brick.select_browser_item("item").should_be_called() - subject.view_brick_item(brick, item="item") + brick.run_view_brick_class(brick_class="brick_class", split_screen=False).should_be_called() + brick.select_browser_item("item", split_screen=False).should_be_called() + subject.view_brick_item(brick, item="item", split_screen=False) + + def test_split_screen(self, brick): + brick.get_item_class("item").should_be_called().will_return("brick_class") + brick.run_view_brick_class(brick_class="brick_class", split_screen=True).should_be_called() + brick.select_browser_item("item", split_screen=True).should_be_called() + subject.view_brick_item(brick, item="item", split_screen=True) class TestRewindBrickClass: def test_run(self, brick): - brick.pop_brick_breadcrumb().should_be_called().will_return("previous_class") - brick.clear_brick_browser().should_be_called() - brick.import_brick_classes("previous_class").should_be_called() - brick.import_brick_items("previous_class").should_be_called() - brick.set_active_brick_class("previous_class").should_be_called() - subject.rewind_brick_class(brick) + brick.pop_brick_breadcrumb(split_screen=False).should_be_called().will_return("previous_class") + brick.clear_brick_browser(split_screen=False).should_be_called() + brick.import_brick_classes("previous_class", split_screen=False).should_be_called() + brick.import_brick_items("previous_class", split_screen=False).should_be_called() + brick.set_active_brick_class("previous_class", split_screen=False).should_be_called() + subject.rewind_brick_class(brick, split_screen=False) + + def test_split_screen(self, brick): + brick.pop_brick_breadcrumb(split_screen=True).should_be_called().will_return("previous_class") + brick.clear_brick_browser(split_screen=True).should_be_called() + brick.import_brick_classes("previous_class", split_screen=True).should_be_called() + brick.import_brick_items("previous_class", split_screen=True).should_be_called() + brick.set_active_brick_class("previous_class", split_screen=True).should_be_called() + subject.rewind_brick_class(brick, split_screen=True) class TestCloseBrickProject: def test_run(self, brick): brick.clear_project().should_be_called() + brick.clear_brick_browser().should_be_called() + brick.clear_brick_browser(split_screen=True).should_be_called() + brick.clear_breadcrumbs().should_be_called() + brick.clear_breadcrumbs(split_screen=True).should_be_called() subject.close_brick_project(brick) @@ -86,84 +120,113 @@ class TestConvertBrickProject: class TestAssignBrickReference: def test_assigning_to_a_new_reference(self, ifc, brick): - brick.get_library_brick_reference("library", "brick").should_be_called().will_return(None) + brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return(None) ifc.run("library.add_reference", library="library").should_be_called().will_return("reference") - brick.export_brick_attributes("brick").should_be_called().will_return("attributes") + brick.export_brick_attributes("brick_uri").should_be_called().will_return("attributes") ifc.run("library.edit_reference", reference="reference", attributes="attributes").should_be_called() ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return("project") - brick.add_brickifc_reference("brick", "element", "project").should_be_called() - subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick") + brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called() + subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri") def test_assigning_to_an_existing_reference(self, ifc, brick): - brick.get_library_brick_reference("library", "brick").should_be_called().will_return("reference") + brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return("reference") ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return("project") - brick.add_brickifc_reference("brick", "element", "project").should_be_called() - subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick") + brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called() + subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri") def test_adding_a_brickifc_project_if_it_doesnt_exist(self, ifc, brick): - brick.get_library_brick_reference("library", "brick").should_be_called().will_return("reference") + brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return("reference") ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return(None) - brick.get_namespace("brick").should_be_called().will_return("namespace") + brick.get_namespace("brick_uri").should_be_called().will_return("namespace") brick.add_brickifc_project("namespace").should_be_called().will_return("project") - brick.add_brickifc_reference("brick", "element", "project").should_be_called() - subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick") + brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called() + subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri") class TestAddBrick: def test_adding_a_brick_from_an_element(self, ifc, brick): brick.add_brick_from_element("element", "namespace", "brick_class").should_be_called().will_return("brick_uri") brick.run_refresh_brick_viewer().should_be_called() - subject.add_brick(ifc, brick, element="element", namespace="namespace", brick_class="brick_class", library=None) + subject.add_brick( + ifc, brick, element="element", namespace="namespace", brick_class="brick_class", library=None, label="label" + ) - def test_adding_a_brick_an_auto_assigning_it_to_the_ifc_element(self, ifc, brick): + def test_adding_a_brick_and_auto_assigning_it_to_the_ifc_element(self, ifc, brick): brick.add_brick_from_element("element", "namespace", "brick_class").should_be_called().will_return("brick_uri") brick.run_assign_brick_reference(element="element", library="library", brick_uri="brick_uri").should_be_called() brick.run_refresh_brick_viewer().should_be_called() subject.add_brick( - ifc, brick, element="element", namespace="namespace", brick_class="brick_class", library="library" + ifc, brick, element="element", namespace="namespace", brick_class="brick_class", library="library", label="label" ) def test_adding_a_plain_brick(self, ifc, brick): - brick.add_brick("namespace", "brick_class").should_be_called() + brick.add_brick("namespace", "brick_class", "label").should_be_called() brick.run_refresh_brick_viewer().should_be_called() - subject.add_brick(ifc, brick, element=None, namespace="namespace", brick_class="brick_class", library=None) + subject.add_brick(ifc, brick, element=None, namespace="namespace", brick_class="brick_class", library=None, label="label") -class TestAddBrickFeed: +class TestAddBrickRelation: def test_run(self, ifc, brick): - brick.get_brick("source").should_be_called().will_return("source_brick") - brick.get_brick("destination").should_be_called().will_return("destination_brick") - brick.add_feed("source_brick", "destination_brick").should_be_called() + brick.add_relation("brick_uri", "predicate", "object").should_be_called() brick.run_refresh_brick_viewer().should_be_called() - subject.add_brick_feed(ifc, brick, source="source", destination="destination") + subject.add_brick_relation(brick, brick_uri="brick_uri", predicate="predicate", object="object") class TestConvertIfcToBrick: def test_run(self, brick): - brick.get_convertable_brick_elements().should_be_called().will_return(["element"]) - brick.get_brick_class("element").should_be_called().will_return("brick_class") - brick.add_brick_from_element("element", "namespace", "brick_class").should_be_called().will_return("brick_uri") - brick.run_assign_brick_reference(element="element", library="library", brick_uri="brick_uri").should_be_called() + brick.get_convertable_brick_spaces().should_be_called().will_return({"space", "parent"}) + brick.get_brick_class("space").should_be_called().will_return("space_class") + brick.add_brick_from_element("space", "namespace", "space_class").should_be_called().will_return("space_uri") + brick.run_assign_brick_reference(element="space", library="library", brick_uri="space_uri").should_be_called() + + brick.get_brick_class("parent").should_be_called().will_return("parent_class") + brick.add_brick_from_element("parent", "namespace", "parent_class").should_be_called().will_return("parent_uri") + brick.run_assign_brick_reference(element="parent", library="library", brick_uri="parent_uri").should_be_called() + + brick.get_parent_space("space").should_be_called().will_return("parent") + brick.get_parent_space("parent").should_be_called().will_return(None) + brick.add_relation("parent_uri", "https://brickschema.org/schema/Brick#hasPart", "space_uri").should_be_called() + + brick.get_convertable_brick_systems().should_be_called().will_return({"system"}) + brick.get_brick_class("system").should_be_called().will_return("system_class") + brick.add_brick_from_element("system", "namespace", "system_class").should_be_called().will_return("system_uri") + brick.run_assign_brick_reference(element="system", library="library", brick_uri="system_uri").should_be_called() + + brick.get_convertable_brick_elements().should_be_called().will_return({"element", "downstream_element"}) + brick.get_brick_class("element").should_be_called().will_return("element_class") + brick.add_brick_from_element("element", "namespace", "element_class").should_be_called().will_return("element_uri") + brick.get_element_container("element").should_be_called().will_return("space") + brick.add_relation("element_uri", "https://brickschema.org/schema/Brick#hasLocation", "space_uri").should_be_called() + brick.get_element_systems("element").should_be_called().will_return(["system"]) + brick.add_relation("system_uri", "https://brickschema.org/schema/Brick#hasPart", "element_uri").should_be_called() + brick.run_assign_brick_reference(element="element", library="library", brick_uri="element_uri").should_be_called() + + brick.get_brick_class("downstream_element").should_be_called().will_return("downstream_element_class") + brick.add_brick_from_element("downstream_element", "namespace", "downstream_element_class").should_be_called().will_return("downstream_element_uri") + brick.get_element_container("downstream_element").should_be_called().will_return("space") + brick.add_relation("downstream_element_uri", "https://brickschema.org/schema/Brick#hasLocation", "space_uri").should_be_called() + brick.get_element_systems("downstream_element").should_be_called().will_return(["system"]) + brick.add_relation("system_uri", "https://brickschema.org/schema/Brick#hasPart", "downstream_element_uri").should_be_called() + brick.run_assign_brick_reference(element="downstream_element", library="library", brick_uri="downstream_element_uri").should_be_called() + + brick.get_element_feeds("element").should_be_called().will_return(["downstream_element"]) + brick.get_element_feeds("downstream_element").should_be_called().will_return([]) + brick.add_relation("element_uri", "https://brickschema.org/schema/Brick#feeds", "downstream_element_uri").should_be_called() brick.run_refresh_brick_viewer().should_be_called() subject.convert_ifc_to_brick(brick, namespace="namespace", library="library") -class TestNewBrickFile: - def test_run(self, brick): - brick.new_brick_file().should_be_called() - brick.import_brick_classes("Class").should_be_called() - brick.set_active_brick_class("Class").should_be_called() - subject.new_brick_file(brick) - - class TestRefreshBrickViewer: def test_run(self, brick): - brick.get_active_brick_class().should_be_called().will_return("class") - brick.run_view_brick_class(brick_class="class").should_be_called() + brick.get_active_brick_class().should_be_called().will_return("brick_class") + brick.run_view_brick_class(brick_class="brick_class").should_be_called() brick.pop_brick_breadcrumb().should_be_called() + brick.get_active_brick_class(split_screen=True).should_be_called().will_return("brick_class") + brick.run_view_brick_class(brick_class="brick_class", split_screen=True).should_be_called() + brick.pop_brick_breadcrumb(split_screen=True).should_be_called() subject.refresh_brick_viewer(brick) @@ -185,3 +248,33 @@ class TestRemoveBrick: brick.remove_brick("brick_uri").should_be_called() brick.run_refresh_brick_viewer().should_be_called() subject.remove_brick(ifc, brick, library=None, brick_uri="brick_uri") + + +class TestSerializeBrick: + def test_run(self, brick): + brick.serialize_brick().should_be_called() + subject.serialize_brick(brick) + + +class TestAddBrickNamespace: + def test_run(self, brick): + brick.add_namespace("alias", "uri").should_be_called() + subject.add_brick_namespace(brick, alias="alias", uri="uri") + + +class TestSetBrickListRoot: + def test_run(self, brick): + brick.run_view_brick_class(brick_class="brick_root", split_screen=False).should_be_called() + brick.clear_breadcrumbs(split_screen=False).should_be_called() + subject.set_brick_list_root(brick, brick_root="brick_root", split_screen=False) + + def test_split_screen(self, brick): + brick.run_view_brick_class(brick_class="brick_root", split_screen=True).should_be_called() + brick.clear_breadcrumbs(split_screen=True).should_be_called() + subject.set_brick_list_root(brick, brick_root="brick_root", split_screen=True) + + +class TestRemoveBrickRelation: + def test_run(self, brick): + brick.remove_relation("brick_uri", "predicate", "object").should_be_called() + subject.remove_brick_relation(brick, brick_uri="brick_uri", predicate="predicate", object="object") \ No newline at end of file From 356bc180789d18b4a4b746c5947fc3942c809747 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 25 Aug 2023 11:21:43 +0500 Subject: [PATCH 79/86] fixed errors after 13309cb8c #3624 for example it was throwing errors when using WindowTool you selected a wall and it would try to assign current type to wall --- src/blenderbim/blenderbim/bim/handler.py | 8 ++- .../blenderbim/bim/module/model/workspace.py | 57 ++++++++++++------- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 3e4636396a..c8a2882475 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -27,7 +27,7 @@ from bpy.app.handlers import persistent from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.owner.prop import get_user_person, get_user_organisation from blenderbim.bim.module.model.data import AuthoringData -from blenderbim.bim.module.model.workspace import LIST_OF_TOOLS +from blenderbim.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MAP from mathutils import Vector from math import cos, degrees @@ -130,9 +130,11 @@ def update_bim_tool_props(): if element.is_a("IfcElementType") or element.is_a("IfcElement"): element_type = ifcopenshell.util.element.get_type(element) if element_type: - if current_tool.idname == "bim.bim_tool": + is_bim_tool = current_tool.idname == "bim.bim_tool" + if is_bim_tool: props.ifc_class = element_type.is_a() - props.relating_type_id = str(element_type.id()) + if is_bim_tool or TOOLS_TO_CLASSES_MAP.get(current_tool.idname) == element_type.is_a(): + props.relating_type_id = str(element_type.id()) extrusion = tool.Model.get_extrusion(representation) if not extrusion: return diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 61815d6ede..ba0ac78390 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -78,9 +78,11 @@ class WallTool(BimTool): bl_description = "Create and edit walls" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.wall") bl_widget = None + ifc_element_type = "IfcWallType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcWallType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class SlabTool(BimTool): @@ -91,9 +93,11 @@ class SlabTool(BimTool): bl_description = "Create and edit slabs" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.slab") bl_widget = None + ifc_element_type = "IfcSlabType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcSlabType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class DoorTool(BimTool): @@ -104,9 +108,11 @@ class DoorTool(BimTool): bl_description = "Create and edit doors" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.door") bl_widget = None + ifc_element_type = "IfcDoorType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcDoorType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class WindowTool(BimTool): @@ -117,9 +123,11 @@ class WindowTool(BimTool): bl_description = "Create and edit windows" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.window") bl_widget = None + ifc_element_type = "IfcWindowType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcWindowType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class ColumnTool(BimTool): @@ -130,9 +138,11 @@ class ColumnTool(BimTool): bl_description = "Create and edit columns" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.column") bl_widget = None + ifc_element_type = "IfcColumnType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcColumnType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class BeamTool(BimTool): @@ -143,9 +153,11 @@ class BeamTool(BimTool): bl_description = "Create and edit beams" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.beam") bl_widget = None + ifc_element_type = "IfcBeamType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcBeamType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class DuctTool(BimTool): @@ -156,9 +168,11 @@ class DuctTool(BimTool): bl_description = "Create and edit ducks" # No, not a typo. bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.duct") bl_widget = None + ifc_element_type = "IfcDuctSegmentType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcDuctSegmentType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class PipeTool(BimTool): @@ -169,9 +183,11 @@ class PipeTool(BimTool): bl_description = "Create and edit pipes" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.pipe") bl_widget = None + ifc_element_type = "IfcPipeSegmentType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcPipeSegmentType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) def add_layout_hotkey_operator(layout, text, hotkey, description): @@ -394,9 +410,7 @@ class BimToolUI: else: row.operator("bim.show_openings", icon="HIDE_OFF", text="") - if AuthoringData.data["active_class"] in ( - "IfcOpeningElement", - ): + if AuthoringData.data["active_class"] in ("IfcOpeningElement",): if len(context.selected_objects) == 2: row = cls.layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") @@ -737,9 +751,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): self.props.z = self.z def hotkey_S_L(self): - if AuthoringData.data["active_class"] in ( - "IfcOpeningElement", - ): + if AuthoringData.data["active_class"] in ("IfcOpeningElement",): if len(bpy.context.selected_objects) == 2: bpy.ops.bim.clone_opening() @@ -764,3 +776,4 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): LIST_OF_TOOLS = [cls.bl_idname for cls in (BimTool.__subclasses__() + [BimTool])] +TOOLS_TO_CLASSES_MAP = {cls.bl_idname: cls.ifc_element_type for cls in BimTool.__subclasses__()} From 66decc604e335de08295fa324dd71439ec507945 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 25 Aug 2023 16:51:14 +1000 Subject: [PATCH 80/86] Fix failing tests and optimise brick tests. --- src/blenderbim/blenderbim/bim/ifc.py | 12 ++- .../blenderbim/bim/module/brick/data.py | 2 +- .../blenderbim/bim/module/profile/data.py | 2 + src/blenderbim/blenderbim/bim/ui.py | 6 +- src/blenderbim/blenderbim/tool/brick.py | 4 +- src/blenderbim/test/bim/feature/brick.feature | 34 ++++++-- .../test/bim/feature/geometry.feature | 6 +- .../test/bim/feature/project.feature | 4 +- src/blenderbim/test/bim/test_feature.py | 10 +++ src/blenderbim/test/files/BrickStub.ttl | 13 +++ src/blenderbim/test/tool/test_brick.py | 79 ++++++++----------- .../test/util/test_brick.py | 7 -- 12 files changed, 107 insertions(+), 72 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py index 97d2f301b3..c35a942aaa 100644 --- a/src/blenderbim/blenderbim/bim/ifc.py +++ b/src/blenderbim/blenderbim/bim/ifc.py @@ -221,7 +221,9 @@ class IfcStore: blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback) elif isinstance(obj, bpy.types.Object): blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback) - blenderbim.bim.handler.subscribe_to(obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback) + blenderbim.bim.handler.subscribe_to( + obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback + ) if IfcStore.history: data = {"id": element.id(), "guid": getattr(element, "GlobalId", None), "obj": obj.name} @@ -248,7 +250,9 @@ class IfcStore: blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback) elif isinstance(obj, bpy.types.Object): blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback) - blenderbim.bim.handler.subscribe_to(obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback) + blenderbim.bim.handler.subscribe_to( + obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback + ) # TODO Listeners are not re-registered. Does this cause nasty problems to debug later on? # TODO We're handling id_map and guid_map, but what about edited_objs? This might cause big problems. @@ -321,7 +325,7 @@ class IfcStore: IfcStore.begin_transaction(operator) if tool.Ifc.get(): tool.Ifc.get().begin_transaction() - if BrickStore.graph: + if BrickStore.graph is not None: # `if BrickStore.graph` by itself takes ages. BrickStore.begin_transaction() # This empty transaction ensures that each operator has at least one transaction IfcStore.add_transaction_operation(operator, rollback=lambda data: True, commit=lambda data: True) @@ -339,7 +343,7 @@ class IfcStore: IfcStore.add_transaction_operation( operator, rollback=lambda d: tool.Ifc.get().undo(), commit=lambda d: tool.Ifc.get().redo() ) - if BrickStore.graph: + if BrickStore.graph is not None: # `if BrickStore.graph` by itself takes ages. BrickStore.end_transaction() IfcStore.end_transaction(operator) blenderbim.bim.handler.refresh_ui_data() diff --git a/src/blenderbim/blenderbim/bim/module/brick/data.py b/src/blenderbim/blenderbim/bim/module/brick/data.py index e5c31a70b6..db8cb856d9 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/data.py +++ b/src/blenderbim/blenderbim/bim/module/brick/data.py @@ -46,7 +46,7 @@ class BrickschemaData: @classmethod def get_is_loaded(cls): - return BrickStore.graph is not None + return BrickStore.graph is not None # `if BrickStore.graph` by itself takes ages. @classmethod def active_relations(cls): diff --git a/src/blenderbim/blenderbim/bim/module/profile/data.py b/src/blenderbim/blenderbim/bim/module/profile/data.py index 89103cc0a4..9d74d2203c 100644 --- a/src/blenderbim/blenderbim/bim/module/profile/data.py +++ b/src/blenderbim/blenderbim/bim/module/profile/data.py @@ -50,6 +50,8 @@ class ProfileData: @classmethod def active_profile_users(cls): profiles_props = bpy.context.scene.BIMProfileProperties + if profiles_props.active_profile_index >= len(profiles_props.profiles): + return 0 profile_prop = profiles_props.profiles[profiles_props.active_profile_index] profile_ifc = tool.Ifc.get().by_id(profile_prop.ifc_definition_id) return tool.Ifc.get().get_total_inverses(profile_ifc) diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 8526bffa49..706a3b8163 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -298,7 +298,7 @@ class BIM_PT_tabs(Panel): self.draw_tab_entry(row, "NETWORK_DRIVE", "SERVICES", is_ifc_project, aprops.tab == "SERVICES") self.draw_tab_entry(row, "EDITMODE_HLT", "STRUCTURE", is_ifc_project, aprops.tab == "STRUCTURE") self.draw_tab_entry(row, "NLA", "SCHEDULING", is_ifc_project, aprops.tab == "SCHEDULING") - self.draw_tab_entry(row, "PACKAGE", "FM", is_ifc_project, aprops.tab == "FM") + self.draw_tab_entry(row, "PACKAGE", "FM", True, aprops.tab == "FM") self.draw_tab_entry(row, "COMMUNITY", "QUALITY", True, aprops.tab == "QUALITY") self.draw_tab_entry(row, "BLENDER", "BLENDER", True, aprops.tab == "BLENDER") row.operator("bim.switch_tab", text="", emboss=False, icon="UV_SYNC_SELECT") @@ -669,7 +669,7 @@ class BIM_PT_tab_handover(Panel): @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "FM") and tool.Ifc.get() + return tool.Blender.is_tab(context, "FM") def draw(self, context): pass @@ -684,7 +684,7 @@ class BIM_PT_tab_operations(Panel): @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "FM") and tool.Ifc.get() + return tool.Blender.is_tab(context, "FM") def draw(self, context): pass diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index ed58180852..847adfe09f 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -144,8 +144,8 @@ class Brick(blenderbim.core.tool.Brick): query = BrickStore.graph.query( """ PREFIX rdfs: - SELECT ?label { - <{brick_uri}> rdfs:label ?label . + SELECT ?label { + <{brick_uri}> rdfs:label ?label . } LIMIT 1 """.replace("{brick_uri}", brick_uri)) diff --git a/src/blenderbim/test/bim/feature/brick.feature b/src/blenderbim/test/bim/feature/brick.feature index f48547609a..06fcfbdefd 100644 --- a/src/blenderbim/test/bim/feature/brick.feature +++ b/src/blenderbim/test/bim/feature/brick.feature @@ -3,28 +3,33 @@ Feature: Brick Scenario: Create Brick project Given an empty Blender session + And the Brickschema is stubbed When I press "bim.new_brick_file" Then nothing happens Scenario: Load Brick project Given an empty Blender session + And the Brickschema is stubbed When I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" Then nothing happens Scenario: View Brick class Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" When I press "bim.view_brick_class(brick_class='Building')" Then nothing happens Scenario: View Brick item Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" When I press "bim.view_brick_item(item='https://example.org/digitaltwin#lighting_zone_1')" Then nothing happens Scenario: Rewind Brick class Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And I press "bim.view_brick_class(brick_class='Building')" When I press "bim.rewind_brick_class" @@ -32,19 +37,23 @@ Scenario: Rewind Brick class Scenario: Close Brick project Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" When I press "bim.close_brick_project" Then nothing happens Scenario: Close Brick project then create Brick project Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And I press "bim.close_brick_project" + And the Brickschema is stubbed When I press "bim.new_brick_file" - Then nothing happens + Then nothing happens Scenario: Add Brick - vanilla Brick with no IFC Given an empty Blender session + And the Brickschema is stubbed And I press "bim.new_brick_file" And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" And I set "scene.BIMBrickProperties.new_brick_label" to "abc123" @@ -53,6 +62,7 @@ Scenario: Add Brick - vanilla Brick with no IFC Scenario: Add Brick - from geometry without a Brick IFC library Given an empty IFC project + And the Brickschema is stubbed And I press "bim.new_brick_file" And I add a cube And the object "Cube" is selected @@ -66,6 +76,7 @@ Scenario: Add Brick - from geometry without a Brick IFC library Scenario: Add Brick - from geometry with a Brick IFC library Given an empty IFC project + And the Brickschema is stubbed And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" @@ -79,12 +90,14 @@ Scenario: Add Brick - from geometry with a Brick IFC library Scenario: Refresh Brick viewer Given an empty Blender session + And the Brickschema is stubbed And I press "bim.new_brick_file" When I press "bim.refresh_brick_viewer" Then nothing happens Scenario: Add Brick relation - vanilla Brick with no IFC Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" And I press "bim.view_brick_class(brick_class='Lighting_Zone')" @@ -96,16 +109,17 @@ Scenario: Add Brick relation - vanilla Brick with no IFC Scenario: Add Brick relation - vanilla Brick with no IFC and with split screen Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" And I set "scene.BIMBrickProperties.brick_entity_create_type" to "Location" And I set "scene.BIMBrickProperties.new_brick_label" to "abc123" - And I set "scene.BIMBrickProperties.brick_entity_class" to "https://brickschema.org/schema/Brick#Room" + And I set "scene.BIMBrickProperties.brick_entity_class" to "https://brickschema.org/schema/Brick#Building" And I press "bim.add_brick" And I press "bim.view_brick_class(brick_class='Lighting_Zone')" And I set "scene.BIMBrickProperties.active_brick_index" to "0" And I set "scene.BIMBrickProperties.split_screen_toggled" to "True" - And I press "bim.view_brick_class(brick_class='Room', split_screen=True)" + And I press "bim.view_brick_class(brick_class='Building', split_screen=True)" And I set "scene.BIMBrickProperties.split_screen_active_brick_index" to "0" And I set "scene.BIMBrickProperties.brick_create_relations_toggled" to "True" When I press "bim.add_brick_relation" @@ -113,6 +127,7 @@ Scenario: Add Brick relation - vanilla Brick with no IFC and with split screen Scenario: Remove Brick - vanilla Brick Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And I press "bim.view_brick_class(brick_class='Lighting_Zone')" And I set "scene.BIMBrickProperties.active_brick_index" to "0" @@ -121,6 +136,7 @@ Scenario: Remove Brick - vanilla Brick Scenario: Remove Brick - with a Brick IFC library reference Given an empty IFC project + And the Brickschema is stubbed And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" @@ -129,13 +145,14 @@ Scenario: Remove Brick - with a Brick IFC library reference And the object "IfcChiller/Cube" is selected And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" And I press "bim.add_brick" - And I press "bim.view_brick_class(brick_class='Chiller')" + And I press "bim.view_brick_class(brick_class='Equipment')" And I set "scene.BIMBrickProperties.active_brick_index" to "0" When I press "bim.remove_brick" Then nothing happens Scenario: Change viewer list root Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And I set "scene.BIMBrickProperties.set_list_root_toggled" to "True" When I set "scene.BIMBrickProperties.brick_list_root" to "Location" @@ -143,6 +160,7 @@ Scenario: Change viewer list root Scenario: Change viewer list root - split screen Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And I set "scene.BIMBrickProperties.set_list_root_toggled" to "True" And I set "scene.BIMBrickProperties.split_screen_toggled" to "True" @@ -151,17 +169,20 @@ Scenario: Change viewer list root - split screen Scenario: Toggle split screen Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" When I set "scene.BIMBrickProperties.split_screen_toggled" to "True" Then nothing happens Scenario: Set active namespace Given an empty Blender session + And the Brickschema is stubbed When I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" Then nothing happens Scenario: Bind new namespace Given an empty Blender session + And the Brickschema is stubbed And I set "scene.BIMBrickProperties.new_brick_namespace_alias" to "digitaltwin2" And I set "scene.BIMBrickProperties.new_brick_namespace_uri" to "https://example.org/digitaltwin2#" When I press "bim.add_brick_namespace" @@ -169,12 +190,14 @@ Scenario: Bind new namespace Scenario: Convert brick project Given an empty IFC project + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" When I press "bim.convert_brick_project" Then nothing happens Scenario: Convert IFC to brick Given an empty IFC project + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And I press "bim.convert_brick_project" And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" @@ -188,6 +211,7 @@ Scenario: Convert IFC to brick Scenario: Assign brick reference Given an empty IFC project + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" And I set "scene.BIMBrickProperties.brick_entity_class" to "https://brickschema.org/schema/Brick#Chiller" @@ -201,4 +225,4 @@ Scenario: Assign brick reference And the object "IfcChiller/Cube" is selected And I press "bim.convert_brick_project" When I press "bim.assign_brick_reference" - Then nothing happens \ No newline at end of file + Then nothing happens diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index 54381e95fe..fc9afdd286 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -47,6 +47,7 @@ Scenario: Add representation - add a representation with a scale factor applied And I add a cube And the object "Cube" is selected When the object "Cube" is scaled to "2" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" Then the object "IfcWall/Cube" has no scale @@ -58,6 +59,7 @@ Scenario: Add representation - add a representation with a scale factor removed And I press "object.duplicate_move_linked" And the object "Cube" is selected When the object "Cube" is scaled to "2" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" Then the object "IfcWall/Cube" has no scale @@ -179,6 +181,7 @@ Scenario: Update representation - updating a tessellation Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.update_representation(obj='IfcWall/Cube')" @@ -188,6 +191,7 @@ Scenario: Update representation - updating a layered extrusion Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -318,7 +322,7 @@ Scenario: Override duplicate move - copying a coloured representation When I duplicate the selected objects And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" And an empty Blender session is started - And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" + And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc', should_start_fresh_session=False)" Then the material "Material" colour is "1,0,0,1" And the object "IfcWall/Cube" has the material "Material" And the object "IfcWall/Cube.001" has the material "Material" diff --git a/src/blenderbim/test/bim/feature/project.feature b/src/blenderbim/test/bim/feature/project.feature index b44228a69c..d61d895b43 100644 --- a/src/blenderbim/test/bim/feature/project.feature +++ b/src/blenderbim/test/bim/feature/project.feature @@ -456,7 +456,7 @@ Scenario: Export IFC - with changed style colour synchronised When the material "Material" colour is set to "1,0,0,1" And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" And an empty Blender session is started - And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" + And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc', should_start_fresh_session=False)" Then the material "Material" colour is "1,0,0,1" Scenario: Export IFC - with changed style element synchronised @@ -471,5 +471,5 @@ Scenario: Export IFC - with changed style element synchronised And the material "Material.001" colour is set to "1,0,0,1" And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" And an empty Blender session is started - And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" + And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc', should_start_fresh_session=False)" Then the material "Material.001" colour is "1,0,0,1" diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index a5e073d510..9fd1c4317f 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -24,6 +24,7 @@ import ifcopenshell import blenderbim.tool as tool import blenderbim.bim from blenderbim.bim.ifc import IfcStore +from blenderbim.tool.brick import BrickStore from blenderbim.bim.module.model.data import AuthoringData from pytest_bdd import scenarios, given, when, then, parsers from mathutils import Vector @@ -71,6 +72,8 @@ def an_empty_blender_session(): if len(bpy.data.objects) > 0: bpy.data.batch_remove(bpy.data.objects) bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) + if len(bpy.data.materials) > 0: + bpy.data.batch_remove(bpy.data.materials) # default project settings bpy.context.scene.unit_settings.system = "METRIC" @@ -92,6 +95,13 @@ def an_empty_ifc_2x3_project(): bpy.ops.bim.create_project() +@given("the Brickschema is stubbed") +def the_brickschema_is_stubbed(): + # This makes things run faster since we don't need to load the entire brick schema + cwd = os.path.dirname(os.path.realpath(__file__)) + BrickStore.schema = os.path.join(cwd, "..", "files", "BrickStub.ttl") + + @when("I load a new pset template file") def i_load_a_new_pset_template_file(): IfcStore.pset_template_path = bpy.context.scene.BIMPsetTemplateProperties.pset_template_files diff --git a/src/blenderbim/test/files/BrickStub.ttl b/src/blenderbim/test/files/BrickStub.ttl index 883ec92353..bca967082d 100644 --- a/src/blenderbim/test/files/BrickStub.ttl +++ b/src/blenderbim/test/files/BrickStub.ttl @@ -22,3 +22,16 @@ brick:Location a owl:Class ; brick:Building a owl:Class ; rdfs:subClassOf brick:Class, brick:Location . + +brick:Chiller a owl:Class, + sh:NodeShape ; + rdfs:subClassOf brick:HVAC_Equipment . + +brick:HVAC_Equipment a owl:Class, + sh:NodeShape ; + rdfs:subClassOf brick:Equipment . + +brick:Equipment a owl:Class, + sh:NodeShape ; + rdfs:subClassOf brick:Class, + brick:Entity . diff --git a/src/blenderbim/test/tool/test_brick.py b/src/blenderbim/test/tool/test_brick.py index 8996796cff..b70b9b2240 100644 --- a/src/blenderbim/test/tool/test_brick.py +++ b/src/blenderbim/test/tool/test_brick.py @@ -39,12 +39,12 @@ class TestImplementsTool(NewFile): class TestAddBrick(NewFile): def test_run(self): BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") - result = subject.add_brick("https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "label") + result = subject.add_brick( + "https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "label" + ) assert "https://example.org/digitaltwin#" in result assert list( - BrickStore.graph.triples( - (URIRef(result), A, URIRef("https://brickschema.org/schema/Brick#Equipment")) - ) + BrickStore.graph.triples((URIRef(result), A, URIRef("https://brickschema.org/schema/Brick#Equipment"))) ) assert list( BrickStore.graph.triples( @@ -89,7 +89,7 @@ class TestAddBrickFromElement(NewFile): (URIRef(uri), URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("Chiller")) ) ) - + def test_run_no_element_name(self): ifc = ifcopenshell.file() element = ifc.createIfcChiller() @@ -120,18 +120,10 @@ class TestAddBrickifcProject(NewFile): result = subject.add_brickifc_project("http://example.org/digitaltwin#") assert result == f"http://example.org/digitaltwin#{project.GlobalId}" brick = URIRef(result) + assert list(BrickStore.graph.triples((brick, A, REF.ifcProject))) + assert list(BrickStore.graph.triples((brick, REF.ifcProjectID, Literal(project.GlobalId)))) assert list( - BrickStore.graph.triples((brick, A, REF.ifcProject)) - ) - assert list( - BrickStore.graph.triples( - (brick, REF.ifcProjectID, Literal(project.GlobalId)) - ) - ) - assert list( - BrickStore.graph.triples( - (brick, REF.ifcFileLocation, Literal(bpy.context.scene.BIMProperties.ifc_file)) - ) + BrickStore.graph.triples((brick, REF.ifcFileLocation, Literal(bpy.context.scene.BIMProperties.ifc_file))) ) assert list( BrickStore.graph.triples( @@ -148,38 +140,20 @@ class TestAddBrickifcReference(NewFile): project = URIRef(f"http://example.org/digitaltwin#{tool.Ifc.get().by_type('IfcProject')[0].GlobalId}") subject.add_brickifc_reference("http://example.org/digitaltwin#foo", element, project) brick = URIRef("http://example.org/digitaltwin#foo") - bnode = list( - BrickStore.graph.triples((brick, A, REF.IFCReference)) - ) - assert list( - BrickStore.graph.triples( - (bnode, REF.hasIfcProjectReference, URIRef(project)) - ) - ) - assert list( - BrickStore.graph.triples( - (bnode, REF.ifcGlobalID, Literal(element.GlobalId)) - ) - ) - assert list( - BrickStore.graph.triples( - (bnode, REF.ifcName, Literal(element.Name)) - ) - ) + bnode = list(BrickStore.graph.triples((brick, A, REF.IFCReference))) + assert list(BrickStore.graph.triples((bnode, REF.hasIfcProjectReference, URIRef(project)))) + assert list(BrickStore.graph.triples((bnode, REF.ifcGlobalID, Literal(element.GlobalId)))) + assert list(BrickStore.graph.triples((bnode, REF.ifcName, Literal(element.Name)))) class TestAddRelation(NewFile): def test_run(self): BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") source = subject.add_brick( - "http://example.org/digitaltwin#", - "https://brickschema.org/schema/Brick#Equipment", - "source" + "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "source" ) destination = subject.add_brick( - "http://example.org/digitaltwin#", - "https://brickschema.org/schema/Brick#Equipment", - "destination" + "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "destination" ) subject.add_relation(source, "https://brickschema.org/schema/Brick#feeds", destination) assert list( @@ -196,7 +170,9 @@ class TestAddRelation(NewFile): class TestRemoveRelation(NewFile): def test_run(self): TestAddRelation().test_run() - source, relation, destination = list(BrickStore.graph.triples((None, URIRef("https://brickschema.org/schema/Brick#feeds"), None)))[0] + source, relation, destination = list( + BrickStore.graph.triples((None, URIRef("https://brickschema.org/schema/Brick#feeds"), None)) + )[0] subject.remove_relation(source, relation, destination) assert not list( BrickStore.graph.triples( @@ -214,7 +190,7 @@ class TestClearBrickBrowser(NewFile): bpy.context.scene.BIMBrickProperties.bricks.add() subject.clear_brick_browser() assert len(bpy.context.scene.BIMBrickProperties.bricks) == 0 - + def test_run_split_screen(self): bpy.context.scene.BIMBrickProperties.split_screen_bricks.add() subject.clear_brick_browser(split_screen=True) @@ -235,7 +211,9 @@ class TestClearProject(NewFile): class TestExportBrickAttributes(NewFile): def test_run(self): BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") - brick = subject.add_brick("https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "name") + brick = subject.add_brick( + "https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "name" + ) assert subject.export_brick_attributes(brick) == { "Identification": brick, "Name": "name", @@ -243,7 +221,9 @@ class TestExportBrickAttributes(NewFile): def test_run_ifc2x3(self): BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") - brick = subject.add_brick("https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "name") + brick = subject.add_brick( + "https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "name" + ) tool.Ifc.set(ifcopenshell.file(schema="IFC2X3")) assert subject.export_brick_attributes(brick) == { "ItemReference": brick, @@ -345,6 +325,7 @@ class TestGetParentSpace(NewFile): assert subject.get_parent_space(subelement) == element assert subject.get_parent_space(element) is None + class TestGetElementContainer(NewFile): def test_nothing(cls): pass @@ -491,13 +472,17 @@ class TestPopBrickBreadcrumb(NewFile): class TestRemoveBrick(NewFile): def test_run(self): BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") - result = subject.add_brick("http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "label") + result = subject.add_brick( + "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "label" + ) subject.remove_brick(result) assert not list(BrickStore.graph.triples((URIRef(result), None, None))) def test_run_with_bnode(self): BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") - result = subject.add_brick("http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "label") + result = subject.add_brick( + "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "label" + ) TestAddBrickifcProject().test_run() element = tool.Ifc.get().createIfcChiller(ifcopenshell.guid.new()) element.Name = "Chiller" @@ -550,4 +535,4 @@ class TestAddNamespace(NewFile): def test_run(self): BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") subject.add_namespace("digitaltwin", "http://example.org/digitaltwin") - assert ("digitaltwin", "http://example.org/digitaltwin") in BrickStore.namespaces \ No newline at end of file + assert ("digitaltwin", "http://example.org/digitaltwin") in BrickStore.namespaces diff --git a/src/ifcopenshell-python/test/util/test_brick.py b/src/ifcopenshell-python/test/util/test_brick.py index cefd7a80d4..52a99b1a1a 100644 --- a/src/ifcopenshell-python/test/util/test_brick.py +++ b/src/ifcopenshell-python/test/util/test_brick.py @@ -38,10 +38,3 @@ class TestGetBrickTypeIFC2X3(test.bootstrap.IFC2X3): type_element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcAirTerminalBoxType") ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=type_element) assert subject.get_brick_type(element) == "https://brickschema.org/schema/Brick#TerminalUnit" - - -class TestGetBrickElementsIFC4(test.bootstrap.IFC4): - def test_run(self): - element = self.file.createIfcAirTerminalBox() - self.file.createIfcWall() - assert subject.get_brick_elements(self.file) == {element} From 256c0baa14256cc24c429d82b9270004ca655e91 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 25 Aug 2023 16:51:45 +1000 Subject: [PATCH 81/86] Accommodate invalid Revit models where they have invalid material relationships. --- src/ifcopenshell-python/ifcopenshell/util/element.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 80923b6c2a..ac84cc7046 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -88,6 +88,7 @@ def get_pset(element, name, prop=None, should_inherit=True): return type_pset return value + def get_psets(element, psets_only=False, qtos_only=False, should_inherit=True): """Retrieve property sets, their related properties' names & values and ids. @@ -451,7 +452,6 @@ def get_styles(element): return styles - def get_elements_by_material(ifc_file, material): """Retrieves the elements related to a material. @@ -475,7 +475,7 @@ def get_elements_by_material(ifc_file, material): results = set() for inverse in ifc_file.get_inverse(material): if inverse.is_a("IfcRelAssociatesMaterial"): - results.update(inverse.RelatedObjects) + results.update(inverse.RelatedObjects or []) # See Revit bug #675 elif inverse.is_a("IfcMaterialLayer"): for material_set in inverse.ToMaterialLayerSet: results.update(get_elements_by_material(ifc_file, material_set)) @@ -869,7 +869,7 @@ def unbatch_remove_deep2(ifc_file): :rtype: ifcopenshell.file.file """ ifc_string = ifc_file.to_string() - lines = iter(ifc_string.split('\n')) + lines = iter(ifc_string.split("\n")) ids_to_delete = iter(sorted([e.id() for e in ifc_file.to_delete])) id_to_delete = next(ids_to_delete, None) result = [] From cc500ae833d7d564d9e6bc199dd5ab719caa8575 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 25 Aug 2023 16:59:08 +1000 Subject: [PATCH 82/86] Fix #3635. Minor fix. --- src/ifccsv/ifccsv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py index b99288ca4b..621f2d6595 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -115,7 +115,7 @@ class IfcCsv: self.group_results(groups, attributes) self.summarise_results(summaries, attributes) - self.sort_results(sort, attributes) + self.sort_results(sort, attributes, include_global_id) if format == "csv": self.export_csv(output, delimiter=delimiter) @@ -225,7 +225,7 @@ class IfcCsv: self.summaries[si] = max(summary_values[si]) self.summaries[si] = summary_type.title() + ": " + str(self.summaries[si]) - def sort_results(self, sort, attributes): + def sort_results(self, sort, attributes, include_global_id): if sort: def natural_sort(value): if isinstance(value, str): From cf21ae22f1c385ec67d7281a512f56ccb6986b50 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 25 Aug 2023 17:15:21 +1000 Subject: [PATCH 83/86] Fix incorrect implementation of regex matching --- src/ifcopenshell-python/ifcopenshell/util/selector.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index f2dda2a9e5..097ea409ff 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -42,14 +42,14 @@ filter_elements_grammar = lark.Lark( location: "location" comparison value query: "query:" keys comparison value - pset: quoted_string | unquoted_string | regex_string - prop: quoted_string | unquoted_string | regex_string + pset: quoted_string | regex_string | unquoted_string + prop: quoted_string | regex_string | unquoted_string keys: quoted_string | unquoted_string attribute_name: /[A-Z]\\w+/ ifc_class: /Ifc\\w+/ - value: special | quoted_string | unquoted_string | regex_string + value: special | quoted_string | regex_string | unquoted_string unquoted_string: /[^.=\\s]+/ quoted_string: ESCAPED_STRING regex_string: "/" /[^\\/]+/ "/" @@ -428,7 +428,7 @@ class FacetTransformer(lark.Transformer): value = float(value) result = element_value == value elif isinstance(value, re.Pattern): - result = bool(value.match(element_value)) + result = bool(value.match(element_value)) if element_value is not None else False elif value in (None, True, False): result = element_value is value return result if comparison == "=" else not result From 5ebde4c79a3c223903bdd712aa048a9dacf025c8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 25 Aug 2023 11:58:41 +0500 Subject: [PATCH 84/86] small info message on bim.get_representation_ifc_parameters just to notify that it worked but haven't found any parameters --- src/blenderbim/blenderbim/bim/module/geometry/operator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index b50a82a4fd..226162559f 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -397,6 +397,8 @@ class GetRepresentationIfcParameters(bpy.types.Operator, Operator): def _execute(self, context): core.get_representation_ifc_parameters(tool.Geometry, obj=context.active_object) + parameters = context.active_object.data.BIMMeshProperties.ifc_parameters + self.report({"INFO"}, f"{len(parameters)} parameters found.") class CopyRepresentation(bpy.types.Operator, Operator): From 93502a05eae8b38252a8d44ae8b3a373fc142272 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 25 Aug 2023 12:43:32 +0200 Subject: [PATCH 85/86] Update draw.py --with-unify-inputs [True] --- src/ifcopenshell-python/ifcopenshell/draw.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/draw.py b/src/ifcopenshell-python/ifcopenshell/draw.py index df4169ddd8..ce2d6278df 100644 --- a/src/ifcopenshell-python/ifcopenshell/draw.py +++ b/src/ifcopenshell-python/ifcopenshell/draw.py @@ -61,6 +61,7 @@ class draw_settings: include_projection: bool = True prefilter: bool = True include_curves: bool = False + unify_inputs: bool = True def main(settings, files, iterators=None, merge_projection=True, progress_function=DO_NOTHING): @@ -135,6 +136,7 @@ def main(settings, files, iterators=None, merge_projection=True, progress_functi sr.setSubtractionSettings(W.ALWAYS) sr.setUsePrefiltering(settings.prefilter) + sr.setUnifyInputs(settings.unify_inputs) try: sh = ["none", "full", "left"].index(settings.storey_heights) From dadcbe61ea432ffde3c46a943e9cac2476f39e69 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 25 Aug 2023 12:48:45 +0200 Subject: [PATCH 86/86] #3158 Fix unify inputs --- src/serializers/SvgSerializer.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/serializers/SvgSerializer.cpp b/src/serializers/SvgSerializer.cpp index f1962e6570..b11db7287c 100644 --- a/src/serializers/SvgSerializer.cpp +++ b/src/serializers/SvgSerializer.cpp @@ -699,6 +699,11 @@ void SvgSerializer::write(const IfcGeom::BRepElement* brep_obj) { IfcUtil::IfcBaseEntity* storey = p ? p->first : nullptr; double elev = p ? p->second : std::numeric_limits::quiet_NaN(); // @todo is it correct to call nameElement() here with a single storey (what if this element spans multiple?) + + if (unify_inputs_) { + compound_local = IfcGeom::util::unify(compound_local, 1.e-6); + } + geometry_data data{ compound_local, dash_arrays, trsf, brep_obj->product(), storey, elev, brep_obj->name(), nameElement(storey, brep_obj) }; if (auto_section_ || auto_elevation_ || section_ref_ || elevation_ref_ || elevation_ref_guid_ || deferred_section_data_) { @@ -1187,14 +1192,7 @@ void SvgSerializer::write(const geometry_data& data) { // Iterate over components of compound to have better chance of matching section edges to closed wires for (; it.More(); it.Next(), ++dash_it) { - const TopoDS_Shape& subshape_before_unification = it.Value(); - TopoDS_Shape subshape; - - if (unify_inputs_) { - subshape = IfcGeom::util::unify(subshape_before_unification, 1. - 6); - } else { - subshape = subshape_before_unification; - } + const TopoDS_Shape& subshape = it.Value(); Bnd_Box bb; try {