From 5cea988911e4f2824f008cafd1a1146510acb0eb Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 8 Apr 2021 21:03:37 +1000 Subject: [PATCH 01/64] You can now assign parameterized profiles to material profiles. Thanks Jesusbill! --- .../bim/module/material/__init__.py | 1 + .../bim/module/material/operator.py | 102 ++++++++++++++++-- .../blenderbim/bim/module/material/prop.py | 30 +++++- .../blenderbim/bim/module/material/ui.py | 36 +++++++ .../api/material/assign_profile.py | 14 +++ .../ifcopenshell/api/material/edit_profile.py | 3 + .../api/profile/add_parameterized_profile.py | 9 ++ .../ifcopenshell/api/profile/data.py | 17 +++ 8 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/profile/add_parameterized_profile.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/profile/data.py diff --git a/src/blenderbim/blenderbim/bim/module/material/__init__.py b/src/blenderbim/blenderbim/bim/module/material/__init__.py index 7680e62122..83ef17c184 100644 --- a/src/blenderbim/blenderbim/bim/module/material/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/material/__init__.py @@ -10,6 +10,7 @@ classes = ( operator.RemoveConstituent, operator.AddProfile, operator.RemoveProfile, + operator.AssignParameterizedProfile, operator.AddLayer, operator.RemoveLayer, operator.ReorderMaterialSetItem, diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index c77e96048b..b5ff702d02 100644 --- a/src/blenderbim/blenderbim/bim/module/material/operator.py +++ b/src/blenderbim/blenderbim/bim/module/material/operator.py @@ -1,8 +1,36 @@ import bpy +import json import ifcopenshell.api import ifcopenshell.util.attribute from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.material.data import Data +from ifcopenshell.api.profile.data import Data as ProfileData + + +class AssignParameterizedProfile(bpy.types.Operator): + bl_idname = "bim.assign_parameterized_profile" + bl_label = "Assign Parameterized Profile" + ifc_class: bpy.props.StringProperty() + material_profile: bpy.props.IntProperty() + obj: bpy.props.StringProperty() + + def execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + self.file = IfcStore.get_file() + profile = ifcopenshell.api.run( + "profile.add_parameterized_profile", + self.file, + **{"ifc_class": self.ifc_class}, + ) + ifcopenshell.api.run( + "material.assign_profile", + self.file, + **{"material_profile": self.file.by_id(self.material_profile), "profile": profile} + ) + Data.load_profiles() + ProfileData.load(self.file) + bpy.ops.bim.enable_editing_material_set_item(obj=obj.name, material_set_item=self.material_profile) + return {"FINISHED"} class AddMaterial(bpy.types.Operator): @@ -370,8 +398,8 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object - props = obj.BIMObjectMaterialProperties - props.active_material_set_item_id = self.material_set_item + self.props = obj.BIMObjectMaterialProperties + self.props.active_material_set_item_id = self.material_set_item product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id] material_set_item = self.file.by_id(self.material_set_item) @@ -384,17 +412,24 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): else: material_set_item_data = {} - props.material_set_item_material = str(material_set_item_data["Material"]) + self.props.material_set_item_material = str(material_set_item_data["Material"]) - while len(props.material_set_item_attributes) > 0: - props.material_set_item_attributes.remove(0) + self.load_set_item_attributes(material_set_item, material_set_item_data) + if material_set_item.is_a("IfcMaterialProfile"): + self.load_profile_attributes(material_set_item, material_set_item_data) + + return {"FINISHED"} + + def load_set_item_attributes(self, material_set_item, material_set_item_data): + while len(self.props.material_set_item_attributes) > 0: + self.props.material_set_item_attributes.remove(0) for attribute in IfcStore.get_schema().declaration_by_name(material_set_item.is_a()).all_attributes(): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) if data_type == "entity": continue if attribute.name() in material_set_item_data: - new = props.material_set_item_attributes.add() + new = self.props.material_set_item_attributes.add() new.name = attribute.name() new.is_null = material_set_item_data[attribute.name()] is None new.data_type = data_type @@ -406,7 +441,45 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): new.int_value = 0 if new.is_null else material_set_item_data[attribute.name()] elif data_type == "boolean": new.bool_value = False if new.is_null else material_set_item_data[attribute.name()] - return {"FINISHED"} + + def load_profile_attributes(self, material_set_item, material_set_item_data): + while len(self.props.material_set_item_profile_attributes) > 0: + self.props.material_set_item_profile_attributes.remove(0) + + if not material_set_item_data["Profile"]: + return + + profile = self.file.by_id(material_set_item_data["Profile"]) + profile_data = ProfileData.profiles[material_set_item_data["Profile"]] + + for attribute in IfcStore.get_schema().declaration_by_name(profile.is_a()).all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + if attribute.name() in profile_data: + new = self.props.material_set_item_profile_attributes.add() + new.name = attribute.name() + new.is_null = profile_data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if data_type == "string": + new.string_value = "" if new.is_null else profile_data[attribute.name()] + elif data_type == "float": + new.float_value = 0.0 if new.is_null else profile_data[attribute.name()] + elif data_type == "integer": + new.int_value = 0 if new.is_null else profile_data[attribute.name()] + elif data_type == "boolean": + new.bool_value = False if new.is_null else profile_data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if profile_data[attribute.name()]: + new.enum_value = profile_data[attribute.name()] + + # Force null to be false if the attribute is mandatory because when we first assign a profile, all of + # its fields are null (which is illegal). + # TODO: find a better solution. + if not new.is_optional: + new.is_null = False class DisableEditingMaterialSetItem(bpy.types.Operator): @@ -468,16 +541,31 @@ class EditMaterialSetItem(bpy.types.Operator): ) Data.load_layers() elif product_data["type"] == "IfcMaterialProfileSet": + profile_attributes = {} + for attribute in props.material_set_item_profile_attributes: + if attribute.data_type == "string": + value = attribute.string_value + elif attribute.data_type == "float": + value = attribute.float_value + elif attribute.data_type == "integer": + value = attribute.int_value + elif attribute.data_type == "boolean": + value = attribute.bool_value + elif attribute.data_type == "enum": + value = attribute.enum_value + profile_attributes[attribute.name] = None if attribute.is_null else value ifcopenshell.api.run( "material.edit_profile", self.file, **{ "profile": self.file.by_id(self.material_set_item), "attributes": attributes, + "profile_attributes": profile_attributes, "material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material_set_item_material)), }, ) Data.load_profiles() + ProfileData.load(self.file) else: pass diff --git a/src/blenderbim/blenderbim/bim/module/material/prop.py b/src/blenderbim/blenderbim/bim/module/material/prop.py index 4795832bbc..ffa7c5d87a 100644 --- a/src/blenderbim/blenderbim/bim/module/material/prop.py +++ b/src/blenderbim/blenderbim/bim/module/material/prop.py @@ -1,5 +1,5 @@ import bpy -import blenderbim.bim.schema # refactor +import blenderbim.bim.schema # refactor from ifcopenshell.api.material.data import Data from blenderbim.bim.ifc import IfcStore from blenderbim.bim.prop import StrProperty, Attribute @@ -17,6 +17,29 @@ from bpy.props import ( materials_enum = [] materialtypes_enum = [] +profileclasses_enum = [] +parameterizedprofileclasses_enum = [] + + +def getProfileClasses(self, context): + global profileclasses_enum + if len(profileclasses_enum) == 0 and IfcStore.get_schema(): + profileclasses_enum.clear() + profileclasses_enum = [ + (t.name(), t.name(), "") for t in IfcStore.get_schema().declaration_by_name("IfcProfileDef").subtypes() + ] + return profileclasses_enum + + +def getParameterizedProfileClasses(self, context): + global parameterizedprofileclasses_enum + if len(parameterizedprofileclasses_enum) == 0 and IfcStore.get_schema(): + parameterizedprofileclasses_enum.clear() + parameterizedprofileclasses_enum = [ + (t.name(), t.name(), "") + for t in IfcStore.get_schema().declaration_by_name("IfcParameterizedProfileDef").subtypes() + ] + return parameterizedprofileclasses_enum def getMaterials(self, context): @@ -52,4 +75,9 @@ class BIMObjectMaterialProperties(PropertyGroup): material_set_attributes: CollectionProperty(name="Material Set Attributes", type=Attribute) active_material_set_item_id: IntProperty(name="Active Material Set ID") material_set_item_attributes: CollectionProperty(name="Material Set Item Attributes", type=Attribute) + material_set_item_profile_attributes: CollectionProperty(name="Material Set Item Profile Attributes", type=Attribute) material_set_item_material: EnumProperty(items=getMaterials, name="Material") + profile_classes: EnumProperty(items=getProfileClasses, name="Profile Classes") + parameterized_profile_classes: EnumProperty( + items=getParameterizedProfileClasses, name="Parameterized Profile Classes" + ) diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index 6d432f673a..591fa8ccb3 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -1,5 +1,6 @@ from bpy.types import Panel from ifcopenshell.api.material.data import Data +from ifcopenshell.api.profile.data import Data as ProfileData from blenderbim.bim.ifc import IfcStore @@ -46,6 +47,8 @@ class BIM_PT_object_material(Panel): Data.load(IfcStore.get_file()) if self.oprops.ifc_definition_id not in Data.products: Data.load(IfcStore.get_file(), self.oprops.ifc_definition_id) + if not ProfileData.is_loaded: + ProfileData.load(self.file) self.product_data = Data.products[self.oprops.ifc_definition_id] if not Data.materials: @@ -172,6 +175,39 @@ class BIM_PT_object_material(Panel): row.prop(attribute, "bool_value", text=attribute.name) row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + if self.set_item_name == "profile": + self.draw_assign_profile_ui(box, item) + self.draw_editable_profile_ui(box, item) + + def draw_assign_profile_ui(self, layout, item): + row = layout.row(align=True) + row.prop(self.props, "profile_classes", text="") + if self.props.profile_classes == "IfcParameterizedProfileDef": + row.prop(self.props, "parameterized_profile_classes", text="") + op = row.operator("bim.assign_parameterized_profile", icon="GREASEPENCIL" if item["Profile"] else "ADD", text="") + op.ifc_class = self.props.parameterized_profile_classes + op.material_profile = item["id"] + else: + # TODO: support non parametric profiles by showing a list of named profiles to select from, or an + # eyedropper to pick profile geometry from the scene + row.operator("bim.disable_editing_material_set_item", icon="X", text="") + + def draw_editable_profile_ui(self, layout, item): + for attribute in self.props.material_set_item_profile_attributes: + row = layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "integer": + row.prop(attribute, "int_value", text=attribute.name) + elif attribute.data_type == "float": + row.prop(attribute, "float_value", text=attribute.name) + elif attribute.data_type == "boolean": + row.prop(attribute, "bool_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + def draw_read_only_set_item_ui(self, set_item_id, index, is_first=False, is_last=False): if self.product_data["type"] == "IfcMaterialList": item = Data.materials[set_item_id] diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py new file mode 100644 index 0000000000..43580552c4 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py @@ -0,0 +1,14 @@ +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"material_profile": None, "profile": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + if ( + self.settings["material_profile"].Profile + and len(self.file.get_inverse(self.settings["material_profile"].Profile)) == 1 + ): + self.file.remove(self.settings["material_profile"].Profile) + self.settings["material_profile"].Profile = self.settings["profile"] diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py index f833922d30..5ab716644b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py @@ -4,6 +4,7 @@ class Usecase(): self.settings = { "profile": None, "attributes": {}, + "profile_attributes": {}, "material": None } for key, value in settings.items(): @@ -13,3 +14,5 @@ class Usecase(): for name, value in self.settings["attributes"].items(): setattr(self.settings["profile"], name, value) self.settings["profile"].Material = self.settings["material"] + for name, value in self.settings["profile_attributes"].items(): + setattr(self.settings["profile"].Profile, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/add_parameterized_profile.py b/src/ifcopenshell-python/ifcopenshell/api/profile/add_parameterized_profile.py new file mode 100644 index 0000000000..39abc879e8 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/add_parameterized_profile.py @@ -0,0 +1,9 @@ +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"ifc_class": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + return self.file.create_entity(self.settings["ifc_class"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/data.py b/src/ifcopenshell-python/ifcopenshell/api/profile/data.py new file mode 100644 index 0000000000..99b96a13e9 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/data.py @@ -0,0 +1,17 @@ +class Data: + is_loaded = False + profiles = {} + + @classmethod + def purge(cls): + cls.is_loaded = False + cls.profiles = {} + + @classmethod + def load(cls, file): + if not file: + return + cls.profiles = {} + for profile in file.by_type("IfcProfileDef"): + cls.profiles[profile.id()] = profile.get_info() + cls.is_loaded = True From fd3a873bbdb28926590df52dfe637b470a4b3fec Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 9 Apr 2021 15:43:41 +1000 Subject: [PATCH 02/64] Fix bug where you couldn't do a partial import when whitelisting only spatial elements --- src/blenderbim/blenderbim/bim/import_ifc.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index 4a3ffa2d75..eb7a298c4f 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -414,7 +414,11 @@ class IfcImporter: self.exclude_elements |= self.native_elements def is_native(self, element): - if not element.Representation or not element.Representation.Representations or element.HasOpenings: + if ( + not element.Representation + or not element.Representation.Representations + or getattr(element, "HasOpenings", None) + ): return representations = self.get_transformed_body_representations(element.Representation.Representations) From 4b637c7049e4c076ed1cc36ec61950ae37526ccf Mon Sep 17 00:00:00 2001 From: johltn Date: Wed, 7 Apr 2021 14:18:06 +0200 Subject: [PATCH 03/64] Typo fix --- .gitignore | 5 +++++ src/ifcopenshell-python/ifcopenshell/ids.py | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 47c55fe281..29a0f337d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ # Dependency and build folders created by the build scripts +/_build-vs2017-x64/ +/_deps-vs2017-x64-installed/ +/_deps/ /deps*/ /build*/ /install*/ @@ -23,3 +26,5 @@ __pycache__ *.mo # Vim *.swp + + diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 372af71eea..8d2daa2082 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -197,10 +197,10 @@ class specification: phrases[0].tagName == "applicability" or error("expected ") phrases[1].tagName == "requirements" or error("expected ") - self.applicabiliy, self.requirements = (boolean_and(parse_rules(phrase)) for phrase in phrases) + self.applicability, self.requirements = (boolean_and(parse_rules(phrase)) for phrase in phrases) def __call__(self, inst, logger): - if self.applicabiliy(inst, logger): + if self.applicability(inst, logger): valid = self.requirements(inst, logger) if valid: logger.info(str(self) + "\n%s has" % inst + " " + str(valid) + " so is compliant") @@ -208,7 +208,7 @@ class specification: logger.error(str(self) + "\n%s has" % inst + " " + str(valid) + " so is not compliant") def __str__(self): - return "Given an instance with %(applicabiliy)s\nWe expect %(requirements)s" % self.__dict__ + return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ class ids: From 0c9d555693c5d29b467c75fddbfe1441cf7ca01d Mon Sep 17 00:00:00 2001 From: johltn Date: Fri, 9 Apr 2021 10:16:08 +0200 Subject: [PATCH 04/64] Handle more restrictions, save validation results to JSON objects, modification to classification class, and partly handle material terms --- src/ifcopenshell-python/ifcopenshell/ids.py | 104 +++++++++++++++----- 1 file changed, 80 insertions(+), 24 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 8d2daa2082..542793a664 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -64,7 +64,11 @@ class facet(metaclass=meta_facet): yield k, getattr(self, k) def __str__(self): - return self.message % dict(list(self)) + di = dict(list(self)) + for k, v in di.items(): + if isinstance(v, str) and not len(v): + di[k] = "not specified" + return self.message % di class entity(facet): @@ -88,19 +92,19 @@ class classification(facet): """ parameters = ["system", "value"] - message = "a classification reference to '%(value)s' from '%(system)s'" + message = "a classification reference '%(value)s' from '%(system)s'" def __call__(self, inst, logger): refs = [] for association in inst.HasAssociations: if association.is_a("IfcRelAssociatesClassification"): cref = association.RelatingClassification - refs.append((cref.ReferencedSource, cref.Name)) + refs.append((cref.ReferencedSource.Name, cref.ItemReference)) return facet_evaluation( (self.system, self.value) in refs, # @todo - "", + "[classification_eval_todo]", ) @@ -109,17 +113,19 @@ class property(facet): The IDS property facet implenented using `ifcopenshell.util.element` """ - parameters = ["property", "propertyset", "value"] - message = "a property '%(property)s' in '%(propertyset)s' with value '%(value)s'" + parameters = ["name", "propertyset", "value"] + + # import pdb;pdb.set_trace() + message = "a property '%(name)s' in '%(propertyset)s' with value '%(value)s'" def __call__(self, inst, logger): props = ifcopenshell.util.element.get_psets(inst) pset = props.get(self.propertyset) - val = pset.get(self.property) if pset else None + val = pset.get(self.name) if pset else None logger.debug("Testing %s == %s", val, self.value) di = { - "property": self.property, + "name": self.name, "propertyset": self.propertyset, "value": val, } @@ -128,13 +134,38 @@ class property(facet): msg = self.message % di else: if pset: - msg = "a set '%(propertyset)s', but no property '%(property)'" % di + msg = "a set '%(propertyset)s', but no property '%(name)'" % di else: msg = "no set '%(propertyset)s'" % di return facet_evaluation(val == self.value, msg) +class material(facet): + """ + The IDS material facet + """ + parameters = ["name", "value"] + message = "a material '%(name)s with value '%(value)s'" + + def __call__(self, inst, logger): + material_relations = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] + names = [] + for rel in material_relations: + if rel.RelatingMaterial.is_a() == "IfcMaterialLayerSetUsage": + layers = rel.RelatingMaterial.ForLayerSet.MaterialLayers + names = [layer.Material.Name for layer in layers] + elif rel.RelatingMaterial.is_a() == "IfcMaterial": + names.append(rel.RelatingMaterial.Name) + + + return facet_evaluation( + 0, + # @todo + "[material_eval_todo]", + ) + + class boolean_logic: """ Boolean conjunction over a collection of functions @@ -166,17 +197,39 @@ class restriction: """ def __init__(self, node): - self.options = [ - n.getAttribute("value") - for n in node.childNodes - if n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("enumeration") - ] + self.restriction_on = node.getAttribute("base") + self.options = [] + self.type = [] + + for n in node.childNodes: + if n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("enumeration"): + self.options.append(n.getAttribute("value")) + self.type = "enumeration" + elif n.nodeType == n.ELEMENT_NODE and (n.tagName.endswith("Inclusive") or n.tagName.endswith("Exclusive")): + self.options.append(n.getAttribute("value")) + self.type = "bounds" + elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("length"): + self.options.append(n.getAttribute("value")) + self.type = "length" + elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"): + self.options.append(n.getAttribute("value")) + self.type = "pattern" + + # "Given an instance with %(applicabiliy)s\nWe expect %(requirements)s" % self.__dict__ def __eq__(self, other): return other in self.options def __repr__(self): - return " or ".join(self.options) + if self.type == "enumeration": + return " or ".join(self.options) + elif self.type == "bounds": + self.options.sort() + return "of type %s, having a value between %s and %s" % (self.restriction_on, self.options[0], self.options[1]) + elif self.type == "length": + return "of type %s with a length of %s" % (self.restriction_on, self.options[0]) + elif self.type == "pattern": + return "of type %s respecting pattern %s" % (self.restriction_on, self.options[0]) class specification: @@ -197,18 +250,19 @@ class specification: phrases[0].tagName == "applicability" or error("expected ") phrases[1].tagName == "requirements" or error("expected ") - self.applicability, self.requirements = (boolean_and(parse_rules(phrase)) for phrase in phrases) + self.applicabiliy, self.requirements = (boolean_and(parse_rules(phrase)) for phrase in phrases) def __call__(self, inst, logger): - if self.applicability(inst, logger): + if self.applicabiliy(inst, logger): valid = self.requirements(inst, logger) + if valid: - logger.info(str(self) + "\n%s has" % inst + " " + str(valid) + " so is compliant") + logger.info({'guid':inst.GlobalId, 'result':valid.success,'sentence':str(self) + "\n%s has" % inst + " " + str(valid) + " so is compliant"}) else: - logger.error(str(self) + "\n%s has" % inst + " " + str(valid) + " so is not compliant") + logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + "\n%s has" % inst + " " + str(valid) + " so is not compliant"}) def __str__(self): - return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ + return "Given an instance with %(applicabiliy)s\nWe expect %(requirements)s" % self.__dict__ class ids: @@ -229,15 +283,17 @@ class ids: for spec in self.specifications: for elem in ifc_file.by_type("IfcObject"): spec(elem, logger) - - + if __name__ == "__main__": - import sys + import sys, os import logging import ifcopenshell + filename = os.path.join(os.getcwd(), "ids.txt") + logger = logging.getLogger("IDS") - logging.basicConfig(level=logging.INFO, format="%(message)s") + logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s") + logging.FileHandler(filename, mode='w') ids_file = ids(sys.argv[1]) ifc_file = ifcopenshell.open(sys.argv[2]) From 9d838db09656194bdb1ea10a004ea93561c3103c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 10 Apr 2021 18:10:33 +1000 Subject: [PATCH 05/64] You can now edit cost schedule attributes. Thanks Jesusbill and myoualid! --- .../blenderbim/bim/module/cost/__init__.py | 10 ++- .../blenderbim/bim/module/cost/operator.py | 71 +++++++++++++++++++ .../blenderbim/bim/module/cost/prop.py | 18 +++++ .../blenderbim/bim/module/cost/ui.py | 23 +++++- .../api/cost/edit_cost_schedule.py | 10 +++ 5 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 src/blenderbim/blenderbim/bim/module/cost/prop.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index 2605c0910c..62142317a1 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -1,16 +1,20 @@ import bpy -from . import ui, operator +from . import ui, prop, operator classes = ( operator.AddCostSchedule, operator.RemoveCostSchedule, + operator.EditCostSchedule, + operator.EnableEditingCostSchedule, + operator.DisableEditingCostSchedule, + prop.BIMCostProperties, ui.BIM_PT_cost_schedules, ) def register(): - pass + bpy.types.Scene.BIMCostProperties = bpy.props.PointerProperty(type=prop.BIMCostProperties) def unregister(): - pass + del bpy.types.Scene.BIMCostProperties diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index ef6997b9ad..35e3a2db21 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -1,4 +1,5 @@ import bpy +import json import ifcopenshell.api from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.cost.data import Data @@ -27,3 +28,73 @@ class RemoveCostSchedule(bpy.types.Operator): ) Data.load(IfcStore.get_file()) return {"FINISHED"} + + +class EnableEditingCostSchedule(bpy.types.Operator): + bl_idname = "bim.enable_editing_cost_schedule" + bl_label = "Enable Editing Cost Schedule" + cost_schedule: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + props.active_cost_schedule_id = self.cost_schedule + + while len(props.cost_schedule_attributes) > 0: + props.cost_schedule_attributes.remove(0) + + data = Data.cost_schedules[self.cost_schedule] + + for attribute in IfcStore.get_schema().declaration_by_name("IfcCostSchedule").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = props.cost_schedule_attributes.add() + new.name = attribute.name() + new.is_null = data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if attribute.name() in ["SubmittedOn", "UpdateDate"]: + new.string_value = "" if new.is_null else data[attribute.name()].isoformat() + elif data_type == "string": + new.string_value = "" if new.is_null else data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if data[attribute.name()]: + new.enum_value = data[attribute.name()] + return {"FINISHED"} + + +class DisableEditingCostSchedule(bpy.types.Operator): + bl_idname = "bim.disable_editing_cost_schedule" + bl_label = "Disable Editing Cost Schedule" + + def execute(self, context): + props = context.scene.BIMCostProperties + props.active_cost_schedule_id = 0 + return {"FINISHED"} + + +class EditCostSchedule(bpy.types.Operator): + bl_idname = "bim.edit_cost_schedule" + bl_label = "Edit Cost Schedule" + + def execute(self, context): + props = context.scene.BIMCostProperties + attributes = {} + for attribute in props.cost_schedule_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.edit_cost_schedule", + self.file, + **{"cost_schedule": self.file.by_id(props.active_cost_schedule_id), "attributes": attributes} + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_cost_schedule() + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cost/prop.py b/src/blenderbim/blenderbim/bim/module/cost/prop.py new file mode 100644 index 0000000000..30db28fd2c --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/cost/prop.py @@ -0,0 +1,18 @@ +import bpy +from blenderbim.bim.prop import StrProperty, Attribute +from bpy.types import PropertyGroup +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) + + +class BIMCostProperties(PropertyGroup): + cost_schedule_attributes: CollectionProperty(name="Cost Schedule Attributes", type=Attribute) + active_cost_schedule_id: IntProperty(name="Active Cost Schedule Id") diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 43ecd1bf19..d527d6dcb5 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -16,6 +16,8 @@ class BIM_PT_cost_schedules(Panel): return IfcStore.get_file() def draw(self, context): + props = context.scene.BIMCostProperties + if not Data.is_loaded: Data.load(IfcStore.get_file()) @@ -25,5 +27,22 @@ class BIM_PT_cost_schedules(Panel): for cost_schedule_id, cost_schedule in Data.cost_schedules.items(): row = self.layout.row(align=True) row.label(text=cost_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") - row.operator("bim.add_cost_schedule", text="", icon="GREASEPENCIL") - row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule_id + + if props.active_cost_schedule_id and props.active_cost_schedule_id == cost_schedule_id: + row.operator("bim.edit_cost_schedule", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_cost_schedule", text="", icon="X") + elif props.active_cost_schedule_id: + row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule_id + else: + row.operator("bim.enable_editing_cost_schedule", text="", icon="GREASEPENCIL").cost_schedule = cost_schedule_id + row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule_id + + if props.active_cost_schedule_id == cost_schedule_id: + for attribute in props.cost_schedule_attributes: + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py new file mode 100644 index 0000000000..efb8650f02 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py @@ -0,0 +1,10 @@ +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"cost_schedule": None, "attributes": {}} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + for name, value in self.settings["attributes"].items(): + setattr(self.settings["cost_schedule"], name, value) From 3102dbcd44c985f649b91315977a00b9df8e08b0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 10 Apr 2021 20:01:37 +1000 Subject: [PATCH 06/64] Fix bug where you couldn't add a presentation layer --- src/blenderbim/blenderbim/bim/module/layer/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/layer/operator.py b/src/blenderbim/blenderbim/bim/module/layer/operator.py index 7c930823e9..52e5a9760d 100644 --- a/src/blenderbim/blenderbim/bim/module/layer/operator.py +++ b/src/blenderbim/blenderbim/bim/module/layer/operator.py @@ -47,7 +47,7 @@ class EnableEditingLayer(bpy.types.Operator): for attribute in IfcStore.get_schema().declaration_by_name("IfcPresentationLayerAssignment").all_attributes(): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity": + if data_type == "entity" or data_type == "select": continue new = props.layer_attributes.add() new.name = attribute.name() From df437fd3e3f68f4c2767e87b536371e7e92c8063 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 10 Apr 2021 20:40:35 +1000 Subject: [PATCH 07/64] You can now add cost items into a cost schedule, and as sub items to a parent cost item. Thanks myoualid and Jesusbill! --- .../blenderbim/bim/module/cost/__init__.py | 3 + .../blenderbim/bim/module/cost/operator.py | 46 +++++++++++++-- .../blenderbim/bim/module/cost/prop.py | 10 ++++ .../blenderbim/bim/module/cost/ui.py | 56 +++++++++++++++---- .../ifcopenshell/api/cost/add_cost_item.py | 28 ++++++++++ .../ifcopenshell/api/cost/data.py | 20 +++++++ .../ifcopenshell/api/nest/assign_object.py | 53 ++++++++++++++++++ 7 files changed, 199 insertions(+), 17 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index 62142317a1..d8cd9ada2d 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -7,8 +7,11 @@ classes = ( operator.EditCostSchedule, operator.EnableEditingCostSchedule, operator.DisableEditingCostSchedule, + operator.AddCostItem, + prop.CostItem, prop.BIMCostProperties, ui.BIM_PT_cost_schedules, + ui.BIM_UL_cost_items, ) diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index 35e3a2db21..bd17d2b79b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -36,11 +36,11 @@ class EnableEditingCostSchedule(bpy.types.Operator): cost_schedule: bpy.props.IntProperty() def execute(self, context): - props = context.scene.BIMCostProperties - props.active_cost_schedule_id = self.cost_schedule + self.props = context.scene.BIMCostProperties + self.props.active_cost_schedule_id = self.cost_schedule - while len(props.cost_schedule_attributes) > 0: - props.cost_schedule_attributes.remove(0) + while len(self.props.cost_schedule_attributes) > 0: + self.props.cost_schedule_attributes.remove(0) data = Data.cost_schedules[self.cost_schedule] @@ -48,7 +48,7 @@ class EnableEditingCostSchedule(bpy.types.Operator): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) if data_type == "entity": continue - new = props.cost_schedule_attributes.add() + new = self.props.cost_schedule_attributes.add() new.name = attribute.name() new.is_null = data[attribute.name()] is None new.is_optional = attribute.optional() @@ -61,8 +61,26 @@ class EnableEditingCostSchedule(bpy.types.Operator): new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) if data[attribute.name()]: new.enum_value = data[attribute.name()] + + while len(self.props.cost_items) > 0: + self.props.cost_items.remove(0) + + for related_object_id in Data.cost_schedules[self.cost_schedule]["RelatedObjects"]: + self.create_new_cost_item_li(related_object_id, 0) return {"FINISHED"} + def create_new_cost_item_li(self, related_object_id, level_index): + cost_item = Data.cost_items[related_object_id] + new = self.props.cost_items.add() + new.name = cost_item["Name"] or "Unnamed" + new.ifc_definition_id = related_object_id + new.is_expanded = False + new.level_index = level_index + if cost_item["RelatedObjects"]: + new.has_children = True + for related_object_id in cost_item["RelatedObjects"]: + self.create_new_cost_item_li(related_object_id, level_index + 1) + class DisableEditingCostSchedule(bpy.types.Operator): bl_idname = "bim.disable_editing_cost_schedule" @@ -98,3 +116,21 @@ class EditCostSchedule(bpy.types.Operator): Data.load(IfcStore.get_file()) bpy.ops.bim.disable_editing_cost_schedule() return {"FINISHED"} + + +class AddCostItem(bpy.types.Operator): + bl_idname = "bim.add_cost_item" + bl_label = "Add Cost Item" + cost_schedule: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() + if len(props.cost_items): + data = {"cost_item": self.file.by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id)} + else: + data = {"cost_schedule": self.file.by_id(self.cost_schedule)} + ifcopenshell.api.run("cost.add_cost_item", self.file, **data) + Data.load(self.file) + bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = self.cost_schedule) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cost/prop.py b/src/blenderbim/blenderbim/bim/module/cost/prop.py index 30db28fd2c..7cc928c95e 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/prop.py +++ b/src/blenderbim/blenderbim/bim/module/cost/prop.py @@ -13,6 +13,16 @@ from bpy.props import ( ) +class CostItem(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + has_children: BoolProperty(name="Has Children") + is_expanded: BoolProperty(name="Is Expanded") + level_index: IntProperty(name="Level Index") + + class BIMCostProperties(PropertyGroup): cost_schedule_attributes: CollectionProperty(name="Cost Schedule Attributes", type=Attribute) active_cost_schedule_id: IntProperty(name="Active Cost Schedule Id") + cost_items: CollectionProperty(name="Work Calendar", type=CostItem) + active_cost_item_index: IntProperty(name="Active Cost Item Index") diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index d527d6dcb5..655fee9994 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -16,7 +16,7 @@ class BIM_PT_cost_schedules(Panel): return IfcStore.get_file() def draw(self, context): - props = context.scene.BIMCostProperties + self.props = context.scene.BIMCostProperties if not Data.is_loaded: Data.load(IfcStore.get_file()) @@ -28,21 +28,53 @@ class BIM_PT_cost_schedules(Panel): row = self.layout.row(align=True) row.label(text=cost_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") - if props.active_cost_schedule_id and props.active_cost_schedule_id == cost_schedule_id: + if self.props.active_cost_schedule_id and self.props.active_cost_schedule_id == cost_schedule_id: row.operator("bim.edit_cost_schedule", text="", icon="CHECKMARK") row.operator("bim.disable_editing_cost_schedule", text="", icon="X") - elif props.active_cost_schedule_id: + elif self.props.active_cost_schedule_id: row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule_id else: row.operator("bim.enable_editing_cost_schedule", text="", icon="GREASEPENCIL").cost_schedule = cost_schedule_id row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule_id - if props.active_cost_schedule_id == cost_schedule_id: - for attribute in props.cost_schedule_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + if self.props.active_cost_schedule_id == cost_schedule_id: + self.draw_editable_cost_schedule_ui(cost_schedule_id, cost_schedule) + + def draw_editable_cost_schedule_ui(self, cost_schedule_id, cost_schedule): + for attribute in self.props.cost_schedule_attributes: + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + + row = self.layout.row(align=True) + row.label(text="X Cost Items") + row.operator("bim.add_cost_item", text="", icon="ADD").cost_schedule = cost_schedule_id + + self.layout.template_list( + "BIM_UL_cost_items", + "", + self.props, + "cost_items", + self.props, + "active_cost_item_index", + ) + + +class BIM_UL_cost_items(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + for i in range(0, item.level_index): + row.label(text="", icon="BLANK1") + if item.has_children: + if item.is_expanded: + row.operator("bim.edit_work_calendar", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN") + else: + row.operator("bim.edit_work_calendar", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT") + else: + row.label(text="", icon="DOT") + row.label(text=item.name) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py new file mode 100644 index 0000000000..9d9b017f18 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py @@ -0,0 +1,28 @@ +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"cost_schedule": None, "cost_item": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + cost_item = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcCostItem") + + if self.settings["cost_schedule"]: + self.file.create_entity( + "IfcRelAssignsToControl", + **{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatedObjects": [cost_item], + "RelatingControl": self.settings["cost_schedule"], + } + ) + elif self.settings["cost_item"]: + ifcopenshell.api.run( + "nest.assign_object", self.file, object=cost_item, relating_object=self.settings["cost_item"] + ) + return cost_item diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/data.py b/src/ifcopenshell-python/ifcopenshell/api/cost/data.py index ee934a5c76..6a25361dea 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/data.py @@ -4,15 +4,19 @@ import ifcopenshell.util.date class Data: is_loaded = False cost_schedules = {} + cost_items = {} @classmethod def purge(cls): cls.is_loaded = False cls.cost_schedules = {} + cls.cost_items = {} @classmethod def load(cls, file): cls.cost_schedules = {} + cls.cost_items = {} + for cost_schedule in file.by_type("IfcCostSchedule"): data = cost_schedule.get_info() del data["OwnerHistory"] @@ -20,5 +24,21 @@ class Data: data["SubmittedOn"] = ifcopenshell.util.date.ifc2datetime(data["SubmittedOn"]) if data["UpdateDate"]: data["UpdateDate"] = ifcopenshell.util.date.ifc2datetime(data["UpdateDate"]) + data["RelatedObjects"] = [] + for rel in cost_schedule.Controls: + for related_object in rel.RelatedObjects: + if related_object.is_a("IfcCostItem"): + data["RelatedObjects"].append(related_object.id()) + break # We are only allowed one summary cost item cls.cost_schedules[cost_schedule.id()] = data + + for cost_item in file.by_type("IfcCostItem"): + data = cost_item.get_info() + del data["OwnerHistory"] + del data["CostValues"] + del data["CostQuantities"] + data["RelatedObjects"] = [] + for rel in cost_item.IsNestedBy: + [data["RelatedObjects"].append(o.id()) for o in rel.RelatedObjects if o.is_a("IfcCostItem")] + cls.cost_items[cost_item.id()] = data cls.is_loaded=True diff --git a/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py b/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py new file mode 100644 index 0000000000..2c948e3f80 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py @@ -0,0 +1,53 @@ +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "object": None, + "relating_object": None, + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + nests = None + if self.settings["object"].Nests: + nests = self.settings["object"].Nests[0] + + is_nested_by = None + for rel in self.settings["relating_object"].IsNestedBy: + if rel.is_a("IfcRelNests"): + is_nested_by = rel + break + + if nests and nests == is_nested_by: + return + + if nests: + related_objects = list(nests.RelatedObjects) + related_objects.remove(self.settings["object"]) + if related_objects: + nests.RelatedObjects = related_objects + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": nests}) + else: + self.file.remove(nests) + + if is_nested_by: + related_objects = list(is_nested_by.RelatedObjects) + related_objects.append(self.settings["object"]) + is_nested_by.RelatedObjects = related_objects + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_nested_by}) + else: + is_nested_by = self.file.create_entity( + "IfcRelNests", + **{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatedObjects": [self.settings["object"]], + "RelatingObject": self.settings["relating_object"], + } + ) + return is_nested_by From a70fb41652644868f0224f4baf679dc582ba9436 Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Sun, 11 Apr 2021 12:52:42 +0200 Subject: [PATCH 08/64] blenderbim: fix reference before assignment in PieAddOpening (#1426) Co-authored-by: Julien Moutinho --- src/blenderbim/blenderbim/bim/module/model/pie.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/pie.py b/src/blenderbim/blenderbim/bim/module/model/pie.py index a93010bd47..e8a0946445 100644 --- a/src/blenderbim/blenderbim/bim/module/model/pie.py +++ b/src/blenderbim/blenderbim/bim/module/model/pie.py @@ -85,8 +85,8 @@ class PieAddOpening(bpy.types.Operator): if "IfcOpeningElement" in obj.name or not obj.BIMObjectProperties.ifc_definition_id: opening_name = obj.name else: - opj_name = obj.name - bpy.ops.bim.add_opening(obj=opj_name, opening=opening_name) + obj_name = obj.name + bpy.ops.bim.add_opening(obj=obj_name, opening=opening_name) return {"FINISHED"} From 4202c084877e03d806a9ceb679ebf56c49c02b05 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 11 Apr 2021 21:15:39 +1000 Subject: [PATCH 09/64] You can now select stuff like doors / windows instead of having to select the opening element itself when adding voids, with some assumptions. See #1426. --- src/blenderbim/blenderbim/bim/module/model/pie.py | 6 ++++-- src/blenderbim/blenderbim/bim/module/void/ui.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/pie.py b/src/blenderbim/blenderbim/bim/module/model/pie.py index e8a0946445..b19a258f4c 100644 --- a/src/blenderbim/blenderbim/bim/module/model/pie.py +++ b/src/blenderbim/blenderbim/bim/module/model/pie.py @@ -84,9 +84,11 @@ class PieAddOpening(bpy.types.Operator): for obj in context.selected_objects: if "IfcOpeningElement" in obj.name or not obj.BIMObjectProperties.ifc_definition_id: opening_name = obj.name + elif len(obj.children) == 1 and not obj.children[0].BIMObjectProperties.ifc_definition_id: + opening_name = obj.children[0].name else: - obj_name = obj.name - bpy.ops.bim.add_opening(obj=obj_name, opening=opening_name) + opj_name = obj.name + bpy.ops.bim.add_opening(obj=opj_name, opening=opening_name) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/void/ui.py b/src/blenderbim/blenderbim/bim/module/void/ui.py index 548ff2f096..20ba1fdb99 100644 --- a/src/blenderbim/blenderbim/bim/module/void/ui.py +++ b/src/blenderbim/blenderbim/bim/module/void/ui.py @@ -27,6 +27,8 @@ class BIM_PT_voids(Panel): for obj in context.selected_objects: if "IfcOpeningElement" in obj.name or not obj.BIMObjectProperties.ifc_definition_id: op.opening = obj.name + elif len(obj.children) == 1 and not obj.children[0].BIMObjectProperties.ifc_definition_id: + op.opening = obj.children[0].name else: op.obj = obj.name From 14cd8289ea2b34016df5c847f0ebba64efb159e2 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 11 Apr 2021 21:56:45 +1000 Subject: [PATCH 10/64] You can now create IFC project libraries --- .../blenderbim/bim/module/project/__init__.py | 1 + .../blenderbim/bim/module/project/operator.py | 24 +++++++++++++++++++ .../blenderbim/bim/module/project/ui.py | 3 +++ .../ifcopenshell/api/context/add_context.py | 7 +++++- 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index 846f082a61..fb41b6e9ed 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -3,6 +3,7 @@ from . import ui, prop, operator classes = ( operator.CreateProject, + operator.CreateProjectLibrary, operator.ValidateIfcFile, prop.BIMProjectProperties, ui.BIM_PT_project, diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 6b56cd45cf..60baeea3a0 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -53,6 +53,30 @@ class CreateProject(bpy.types.Operator): return {"FINISHED"} +class CreateProjectLibrary(bpy.types.Operator): + bl_idname = "bim.create_project_library" + bl_label = "Create Project Library" + + def execute(self, context): + self.file = IfcStore.get_file() + if self.file: + return {"FINISHED"} + + IfcStore.file = ifcopenshell.api.run( + "project.create_file", **{"version": bpy.context.scene.BIMProperties.export_schema} + ) + self.file = IfcStore.get_file() + + if self.file.schema == "IFC2X3": + bpy.ops.bim.add_person() + bpy.ops.bim.add_organisation() + + project_library = bpy.data.objects.new("My Project Library", None) + bpy.ops.bim.assign_class(obj=project_library.name, ifc_class="IfcProjectLibrary") + bpy.ops.bim.assign_unit() + return {"FINISHED"} + + class ValidateIfcFile(bpy.types.Operator): bl_idname = "bim.validate_ifc_file" bl_label = "Validate IFC File" diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 1da1f0f64d..7e255ea0d3 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -63,3 +63,6 @@ class BIM_PT_project(Panel): row.prop(props, "volume_unit", text="Volume Unit") row = self.layout.row() row.operator("bim.create_project") + if props.export_schema != "IFC2X3": + row = self.layout.row() + row.operator("bim.create_project_library") diff --git a/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py b/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py index 4c4d310834..fe96c66ded 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py +++ b/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py @@ -21,7 +21,12 @@ class Usecase: context = self.file.createIfcGeometricRepresentationContext(None, "Plan", 2, 1.0e-05, self.origin) else: context = self.file.createIfcGeometricRepresentationContext(None, "Model", 3, 1.0e-05, self.origin) - project = self.file.by_type("IfcProject")[0] + + if self.file.schema == "IFC2X3": + project = self.file.by_type("IfcProject")[0] + else: + project = self.file.by_type("IfcContext")[0] + if project.RepresentationContexts: contexts = list(project.RepresentationContexts) else: From cab9e4d892a971774ae1fade031f9298b98e0bb7 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 13 Apr 2021 08:25:43 +1000 Subject: [PATCH 11/64] You can now assign aggregates in bulk --- .../bim/module/aggregate/operator.py | 59 ++++++++++--------- .../blenderbim/bim/module/aggregate/ui.py | 2 +- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index ef943fad44..5d366d5191 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -12,37 +12,40 @@ class AssignObject(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() - related_object = bpy.data.objects.get(self.related_object) if self.related_object else bpy.context.active_object - props = related_object.BIMObjectProperties - relating_object = bpy.data.objects.get(self.relating_object) if self.relating_object else props.relating_object + related_objects = ( + [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects + ) + relating_object = bpy.data.objects.get(self.relating_object) if not relating_object or not relating_object.BIMObjectProperties.ifc_definition_id: return {"FINISHED"} - product = self.file.by_id(props.ifc_definition_id) - ifcopenshell.api.run( - "aggregate.assign_object", - self.file, - **{ - "product": product, - "relating_object": self.file.by_id(relating_object.BIMObjectProperties.ifc_definition_id), - }, - ) - bpy.ops.bim.edit_object_placement(obj=related_object.name) - Data.load(IfcStore.get_file(), props.ifc_definition_id) - bpy.ops.bim.disable_editing_aggregate(obj=related_object.name) + for related_object in related_objects: + oprops = related_object.BIMObjectProperties + product = self.file.by_id(oprops.ifc_definition_id) + ifcopenshell.api.run( + "aggregate.assign_object", + self.file, + **{ + "product": product, + "relating_object": self.file.by_id(relating_object.BIMObjectProperties.ifc_definition_id), + }, + ) + bpy.ops.bim.edit_object_placement(obj=related_object.name) + Data.load(IfcStore.get_file(), oprops.ifc_definition_id) + bpy.ops.bim.disable_editing_aggregate(obj=related_object.name) - spatial_collection = bpy.data.collections.get(related_object.name) - relating_collection = bpy.data.collections.get(relating_object.name) - if spatial_collection: - self.remove_collection(bpy.context.scene.collection, spatial_collection) - for collection in bpy.data.collections: - if collection == relating_collection: - collection.children.link(spatial_collection) - continue - self.remove_collection(collection, spatial_collection) - else: - for collection in related_object.users_collection: - collection.objects.unlink(related_object) - relating_collection.objects.link(related_object) + spatial_collection = bpy.data.collections.get(related_object.name) + relating_collection = bpy.data.collections.get(relating_object.name) + if spatial_collection: + self.remove_collection(bpy.context.scene.collection, spatial_collection) + for collection in bpy.data.collections: + if collection == relating_collection: + collection.children.link(spatial_collection) + continue + self.remove_collection(collection, spatial_collection) + else: + for collection in related_object.users_collection: + collection.objects.unlink(related_object) + relating_collection.objects.link(related_object) return {"FINISHED"} def remove_collection(self, parent, child): diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py index 744e369350..7e593d3020 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py @@ -34,7 +34,7 @@ class BIM_PT_aggregate(Panel): if props.is_editing_aggregate: row = self.layout.row(align=True) row.prop(props, "relating_object", text="") - row.operator("bim.assign_object", icon="CHECKMARK", text="") + row.operator("bim.assign_object", icon="CHECKMARK", text="").relating_object = props.relating_object.name row.operator("bim.disable_editing_aggregate", icon="X", text="") else: row = self.layout.row(align=True) From d5c70e51f2f17f72c2a50a304edcc99afc4fc580 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 13 Apr 2021 13:04:05 +1000 Subject: [PATCH 12/64] You can now edit structural member axis orientations. Thanks Jesusbill! --- .../blenderbim/bim/module/aggregate/ui.py | 3 +- .../bim/module/structural/__init__.py | 4 + .../bim/module/structural/operator.py | 73 +++++++++++++++++++ .../blenderbim/bim/module/structural/prop.py | 17 +++++ .../blenderbim/bim/module/structural/ui.py | 38 ++++++++++ .../structural/edit_structural_member_axis.py | 11 +++ 6 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_member_axis.py diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py index 7e593d3020..474b51c23f 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py @@ -34,7 +34,8 @@ class BIM_PT_aggregate(Panel): if props.is_editing_aggregate: row = self.layout.row(align=True) row.prop(props, "relating_object", text="") - row.operator("bim.assign_object", icon="CHECKMARK", text="").relating_object = props.relating_object.name + if props.relating_object: + row.operator("bim.assign_object", icon="CHECKMARK", text="").relating_object = props.relating_object.name row.operator("bim.disable_editing_aggregate", icon="X", text="") else: row = self.layout.row(align=True) diff --git a/src/blenderbim/blenderbim/bim/module/structural/__init__.py b/src/blenderbim/blenderbim/bim/module/structural/__init__.py index 816b5b374e..a361a238ad 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/structural/__init__.py @@ -20,12 +20,16 @@ classes = ( operator.EnableEditingStructuralConnectionCondition, operator.DisableEditingStructuralConnectionCondition, operator.RemoveStructuralConnectionCondition, + operator.EnableEditingStructuralMemberAxis, + operator.DisableEditingStructuralMemberAxis, + operator.EditStructuralMemberAxis, prop.StructuralAnalysisModel, prop.BIMStructuralProperties, prop.BIMObjectStructuralProperties, ui.BIM_PT_structural_analysis_models, ui.BIM_PT_structural_boundary_conditions, ui.BIM_PT_connected_structural_members, + ui.BIM_PT_structural_member, ui.BIM_UL_structural_analysis_models, ) diff --git a/src/blenderbim/blenderbim/bim/module/structural/operator.py b/src/blenderbim/blenderbim/bim/module/structural/operator.py index a997d2e91f..5d735ef84c 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/operator.py +++ b/src/blenderbim/blenderbim/bim/module/structural/operator.py @@ -2,6 +2,8 @@ import bpy import json import ifcopenshell import ifcopenshell.api +from math import degrees +from mathutils import Vector, Matrix from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.structural.data import Data from ifcopenshell.api.context.data import Data as ContextData @@ -363,3 +365,74 @@ class UnassignStructuralAnalysisModel(bpy.types.Operator): ) Data.load(IfcStore.get_file()) return {"FINISHED"} + + +class EnableEditingStructuralMemberAxis(bpy.types.Operator): + bl_idname = "bim.enable_editing_structural_member_axis" + bl_label = "Enable Editing Structural Member Axis" + + def execute(self, context): + obj = bpy.context.active_object + oprops = obj.BIMObjectProperties + props = obj.BIMStructuralProperties + + self.file = IfcStore.get_file() + member = self.file.by_id(oprops.ifc_definition_id) + z_axis = Vector(member.Axis.DirectionRatios).normalized() @ obj.matrix_world if member.Axis else None + x_axis = (obj.data.vertices[1].co - obj.data.vertices[0].co).normalized() + location = obj.data.vertices[0].co + empty = bpy.data.objects.new("Member Axis", None) + empty.empty_display_type = "ARROWS" + if z_axis: + y_axis = (z_axis.cross(x_axis)).normalized() + empty.matrix_world = Matrix(( + (x_axis[0], y_axis[0], z_axis[0], location[0]), + (x_axis[1], y_axis[1], z_axis[1], location[1]), + (x_axis[2], y_axis[2], z_axis[2], location[2]), + (0, 0, 0, 1), + )) + else: + empty.location = location + empty.rotation_mode = "QUATERNION" + empty.rotation_quaternion = x_axis.to_track_quat("X", "Z") + + props.axis_angle = degrees(empty.rotation_euler[0]) + props.axis_empty = empty + context.scene.collection.objects.link(empty) + + props.is_editing_axis = True + return {"FINISHED"} + + +class DisableEditingStructuralMemberAxis(bpy.types.Operator): + bl_idname = "bim.disable_editing_structural_member_axis" + bl_label = "Disable Editing Structural Member Axis" + + def execute(self, context): + obj = bpy.context.active_object + props = obj.BIMStructuralProperties + props.is_editing_axis = False + if props.axis_empty: + bpy.data.objects.remove(props.axis_empty) + return {"FINISHED"} + + +class EditStructuralMemberAxis(bpy.types.Operator): + bl_idname = "bim.edit_structural_member_axis" + bl_label = "Edit Structural Member Axis" + + def execute(self, context): + obj = bpy.context.active_object + oprops = obj.BIMObjectProperties + props = obj.BIMStructuralProperties + relative_matrix = props.axis_empty.matrix_world @ obj.matrix_world.inverted() + z_axis = relative_matrix.col[2][0:3] + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "structural.edit_structural_member_axis", + self.file, + structural_member=self.file.by_id(oprops.ifc_definition_id), + axis=z_axis, + ) + bpy.ops.bim.disable_editing_structural_member_axis() + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/structural/prop.py b/src/blenderbim/blenderbim/bim/module/structural/prop.py index f8cc972b87..71709c470d 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/prop.py +++ b/src/blenderbim/blenderbim/bim/module/structural/prop.py @@ -1,4 +1,5 @@ import bpy +from math import radians from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -13,6 +14,19 @@ from bpy.props import ( ) +def updateAxisAngle(self, context): + if not self.axis_empty: + return + obj = context.active_object + empty = self.axis_empty + x_axis = obj.data.vertices[1].co - obj.data.vertices[0].co + empty.location = obj.data.vertices[0].co + empty.rotation_mode = "QUATERNION" + empty.rotation_quaternion = x_axis.to_track_quat("X", "Z") + empty.rotation_mode = "XYZ" + empty.rotation_euler[0] = radians(self.axis_angle) + + class StructuralAnalysisModel(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") @@ -33,3 +47,6 @@ class BIMObjectStructuralProperties(PropertyGroup): active_boundary_condition: IntProperty(name="Active Boundary Condition") active_connects_structural_member: IntProperty(name="Active Connects Structural Member") relating_structural_member: PointerProperty(name="Relating Structural Member", type=bpy.types.Object) + is_editing_axis: BoolProperty(name="Is Editing Axis", default=False) + axis_angle: FloatProperty(name="Axis Angle", update=updateAxisAngle) + axis_empty: PointerProperty(name="Axis Empty", type=bpy.types.Object) diff --git a/src/blenderbim/blenderbim/bim/module/structural/ui.py b/src/blenderbim/blenderbim/bim/module/structural/ui.py index 87aff9b060..b1b51edd4d 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/ui.py +++ b/src/blenderbim/blenderbim/bim/module/structural/ui.py @@ -141,6 +141,44 @@ class BIM_PT_connected_structural_members(Panel): draw_boundary_condition_ui(box, data["AppliedCondition"], data["id"], self.props) +class BIM_PT_structural_member(Panel): + bl_label = "IFC Structural Member" + bl_idname = "BIM_PT_structural_member" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + @classmethod + def poll(cls, context): + if not context.active_object: + return False + props = context.active_object.BIMObjectProperties + if not props.ifc_definition_id: + return False + if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralMember"): + return False + return True + + def draw(self, context): + self.oprops = context.active_object.BIMObjectProperties + self.props = context.active_object.BIMStructuralProperties + self.file = IfcStore.get_file() + + if self.file.by_id(self.oprops.ifc_definition_id).is_a("IfcStructuralCurveMember"): + if self.props.is_editing_axis: + row = self.layout.row(align=True) + row.prop(self.props, "axis_angle") + row.operator("bim.edit_structural_member_axis", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_structural_member_axis", text="", icon="CANCEL") + else: + row = self.layout.row() + row.operator("bim.enable_editing_structural_member_axis", text="Edit Axis", icon="GREASEPENCIL") + else: + row = self.layout.row() + row.label(text="TODO") + + class BIM_PT_structural_analysis_models(Panel): bl_label = "IFC Structural Analysis Models" bl_idname = "BIM_PT_structural_analysis_models" diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_member_axis.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_member_axis.py new file mode 100644 index 0000000000..8701699914 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_member_axis.py @@ -0,0 +1,11 @@ +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"structural_member": None, "axis": [0.0, 0.0, 1.0]} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + if self.file.get_inverse(self.settings["structural_member"].Axis) == 1: + self.file.remove(self.settings["structural_member"].Axis) + self.settings["structural_member"].Axis = self.file.createIfcDirection(self.settings["axis"]) From f6c054066bb5ab7a7335a91562c8ed3032ba9655 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 13 Apr 2021 20:09:01 +1000 Subject: [PATCH 13/64] You can now assign spatial containers in bulk --- .../blenderbim/bim/module/spatial/operator.py | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/spatial/operator.py b/src/blenderbim/blenderbim/bim/module/spatial/operator.py index a51948d3d9..b83d7ff223 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/operator.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/operator.py @@ -13,44 +13,45 @@ class AssignContainer(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() - related_element = ( - bpy.data.objects.get(self.related_element) if self.related_element else bpy.context.active_object + related_elements = ( + [bpy.data.objects.get(self.related_element)] if self.related_element else bpy.context.selected_objects ) - oprops = related_element.BIMObjectProperties sprops = context.scene.BIMSpatialProperties - props = related_element.BIMObjectSpatialProperties relating_structure = ( self.relating_structure or sprops.spatial_elements[sprops.active_spatial_element_index].ifc_definition_id ) + for related_element in related_elements: + oprops = related_element.BIMObjectProperties + props = related_element.BIMObjectSpatialProperties - ifcopenshell.api.run( - "spatial.assign_container", - self.file, - **{ - "product": self.file.by_id(oprops.ifc_definition_id), - "relating_structure": self.file.by_id(relating_structure), - }, - ) - bpy.ops.bim.edit_object_placement(obj=related_element.name) - Data.load(IfcStore.get_file(), oprops.ifc_definition_id) - bpy.ops.bim.disable_editing_container(obj=related_element.name) + ifcopenshell.api.run( + "spatial.assign_container", + self.file, + **{ + "product": self.file.by_id(oprops.ifc_definition_id), + "relating_structure": self.file.by_id(relating_structure), + }, + ) + bpy.ops.bim.edit_object_placement(obj=related_element.name) + Data.load(IfcStore.get_file(), oprops.ifc_definition_id) + bpy.ops.bim.disable_editing_container(obj=related_element.name) - aggregate_collection = bpy.data.collections.get(related_element.name) + aggregate_collection = bpy.data.collections.get(related_element.name) - relating_structure_obj = IfcStore.id_map.get(relating_structure) - relating_collection = None - if relating_structure_obj: - relating_collection = bpy.data.collections.get(relating_structure_obj.name) + relating_structure_obj = IfcStore.id_map.get(relating_structure) + relating_collection = None + if relating_structure_obj: + relating_collection = bpy.data.collections.get(relating_structure_obj.name) - if aggregate_collection: - self.remove_collection(bpy.context.scene.collection, aggregate_collection) - for collection in bpy.data.collections: - self.remove_collection(collection, aggregate_collection) - relating_collection.children.link(aggregate_collection) - elif relating_collection: - for collection in related_element.users_collection: - collection.objects.unlink(related_element) - relating_collection.objects.link(related_element) + if aggregate_collection: + self.remove_collection(bpy.context.scene.collection, aggregate_collection) + for collection in bpy.data.collections: + self.remove_collection(collection, aggregate_collection) + relating_collection.children.link(aggregate_collection) + elif relating_collection: + for collection in related_element.users_collection: + collection.objects.unlink(related_element) + relating_collection.objects.link(related_element) return {"FINISHED"} def remove_collection(self, parent, child): From 4484b7723267213b398515947d21e3e9fccb7b9c Mon Sep 17 00:00:00 2001 From: Tigran Khachatryan <65066173+Geometrein@users.noreply.github.com> Date: Tue, 13 Apr 2021 17:44:18 +0300 Subject: [PATCH 14/64] broken link fix OCCT download link has changed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 87eba9c96a..1f05cdaab5 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Note: where `make -j` is written, add a number roughly equal to the amount of CP $ make -j $ sudo make install -**2c)** or obtain and compile OCCT from http://www.opencascade.org/getocc/download/loadocc/ +**2c)** or obtain and compile OCCT from https://dev.opencascade.org/release **3)** For building IfcConvert with COLLADA (.dae) support (on by default), OpenCOLLADA is needed: From 56f09d2541f2a1717ca3364345399bb7d370a906 Mon Sep 17 00:00:00 2001 From: Tigran Khachatryan <65066173+Geometrein@users.noreply.github.com> Date: Tue, 13 Apr 2021 17:52:33 +0300 Subject: [PATCH 15/64] Broken link fixes Links to Open Cascade were broken --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1f05cdaab5..0cdd3b2653 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Prerequisites Dependencies ------------- * [Boost](http://www.boost.org/) -* [Open Cascade](http://opencascade.org) - *optional*, but required for building IfcGeom - ([official](http://www.opencascade.org/getocc/download/loadocc/), "OCCT", or [community edition](https://github.com/tpaviot/oce), "OCE") +* [Open Cascade](https://dev.opencascade.org/) - *optional*, but required for building IfcGeom + ([official](https://dev.opencascade.org/release), "OCCT", or [community edition](https://github.com/tpaviot/oce), "OCE") For converting IFC representation items into BRep solids and tesselated meshes * [OpenCOLLADA](https://github.com/khronosGroup/OpenCOLLADA/) - *optional* For IfcConvert to be able to write tessellated Collada (.dae) files From b0fc98e384c1868f46d73bddaa09cff692ec9a8a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 14 Apr 2021 18:40:34 +1000 Subject: [PATCH 16/64] Fix bug where Blender prop enums persisted when starting new files --- src/blenderbim/blenderbim/bim/handler.py | 7 ++++++- src/blenderbim/blenderbim/bim/module/bcf/prop.py | 5 +++++ .../blenderbim/bim/module/bimtester/prop.py | 7 +++++++ .../blenderbim/bim/module/classification/prop.py | 5 +++++ src/blenderbim/blenderbim/bim/module/material/prop.py | 11 +++++++++++ src/blenderbim/blenderbim/bim/module/patch/prop.py | 5 +++++ src/blenderbim/blenderbim/bim/module/pset/prop.py | 7 +++++++ .../blenderbim/bim/module/pset_template/prop.py | 7 +++++++ src/blenderbim/blenderbim/bim/module/root/prop.py | 9 +++++++++ src/blenderbim/blenderbim/bim/module/type/prop.py | 11 +++++++++++ 10 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 5f462f8a5e..7d7d5b6bfc 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -52,12 +52,17 @@ def subscribe_to(object, data_path, callback): def purge_module_data(): from blenderbim.bim import modules - for name in modules.keys(): + for name, value in modules.items(): try: getattr(getattr(getattr(ifcopenshell.api, name), "data"), "Data").purge() except AttributeError: pass + try: + getattr(value, "prop").purge() + except AttributeError: + pass + @persistent def loadIfcStore(scene): diff --git a/src/blenderbim/blenderbim/bim/module/bcf/prop.py b/src/blenderbim/blenderbim/bim/module/bcf/prop.py index 82426afc42..5c7baa1bd1 100644 --- a/src/blenderbim/blenderbim/bim/module/bcf/prop.py +++ b/src/blenderbim/blenderbim/bim/module/bcf/prop.py @@ -17,6 +17,11 @@ from bpy.props import ( bcfviewpoints_enum = None +def purge(): + global bcfviewpoints_enum + bcfviewpoints_enum = None + + def updateBcfReferenceLink(self, context): if bpy.context.scene.BCFProperties.is_loaded: bpy.ops.bim.edit_bcf_reference_links() diff --git a/src/blenderbim/blenderbim/bim/module/bimtester/prop.py b/src/blenderbim/blenderbim/bim/module/bimtester/prop.py index cd49e9edc1..db245767c7 100644 --- a/src/blenderbim/blenderbim/bim/module/bimtester/prop.py +++ b/src/blenderbim/blenderbim/bim/module/bimtester/prop.py @@ -18,6 +18,13 @@ scenarios_enum = [] classes_enum = [] +def purge(): + global scenarios_enum + global classes_enum + scenarios_enum = [] + classes_enum = [] + + def getScenarios(self, context): global scenarios_enum if len(scenarios_enum) < 1: diff --git a/src/blenderbim/blenderbim/bim/module/classification/prop.py b/src/blenderbim/blenderbim/bim/module/classification/prop.py index 85ffb7ce8b..932335a3a3 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/prop.py +++ b/src/blenderbim/blenderbim/bim/module/classification/prop.py @@ -16,6 +16,11 @@ from bpy.props import ( classification_enum = [] +def purge(): + global classification_enum + classification_enum = [] + + def getClassifications(self, context): global classification_enum if len(classification_enum) < 1: diff --git a/src/blenderbim/blenderbim/bim/module/material/prop.py b/src/blenderbim/blenderbim/bim/module/material/prop.py index ffa7c5d87a..6b39d95691 100644 --- a/src/blenderbim/blenderbim/bim/module/material/prop.py +++ b/src/blenderbim/blenderbim/bim/module/material/prop.py @@ -21,6 +21,17 @@ profileclasses_enum = [] parameterizedprofileclasses_enum = [] +def purge(): + global materials_enum + global materialtypes_enum + global profileclasses_enum + global parameterizedprofileclasses_enum + materials_enum = [] + materialtypes_enum = [] + profileclasses_enum = [] + parameterizedprofileclasses_enum = [] + + def getProfileClasses(self, context): global profileclasses_enum if len(profileclasses_enum) == 0 and IfcStore.get_schema(): diff --git a/src/blenderbim/blenderbim/bim/module/patch/prop.py b/src/blenderbim/blenderbim/bim/module/patch/prop.py index b2d4315878..bfee66f4b0 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/prop.py +++ b/src/blenderbim/blenderbim/bim/module/patch/prop.py @@ -18,6 +18,11 @@ from bpy.props import ( ifcpatchrecipes_enum = [] +def purge(): + global ifcpatchrecipes_enum + ifcpatchrecipes_enum = [] + + def getIfcPatchRecipes(self, context): global ifcpatchrecipes_enum if len(ifcpatchrecipes_enum) < 1: diff --git a/src/blenderbim/blenderbim/bim/module/pset/prop.py b/src/blenderbim/blenderbim/bim/module/pset/prop.py index 42c2904e8a..d4dcbb75c9 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/prop.py +++ b/src/blenderbim/blenderbim/bim/module/pset/prop.py @@ -19,6 +19,13 @@ psetnames = {} qtonames = {} +def purge(): + global psetnames + global qtonames + psetnames = {} + qtonames = {} + + def getPsetNames(self, context): global psetnames obj = context.active_object diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/prop.py b/src/blenderbim/blenderbim/bim/module/pset_template/prop.py index 9a570bf5a0..36b8bc8c05 100644 --- a/src/blenderbim/blenderbim/bim/module/pset_template/prop.py +++ b/src/blenderbim/blenderbim/bim/module/pset_template/prop.py @@ -21,6 +21,13 @@ psettemplatefiles_enum = [] psettemplates_enum = [] +def purge(): + global psettemplatefiles_enum + global psettemplates_enum + psettemplatefiles_enum = [] + psettemplates_enum = [] + + def updatePsetTemplateFiles(self, context): global psettemplates_enum IfcStore.pset_template_path = os.path.join( diff --git a/src/blenderbim/blenderbim/bim/module/root/prop.py b/src/blenderbim/blenderbim/bim/module/root/prop.py index 20a4b63fec..80ba2a20e4 100644 --- a/src/blenderbim/blenderbim/bim/module/root/prop.py +++ b/src/blenderbim/blenderbim/bim/module/root/prop.py @@ -17,6 +17,15 @@ classes_enum = [] types_enum = [] +def purge(): + global products_enum + global classes_enum + global types_enum + products_enum = [] + classes_enum = [] + types_enum = [] + + def getIfcPredefinedTypes(self, context): global types_enum file = IfcStore.get_file() diff --git a/src/blenderbim/blenderbim/bim/module/type/prop.py b/src/blenderbim/blenderbim/bim/module/type/prop.py index 9fdce92635..bb895a7a36 100644 --- a/src/blenderbim/blenderbim/bim/module/type/prop.py +++ b/src/blenderbim/blenderbim/bim/module/type/prop.py @@ -20,6 +20,17 @@ type_classes_enum = [] available_types_enum = [] +def purge(): + global applicable_types_enum + global relating_types_enum + global type_classes_enum + global available_types_enum + applicable_types_enum = [] + relating_types_enum = [] + type_classes_enum = [] + available_types_enum = [] + + def getIfcTypes(self, context): global type_classes_enum file = IfcStore.get_file() From d11b91f15240ad9cf0c63a9154cd5904deabb9ba Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 14 Apr 2021 18:40:52 +1000 Subject: [PATCH 17/64] Fix bug where adding a new material didn't make it immediately available --- src/blenderbim/blenderbim/bim/module/material/operator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index b5ff702d02..e2e9919551 100644 --- a/src/blenderbim/blenderbim/bim/module/material/operator.py +++ b/src/blenderbim/blenderbim/bim/module/material/operator.py @@ -2,6 +2,7 @@ import bpy import json import ifcopenshell.api import ifcopenshell.util.attribute +from blenderbim.bim.module.material.prop import purge as material_prop_purge from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.material.data import Data from ifcopenshell.api.profile.data import Data as ProfileData @@ -44,6 +45,7 @@ class AddMaterial(bpy.types.Operator): result = ifcopenshell.api.run("material.add_material", self.file, **{"Name": obj.name}) obj.BIMObjectProperties.ifc_definition_id = result.id() Data.load(IfcStore.get_file()) + material_prop_purge() return {"FINISHED"} From 5dec6365843279d78060bd88a45959c4ef2dbd0c Mon Sep 17 00:00:00 2001 From: Sigma Dimensions <79010126+myoualid@users.noreply.github.com> Date: Wed, 14 Apr 2021 23:40:29 +0000 Subject: [PATCH 18/64] New Feature to add Tasks and Task Relationships in the python ifcopenshell API (#1432) --- .../ifcopenshell/api/sequence/add_task.py | 20 +++++++++++++++ .../api/sequence/assign_task_predecessor.py | 25 +++++++++++++++++++ .../api/sequence/assign_task_successor.py | 25 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_predecessor.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_successor.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py new file mode 100644 index 0000000000..c4b7a8061c --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py @@ -0,0 +1,20 @@ +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"name": None, "predefined_type": "NOTDEFINED", "is_milestone":False, "identification": "none", "predecessor_to":None, "successor_from":None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + task = ifcopenshell.api.run( + "root.create_entity", + self.file, + ifc_class="IfcTask", + predefined_type=self.settings["predefined_type"], + name=self.settings["name"], + ) + task.IsMilestone = self.settings["is_milestone"] + return task \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_predecessor.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_predecessor.py new file mode 100644 index 0000000000..36c0140728 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_predecessor.py @@ -0,0 +1,25 @@ +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "predecessor_task": None, + "sequence_type":"FINISH_START", + "task": None, + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + #TODO: tasks can only have one relationship between oneanother: if a relationship is already assigned, it should overide the previous one's settings. + rel_sequence = self.file.create_entity("IfcRelSequence",**{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatingProcess": self.settings["predecessor_task"], + "SequenceType": self.settings["sequence_type"], + "RelatedProcess": self.settings["task"], + }) + return rel_sequence \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_successor.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_successor.py new file mode 100644 index 0000000000..21f818949f --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_successor.py @@ -0,0 +1,25 @@ +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "task": None, + "sequence_type":"FINISH_START", + "successor_task": None, + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + #TODO: tasks can only have one relationship between oneanother: if a relationship is already assigned, it should overide the previous one's settings. + rel_sequence = self.file.create_entity("IfcRelSequence",**{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatingProcess": self.settings["task"], + "SequenceType": self.settings["sequence_type"], + "RelatedProcess": self.settings["successor_task"], + }) + return rel_sequence \ No newline at end of file From 29b33661d787e9d94229288d15efaa3d89472dbb Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 15 Apr 2021 10:38:45 +1000 Subject: [PATCH 19/64] You can now assign summary tasks to a work schedule. Thanks myoualid! --- .../bim/module/sequence/__init__.py | 7 +-- .../bim/module/sequence/operator.py | 25 ++++++-- .../blenderbim/bim/module/sequence/prop.py | 8 +-- .../blenderbim/bim/module/sequence/ui.py | 57 ++++++------------- .../api/control/assign_control.py | 52 +++++++++++++++++ .../ifcopenshell/api/sequence/add_task.py | 11 +++- .../ifcopenshell/api/sequence/data.py | 5 ++ 7 files changed, 107 insertions(+), 58 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 16af753c26..27102da316 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -25,32 +25,29 @@ classes = ( operator.RemoveWorkCalendar, operator.EnableEditingWorkCalendar, operator.DisableEditingWorkCalendar, + operator.AddTask, prop.WorkPlan, prop.BIMWorkPlanProperties, + prop.Task, prop.WorkSchedule, prop.BIMWorkScheduleProperties, prop.WorkCalendar, prop.BIMWorkCalendarProperties, - prop.Task, - prop.BIMTaskProperties, ui.BIM_PT_work_plans, ui.BIM_UL_work_plans, ui.BIM_PT_work_schedules, ui.BIM_UL_work_schedules, ui.BIM_PT_work_calendars, ui.BIM_UL_work_calendars, - ui.BIM_PT_tasks, ui.BIM_UL_tasks, ) def register(): - bpy.types.Scene.BIMTaskProperties = bpy.props.PointerProperty(type=prop.BIMTaskProperties) bpy.types.Scene.BIMWorkPlanProperties = bpy.props.PointerProperty(type=prop.BIMWorkPlanProperties) bpy.types.Scene.BIMWorkScheduleProperties = bpy.props.PointerProperty(type=prop.BIMWorkScheduleProperties) bpy.types.Scene.BIMWorkCalendarProperties = bpy.props.PointerProperty(type=prop.BIMWorkCalendarProperties) def unregister(): - del bpy.types.Scene.BIMTaskProperties del bpy.types.Scene.BIMWorkPlanProperties del bpy.types.Scene.BIMWorkScheduleProperties del bpy.types.Scene.BIMWorkCalendarProperties diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index f78fbf5513..1b0ecad74a 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -231,6 +231,7 @@ class EnableEditingWorkSchedule(bpy.types.Operator): if data[attribute.name()]: new.enum_value = data[attribute.name()] props.active_work_schedule_id = self.work_schedule + bpy.ops.bim.load_tasks(work_schedule=self.work_schedule) return {"FINISHED"} @@ -364,17 +365,18 @@ class DisableEditingWorkCalendar(bpy.types.Operator): class LoadTasks(bpy.types.Operator): bl_idname = "bim.load_tasks" bl_label = "Load Tasks" + work_schedule: bpy.props.IntProperty() def execute(self, context): - props = context.scene.BIMTaskProperties + props = context.scene.BIMWorkScheduleProperties while len(props.tasks) > 0: props.tasks.remove(0) - for ifc_definition_id, task in Data.tasks.items(): + for ifc_definition_id in Data.work_schedules[self.work_schedule]["RelatedObjects"]: + task = Data.tasks[ifc_definition_id] new = props.tasks.add() new.ifc_definition_id = ifc_definition_id - new.name = task["Name"] + new.name = task["Name"] or "Unnamed" new.identification = task["Identification"] - props.is_editing = True return {"FINISHED"} @@ -385,3 +387,18 @@ class DisableTaskEditingUI(bpy.types.Operator): def execute(self, context): context.scene.BIMTaskProperties.is_editing = False return {"FINISHED"} + + +class AddTask(bpy.types.Operator): + bl_idname = "bim.add_task" + bl_label = "Add Task" + work_schedule: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + task = ifcopenshell.api.run("sequence.add_task", self.file) + control = self.file.by_id(self.work_schedule) + ifcopenshell.api.run("control.assign_control", self.file, related_object=task, relating_control=control) + Data.load(self.file) + bpy.ops.bim.enable_editing_work_schedule(work_schedule = self.work_schedule) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 29a1af6b14..a21ec53a48 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -19,12 +19,6 @@ class Task(PropertyGroup): ifc_definition_id: IntProperty(name="IFC Definition ID") -class BIMTaskProperties(PropertyGroup): - is_editing: BoolProperty(name="Is Editing", default=False) - tasks: CollectionProperty(name="Tasks", type=Task) - active_task_index: IntProperty(name="Active Task Index") - - class WorkPlan(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") @@ -49,6 +43,8 @@ class BIMWorkScheduleProperties(PropertyGroup): work_schedules: CollectionProperty(name="Work Schedules", type=WorkSchedule) active_work_schedule_index: IntProperty(name="Active Work Schedules Index") active_work_schedule_id: IntProperty(name="Active Work Schedules Id") + tasks: CollectionProperty(name="Tasks", type=Task) + active_task_index: IntProperty(name="Active Task Index") class WorkCalendar(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 9ed96cef3c..d0eba710f5 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -114,6 +114,22 @@ class BIM_PT_work_schedules(Panel): if attribute.is_optional: row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + self.draw_task_ui(context) + + def draw_task_ui(self, context): + row = self.layout.row(align=True) + row.label(text="{} Tasks Found".format(len(Data.tasks)), icon="ACTION") + row.operator("bim.add_task", text="", icon="ADD").work_schedule = self.props.active_work_schedule_id + + self.layout.template_list( + "BIM_UL_tasks", + "", + self.props, + "tasks", + self.props, + "active_task_index", + ) + class BIM_UL_work_schedules(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): @@ -195,47 +211,6 @@ class BIM_UL_work_calendars(UIList): row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = item.ifc_definition_id -class BIM_PT_tasks(Panel): - bl_label = "IFC Tasks" - bl_idname = "BIM_PT_tasks" - bl_options = {"DEFAULT_CLOSED"} - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - - @classmethod - def poll(cls, context): - return IfcStore.get_file() - - def draw(self, context): - if not Data.is_loaded: - Data.load(IfcStore.get_file()) - self.props = context.scene.BIMTaskProperties - - row = self.layout.row(align=True) - row.label(text="{} Tasks Found".format(len(Data.tasks)), icon="ACTION") - if self.props.is_editing: - row.operator("bim.disable_task_editing_ui", text="", icon="CHECKMARK") - else: - row.operator("bim.load_tasks", text="", icon="GREASEPENCIL") - - if self.props.is_editing: - self.layout.template_list( - "BIM_UL_tasks", - "", - self.props, - "tasks", - self.props, - "active_task_index", - ) - - if self.props.active_task_index: - self.draw_editable_ui(context) - - def draw_editable_ui(self, context): - pass - - class BIM_UL_tasks(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: diff --git a/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py b/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py new file mode 100644 index 0000000000..d684f63f44 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py @@ -0,0 +1,52 @@ +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "related_object": None, + "relating_control": None, + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + has_assignments = None + if self.settings["related_object"].HasAssignments: + for assignement in self.settings["related_object"].HasAssignments: + if assignement.is_a("IfclRelAssignsToControl"): + has_assignments = assignement + + controls = None + for rel in self.settings["relating_control"].Controls: + if rel.is_a("IfcRelAssignsToControl"): + controls = rel + break + if has_assignments and has_assignments == controls: + return + if has_assignments: + related_objects = list(has_assignments.RelatedObjects) + related_objects.remove(self.settings["related_object"]) + if related_objects: + has_assignments.RelatedObjects = related_objects + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": has_assignments}) + else: + self.file.remove(has_assignments) + if controls: + related_objects = list(controls.RelatedObjects) + related_objects.append(self.settings["related_object"]) + controls.RelatedObjects = related_objects + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": controls}) + else: + controls = self.file.create_entity( + "IfcRelAssignsToControl", + **{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatedObjects": [self.settings["related_object"]], + "RelatingControl": self.settings["relating_control"], + } + ) + return controls diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py index c4b7a8061c..e27acee45a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py @@ -4,7 +4,14 @@ import ifcopenshell.api class Usecase: def __init__(self, file, **settings): self.file = file - self.settings = {"name": None, "predefined_type": "NOTDEFINED", "is_milestone":False, "identification": "none", "predecessor_to":None, "successor_from":None} + self.settings = { + "name": None, + "predefined_type": "NOTDEFINED", + "is_milestone": False, + "identification": "none", + "predecessor_to": None, + "successor_from": None, + } for key, value in settings.items(): self.settings[key] = value @@ -17,4 +24,4 @@ class Usecase: name=self.settings["name"], ) task.IsMilestone = self.settings["is_milestone"] - return task \ No newline at end of file + return task diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py index 501af373c4..edfa8cbe45 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py @@ -52,6 +52,11 @@ class Data: data["StartTime"] = ifcopenshell.util.date.ifc2datetime(data["StartTime"]) if data["FinishTime"]: data["FinishTime"] = ifcopenshell.util.date.ifc2datetime(data["FinishTime"]) + data["RelatedObjects"] = [] + for rel in work_schedule.Controls: + for obj in rel.RelatedObjects: + if obj.is_a("IfcTask"): + data["RelatedObjects"].append(obj.id()) cls.work_schedules[work_schedule.id()] = data @classmethod From dab2d5ce04ee6c4f8e37637184a1b0f083a7e2ee Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 15 Apr 2021 11:14:03 +1000 Subject: [PATCH 20/64] You can now toggle expansion of the cost item tree --- .../blenderbim/bim/module/cost/__init__.py | 2 ++ .../blenderbim/bim/module/cost/operator.py | 36 +++++++++++++++++-- .../blenderbim/bim/module/cost/prop.py | 1 + .../blenderbim/bim/module/cost/ui.py | 5 +-- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index d8cd9ada2d..ce2d98dc85 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -8,6 +8,8 @@ classes = ( operator.EnableEditingCostSchedule, operator.DisableEditingCostSchedule, operator.AddCostItem, + operator.ExpandCostItem, + operator.ContractCostItem, prop.CostItem, prop.BIMCostProperties, ui.BIM_PT_cost_schedules, diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index bd17d2b79b..afc937ea78 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -65,6 +65,7 @@ class EnableEditingCostSchedule(bpy.types.Operator): while len(self.props.cost_items) > 0: self.props.cost_items.remove(0) + self.contracted_cost_items = json.loads(self.props.contracted_cost_items) for related_object_id in Data.cost_schedules[self.cost_schedule]["RelatedObjects"]: self.create_new_cost_item_li(related_object_id, 0) return {"FINISHED"} @@ -74,12 +75,13 @@ class EnableEditingCostSchedule(bpy.types.Operator): new = self.props.cost_items.add() new.name = cost_item["Name"] or "Unnamed" new.ifc_definition_id = related_object_id - new.is_expanded = False + new.is_expanded = related_object_id not in self.contracted_cost_items new.level_index = level_index if cost_item["RelatedObjects"]: new.has_children = True - for related_object_id in cost_item["RelatedObjects"]: - self.create_new_cost_item_li(related_object_id, level_index + 1) + if new.is_expanded: + for related_object_id in cost_item["RelatedObjects"]: + self.create_new_cost_item_li(related_object_id, level_index + 1) class DisableEditingCostSchedule(bpy.types.Operator): @@ -134,3 +136,31 @@ class AddCostItem(bpy.types.Operator): Data.load(self.file) bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = self.cost_schedule) return {"FINISHED"} + + +class ExpandCostItem(bpy.types.Operator): + bl_idname = "bim.expand_cost_item" + bl_label = "Expand Cost Item" + cost_item: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + contracted_cost_items = json.loads(props.contracted_cost_items) + contracted_cost_items.remove(self.cost_item) + props.contracted_cost_items = json.dumps(contracted_cost_items) + bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = props.active_cost_schedule_id) + return {"FINISHED"} + + +class ContractCostItem(bpy.types.Operator): + bl_idname = "bim.contract_cost_item" + bl_label = "Contract Cost Item" + cost_item: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + contracted_cost_items = json.loads(props.contracted_cost_items) + contracted_cost_items.append(self.cost_item) + props.contracted_cost_items = json.dumps(contracted_cost_items) + bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = props.active_cost_schedule_id) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cost/prop.py b/src/blenderbim/blenderbim/bim/module/cost/prop.py index 7cc928c95e..b2d2f26832 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/prop.py +++ b/src/blenderbim/blenderbim/bim/module/cost/prop.py @@ -26,3 +26,4 @@ class BIMCostProperties(PropertyGroup): active_cost_schedule_id: IntProperty(name="Active Cost Schedule Id") cost_items: CollectionProperty(name="Work Calendar", type=CostItem) active_cost_item_index: IntProperty(name="Active Cost Item Index") + contracted_cost_items: StringProperty(name="Contracted Cost Items", default="[]") diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 655fee9994..abfc20074b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -72,9 +72,10 @@ class BIM_UL_cost_items(UIList): row.label(text="", icon="BLANK1") if item.has_children: if item.is_expanded: - row.operator("bim.edit_work_calendar", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN") + op = row.operator("bim.contract_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN") else: - row.operator("bim.edit_work_calendar", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT") + op = row.operator("bim.expand_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT") + op.cost_item = item.ifc_definition_id else: row.label(text="", icon="DOT") row.label(text=item.name) From 776c68e2b07932b52caf3dac68ec1b86147bf03c Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 15 Apr 2021 10:36:37 +0200 Subject: [PATCH 21/64] --svg-write-poly --- src/ifcconvert/IfcConvert.cpp | 3 +++ src/serializers/SvgSerializer.cpp | 4 ++-- src/serializers/SvgSerializer.h | 7 ++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ifcconvert/IfcConvert.cpp b/src/ifcconvert/IfcConvert.cpp index 0a15bc0a20..8ac40cff6f 100644 --- a/src/ifcconvert/IfcConvert.cpp +++ b/src/ifcconvert/IfcConvert.cpp @@ -392,6 +392,8 @@ int main(int argc, char** argv) { "Stores name and guid in a separate namespace as opposed to data-name, data-guid") ("svg-poly", "Uses the polygonal algorithm for hidden line rendering") + ("svg-write-poly", + "Approximate every curve as polygonal in SVG output") ("svg-project", "Always enable hidden line rendering instead of only on elevations") ("door-arcs", "Draw door openings arcs for IfcDoor elements") @@ -1008,6 +1010,7 @@ int main(int argc, char** argv) { } static_cast(serializer.get())->setUseNamespace(vmap.count("svg-xmlns") > 0); static_cast(serializer.get())->setUseHlrPoly(vmap.count("svg-poly") > 0); + static_cast(serializer.get())->setPolygonal(vmap.count("svg-write-poly") > 0); static_cast(serializer.get())->setAlwaysProject(vmap.count("svg-project") > 0); if (relative_center_x && relative_center_y) { static_cast(serializer.get())->setDrawingCenter(*relative_center_x, *relative_center_y); diff --git a/src/serializers/SvgSerializer.cpp b/src/serializers/SvgSerializer.cpp index 3b3d8ae8fd..b24af6b7a5 100644 --- a/src/serializers/SvgSerializer.cpp +++ b/src/serializers/SvgSerializer.cpp @@ -135,7 +135,7 @@ void SvgSerializer::write(path_object& p, const TopoDS_Wire& wire, boost::option // TODO: ALMOST_THE_SAME utilities in separate header bool closed = fabs((u1 + PI2) - u2) < 1.e-9; - if (conical && closed) { + if (!polygonal_ && (conical && closed)) { if (first) { if (ty == STANDARD_TYPE(Geom_Circle)) { Handle(Geom_Circle) circle = Handle(Geom_Circle)::DownCast(curve); @@ -222,7 +222,7 @@ void SvgSerializer::write(path_object& p, const TopoDS_Wire& wire, boost::option growBoundingBox(p2.X(), p2.Y()); - if (ty == STANDARD_TYPE(Geom_Circle) || ty == STANDARD_TYPE(Geom_Ellipse)) { + if (!polygonal_ && (ty == STANDARD_TYPE(Geom_Circle) || ty == STANDARD_TYPE(Geom_Ellipse))) { Handle(Geom_Conic) conic = Handle(Geom_Conic)::DownCast(curve); const bool mirrored = conic->Position().Axis().Direction().Z() < 0; diff --git a/src/serializers/SvgSerializer.h b/src/serializers/SvgSerializer.h index ede299dea3..51eac2ed0a 100644 --- a/src/serializers/SvgSerializer.h +++ b/src/serializers/SvgSerializer.h @@ -145,7 +145,7 @@ protected: storey_height_display_types storey_height_display_; bool draw_door_arcs_, is_floor_plan_; bool auto_section_, auto_elevation_; - bool use_namespace_, use_hlr_poly_, always_project_; + bool use_namespace_, use_hlr_poly_, always_project_, polygonal_; IfcParse::IfcFile* file; IfcUtil::IfcBaseEntity* storey_; @@ -188,6 +188,7 @@ public: , use_namespace_(false) , use_hlr_poly_(false) , always_project_(false) + , polygonal_(false) , file(0) , storey_(0) , xcoords_begin(0) @@ -249,6 +250,10 @@ public: use_hlr_poly_ = b; } + void setPolygonal(bool b) { + polygonal_ = b; + } + void setAlwaysProject(bool b) { always_project_ = b; } From 65d0abd0725d4c52e01d8a346f887a90813428d5 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 15 Apr 2021 18:38:28 +1000 Subject: [PATCH 22/64] Material profile set usages are now supported. Hooray! Thanks Jesusbill! --- .../bim/module/material/operator.py | 8 +++++-- .../blenderbim/bim/module/material/prop.py | 1 + .../blenderbim/bim/module/material/ui.py | 7 ++++++ .../api/material/assign_material.py | 22 ++++++++++++++----- .../ifcopenshell/api/material/data.py | 11 ++++++++-- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index e2e9919551..11745bdf7a 100644 --- a/src/blenderbim/blenderbim/bim/module/material/operator.py +++ b/src/blenderbim/blenderbim/bim/module/material/operator.py @@ -318,6 +318,10 @@ class EnableEditingAssignedMaterial(bpy.types.Operator): material_set_class = "IfcMaterialLayerSet" elif product_data["type"] == "IfcMaterialProfileSet": material_set_data = Data.profile_sets[product_data["id"]] + elif product_data["type"] == "IfcMaterialProfileSetUsage": + profile_set_usage = Data.profile_set_usages[product_data["id"]] + material_set_data = Data.profile_sets[profile_set_usage["ForProfileSet"]] + material_set_class = "IfcMaterialProfileSet" elif product_data["type"] == "IfcMaterialList": material_set_data = Data.lists[product_data["id"]] else: @@ -409,7 +413,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): material_set_item_data = Data.constituents[self.material_set_item] elif product_data["type"] == "IfcMaterialLayerSet" or product_data["type"] == "IfcMaterialLayerSetUsage": material_set_item_data = Data.layers[self.material_set_item] - elif product_data["type"] == "IfcMaterialProfileSet": + elif product_data["type"] == "IfcMaterialProfileSet" or product_data["type"] == "IfcMaterialProfileSetUsage": material_set_item_data = Data.profiles[self.material_set_item] else: material_set_item_data = {} @@ -542,7 +546,7 @@ class EditMaterialSetItem(bpy.types.Operator): }, ) Data.load_layers() - elif product_data["type"] == "IfcMaterialProfileSet": + elif product_data["type"] == "IfcMaterialProfileSet" or product_data["type"] == "IfcMaterialProfileSetUsage": profile_attributes = {} for attribute in props.material_set_item_profile_attributes: if attribute.data_type == "string": diff --git a/src/blenderbim/blenderbim/bim/module/material/prop.py b/src/blenderbim/blenderbim/bim/module/material/prop.py index 6b39d95691..4bd7093d69 100644 --- a/src/blenderbim/blenderbim/bim/module/material/prop.py +++ b/src/blenderbim/blenderbim/bim/module/material/prop.py @@ -70,6 +70,7 @@ def getMaterialTypes(self, context): "IfcMaterialLayerSet", "IfcMaterialLayerSetUsage", "IfcMaterialProfileSet", + "IfcMaterialProfileSetUsage", "IfcMaterialList", ] if IfcStore.get_file().schema == "IFC2X3": diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index 591fa8ccb3..262a920ef3 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -82,6 +82,13 @@ class BIM_PT_object_material(Panel): self.set_items = self.material_set_data["MaterialProfiles"] or [] self.set_data = Data.profiles self.set_item_name = "profile" + elif self.product_data["type"] == "IfcMaterialProfileSetUsage": + self.material_set_usage = Data.profile_set_usages[self.product_data["id"]] + self.material_set_id = self.material_set_usage["ForProfileSet"] + self.material_set_data = Data.profile_sets[self.material_set_id] + self.set_items = self.material_set_data["MaterialProfiles"] or [] + self.set_data = Data.profiles + self.set_item_name = "profile" elif self.product_data["type"] == "IfcMaterialList": self.material_set_id = self.product_data["id"] self.material_set_data = Data.lists[self.material_set_id] diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py index f7fc4be564..8e7147ee67 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py @@ -24,18 +24,28 @@ class Usecase: elif self.settings["type"] == "IfcMaterialProfileSet": material_set = self.file.create_entity(self.settings["type"]) self.create_material_association(material_set) + elif self.settings["type"] == "IfcMaterialProfileSetUsage": + material_set = self.file.create_entity("IfcMaterialProfileSet") + material_set_usage = self.create_profile_set_usage(material_set) + self.create_material_association(material_set_usage) elif self.settings["type"] == "IfcMaterialList": material_set = self.file.create_entity(self.settings["type"]) material_set.Materials = [self.settings["material"]] self.create_material_association(material_set) def create_layer_set_usage(self, material_set): - return self.file.create_entity("IfcMaterialLayerSetUsage", **{ - "ForLayerSet": material_set, - "LayerSetDirection": "AXIS2" if self.settings["product"].is_a("IfcWall") else "AXIS3", - "DirectionSense": "POSITIVE", - "OffsetFromReferenceLine": 0 - }) + return self.file.create_entity( + "IfcMaterialLayerSetUsage", + **{ + "ForLayerSet": material_set, + "LayerSetDirection": "AXIS2" if self.settings["product"].is_a("IfcWall") else "AXIS3", + "DirectionSense": "POSITIVE", + "OffsetFromReferenceLine": 0, + } + ) + + def create_profile_set_usage(self, material_set): + return self.file.create_entity("IfcMaterialProfileSetUsage", **{"ForProfileSet": material_set}) def assign_ifc_material(self): rel = self.get_rel_associates_material(self.settings["material"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/data.py b/src/ifcopenshell-python/ifcopenshell/api/material/data.py index 1c39d5b20f..6d1c68e5da 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/data.py @@ -6,8 +6,10 @@ class Data: materials = {} constituent_sets = {} constituents = {} + layer_sets_usages = {} layer_sets = {} layers = {} + profile_set_usages = {} profile_sets = {} profiles = {} lists = {} @@ -23,6 +25,7 @@ class Data: cls.layer_set_usages = {} cls.layer_sets = {} cls.layers = {} + cls.profile_set_usages = {} cls.profile_sets = {} cls.profiles = {} cls.lists = {} @@ -41,6 +44,7 @@ class Data: cls.load_layers() cls.load_layer_usages() cls.load_profiles() + cls.load_profile_usages() cls.load_lists() cls.is_loaded = True @@ -68,6 +72,11 @@ class Data: cls.layer_set_usages = {} cls.load_element("IfcMaterialLayerSetUsage", cls.layer_set_usages) + @classmethod + def load_profile_usages(cls): + cls.profile_set_usages = {} + cls.load_element("IfcMaterialProfileSetUsage", cls.profile_set_usages) + @classmethod def load_profiles(cls): cls.profile_sets = {} @@ -109,6 +118,4 @@ class Data: @classmethod def load_association(cls, association, product_id): material_select = association.RelatingMaterial - if material_select.is_a("IfcMaterialProfileSetUsage"): # TODO: implement usages - material_select = material_select.ForProfileSet cls.products[product_id] = {"type": material_select.is_a(), "id": material_select.id()} From bcfc97037ccd21a91458579d664d32e4d50b9e4b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 15 Apr 2021 19:33:01 +1000 Subject: [PATCH 23/64] Spatial aggregations are auto assigned when you create them. Neat. --- src/blenderbim/blenderbim/bim/module/aggregate/operator.py | 3 ++- src/blenderbim/blenderbim/bim/module/root/operator.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index 5d366d5191..27a62fd4d2 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -39,7 +39,8 @@ class AssignObject(bpy.types.Operator): self.remove_collection(bpy.context.scene.collection, spatial_collection) for collection in bpy.data.collections: if collection == relating_collection: - collection.children.link(spatial_collection) + if not collection.children.get(spatial_collection.name): + collection.children.link(spatial_collection) continue self.remove_collection(collection, spatial_collection) else: diff --git a/src/blenderbim/blenderbim/bim/module/root/operator.py b/src/blenderbim/blenderbim/bim/module/root/operator.py index 0af5b8bddb..a785741d60 100644 --- a/src/blenderbim/blenderbim/bim/module/root/operator.py +++ b/src/blenderbim/blenderbim/bim/module/root/operator.py @@ -153,6 +153,7 @@ class AssignClass(bpy.types.Operator): collection.objects.link(obj) if parent_collection: parent_collection.children.link(collection) + bpy.ops.bim.assign_object(related_object=obj.name, relating_object=parent_collection.name) else: bpy.context.scene.collection.children.link(collection) From f4e6e924b13549dbe20729047350eb591375fce1 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 15 Apr 2021 19:56:18 +1000 Subject: [PATCH 24/64] Spaces are now always imported, and consistent with the rest of the spatial tree. Yes, I don't know why I held off for so long. Deprecate "import spaces". Spaces are important. --- src/blenderbim/blenderbim/bim/import_ifc.py | 21 ++------------------- src/blenderbim/blenderbim/bim/operator.py | 2 -- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index eb7a298c4f..f98f0f5082 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -780,9 +780,6 @@ class IfcImporter: if element is None: return - if not self.ifc_import_settings.should_import_spaces and element.is_a("IfcSpace"): - return - self.ifc_import_settings.logger.info("Creating object %s", element) if mesh: @@ -1084,8 +1081,6 @@ class IfcImporter: def add_related_objects(self, parent, related_objects): for element in related_objects: - if element.is_a("IfcSpace"): - continue global_id = element.GlobalId collection = bpy.data.collections.new(self.get_name(element)) self.spatial_structure_elements[global_id] = {"blender": collection} @@ -1129,8 +1124,6 @@ class IfcImporter: container = element.ContainedInStructure[0].RelatingStructure elif hasattr(element, "Decomposes") and element.Decomposes: container = element.Decomposes[0].RelatingObject - if container.is_a("IfcSpace"): - return self.get_aggregate_container(container) return container def create_openings_collection(self): @@ -1179,9 +1172,7 @@ class IfcImporter: and element.ContainedInStructure[0].RelatingStructure ): container = element.ContainedInStructure[0].RelatingStructure - if container.is_a("IfcSpace"): - return self.place_object_in_spatial_tree(container, obj) - elif element.is_a("IfcGrid"): + if element.is_a("IfcGrid"): grid_collection = bpy.data.collections.get(obj.name) if grid_collection: # Just in case we ran into invalid grids from Revit self.spatial_structure_elements[container.GlobalId]["blender"].children.link(grid_collection) @@ -1193,22 +1184,15 @@ class IfcImporter: if element.Decomposes[0].RelatingObject.is_a("IfcProject"): collection = self.project["blender"] elif element.Decomposes[0].RelatingObject.is_a("IfcSpatialStructureElement"): - if element.is_a("IfcSpatialStructureElement") and not element.is_a("IfcSpace"): + if element.is_a("IfcSpatialStructureElement"): global_id = element.GlobalId - else: - global_id = element.Decomposes[0].RelatingObject.GlobalId if global_id in self.spatial_structure_elements: if ( element.is_a("IfcSpatialStructureElement") - and not element.is_a("IfcSpace") and "blender_obj" in self.spatial_structure_elements[global_id] ): bpy.data.objects.remove(self.spatial_structure_elements[global_id]["blender_obj"]) collection = self.spatial_structure_elements[global_id]["blender"] - # This may occur if we are nesting an IfcSpace (which is special - # since it does not have a collection within an IfcSpace - if not collection: - return self.place_object_in_spatial_tree(element.Decomposes[0].RelatingObject, obj) else: collection = self.aggregates[element.Decomposes[0].RelatingObject.GlobalId]["blender"] if collection: @@ -1432,7 +1416,6 @@ class IfcImportSettings: self.logger = None self.input_file = None self.diff_file = None - self.should_import_spaces = False self.should_auto_set_workarounds = True self.should_use_cpu_multiprocessing = True self.should_merge_by_class = False diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index 3851f1aff2..4c4cf82091 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -115,7 +115,6 @@ class ImportIFC(bpy.types.Operator, ImportHelper): filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) - should_import_spaces: bpy.props.BoolProperty(name="Import Spaces", default=False) should_auto_set_workarounds: bpy.props.BoolProperty(name="Automatically Set Vendor Workarounds", default=True) should_use_cpu_multiprocessing: bpy.props.BoolProperty(name="Import with CPU Multiprocessing", default=True) should_merge_by_class: bpy.props.BoolProperty(name="Import and Merge by Class", default=False) @@ -140,7 +139,6 @@ class ImportIFC(bpy.types.Operator, ImportHelper): ) settings = import_ifc.IfcImportSettings.factory(context, self.filepath, logger) - settings.should_import_spaces = self.should_import_spaces settings.should_auto_set_workarounds = self.should_auto_set_workarounds settings.should_use_cpu_multiprocessing = self.should_use_cpu_multiprocessing settings.should_merge_by_class = self.should_merge_by_class From 5f9474acb7eeea6f169d0f2617cf28a3560382dd Mon Sep 17 00:00:00 2001 From: bosonprojets Date: Thu, 15 Apr 2021 23:08:45 +0000 Subject: [PATCH 25/64] Feature to add multiple summary cost items and remove cost items --- .../blenderbim/bim/module/cost/__init__.py | 2 + .../blenderbim/bim/module/cost/operator.py | 52 ++++++++++++++++--- .../blenderbim/bim/module/cost/ui.py | 13 ++--- .../ifcopenshell/api/cost/remove_cost_item.py | 10 ++++ 4 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item.py diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index ce2d98dc85..7a8a31b1d3 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -8,8 +8,10 @@ classes = ( operator.EnableEditingCostSchedule, operator.DisableEditingCostSchedule, operator.AddCostItem, + operator.AddSummaryCostItem, operator.ExpandCostItem, operator.ContractCostItem, + operator.RemoveCostItem, prop.CostItem, prop.BIMCostProperties, ui.BIM_PT_cost_schedules, diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index afc937ea78..86e0cf03df 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -120,24 +120,37 @@ class EditCostSchedule(bpy.types.Operator): return {"FINISHED"} -class AddCostItem(bpy.types.Operator): - bl_idname = "bim.add_cost_item" +class AddSummaryCostItem(bpy.types.Operator): + bl_idname = "bim.add_summary_cost_item" bl_label = "Add Cost Item" cost_schedule: bpy.props.IntProperty() def execute(self, context): props = context.scene.BIMCostProperties self.file = IfcStore.get_file() - if len(props.cost_items): - data = {"cost_item": self.file.by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id)} - else: - data = {"cost_schedule": self.file.by_id(self.cost_schedule)} - ifcopenshell.api.run("cost.add_cost_item", self.file, **data) + ifcopenshell.api.run("cost.add_cost_item", self.file, **{ + "cost_schedule": self.file.by_id(self.cost_schedule) + }) Data.load(self.file) bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = self.cost_schedule) return {"FINISHED"} +class AddCostItem(bpy.types.Operator): + bl_idname = "bim.add_cost_item" + bl_label = "Add Cost Item" + cost_item: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() + data = {"cost_item": self.file.by_id(self.cost_item)} + ifcopenshell.api.run("cost.add_cost_item", self.file, **data) + Data.load(self.file) + bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = props.active_cost_schedule_id) + return {"FINISHED"} + + class ExpandCostItem(bpy.types.Operator): bl_idname = "bim.expand_cost_item" bl_label = "Expand Cost Item" @@ -145,9 +158,11 @@ class ExpandCostItem(bpy.types.Operator): def execute(self, context): props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() contracted_cost_items = json.loads(props.contracted_cost_items) contracted_cost_items.remove(self.cost_item) props.contracted_cost_items = json.dumps(contracted_cost_items) + Data.load(self.file) bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = props.active_cost_schedule_id) return {"FINISHED"} @@ -159,8 +174,31 @@ class ContractCostItem(bpy.types.Operator): def execute(self, context): props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() contracted_cost_items = json.loads(props.contracted_cost_items) contracted_cost_items.append(self.cost_item) props.contracted_cost_items = json.dumps(contracted_cost_items) + Data.load(self.file) + bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = props.active_cost_schedule_id) + return {"FINISHED"} + + +class RemoveCostItem(bpy.types.Operator): + bl_idname = "bim.remove_cost_item" + bl_label = "Remove Cost item" + cost_item: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.remove_cost_item", + self.file, + cost_item=IfcStore.get_file().by_id(self.cost_item), + ) + Data.load(self.file) + # contracted_cost_items = json.loads(props.contracted_cost_items) + # contracted_cost_items.remove(props.active_cost_item_index) + # props.contracted_cost_items = json.dumps(contracted_cost_items) bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = props.active_cost_schedule_id) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index abfc20074b..7c9edc329e 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -51,9 +51,8 @@ class BIM_PT_cost_schedules(Panel): row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") row = self.layout.row(align=True) - row.label(text="X Cost Items") - row.operator("bim.add_cost_item", text="", icon="ADD").cost_schedule = cost_schedule_id - + row.label(text="X Summary Cost Items") + row.operator("bim.add_summary_cost_item", text="", icon="ADD").cost_schedule = cost_schedule_id self.layout.template_list( "BIM_UL_cost_items", "", @@ -72,10 +71,12 @@ class BIM_UL_cost_items(UIList): row.label(text="", icon="BLANK1") if item.has_children: if item.is_expanded: - op = row.operator("bim.contract_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN") + row.operator("bim.contract_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN").cost_item = item.ifc_definition_id else: - op = row.operator("bim.expand_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT") - op.cost_item = item.ifc_definition_id + row.operator("bim.expand_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT").cost_item = item.ifc_definition_id else: row.label(text="", icon="DOT") row.label(text=item.name) + row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = item.ifc_definition_id + op = row.operator("bim.remove_cost_item", text="", icon="X") + op.cost_item = item.ifc_definition_id diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item.py new file mode 100644 index 0000000000..003cf31931 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item.py @@ -0,0 +1,10 @@ +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"cost_item": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + # TODO: do a deep purge + self.file.remove(self.settings["cost_item"]) From 3e1eb9648fc20c0fef21977ac09d74ffab054955 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 17 Apr 2021 17:57:52 +1000 Subject: [PATCH 26/64] Fix showstopper bug with editing pset templates module --- src/blenderbim/blenderbim/bim/module/pset_template/prop.py | 2 +- src/blenderbim/blenderbim/bim/module/pset_template/ui.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/prop.py b/src/blenderbim/blenderbim/bim/module/pset_template/prop.py index 36b8bc8c05..83b134c399 100644 --- a/src/blenderbim/blenderbim/bim/module/pset_template/prop.py +++ b/src/blenderbim/blenderbim/bim/module/pset_template/prop.py @@ -65,7 +65,7 @@ def getPsetTemplates(self, context): IfcStore.pset_template_file = ifcopenshell.open(IfcStore.pset_template_path) templates = IfcStore.pset_template_file.by_type("IfcPropertySetTemplate") psettemplates_enum.extend([(str(t.id()), t.Name, "") for t in templates]) - Data.load(IfcStore.get_file()) + Data.load(IfcStore.pset_template_file) return psettemplates_enum diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/ui.py b/src/blenderbim/blenderbim/bim/module/pset_template/ui.py index 49813c0c96..b252e4e496 100644 --- a/src/blenderbim/blenderbim/bim/module/pset_template/ui.py +++ b/src/blenderbim/blenderbim/bim/module/pset_template/ui.py @@ -35,10 +35,8 @@ class BIM_PT_pset_template(Panel): row.operator("bim.enable_editing_pset_template", text="", icon="GREASEPENCIL") row.operator("bim.remove_pset_template", text="", icon="X") - # row.operator("bim.save_pset_template", text="", icon="EXPORT") - if not Data.is_loaded and props.pset_template_files: - Data.load(IfcStore.get_file()) + Data.load(IfcStore.pset_template_file) if not Data.pset_templates: return From f5d27621e77c4c127b772f2ad2f9e1679fa73a0b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 17 Apr 2021 20:18:01 +1000 Subject: [PATCH 27/64] Fix #1436. Bug where saving is done twice in authoring mode. --- src/blenderbim/blenderbim/bim/export_ifc.py | 3 --- src/blenderbim/blenderbim/bim/handler.py | 4 +--- src/blenderbim/blenderbim/bim/operator.py | 5 ++--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index 5290b1215f..c0b52e7ce6 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -46,9 +46,6 @@ class IfcExporter: jsonData = ifcjson.IFC2JSON5a(self.file, self.ifc_export_settings.json_compact).spf2Json() with open(self.ifc_export_settings.output_file, "w") as outfile: json.dump(jsonData, outfile, indent=None if self.ifc_export_settings.json_compact else 4) - if bpy.context.scene.BIMProjectProperties.is_authoring: - if bpy.data.filepath: - bpy.ops.wm.save_mainfile() def set_header(self): # TODO: add all metadata, pending bug #747 diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 7d7d5b6bfc..0cdd9ea2b6 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -81,9 +81,7 @@ def loadIfcStore(scene): @persistent def ensureIfcExported(scene): if IfcStore.get_file() and not bpy.context.scene.BIMProperties.ifc_file: - # The invocation pops up a file select window. - # This is non-blocking, therefore the Blend file is saved before we export. - bpy.ops.export_ifc.bim("INVOKE_DEFAULT", should_force_resave=True) + bpy.ops.export_ifc.bim("INVOKE_DEFAULT") @persistent diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index 4c4cf82091..ff31c20ba8 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -67,7 +67,6 @@ class ExportIFC(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version") json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False) - should_force_resave: bpy.props.BoolProperty(name="Resave .blend", default=False) def invoke(self, context, event): if not self.filepath: @@ -104,8 +103,8 @@ class ExportIFC(bpy.types.Operator): new.name = output_file if not bpy.context.scene.BIMProperties.ifc_file: bpy.context.scene.BIMProperties.ifc_file = output_file - if self.should_force_resave: - bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) + if bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath: + bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath) return {"FINISHED"} From 3201c234fa657a3cb2fff752963acae6829d5f3a Mon Sep 17 00:00:00 2001 From: bosonprojets Date: Sun, 18 Apr 2021 00:49:43 +0000 Subject: [PATCH 28/64] Feature to create IfcTask trees for entities of IfcWorkSchedule --- .../bim/module/sequence/__init__.py | 7 +- .../bim/module/sequence/operator.py | 133 ++++++++++++++---- .../blenderbim/bim/module/sequence/prop.py | 12 +- .../blenderbim/bim/module/sequence/ui.py | 79 +++++------ .../ifcopenshell/api/sequence/add_task.py | 31 ++-- .../ifcopenshell/api/sequence/data.py | 8 +- .../ifcopenshell/api/sequence/remove_task.py | 19 +++ .../api/sequence/remove_work_schedule.py | 3 + 8 files changed, 196 insertions(+), 96 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/remove_task.py diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 27102da316..9f57dc8d18 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -9,7 +9,6 @@ classes = ( operator.RemoveWorkPlan, operator.EnableEditingWorkPlan, operator.DisableEditingWorkPlan, - operator.LoadWorkSchedules, operator.DisableWorkScheduleEditingUI, operator.AddWorkSchedule, operator.EditWorkSchedule, @@ -26,17 +25,19 @@ classes = ( operator.EnableEditingWorkCalendar, operator.DisableEditingWorkCalendar, operator.AddTask, + operator.AddSummaryTask, + operator.ExpandTask, + operator.ContractTask, + operator.RemoveTask, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, - prop.WorkSchedule, prop.BIMWorkScheduleProperties, prop.WorkCalendar, prop.BIMWorkCalendarProperties, ui.BIM_PT_work_plans, ui.BIM_UL_work_plans, ui.BIM_PT_work_schedules, - ui.BIM_UL_work_schedules, ui.BIM_PT_work_calendars, ui.BIM_UL_work_calendars, ui.BIM_UL_tasks, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 1b0ecad74a..b344b7ab3d 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -123,23 +123,6 @@ class DisableEditingWorkPlan(bpy.types.Operator): return {"FINISHED"} -class LoadWorkSchedules(bpy.types.Operator): - bl_idname = "bim.load_work_schedules" - bl_label = "Load Work Schedules" - - def execute(self, context): - props = context.scene.BIMWorkScheduleProperties - while len(props.work_schedules) > 0: - props.work_schedules.remove(0) - for ifc_definition_id, work_schedule in Data.work_schedules.items(): - new = props.work_schedules.add() - new.ifc_definition_id = ifc_definition_id - new.name = work_schedule["Name"] or "Unnamed" - props.is_editing = True - bpy.ops.bim.disable_editing_work_schedule() - return {"FINISHED"} - - class DisableWorkScheduleEditingUI(bpy.types.Operator): bl_idname = "bim.disable_work_schedule_editing_ui" bl_label = "Disable WorkSchedule Editing UI" @@ -156,7 +139,6 @@ class AddWorkSchedule(bpy.types.Operator): def execute(self, context): ifcopenshell.api.run("sequence.add_work_schedule", IfcStore.get_file()) Data.load(IfcStore.get_file()) - bpy.ops.bim.load_work_schedules() return {"FINISHED"} @@ -182,7 +164,7 @@ class EditWorkSchedule(bpy.types.Operator): **{"work_schedule": self.file.by_id(props.active_work_schedule_id), "attributes": attributes} ) Data.load(IfcStore.get_file()) - bpy.ops.bim.load_work_schedules() + bpy.ops.bim.disable_editing_work_schedule() return {"FINISHED"} @@ -194,10 +176,11 @@ class RemoveWorkSchedule(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( - "sequence.remove_work_schedule", self.file, **{"work_schedule": self.file.by_id(self.work_schedule)} + "sequence.remove_work_schedule", + self.file, + work_schedule= self.file.by_id(self.work_schedule) ) Data.load(self.file) - bpy.ops.bim.load_work_schedules() return {"FINISHED"} @@ -207,9 +190,10 @@ class EnableEditingWorkSchedule(bpy.types.Operator): work_schedule: bpy.props.IntProperty() def execute(self, context): - props = context.scene.BIMWorkScheduleProperties - while len(props.work_schedule_attributes) > 0: - props.work_schedule_attributes.remove(0) + self.props = context.scene.BIMWorkScheduleProperties + self.props.active_work_schedule_id = self.work_schedule + while len(self.props.work_schedule_attributes) > 0: + self.props.work_schedule_attributes.remove(0) data = Data.work_schedules[self.work_schedule] @@ -217,7 +201,7 @@ class EnableEditingWorkSchedule(bpy.types.Operator): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) if data_type == "entity": continue - new = props.work_schedule_attributes.add() + new = self.props.work_schedule_attributes.add() new.name = attribute.name() new.is_null = data[attribute.name()] is None new.is_optional = attribute.optional() @@ -230,10 +214,30 @@ class EnableEditingWorkSchedule(bpy.types.Operator): new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) if data[attribute.name()]: new.enum_value = data[attribute.name()] - props.active_work_schedule_id = self.work_schedule - bpy.ops.bim.load_tasks(work_schedule=self.work_schedule) + self.props.active_work_schedule_id = self.work_schedule + + while len(self.props.tasks) > 0: + self.props.tasks.remove(0) + + self.contracted_tasks = json.loads(self.props.contracted_tasks) + for related_object_id in Data.work_schedules[self.work_schedule]["RelatedObjects"]: + self.create_new_task_li(related_object_id, 0) return {"FINISHED"} + def create_new_task_li(self, related_object_id, level_index): + task = Data.tasks[related_object_id] + new = self.props.tasks.add() + new.name = task["Name"] or "Unnamed" + new.ifc_definition_id = related_object_id + new.is_expanded = related_object_id not in self.contracted_tasks + new.level_index = level_index + if task["RelatedObjects"]: + new.has_children = True + if new.is_expanded: + for related_object_id in task["RelatedObjects"]: + self.create_new_task_li(related_object_id, level_index + 1) + # return {"FINISHED"} + class DisableEditingWorkSchedule(bpy.types.Operator): bl_idname = "bim.disable_editing_work_schedule" @@ -392,13 +396,80 @@ class DisableTaskEditingUI(bpy.types.Operator): class AddTask(bpy.types.Operator): bl_idname = "bim.add_task" bl_label = "Add Task" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run("sequence.add_task", self.file, **{ + "parent_task": self.file.by_id(self.task) + }) + Data.load(self.file) + bpy.ops.bim.enable_editing_work_schedule(work_schedule = props.active_work_schedule_id) + return {"FINISHED"} + + +class AddSummaryTask(bpy.types.Operator): + bl_idname = "bim.add_summary_task" + bl_label = "Add Task" work_schedule: bpy.props.IntProperty() def execute(self, context): + props = context.scene.BIMWorkScheduleProperties self.file = IfcStore.get_file() - task = ifcopenshell.api.run("sequence.add_task", self.file) - control = self.file.by_id(self.work_schedule) - ifcopenshell.api.run("control.assign_control", self.file, related_object=task, relating_control=control) + ifcopenshell.api.run("sequence.add_task", self.file, **{ + "work_schedule": self.file.by_id(self.work_schedule) + }) Data.load(self.file) - bpy.ops.bim.enable_editing_work_schedule(work_schedule = self.work_schedule) + bpy.ops.bim.enable_editing_work_schedule(work_schedule = props.active_work_schedule_id) + return {"FINISHED"} + + +class ExpandTask(bpy.types.Operator): + bl_idname = "bim.expand_task" + bl_label = "Expand Task" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + contracted_tasks = json.loads(props.contracted_tasks) + contracted_tasks.remove(self.task) + props.contracted_tasks = json.dumps(contracted_tasks) + Data.load(self.file) + bpy.ops.bim.enable_editing_work_schedule(work_schedule = props.active_work_schedule_id) + return {"FINISHED"} + + +class ContractTask(bpy.types.Operator): + bl_idname = "bim.contract_task" + bl_label = "Contract Task" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + contracted_tasks = json.loads(props.contracted_tasks) + contracted_tasks.append(self.task) + props.contracted_tasks = json.dumps(contracted_tasks) + Data.load(self.file) + bpy.ops.bim.enable_editing_work_schedule(work_schedule = props.active_work_schedule_id) + return {"FINISHED"} + + +class RemoveTask(bpy.types.Operator): + bl_idname = "bim.remove_task" + bl_label = "Remove Task" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.remove_task", + self.file, + task=IfcStore.get_file().by_id(self.task), + ) + Data.load(self.file) + bpy.ops.bim.enable_editing_work_schedule(work_schedule = props.active_work_schedule_id) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index a21ec53a48..6d1d089537 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -17,7 +17,9 @@ class Task(PropertyGroup): name: StringProperty(name="Name") identification: StringProperty(name="Identification") ifc_definition_id: IntProperty(name="IFC Definition ID") - + has_children: BoolProperty(name="Has Children") + is_expanded: BoolProperty(name="Is Expanded") + level_index: IntProperty(name="Level Index") class WorkPlan(PropertyGroup): name: StringProperty(name="Name") @@ -32,19 +34,15 @@ class BIMWorkPlanProperties(PropertyGroup): active_work_plan_id: IntProperty(name="Active Work Plan Id") -class WorkSchedule(PropertyGroup): - name: StringProperty(name="Name") - ifc_definition_id: IntProperty(name="IFC Definition ID") - - class BIMWorkScheduleProperties(PropertyGroup): work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute) is_editing: BoolProperty(name="Is Editing", default=False) - work_schedules: CollectionProperty(name="Work Schedules", type=WorkSchedule) active_work_schedule_index: IntProperty(name="Active Work Schedules Index") active_work_schedule_id: IntProperty(name="Active Work Schedules Id") tasks: CollectionProperty(name="Tasks", type=Task) active_task_index: IntProperty(name="Active Task Index") + contracted_tasks: StringProperty(name="Contracted Task Items", default="[]") + class WorkCalendar(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index d0eba710f5..54710c9621 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -80,31 +80,33 @@ class BIM_PT_work_schedules(Panel): return IfcStore.get_file() def draw(self, context): + self.props = context.scene.BIMWorkScheduleProperties + if not Data.is_loaded: Data.load(IfcStore.get_file()) - self.props = context.scene.BIMWorkScheduleProperties + row = self.layout.row(align=True) row.label(text="{} Work Schedules Found".format(len(Data.work_schedules)), icon="TEXT") - if self.props.is_editing: - row.operator("bim.add_work_schedule", text="", icon="ADD") - row.operator("bim.disable_work_schedule_editing_ui", text="", icon="CHECKMARK") - else: - row.operator("bim.load_work_schedules", text="", icon="GREASEPENCIL") + row = self.layout.row() + row.operator("bim.add_work_schedule", icon="ADD") - if self.props.is_editing: - self.layout.template_list( - "BIM_UL_work_schedules", - "", - self.props, - "work_schedules", - self.props, - "active_work_schedule_index", - ) + for work_schedule_id, work_schedule in Data.work_schedules.items(): + row = self.layout.row(align=True) + row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") - if self.props.active_work_schedule_id: - self.draw_editable_ui(context) + if self.props.active_work_schedule_id and self.props.active_work_schedule_id == work_schedule_id: + row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_work_schedule", text="", icon="X") + elif self.props.active_work_schedule_id: + row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id + else: + row.operator("bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL").work_schedule = work_schedule_id + row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id - def draw_editable_ui(self, context): + if self.props.active_work_schedule_id == work_schedule_id: + self.draw_editable_work_schedule_ui(work_schedule_id, work_schedule) + + def draw_editable_work_schedule_ui(self, work_schedule_id, work_schedule): for attribute in self.props.work_schedule_attributes: row = self.layout.row(align=True) if attribute.data_type == "string": @@ -114,13 +116,9 @@ class BIM_PT_work_schedules(Panel): if attribute.is_optional: row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") - self.draw_task_ui(context) - - def draw_task_ui(self, context): row = self.layout.row(align=True) - row.label(text="{} Tasks Found".format(len(Data.tasks)), icon="ACTION") - row.operator("bim.add_task", text="", icon="ADD").work_schedule = self.props.active_work_schedule_id - + row.label(text="X Summary Tasks") + row.operator("bim.add_summary_task", text="", icon="ADD").work_schedule = work_schedule_id self.layout.template_list( "BIM_UL_tasks", "", @@ -130,21 +128,23 @@ class BIM_PT_work_schedules(Panel): "active_task_index", ) - -class BIM_UL_work_schedules(UIList): +class BIM_UL_tasks(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: row = layout.row(align=True) - row.label(text=item.name) - if context.scene.BIMWorkScheduleProperties.active_work_schedule_id == item.ifc_definition_id: - row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_work_schedule", text="", icon="X") - elif context.scene.BIMWorkScheduleProperties.active_work_schedule_id: - row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = item.ifc_definition_id + for i in range(0, item.level_index): + row.label(text="", icon="BLANK1") + if item.has_children: + if item.is_expanded: + row.operator("bim.contract_task", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN").task = item.ifc_definition_id + else: + row.operator("bim.expand_task", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT").task = item.ifc_definition_id else: - op = row.operator("bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL") - op.work_schedule = item.ifc_definition_id - row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = item.ifc_definition_id + row.label(text="", icon="DOT") + row.label(text=item.name) + op = row.operator("bim.add_task", text="", icon="ADD") + op.task = item.ifc_definition_id + row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id class BIM_PT_work_calendars(Panel): @@ -209,12 +209,3 @@ class BIM_UL_work_calendars(UIList): op = row.operator("bim.enable_editing_work_calendar", text="", icon="GREASEPENCIL") op.work_calendar = item.ifc_definition_id row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = item.ifc_definition_id - - -class BIM_UL_tasks(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - if item: - row = layout.row(align=True) - if item.identification: - layout.label(text=item.identification) - layout.label(text=item.name) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py index e27acee45a..1102606064 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py @@ -1,16 +1,12 @@ import ifcopenshell.api - +import ifcopenshell class Usecase: def __init__(self, file, **settings): self.file = file self.settings = { - "name": None, - "predefined_type": "NOTDEFINED", - "is_milestone": False, - "identification": "none", - "predecessor_to": None, - "successor_from": None, + "work_schedule": None, + "parent_task": None, } for key, value in settings.items(): self.settings[key] = value @@ -20,8 +16,23 @@ class Usecase: "root.create_entity", self.file, ifc_class="IfcTask", - predefined_type=self.settings["predefined_type"], - name=self.settings["name"], + name= None, + predefined_type= "NOTDEFINED", + identification= "none", ) - task.IsMilestone = self.settings["is_milestone"] + task.IsMilestone = False + if self.settings["work_schedule"]: + self.file.create_entity( + "IfcRelAssignsToControl", + **{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatedObjects": [task], + "RelatingControl": self.settings["work_schedule"], + } + ) + elif self.settings["parent_task"]: + ifcopenshell.api.run( + "nest.assign_object", self.file, object=task, relating_object=self.settings["parent_task"] + ) return task diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py index edfa8cbe45..04ba1e7a63 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py @@ -61,6 +61,7 @@ class Data: @classmethod def load_work_calendars(cls): + cls.work_calendars = {} for work_calendar in cls._file.by_type("IfcWorkCalendar"): data = work_calendar.get_info() del data["OwnerHistory"] @@ -72,4 +73,9 @@ class Data: def load_tasks(cls): cls.tasks = {} for task in cls._file.by_type("IfcTask"): - cls.tasks[task.id()] = {"Name": task.Name, "Identification": task.Identification or ""} + data = task.get_info() + del data["OwnerHistory"] + data["RelatedObjects"] = [] + for rel in task.IsNestedBy: + [data["RelatedObjects"].append(o.id()) for o in rel.RelatedObjects if o.is_a("IfcTask")] + cls.tasks[task.id()] = data diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_task.py new file mode 100644 index 0000000000..941a335663 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_task.py @@ -0,0 +1,19 @@ +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"task": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + # TODO: do a deep purge + ifcopenshell.api.run( + "project.unassign_declaration", + self.file, + definition=self.settings["task"], + relating_context=self.file.by_type("IfcContext")[0], + ) + self.file.remove(self.settings["task"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_schedule.py index c0af69b25d..2fd21d975f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_schedule.py @@ -1,3 +1,6 @@ +import ifcopenshell.api + + class Usecase: def __init__(self, file, **settings): self.file = file From 1e299a0dddb0aa4e322cfb3d1868a06419935109 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 18 Apr 2021 19:02:39 +1000 Subject: [PATCH 29/64] Make remove_deep_batched the default since I'm the only guy using it so far and I'm feeling lucky! Let's battle test it! --- src/ifcopenshell-python/ifcopenshell/util/element.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 788acf7f34..1c90f3d391 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -119,17 +119,13 @@ def is_representation_of_context(representation, context, subcontext=None, targe def remove_deep(ifc_file, element): + # @todo maybe some sort of try-finally mechanism. + ifc_file.batch() subgraph = list(ifc_file.traverse(element)) subgraph_set = set(subgraph) for ref in subgraph[::-1]: if ref.id() and len(set(ifc_file.get_inverse(ref)) - subgraph_set) == 0: ifc_file.remove(ref) - - -def remove_deep_batched(ifc_file, element): - # @todo maybe some sort of try-finally mechanism. - ifc_file.batch() - remove_deep(ifc_file, element) ifc_file.unbatch() From 6bb63817af9dbece5542876d0eed7aa94c0662f8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 18 Apr 2021 19:28:05 +1000 Subject: [PATCH 30/64] Fix #1435. Fix bug where box reps could be duplicated upon adding a new body, and ensure all styles are created prior to adding new body geometry. --- .../blenderbim/bim/module/geometry/operator.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 19f86fa5d5..0047d012eb 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -104,18 +104,27 @@ class AddRepresentation(bpy.types.Operator): return {"FINISHED"} box_context_id = get_context_id("Model", "Box", "MODEL_VIEW") + old_box = ifcopenshell.util.element.get_representation(product, "Model", "Box", "MODEL_VIEW") if ( box_context_id and context_of_items.ContextType == "Model" and context_of_items.ContextIdentifier and context_of_items.ContextIdentifier == "Body" ): + if old_box: + bpy.ops.bim.remove_representation(representation_id=old_box.id(), obj=obj.name) representation_data["context"] = self.file.by_id(box_context_id) new_box = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data) ifcopenshell.api.run( "geometry.assign_representation", self.file, **{"product": product, "representation": new_box} ) + [ + bpy.ops.bim.add_style(material=s.material.name) + for s in obj.material_slots + if not s.material.BIMMaterialProperties.ifc_style_id + ] + ifcopenshell.api.run( "geometry.assign_styles", self.file, From 6c8858e51d427e1375ba457054f990425233682e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 18 Apr 2021 19:58:07 +1000 Subject: [PATCH 31/64] Fix bug where edit mode syncing didn't work if you edited many objects at once --- src/blenderbim/blenderbim/bim/handler.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 0cdd9ea2b6..2626d5335d 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -9,17 +9,18 @@ from ifcopenshell.api.attribute.data import Data as AttributeData def mode_callback(obj, data): - if ( - obj.mode != "OBJECT" - or not obj.data - or not isinstance(obj.data, bpy.types.Mesh) - or not obj.data.BIMMeshProperties.ifc_definition_id - or not bpy.context.scene.BIMProjectProperties.is_authoring - ): - return - representation = IfcStore.get_file().by_id(obj.data.BIMMeshProperties.ifc_definition_id) - if representation.RepresentationType == "Tessellation" or representation.RepresentationType == "Brep": - IfcStore.edited_objs.add(obj.name) + for obj in bpy.context.selected_objects: + if ( + obj.mode != "OBJECT" + or not obj.data + or not isinstance(obj.data, bpy.types.Mesh) + or not obj.data.BIMMeshProperties.ifc_definition_id + or not bpy.context.scene.BIMProjectProperties.is_authoring + ): + return + representation = IfcStore.get_file().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + if representation.RepresentationType == "Tessellation" or representation.RepresentationType == "Brep": + IfcStore.edited_objs.add(obj.name) def name_callback(obj, data): From 75040b6ffb83e50018ed81df3dd7532423a1e5b3 Mon Sep 17 00:00:00 2001 From: Rick Brice Date: Sat, 17 Apr 2021 19:33:06 -0700 Subject: [PATCH 32/64] Fixes initialization issues in IfcEntityInstanceData constructors attributes_ was allocated as an array of Argument pointers but the pointers were uninitialized in the construction. Initialization was done in the HeaderEntity class constructor. offset_in_file_ is uninitialized in one constructor --- src/ifcparse/IfcEntityInstanceData.h | 14 +++++--------- src/ifcparse/IfcSpfHeader.cpp | 5 ----- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/ifcparse/IfcEntityInstanceData.h b/src/ifcparse/IfcEntityInstanceData.h index 7d2c4ee8ca..6fb0386f38 100644 --- a/src/ifcparse/IfcEntityInstanceData.h +++ b/src/ifcparse/IfcEntityInstanceData.h @@ -48,17 +48,13 @@ public: : file(file_), id_(id), type_(type), attributes_(0), offset_in_file_(offset_in_file) {} - IfcEntityInstanceData(IfcParse::IfcFile* file_, size_t size) - : file(file_), id_(0), type_(0), attributes_(new Argument*[size]), offset_in_file_(0) + IfcEntityInstanceData(IfcParse::IfcFile* file_, size_t size) + : file(file_), id_(0), type_(0), attributes_(new Argument*[size] {0}), offset_in_file_(0) {} - IfcEntityInstanceData(const IfcParse::declaration* type) - : file(0), id_(0), type_(type), attributes_(new Argument*[getArgumentCount()]) - { - for (size_t i = 0; i < getArgumentCount(); ++i) { - attributes_[i] = 0; - } - } + IfcEntityInstanceData(const IfcParse::declaration* type) + : file(0), id_(0), type_(type), attributes_(new Argument*[getArgumentCount()]{ 0 }), offset_in_file_(0) + {} void load() const; diff --git a/src/ifcparse/IfcSpfHeader.cpp b/src/ifcparse/IfcSpfHeader.cpp index 5b95e68dcf..779b717152 100644 --- a/src/ifcparse/IfcSpfHeader.cpp +++ b/src/ifcparse/IfcSpfHeader.cpp @@ -43,11 +43,6 @@ HeaderEntity::HeaderEntity(const char * const datatype, size_t size, IfcFile* fi if (file) { offset_in_file_ = file->stream->Tell(); load(); - } else { - // attributes_ = new Argument*[size]; - for (size_t i = 0; i < size; ++i) { - attributes_[i] = 0; - } } } From 01cfcebb099f9d80bc21c1958604add0057a01b6 Mon Sep 17 00:00:00 2001 From: Rick Brice Date: Sat, 17 Apr 2021 19:33:46 -0700 Subject: [PATCH 33/64] Fixes problem with dereferencing the end iterator for empty std::string objects --- src/ifcparse/IfcCharacterDecoder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcparse/IfcCharacterDecoder.cpp b/src/ifcparse/IfcCharacterDecoder.cpp index 1c2fd38228..0ad9cc787d 100644 --- a/src/ifcparse/IfcCharacterDecoder.cpp +++ b/src/ifcparse/IfcCharacterDecoder.cpp @@ -317,7 +317,7 @@ IfcCharacterEncoder::operator std::string() { // Either 2 or 4 to uses \X2 or \X4 respectively. // Currently hardcoded to 4, but \X2 might be // sufficient for nearly all purposes. - const int num_bytes = *std::max_element(str.begin(), str.end()) > 0xffff ? 4 : 2; + const int num_bytes = (str.empty() || *std::max_element(str.begin(), str.end())) > 0xffff ? 4 : 2; const std::string num_bytes_str = std::string(1,num_bytes + 0x30); bool in_extended = false; From 8187c7518326fcc094ce370f98ed23795bf3e309 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 18 Apr 2021 20:17:45 +1000 Subject: [PATCH 34/64] If you are already authoring a file, exporting now no longer prompts for a file location. --- src/blenderbim/blenderbim/bim/operator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index ff31c20ba8..51eb123f52 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -69,6 +69,9 @@ class ExportIFC(bpy.types.Operator): json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False) def invoke(self, context, event): + if bpy.context.scene.BIMProperties.ifc_file: + self.filepath = bpy.context.scene.BIMProperties.ifc_file + return self.execute(context) if not self.filepath: self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".ifc") WindowManager = context.window_manager From d6d70af5a067a811f8939c9af430e97dfd175415 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 18 Apr 2021 20:24:08 +1000 Subject: [PATCH 35/64] You can now copy classes in bulk. --- src/blenderbim/blenderbim/bim/module/root/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/root/ui.py b/src/blenderbim/blenderbim/bim/module/root/ui.py index d30a7ffe40..6991dbca58 100644 --- a/src/blenderbim/blenderbim/bim/module/root/ui.py +++ b/src/blenderbim/blenderbim/bim/module/root/ui.py @@ -39,7 +39,7 @@ class BIM_PT_class(Panel): name += "[{}]".format(data["PredefinedType"]) row = self.layout.row(align=True) row.label(text=name) - row.operator("bim.copy_class", icon="DUPLICATE", text="").obj = context.active_object.name + row.operator("bim.copy_class", icon="DUPLICATE", text="") row.operator("bim.unlink_object", icon="UNLINKED", text="") row.operator("bim.enable_reassign_class", icon="GREASEPENCIL", text="") if context.selected_objects: From bb6312e174787b4d42cf2e92865cf5529395797b Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 18 Apr 2021 15:03:28 +0200 Subject: [PATCH 36/64] #1153 --svg-without-storeys and correct positioning and scale based on annotation block --- src/ifcconvert/IfcConvert.cpp | 2 ++ src/serializers/SvgSerializer.cpp | 46 ++++++++++++++++++++++++++++--- src/serializers/SvgSerializer.h | 11 ++++++-- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/ifcconvert/IfcConvert.cpp b/src/ifcconvert/IfcConvert.cpp index 8ac40cff6f..536881401b 100644 --- a/src/ifcconvert/IfcConvert.cpp +++ b/src/ifcconvert/IfcConvert.cpp @@ -396,6 +396,7 @@ int main(int argc, char** argv) { "Approximate every curve as polygonal in SVG output") ("svg-project", "Always enable hidden line rendering instead of only on elevations") + ("svg-without-storeys", "Don't emit drawings for building storeys") ("door-arcs", "Draw door openings arcs for IfcDoor elements") ("section-height", po::value(§ion_height), "Specifies the cut section height for SVG 2D geometry.") @@ -1012,6 +1013,7 @@ int main(int argc, char** argv) { static_cast(serializer.get())->setUseHlrPoly(vmap.count("svg-poly") > 0); static_cast(serializer.get())->setPolygonal(vmap.count("svg-write-poly") > 0); static_cast(serializer.get())->setAlwaysProject(vmap.count("svg-project") > 0); + static_cast(serializer.get())->setWithoutStoreys(vmap.count("svg-without-storeys") > 0); if (relative_center_x && relative_center_y) { static_cast(serializer.get())->setDrawingCenter(*relative_center_x, *relative_center_y); } diff --git a/src/serializers/SvgSerializer.cpp b/src/serializers/SvgSerializer.cpp index b24af6b7a5..9662544c8a 100644 --- a/src/serializers/SvgSerializer.cpp +++ b/src/serializers/SvgSerializer.cpp @@ -603,14 +603,37 @@ void SvgSerializer::write(const IfcGeom::BRepElement* brep_obj) { } } + if (!emit_building_storeys_ && scale && size) { + scale_ = scale; + size_ = std::make_pair( + // The header writes values in mm + size->first * 1000 * *scale_, + size->second * 1000 * *scale_ + ); + } + if (pln) { // Move pln to have projection of origin at plane center. // This is necessary to have Poly and BRep HLR at the same position // (Poly) is wrong otherwise. Extrema_ExtPElS ext; ext.Perform(gp::Origin(), *pln, 1.e-5); + auto P0 = pln->Location(); pln->SetLocation(ext.Point(1).Value()); + if (!emit_building_storeys_ && scale && size) { + auto P1 = pln->Location(); + gp_Vec v(P1.XYZ() - P0.XYZ()); + gp_Trsf pi; + pi.SetTransformation(pln->Position()); + pi.Invert(); + v.Transform(pi); + offset_2d_ = std::make_pair( + (-size->first / 2. - v.X()) * 1000 * *scale_, + (-size->second / 2. + v.Y()) * 1000 * *scale_ + ); + } + if (!deferred_section_data_) { deferred_section_data_.emplace(); } @@ -639,7 +662,9 @@ void SvgSerializer::write(const IfcGeom::BRepElement* brep_obj) { element_buffer_.push_back(data); } - write(data); + if (emit_building_storeys_) { + write(data); + } } namespace { @@ -1408,16 +1433,22 @@ std::array, 3> SvgSerializer::resize() { if (size_) { // Scale the resulting image to a bounding rectangle specified by command line arguments + // or specified by IfcAnnotation[ObjectType=DRAWING] const double dx = xmax - xmin; const double dy = ymax - ymin; double sc, cx, cy; - if (scale_) { + if (offset_2d_ && scale_) { + // offset_2d is the offset in plane u,v coordinates as we want to keep the + // plane coordinates used for HLR close to the model origin. + sc = (*scale_) * 1000; + cx = offset_2d_->first; + cy = offset_2d_->second; + } else if (scale_) { sc = (*scale_) * 1000; cx = (xmax + xmin) / 2. * sc - size_->first * center_x_.get_value_or(0.5); cy = (ymax + ymin) / 2. * sc - size_->second * center_y_.get_value_or(0.5); - } - else { + } else { if (calculated_scale_) { sc = *calculated_scale_; } @@ -1708,6 +1739,8 @@ void SvgSerializer::addTextAnnotations(const drawing_key& k) { } void SvgSerializer::finalize() { + doWriteHeader(); + for (auto& p : drawing_metadata) { addTextAnnotations(p.first); } @@ -1900,6 +1933,11 @@ void SvgSerializer::finalize() { } void SvgSerializer::writeHeader() { + // This doesn't do anything anymore because there is now the option that an + // IfcAnnotation[ObjectType=DRAWING] defines the SVG viewBox and dimensions +} + +void SvgSerializer::doWriteHeader() { svg_file << "> section_data_; boost::optional> deferred_section_data_; - boost::optional scale_, calculated_scale_, center_x_, center_y_, scale_backup_; + boost::optional scale_, calculated_scale_, center_x_, center_y_; boost::optional storey_height_line_length_; - boost::optional> size_, size_backup_; + boost::optional> size_, offset_2d_; boost::optional space_name_transform_; bool with_section_heights_from_storey_, print_space_names_, print_space_areas_; @@ -146,6 +146,7 @@ protected: bool draw_door_arcs_, is_floor_plan_; bool auto_section_, auto_elevation_; bool use_namespace_, use_hlr_poly_, always_project_, polygonal_; + bool emit_building_storeys_; IfcParse::IfcFile* file; IfcUtil::IfcBaseEntity* storey_; @@ -189,6 +190,7 @@ public: , use_hlr_poly_(false) , always_project_(false) , polygonal_(false) + , emit_building_storeys_(true) , file(0) , storey_(0) , xcoords_begin(0) @@ -201,6 +203,7 @@ public: void addSizeComponent(const boost::shared_ptr& fi) { radii.push_back(fi); } void growBoundingBox(double x, double y) { if (x < xmin) xmin = x; if (x > xmax) xmax = x; if (y < ymin) ymin = y; if (y > ymax) ymax = y; } void writeHeader(); + void doWriteHeader(); bool ready(); void write(const IfcGeom::TriangulationElement* /*o*/) {} void write(const IfcGeom::BRepElement* o); @@ -258,6 +261,10 @@ public: always_project_ = b; } + void setWithoutStoreys(bool b) { + emit_building_storeys_ = !b; + } + void setScale(double s) { scale_ = s; } void setDrawingCenter(double x, double y) { center_x_ = x; center_y_ = y; From 85426b3fef67f6afbe47a6bdb342452e022e478f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 19 Apr 2021 10:27:47 +1000 Subject: [PATCH 37/64] Minor tweak --- .../blenderbim/bim/module/cost/operator.py | 25 ++++++++----------- .../bim/module/sequence/operator.py | 24 ++++++------------ .../blenderbim/bim/module/sequence/prop.py | 2 +- 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index 86e0cf03df..80b6df245b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -128,11 +128,9 @@ class AddSummaryCostItem(bpy.types.Operator): def execute(self, context): props = context.scene.BIMCostProperties self.file = IfcStore.get_file() - ifcopenshell.api.run("cost.add_cost_item", self.file, **{ - "cost_schedule": self.file.by_id(self.cost_schedule) - }) + ifcopenshell.api.run("cost.add_cost_item", self.file, **{"cost_schedule": self.file.by_id(self.cost_schedule)}) Data.load(self.file) - bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = self.cost_schedule) + bpy.ops.bim.enable_editing_cost_schedule(cost_schedule=self.cost_schedule) return {"FINISHED"} @@ -147,7 +145,7 @@ class AddCostItem(bpy.types.Operator): data = {"cost_item": self.file.by_id(self.cost_item)} ifcopenshell.api.run("cost.add_cost_item", self.file, **data) Data.load(self.file) - bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = props.active_cost_schedule_id) + bpy.ops.bim.enable_editing_cost_schedule(cost_schedule=props.active_cost_schedule_id) return {"FINISHED"} @@ -162,8 +160,7 @@ class ExpandCostItem(bpy.types.Operator): contracted_cost_items = json.loads(props.contracted_cost_items) contracted_cost_items.remove(self.cost_item) props.contracted_cost_items = json.dumps(contracted_cost_items) - Data.load(self.file) - bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = props.active_cost_schedule_id) + bpy.ops.bim.enable_editing_cost_schedule(cost_schedule=props.active_cost_schedule_id) return {"FINISHED"} @@ -178,8 +175,7 @@ class ContractCostItem(bpy.types.Operator): contracted_cost_items = json.loads(props.contracted_cost_items) contracted_cost_items.append(self.cost_item) props.contracted_cost_items = json.dumps(contracted_cost_items) - Data.load(self.file) - bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = props.active_cost_schedule_id) + bpy.ops.bim.enable_editing_cost_schedule(cost_schedule=props.active_cost_schedule_id) return {"FINISHED"} @@ -194,11 +190,12 @@ class RemoveCostItem(bpy.types.Operator): ifcopenshell.api.run( "cost.remove_cost_item", self.file, - cost_item=IfcStore.get_file().by_id(self.cost_item), + cost_item=self.file.by_id(self.cost_item), ) + contracted_cost_items = json.loads(props.contracted_cost_items) + if props.active_cost_item_index in contracted_cost_items: + contracted_cost_items.remove(props.active_cost_item_index) + props.contracted_cost_items = json.dumps(contracted_cost_items) Data.load(self.file) - # contracted_cost_items = json.loads(props.contracted_cost_items) - # contracted_cost_items.remove(props.active_cost_item_index) - # props.contracted_cost_items = json.dumps(contracted_cost_items) - bpy.ops.bim.enable_editing_cost_schedule(cost_schedule = props.active_cost_schedule_id) + bpy.ops.bim.enable_editing_cost_schedule(cost_schedule=props.active_cost_schedule_id) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index b344b7ab3d..18d41af04c 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -176,9 +176,7 @@ class RemoveWorkSchedule(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( - "sequence.remove_work_schedule", - self.file, - work_schedule= self.file.by_id(self.work_schedule) + "sequence.remove_work_schedule", self.file, work_schedule=self.file.by_id(self.work_schedule) ) Data.load(self.file) return {"FINISHED"} @@ -214,7 +212,6 @@ class EnableEditingWorkSchedule(bpy.types.Operator): new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) if data[attribute.name()]: new.enum_value = data[attribute.name()] - self.props.active_work_schedule_id = self.work_schedule while len(self.props.tasks) > 0: self.props.tasks.remove(0) @@ -236,7 +233,6 @@ class EnableEditingWorkSchedule(bpy.types.Operator): if new.is_expanded: for related_object_id in task["RelatedObjects"]: self.create_new_task_li(related_object_id, level_index + 1) - # return {"FINISHED"} class DisableEditingWorkSchedule(bpy.types.Operator): @@ -401,11 +397,9 @@ class AddTask(bpy.types.Operator): def execute(self, context): props = context.scene.BIMWorkScheduleProperties self.file = IfcStore.get_file() - ifcopenshell.api.run("sequence.add_task", self.file, **{ - "parent_task": self.file.by_id(self.task) - }) + ifcopenshell.api.run("sequence.add_task", self.file, **{"parent_task": self.file.by_id(self.task)}) Data.load(self.file) - bpy.ops.bim.enable_editing_work_schedule(work_schedule = props.active_work_schedule_id) + bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) return {"FINISHED"} @@ -417,11 +411,9 @@ class AddSummaryTask(bpy.types.Operator): def execute(self, context): props = context.scene.BIMWorkScheduleProperties self.file = IfcStore.get_file() - ifcopenshell.api.run("sequence.add_task", self.file, **{ - "work_schedule": self.file.by_id(self.work_schedule) - }) + ifcopenshell.api.run("sequence.add_task", self.file, **{"work_schedule": self.file.by_id(self.work_schedule)}) Data.load(self.file) - bpy.ops.bim.enable_editing_work_schedule(work_schedule = props.active_work_schedule_id) + bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) return {"FINISHED"} @@ -437,7 +429,7 @@ class ExpandTask(bpy.types.Operator): contracted_tasks.remove(self.task) props.contracted_tasks = json.dumps(contracted_tasks) Data.load(self.file) - bpy.ops.bim.enable_editing_work_schedule(work_schedule = props.active_work_schedule_id) + bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) return {"FINISHED"} @@ -453,7 +445,7 @@ class ContractTask(bpy.types.Operator): contracted_tasks.append(self.task) props.contracted_tasks = json.dumps(contracted_tasks) Data.load(self.file) - bpy.ops.bim.enable_editing_work_schedule(work_schedule = props.active_work_schedule_id) + bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) return {"FINISHED"} @@ -471,5 +463,5 @@ class RemoveTask(bpy.types.Operator): task=IfcStore.get_file().by_id(self.task), ) Data.load(self.file) - bpy.ops.bim.enable_editing_work_schedule(work_schedule = props.active_work_schedule_id) + bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 6d1d089537..6e8b79fd77 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -21,6 +21,7 @@ class Task(PropertyGroup): is_expanded: BoolProperty(name="Is Expanded") level_index: IntProperty(name="Level Index") + class WorkPlan(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") @@ -44,7 +45,6 @@ class BIMWorkScheduleProperties(PropertyGroup): contracted_tasks: StringProperty(name="Contracted Task Items", default="[]") - class WorkCalendar(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") From 209eea6a18cb5a0c14b59c9e7a87b45b94365d11 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 19 Apr 2021 11:10:49 +1000 Subject: [PATCH 38/64] You can now edit tasks. Thanks myoualid! --- .../bim/module/sequence/__init__.py | 3 + .../bim/module/sequence/operator.py | 78 ++++++++++++++++++- .../blenderbim/bim/module/sequence/prop.py | 41 +++++++++- .../blenderbim/bim/module/sequence/ui.py | 45 +++++++++-- .../ifcopenshell/api/sequence/edit_task.py | 10 +++ 5 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 9f57dc8d18..a15ff29551 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -29,6 +29,9 @@ classes = ( operator.ExpandTask, operator.ContractTask, operator.RemoveTask, + operator.EnableEditingTask, + operator.DisableEditingTask, + operator.EditTask, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 18d41af04c..686ba03606 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -224,8 +224,9 @@ class EnableEditingWorkSchedule(bpy.types.Operator): def create_new_task_li(self, related_object_id, level_index): task = Data.tasks[related_object_id] new = self.props.tasks.add() - new.name = task["Name"] or "Unnamed" new.ifc_definition_id = related_object_id + new.name = task["Name"] or "Unnamed" + new.identification = task["Identification"] or "X" new.is_expanded = related_object_id not in self.contracted_tasks new.level_index = level_index if task["RelatedObjects"]: @@ -465,3 +466,78 @@ class RemoveTask(bpy.types.Operator): Data.load(self.file) bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) return {"FINISHED"} + + +class EnableEditingTask(bpy.types.Operator): + bl_idname = "bim.enable_editing_task" + bl_label = "Enable Editing Task" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + while len(props.task_attributes) > 0: + props.task_attributes.remove(0) + + data = Data.tasks[self.task] + + for attribute in IfcStore.get_schema().declaration_by_name("IfcTask").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = props.task_attributes.add() + new.name = attribute.name() + new.is_null = data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if data_type == "string": + new.string_value = "" if new.is_null else data[attribute.name()] + elif data_type == "boolean": + new.bool_value = False if new.is_null else data[attribute.name()] + elif data_type == "integer": + new.int_value = 0 if new.is_null else data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if data[attribute.name()]: + new.enum_value = data[attribute.name()] + props.active_task_id = self.task + return {"FINISHED"} + + +class DisableEditingTask(bpy.types.Operator): + bl_idname = "bim.disable_editing_task" + bl_label = "Disable Editing Task" + + def execute(self, context): + context.scene.BIMWorkScheduleProperties.active_task_id = 0 + return {"FINISHED"} + + +class EditTask(bpy.types.Operator): + bl_idname = "bim.edit_task" + bl_label = "Edit Task" + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + attributes = {} + for attribute in props.task_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "boolean": + attributes[attribute.name] = attribute.bool_value + elif attribute.data_type == "integer": + attributes[attribute.name] = attribute.int_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.edit_task", + self.file, + **{"task": self.file.by_id(props.active_task_id), "attributes": attributes} + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_task() + bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 6e8b79fd77..23774c79be 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -1,4 +1,7 @@ import bpy +import ifcopenshell.api +from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.sequence.data import Data from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -13,9 +16,41 @@ from bpy.props import ( ) +def updateTaskName(self, context): + if self.name == "Unnamed": + return + self.file = IfcStore.get_file() + props = context.scene.BIMWorkScheduleProperties + ifcopenshell.api.run( + "sequence.edit_task", + self.file, + **{"task": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}} + ) + Data.load(IfcStore.get_file()) + if props.active_task_id == self.ifc_definition_id: + attribute = context.scene.BIMWorkScheduleProperties.task_attributes.get("Name") + attribute.string_value = self.name + + +def updateTaskIdentification(self, context): + if self.identification == "X": + return + self.file = IfcStore.get_file() + props = context.scene.BIMWorkScheduleProperties + ifcopenshell.api.run( + "sequence.edit_task", + self.file, + **{"task": self.file.by_id(self.ifc_definition_id), "attributes": {"Identification": self.identification}} + ) + Data.load(IfcStore.get_file()) + if props.active_task_id == self.ifc_definition_id: + attribute = context.scene.BIMWorkScheduleProperties.task_attributes.get("Identification") + attribute.string_value = self.identification + + class Task(PropertyGroup): - name: StringProperty(name="Name") - identification: StringProperty(name="Identification") + name: StringProperty(name="Name", update=updateTaskName) + identification: StringProperty(name="Identification", update=updateTaskIdentification) ifc_definition_id: IntProperty(name="IFC Definition ID") has_children: BoolProperty(name="Has Children") is_expanded: BoolProperty(name="Is Expanded") @@ -42,6 +77,8 @@ class BIMWorkScheduleProperties(PropertyGroup): active_work_schedule_id: IntProperty(name="Active Work Schedules Id") tasks: CollectionProperty(name="Tasks", type=Task) active_task_index: IntProperty(name="Active Task Index") + active_task_id: IntProperty(name="Active Task Id") + task_attributes: CollectionProperty(name="Task Attributes", type=Attribute) contracted_tasks: StringProperty(name="Contracted Task Items", default="[]") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 54710c9621..39a3f97632 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -100,7 +100,9 @@ class BIM_PT_work_schedules(Panel): elif self.props.active_work_schedule_id: row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id else: - row.operator("bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL").work_schedule = work_schedule_id + row.operator( + "bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL" + ).work_schedule = work_schedule_id row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id if self.props.active_work_schedule_id == work_schedule_id: @@ -127,6 +129,23 @@ class BIM_PT_work_schedules(Panel): self.props, "active_task_index", ) + if self.props.active_task_id: + self.draw_editable_task_ui() + + def draw_editable_task_ui(self): + for attribute in self.props.task_attributes: + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "boolean": + row.prop(attribute, "bool_value", text=attribute.name) + elif attribute.data_type == "integer": + row.prop(attribute, "int_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + class BIM_UL_tasks(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): @@ -136,15 +155,27 @@ class BIM_UL_tasks(UIList): row.label(text="", icon="BLANK1") if item.has_children: if item.is_expanded: - row.operator("bim.contract_task", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN").task = item.ifc_definition_id + row.operator( + "bim.contract_task", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN" + ).task = item.ifc_definition_id else: - row.operator("bim.expand_task", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT").task = item.ifc_definition_id + row.operator( + "bim.expand_task", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT" + ).task = item.ifc_definition_id else: row.label(text="", icon="DOT") - row.label(text=item.name) - op = row.operator("bim.add_task", text="", icon="ADD") - op.task = item.ifc_definition_id - row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id + row.prop(item, "identification", emboss=False, text="") + row.prop(item, "name", emboss=False, text="") + if context.scene.BIMWorkScheduleProperties.active_task_id == item.ifc_definition_id: + row.operator("bim.edit_task", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_task", text="", icon="CANCEL") + if context.scene.BIMWorkScheduleProperties.active_task_id: + row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id + row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id + else: + row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id + row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = item.ifc_definition_id + row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id class BIM_PT_work_calendars(Panel): diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py new file mode 100644 index 0000000000..165c0606dd --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py @@ -0,0 +1,10 @@ +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"task": None, "attributes": {}} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + for name, value in self.settings["attributes"].items(): + setattr(self.settings["task"], name, value) From d2d41127240b6f9b395ee3bc10dbc78068c1f44b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 19 Apr 2021 11:42:39 +1000 Subject: [PATCH 39/64] Clean up UX to either edit a work schedule attributes or task tree but not both simultaneously --- .../bim/module/sequence/__init__.py | 2 +- .../bim/module/sequence/operator.py | 35 +++++----- .../blenderbim/bim/module/sequence/prop.py | 2 +- .../blenderbim/bim/module/sequence/ui.py | 65 ++++++++++--------- 4 files changed, 56 insertions(+), 48 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index a15ff29551..973cbe3e6e 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -9,11 +9,11 @@ classes = ( operator.RemoveWorkPlan, operator.EnableEditingWorkPlan, operator.DisableEditingWorkPlan, - operator.DisableWorkScheduleEditingUI, operator.AddWorkSchedule, operator.EditWorkSchedule, operator.RemoveWorkSchedule, operator.EnableEditingWorkSchedule, + operator.EnableEditingTasks, operator.DisableEditingWorkSchedule, operator.LoadTasks, operator.DisableTaskEditingUI, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 686ba03606..e3d4d44425 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -123,15 +123,6 @@ class DisableEditingWorkPlan(bpy.types.Operator): return {"FINISHED"} -class DisableWorkScheduleEditingUI(bpy.types.Operator): - bl_idname = "bim.disable_work_schedule_editing_ui" - bl_label = "Disable WorkSchedule Editing UI" - - def execute(self, context): - context.scene.BIMWorkScheduleProperties.is_editing = False - return {"FINISHED"} - - class AddWorkSchedule(bpy.types.Operator): bl_idname = "bim.add_work_schedule" bl_label = "Add Work Schedule" @@ -192,7 +183,11 @@ class EnableEditingWorkSchedule(bpy.types.Operator): self.props.active_work_schedule_id = self.work_schedule while len(self.props.work_schedule_attributes) > 0: self.props.work_schedule_attributes.remove(0) + self.enable_editing_work_schedule() + self.props.is_editing = "WORK_SCHEDULE" + return {"FINISHED"} + def enable_editing_work_schedule(self): data = Data.work_schedules[self.work_schedule] for attribute in IfcStore.get_schema().declaration_by_name("IfcWorkSchedule").all_attributes(): @@ -213,12 +208,22 @@ class EnableEditingWorkSchedule(bpy.types.Operator): if data[attribute.name()]: new.enum_value = data[attribute.name()] + +class EnableEditingTasks(bpy.types.Operator): + bl_idname = "bim.enable_editing_tasks" + bl_label = "Enable Editing Tasks" + work_schedule: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMWorkScheduleProperties + self.props.active_work_schedule_id = self.work_schedule while len(self.props.tasks) > 0: self.props.tasks.remove(0) self.contracted_tasks = json.loads(self.props.contracted_tasks) for related_object_id in Data.work_schedules[self.work_schedule]["RelatedObjects"]: self.create_new_task_li(related_object_id, 0) + self.props.is_editing = "TASKS" return {"FINISHED"} def create_new_task_li(self, related_object_id, level_index): @@ -400,7 +405,7 @@ class AddTask(bpy.types.Operator): self.file = IfcStore.get_file() ifcopenshell.api.run("sequence.add_task", self.file, **{"parent_task": self.file.by_id(self.task)}) Data.load(self.file) - bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) + bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) return {"FINISHED"} @@ -414,7 +419,7 @@ class AddSummaryTask(bpy.types.Operator): self.file = IfcStore.get_file() ifcopenshell.api.run("sequence.add_task", self.file, **{"work_schedule": self.file.by_id(self.work_schedule)}) Data.load(self.file) - bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) + bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) return {"FINISHED"} @@ -430,7 +435,7 @@ class ExpandTask(bpy.types.Operator): contracted_tasks.remove(self.task) props.contracted_tasks = json.dumps(contracted_tasks) Data.load(self.file) - bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) + bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) return {"FINISHED"} @@ -446,7 +451,7 @@ class ContractTask(bpy.types.Operator): contracted_tasks.append(self.task) props.contracted_tasks = json.dumps(contracted_tasks) Data.load(self.file) - bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) + bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) return {"FINISHED"} @@ -464,7 +469,7 @@ class RemoveTask(bpy.types.Operator): task=IfcStore.get_file().by_id(self.task), ) Data.load(self.file) - bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) + bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) return {"FINISHED"} @@ -539,5 +544,5 @@ class EditTask(bpy.types.Operator): ) Data.load(IfcStore.get_file()) bpy.ops.bim.disable_editing_task() - bpy.ops.bim.enable_editing_work_schedule(work_schedule=props.active_work_schedule_id) + bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 23774c79be..a4d87d6016 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -72,7 +72,7 @@ class BIMWorkPlanProperties(PropertyGroup): class BIMWorkScheduleProperties(PropertyGroup): work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute) - is_editing: BoolProperty(name="Is Editing", default=False) + is_editing: StringProperty(name="Is Editing") active_work_schedule_index: IntProperty(name="Active Work Schedules Index") active_work_schedule_id: IntProperty(name="Active Work Schedules Id") tasks: CollectionProperty(name="Tasks", type=Task) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 39a3f97632..2531a574cf 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -85,30 +85,35 @@ class BIM_PT_work_schedules(Panel): if not Data.is_loaded: Data.load(IfcStore.get_file()) - row = self.layout.row(align=True) - row.label(text="{} Work Schedules Found".format(len(Data.work_schedules)), icon="TEXT") row = self.layout.row() row.operator("bim.add_work_schedule", icon="ADD") for work_schedule_id, work_schedule in Data.work_schedules.items(): - row = self.layout.row(align=True) - row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") + self.draw_work_schedule_ui(work_schedule_id, work_schedule) - if self.props.active_work_schedule_id and self.props.active_work_schedule_id == work_schedule_id: + def draw_work_schedule_ui(self, work_schedule_id, work_schedule): + row = self.layout.row(align=True) + row.label(text=work_schedule["Name"] or "Unnamed", icon="TEXT") + + if self.props.active_work_schedule_id and self.props.active_work_schedule_id == work_schedule_id: + if self.props.is_editing == "WORK_SCHEDULE": row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_work_schedule", text="", icon="X") - elif self.props.active_work_schedule_id: - row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id - else: - row.operator( - "bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL" - ).work_schedule = work_schedule_id - row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id + row.operator("bim.disable_editing_work_schedule", text="", icon="CANCEL") + elif self.props.active_work_schedule_id: + row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id + else: + row.operator("bim.enable_editing_tasks", text="", icon="ACTION").work_schedule = work_schedule_id + op = row.operator("bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL") + op.work_schedule = work_schedule_id + row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id - if self.props.active_work_schedule_id == work_schedule_id: - self.draw_editable_work_schedule_ui(work_schedule_id, work_schedule) + if self.props.active_work_schedule_id == work_schedule_id: + if self.props.is_editing == "WORK_SCHEDULE": + self.draw_editable_work_schedule_ui() + elif self.props.is_editing == "TASKS": + self.draw_editable_task_ui(work_schedule_id) - def draw_editable_work_schedule_ui(self, work_schedule_id, work_schedule): + def draw_editable_work_schedule_ui(self): for attribute in self.props.work_schedule_attributes: row = self.layout.row(align=True) if attribute.data_type == "string": @@ -118,6 +123,7 @@ class BIM_PT_work_schedules(Panel): if attribute.is_optional: row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + def draw_editable_task_ui(self, work_schedule_id): row = self.layout.row(align=True) row.label(text="X Summary Tasks") row.operator("bim.add_summary_task", text="", icon="ADD").work_schedule = work_schedule_id @@ -130,21 +136,18 @@ class BIM_PT_work_schedules(Panel): "active_task_index", ) if self.props.active_task_id: - self.draw_editable_task_ui() - - def draw_editable_task_ui(self): - for attribute in self.props.task_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + for attribute in self.props.task_attributes: + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "boolean": + row.prop(attribute, "bool_value", text=attribute.name) + elif attribute.data_type == "integer": + row.prop(attribute, "int_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") class BIM_UL_tasks(UIList): From 8f0e67a35bfa6510b80c9265b83a3f76f00254bc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 19 Apr 2021 13:09:00 +1000 Subject: [PATCH 40/64] You can now assign task successor and predecessors. Sweet. Thanks myoualid! --- .../bim/module/sequence/__init__.py | 4 ++ .../bim/module/sequence/operator.py | 72 +++++++++++++++++++ .../blenderbim/bim/module/sequence/ui.py | 15 +++- .../api/sequence/assign_sequence.py | 27 +++++++ .../api/sequence/assign_task_predecessor.py | 25 ------- .../api/sequence/assign_task_successor.py | 25 ------- .../ifcopenshell/api/sequence/data.py | 4 ++ .../api/sequence/unassign_sequence.py | 18 +++++ 8 files changed, 138 insertions(+), 52 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py delete mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_predecessor.py delete mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_successor.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_sequence.py diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 973cbe3e6e..3d3a176149 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -32,6 +32,10 @@ classes = ( operator.EnableEditingTask, operator.DisableEditingTask, operator.EditTask, + operator.AssignPredecessor, + operator.AssignSuccessor, + operator.UnassignPredecessor, + operator.UnassignSuccessor, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index e3d4d44425..23969b1fc0 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -546,3 +546,75 @@ class EditTask(bpy.types.Operator): bpy.ops.bim.disable_editing_task() bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) return {"FINISHED"} + + +class AssignPredecessor(bpy.types.Operator): + bl_idname = "bim.assign_predecessor" + bl_label = "Assign Predecessor" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.assign_sequence", + self.file, + relating_process=IfcStore.get_file().by_id(self.task), + related_process=IfcStore.get_file().by_id(props.active_task_id), + ) + Data.load(self.file) + return {"FINISHED"} + + +class AssignSuccessor(bpy.types.Operator): + bl_idname = "bim.assign_successor" + bl_label = "Assign Successor" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.assign_sequence", + self.file, + relating_process=IfcStore.get_file().by_id(props.active_task_id), + related_process=IfcStore.get_file().by_id(self.task), + ) + Data.load(self.file) + return {"FINISHED"} + + +class UnassignPredecessor(bpy.types.Operator): + bl_idname = "bim.unassign_predecessor" + bl_label = "Unassign Predecessor" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.unassign_sequence", + self.file, + relating_process=IfcStore.get_file().by_id(self.task), + related_process=IfcStore.get_file().by_id(props.active_task_id), + ) + Data.load(self.file) + return {"FINISHED"} + + +class UnassignSuccessor(bpy.types.Operator): + bl_idname = "bim.unassign_successor" + bl_label = "Unassign Successor" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.unassign_sequence", + self.file, + relating_process=IfcStore.get_file().by_id(props.active_task_id), + related_process=IfcStore.get_file().by_id(self.task), + ) + Data.load(self.file) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 2531a574cf..25d90b949f 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -153,6 +153,7 @@ class BIM_PT_work_schedules(Panel): class BIM_UL_tasks(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: + props = context.scene.BIMWorkScheduleProperties row = layout.row(align=True) for i in range(0, item.level_index): row.label(text="", icon="BLANK1") @@ -169,10 +170,20 @@ class BIM_UL_tasks(UIList): row.label(text="", icon="DOT") row.prop(item, "identification", emboss=False, text="") row.prop(item, "name", emboss=False, text="") - if context.scene.BIMWorkScheduleProperties.active_task_id == item.ifc_definition_id: + if props.active_task_id == item.ifc_definition_id: row.operator("bim.edit_task", text="", icon="CHECKMARK") row.operator("bim.disable_editing_task", text="", icon="CANCEL") - if context.scene.BIMWorkScheduleProperties.active_task_id: + elif props.active_task_id: + if props.active_task_id in Data.tasks[item.ifc_definition_id]["IsPredecessorTo"]: + row.operator("bim.unassign_predecessor", text="", icon="BACK", emboss=False).task = item.ifc_definition_id + else: + row.operator("bim.assign_predecessor", text="", icon="TRACKING_BACKWARDS", emboss=False).task = item.ifc_definition_id + + if props.active_task_id in Data.tasks[item.ifc_definition_id]["IsSuccessorFrom"]: + row.operator("bim.unassign_successor", text="", icon="FORWARD", emboss=False).task = item.ifc_definition_id + else: + row.operator("bim.assign_successor", text="", icon="TRACKING_FORWARDS", emboss=False).task = item.ifc_definition_id + row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id else: diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py new file mode 100644 index 0000000000..d72d237326 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py @@ -0,0 +1,27 @@ +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "relating_process": None, + "related_process": None, + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + for rel in self.settings["related_process"].IsSuccessorFrom or []: + if rel.RelatingProcess == self.settings["relating_process"]: + return rel + return self.file.create_entity( + "IfcRelSequence", + **{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatingProcess": self.settings["relating_process"], + "RelatedProcess": self.settings["related_process"], + } + ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_predecessor.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_predecessor.py deleted file mode 100644 index 36c0140728..0000000000 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_predecessor.py +++ /dev/null @@ -1,25 +0,0 @@ -import ifcopenshell -import ifcopenshell.api - - -class Usecase: - def __init__(self, file, **settings): - self.file = file - self.settings = { - "predecessor_task": None, - "sequence_type":"FINISH_START", - "task": None, - } - for key, value in settings.items(): - self.settings[key] = value - - def execute(self): - #TODO: tasks can only have one relationship between oneanother: if a relationship is already assigned, it should overide the previous one's settings. - rel_sequence = self.file.create_entity("IfcRelSequence",**{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatingProcess": self.settings["predecessor_task"], - "SequenceType": self.settings["sequence_type"], - "RelatedProcess": self.settings["task"], - }) - return rel_sequence \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_successor.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_successor.py deleted file mode 100644 index 21f818949f..0000000000 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_task_successor.py +++ /dev/null @@ -1,25 +0,0 @@ -import ifcopenshell -import ifcopenshell.api - - -class Usecase: - def __init__(self, file, **settings): - self.file = file - self.settings = { - "task": None, - "sequence_type":"FINISH_START", - "successor_task": None, - } - for key, value in settings.items(): - self.settings[key] = value - - def execute(self): - #TODO: tasks can only have one relationship between oneanother: if a relationship is already assigned, it should overide the previous one's settings. - rel_sequence = self.file.create_entity("IfcRelSequence",**{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatingProcess": self.settings["task"], - "SequenceType": self.settings["sequence_type"], - "RelatedProcess": self.settings["successor_task"], - }) - return rel_sequence \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py index 04ba1e7a63..3aa298bafc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py @@ -76,6 +76,10 @@ class Data: data = task.get_info() del data["OwnerHistory"] data["RelatedObjects"] = [] + data["IsPredecessorTo"] = [] + data["IsSuccessorFrom"] = [] for rel in task.IsNestedBy: [data["RelatedObjects"].append(o.id()) for o in rel.RelatedObjects if o.is_a("IfcTask")] + [data["IsPredecessorTo"].append(rel.RelatedProcess.id()) for rel in task.IsPredecessorTo or []] + [data["IsSuccessorFrom"].append(rel.RelatingProcess.id()) for rel in task.IsSuccessorFrom or []] cls.tasks[task.id()] = data diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_sequence.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_sequence.py new file mode 100644 index 0000000000..496b8bfec2 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_sequence.py @@ -0,0 +1,18 @@ +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "relating_process": None, + "related_process": None, + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + for rel in self.settings["related_process"].IsSuccessorFrom or []: + if rel.RelatingProcess == self.settings["relating_process"]: + self.file.remove(rel) From 44bbd17d974894bec02eda0f7b5add02db9de7f7 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 19 Apr 2021 16:08:57 +1000 Subject: [PATCH 41/64] Collection assignment is now synced on export, so you don't need to manually manage your tree --- src/blenderbim/blenderbim/bim/export_ifc.py | 34 +++++++++++++++++-- .../ifcopenshell/util/element.py | 5 +++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index c0b52e7ce6..219aad1b3f 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -8,6 +8,7 @@ import tempfile import ifcopenshell import ifcopenshell.util.placement import ifcopenshell.api +from ifcopenshell.api.spatial.data import Data as SpatialData from blenderbim.bim.ifc import IfcStore import addon_utils @@ -81,9 +82,12 @@ class IfcExporter: self.sync_object_placement(obj) except: pass - if self.should_delete(guid, obj): + self.sync_object_container(guid, obj) + if self.should_delete(obj): to_delete.append(guid) + SpatialData.purge() + for guid in to_delete: product = self.file.by_id(guid) IfcStore.unlink_element(product) @@ -121,7 +125,33 @@ class IfcExporter: if not np.allclose(ifc_matrix, blender_matrix, atol=0.0001): bpy.ops.bim.edit_object_placement(obj=obj.name) - def should_delete(self, guid, obj): + def sync_object_container(self, guid, obj): + element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element_collection = bpy.data.collections.get(obj.name) + + if element.is_a("IfcProject"): + return + + if (element.is_a("IfcElement") and element_collection) or element.is_a("IfcSpatialStructureElement"): + try: + parent_collection = [c for c in bpy.data.collections if c.children.get(element_collection.name)][0] + except: + return # Out of the spatial tree + else: + parent_collection = obj.users_collection[0] + + parent_obj = bpy.data.objects.get(parent_collection.name) + if not parent_obj or not parent_obj.BIMObjectProperties.ifc_definition_id: + return + parent = self.file.by_id(parent_obj.BIMObjectProperties.ifc_definition_id) + + if parent.is_a("IfcSpatialStructureElement") and not element.is_a("IfcSpatialStructureElement"): + if parent != ifcopenshell.util.element.get_container(element): + bpy.ops.bim.assign_container(relating_structure=parent.id(), related_element=obj.name) + elif parent != ifcopenshell.util.element.get_aggregate(element): + bpy.ops.bim.assign_object(relating_object=parent_obj.name, related_object=obj.name) + + def should_delete(self, obj): try: # This will throw an exception if the Blender object no longer exists foo = obj.name diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 1c90f3d391..b232721482 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -88,6 +88,11 @@ def get_container(element): return element.ContainedInStructure[0].RelatingStructure +def get_aggregate(element): + if hasattr(element, "Decomposes") and element.Decomposes: + return element.Decomposes[0].RelatingObject + + def replace_attribute(element, old, new): for i, attribute in enumerate(element): if attribute == old: From 8ac8ad33b373491042e3a60b2843eb91ee859057 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 20 Apr 2021 12:45:20 +1000 Subject: [PATCH 42/64] You can now load and browse a tree of project library types products --- src/blenderbim/blenderbim/bim/ifc.py | 2 + .../blenderbim/bim/module/project/__init__.py | 7 ++ .../blenderbim/bim/module/project/operator.py | 83 +++++++++++++++++++ .../blenderbim/bim/module/project/prop.py | 10 +++ .../blenderbim/bim/module/project/ui.py | 49 ++++++++++- 5 files changed, 150 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py index 371e05e51b..7a5833a852 100644 --- a/src/blenderbim/blenderbim/bim/ifc.py +++ b/src/blenderbim/blenderbim/bim/ifc.py @@ -12,6 +12,8 @@ class IfcStore: edited_objs = set() pset_template_path = "" pset_template_file = None + library_path = "" + library_file = None @staticmethod def get_file(): diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index fb41b6e9ed..d213d93f23 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -5,8 +5,15 @@ classes = ( operator.CreateProject, operator.CreateProjectLibrary, operator.ValidateIfcFile, + operator.SelectLibraryFile, + operator.ChangeLibraryElement, + operator.RefreshLibrary, + operator.RewindLibrary, + prop.LibraryElement, prop.BIMProjectProperties, ui.BIM_PT_project, + ui.BIM_PT_project_library, + ui.BIM_UL_library, ) diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 60baeea3a0..70da02d928 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -88,3 +88,86 @@ class ValidateIfcFile(bpy.types.Operator): logger.setLevel(logging.DEBUG) ifcopenshell.validate.validate(IfcStore.get_file(), logger) return {"FINISHED"} + + +class SelectLibraryFile(bpy.types.Operator): + bl_idname = "bim.select_library_file" + bl_label = "Select Library File" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) + + def execute(self, context): + IfcStore.library_path = self.filepath + IfcStore.library_file = ifcopenshell.open(self.filepath) + bpy.ops.bim.refresh_library() + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class RefreshLibrary(bpy.types.Operator): + bl_idname = "bim.refresh_library" + bl_label = "Refresh Library" + + def execute(self, context): + self.props = context.scene.BIMProjectProperties + + while len(self.props.library_elements) > 0: + self.props.library_elements.remove(0) + + while len(self.props.library_breadcrumb) > 0: + self.props.library_breadcrumb.remove(0) + + self.props.active_library_element = "" + + types = IfcStore.library_file.wrapped_data.types_with_super() + if "IfcTypeProduct" in types: + new = self.props.library_elements.add() + new.name = "IfcTypeProduct" + return {"FINISHED"} + + +class ChangeLibraryElement(bpy.types.Operator): + bl_idname = "bim.change_library_element" + bl_label = "Change Library Element" + element_name: bpy.props.StringProperty() + + def execute(self, context): + self.props = context.scene.BIMProjectProperties + ifc_classes = set() + self.props.active_library_element = self.element_name + crumb = self.props.library_breadcrumb.add() + crumb.name = self.element_name + elements = IfcStore.library_file.by_type(self.element_name) + [ifc_classes.add(e.is_a()) for e in elements] + while len(self.props.library_elements) > 0: + self.props.library_elements.remove(0) + if len(ifc_classes) == 1: + for element in elements: + new = self.props.library_elements.add() + new.name = element.Name or "Unnamed" + new.ifc_definition_id = element.id() + else: + for ifc_class in ifc_classes: + new = self.props.library_elements.add() + new.name = ifc_class + return {"FINISHED"} + + +class RewindLibrary(bpy.types.Operator): + bl_idname = "bim.rewind_library" + bl_label = "Rewind Library" + + def execute(self, context): + self.props = context.scene.BIMProjectProperties + total_breadcrumbs = len(self.props.library_breadcrumb) + if total_breadcrumbs < 2: + bpy.ops.bim.refresh_library() + return {"FINISHED"} + element_name = self.props.library_breadcrumb[total_breadcrumbs - 2].name + self.props.library_breadcrumb.remove(total_breadcrumbs - 1) + self.props.library_breadcrumb.remove(total_breadcrumbs - 2) + bpy.ops.bim.change_library_element(element_name = element_name) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/project/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py index d90ceea623..503bed6388 100644 --- a/src/blenderbim/blenderbim/bim/module/project/prop.py +++ b/src/blenderbim/blenderbim/bim/module/project/prop.py @@ -1,4 +1,5 @@ import bpy +from blenderbim.bim.prop import StrProperty from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -12,5 +13,14 @@ from bpy.props import ( ) +class LibraryElement(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + + class BIMProjectProperties(PropertyGroup): is_authoring: BoolProperty(name="Enable Authoring Mode", default=True) + active_library_element: StringProperty(name="Enable Authoring Mode", default="") + library_breadcrumb: CollectionProperty(name="Library Breadcrumb", type=StrProperty) + library_elements: CollectionProperty(name="Library Elements", type=LibraryElement) + active_library_element_index: IntProperty(name="Active Library Element Index") diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 7e255ea0d3..c02685c3db 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -1,5 +1,5 @@ import os -from bpy.types import Panel +from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore @@ -66,3 +66,50 @@ class BIM_PT_project(Panel): if props.export_schema != "IFC2X3": row = self.layout.row() row.operator("bim.create_project_library") + + +class BIM_PT_project_library(Panel): + bl_label = "IFC Project Library" + bl_idname = "BIM_PT_project_library" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + def draw(self, context): + self.layout.use_property_decorate = False + self.layout.use_property_split = True + self.props = context.scene.BIMProjectProperties + row = self.layout.row(align=True) + row.label(text=IfcStore.library_path or "No Library Loaded", icon="ASSET_MANAGER") + row.operator("bim.select_library_file", icon="FILE_FOLDER", text="") + if IfcStore.library_file: + self.draw_library_ul() + + def draw_library_ul(self): + if not self.props.library_elements: + row = self.layout.row() + row.label(text="No Assets Found", icon="ERROR") + return + row = self.layout.row(align=True) + row.label(text=self.props.active_library_element or "Top Level Assets") + if self.props.active_library_element: + row.operator("bim.rewind_library", icon="FRAME_PREV", text="") + row.operator("bim.refresh_library", icon="FILE_REFRESH", text="") + self.layout.template_list( + "BIM_UL_library", + "", + self.props, + "library_elements", + self.props, + "active_library_element_index", + ) + + +class BIM_UL_library(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + if not item.ifc_definition_id: + op = row.operator("bim.change_library_element", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False) + op.element_name = item.name + row.label(text=item.name) From f2bd46d699e00731755cdff04796b013540bf7bc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 20 Apr 2021 12:45:42 +1000 Subject: [PATCH 43/64] Selecting an IFC file dialog now filters ifc files for convenience --- src/blenderbim/blenderbim/bim/operator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index 51eb123f52..6514d2bc54 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -324,6 +324,7 @@ class SelectIfcFile(bpy.types.Operator): bl_idname = "bim.select_ifc_file" bl_label = "Select IFC File" filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) def execute(self, context): bpy.context.scene.BIMProperties.ifc_file = self.filepath From b8963b3d6e5474e4dc76ad67a9d3bbbab26d836a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 20 Apr 2021 13:51:41 +1000 Subject: [PATCH 44/64] You can now modify project context declarations for project libraries, denoting which assets are part of the library --- src/blenderbim/blenderbim/bim/export_ifc.py | 5 +- .../blenderbim/bim/module/project/__init__.py | 3 ++ .../blenderbim/bim/module/project/operator.py | 53 ++++++++++++++++++- .../blenderbim/bim/module/project/prop.py | 1 + .../blenderbim/bim/module/project/ui.py | 15 ++++++ 5 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index 219aad1b3f..d3cbdbda11 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -129,7 +129,10 @@ class IfcExporter: element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) element_collection = bpy.data.collections.get(obj.name) - if element.is_a("IfcProject"): + if self.file.schema == "IFC2X3": + if element.is_a("IfcProject"): + return + elif element.is_a("IfcContext"): return if (element.is_a("IfcElement") and element_collection) or element.is_a("IfcSpatialStructureElement"): diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index d213d93f23..576a026de3 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -9,6 +9,9 @@ classes = ( operator.ChangeLibraryElement, operator.RefreshLibrary, operator.RewindLibrary, + operator.AssignLibraryDeclaration, + operator.UnassignLibraryDeclaration, + operator.SaveLibraryFile, prop.LibraryElement, prop.BIMProjectProperties, ui.BIM_PT_project, diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 70da02d928..36eebc2549 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -5,8 +5,6 @@ import ifcopenshell.api import bpy from blenderbim.bim.ifc import IfcStore -# from ifcopenshell.api.project.data import Data - class CreateProject(bpy.types.Operator): bl_idname = "bim.create_project" @@ -149,6 +147,10 @@ class ChangeLibraryElement(bpy.types.Operator): new = self.props.library_elements.add() new.name = element.Name or "Unnamed" new.ifc_definition_id = element.id() + if IfcStore.library_file.schema == "IFC2X3" or not IfcStore.library_file.by_type("IfcProjectLibrary"): + new.is_declared = False + elif element.HasContext and element.HasContext[0].RelatingContext.is_a("IfcProjectLibrary"): + new.is_declared = True else: for ifc_class in ifc_classes: new = self.props.library_elements.add() @@ -169,5 +171,52 @@ class RewindLibrary(bpy.types.Operator): element_name = self.props.library_breadcrumb[total_breadcrumbs - 2].name self.props.library_breadcrumb.remove(total_breadcrumbs - 1) self.props.library_breadcrumb.remove(total_breadcrumbs - 2) + bpy.ops.bim.change_library_element(element_name=element_name) + return {"FINISHED"} + + +class AssignLibraryDeclaration(bpy.types.Operator): + bl_idname = "bim.assign_library_declaration" + bl_label = "Assign Library Declaration" + definition: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMProjectProperties + ifcopenshell.api.run( + "project.assign_declaration", + IfcStore.library_file, + definition=IfcStore.library_file.by_id(self.definition), + relating_context=IfcStore.library_file.by_type("IfcProjectLibrary")[0], + ) + element_name = self.props.active_library_element + bpy.ops.bim.rewind_library() bpy.ops.bim.change_library_element(element_name = element_name) return {"FINISHED"} + + +class UnassignLibraryDeclaration(bpy.types.Operator): + bl_idname = "bim.unassign_library_declaration" + bl_label = "Unassign Library Declaration" + definition: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMProjectProperties + ifcopenshell.api.run( + "project.unassign_declaration", + IfcStore.library_file, + definition=IfcStore.library_file.by_id(self.definition), + relating_context=IfcStore.library_file.by_type("IfcProjectLibrary")[0], + ) + element_name = self.props.active_library_element + bpy.ops.bim.rewind_library() + bpy.ops.bim.change_library_element(element_name = element_name) + return {"FINISHED"} + + +class SaveLibraryFile(bpy.types.Operator): + bl_idname = "bim.save_library_file" + bl_label = "Save Library File" + + def execute(self, context): + IfcStore.library_file.write(IfcStore.library_path) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/project/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py index 503bed6388..3cb43b6128 100644 --- a/src/blenderbim/blenderbim/bim/module/project/prop.py +++ b/src/blenderbim/blenderbim/bim/module/project/prop.py @@ -16,6 +16,7 @@ from bpy.props import ( class LibraryElement(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") + is_declared: BoolProperty(name="Is Declared", default=False) class BIMProjectProperties(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index c02685c3db..4c62ad5c8d 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -81,6 +81,9 @@ class BIM_PT_project_library(Panel): self.props = context.scene.BIMProjectProperties row = self.layout.row(align=True) row.label(text=IfcStore.library_path or "No Library Loaded", icon="ASSET_MANAGER") + if IfcStore.library_file: + row.label(text=IfcStore.library_file.schema) + row.operator("bim.save_library_file", text="", icon="EXPORT") row.operator("bim.select_library_file", icon="FILE_FOLDER", text="") if IfcStore.library_file: self.draw_library_ul() @@ -113,3 +116,15 @@ class BIM_UL_library(UIList): op = row.operator("bim.change_library_element", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False) op.element_name = item.name row.label(text=item.name) + if ( + not item.ifc_definition_id + or IfcStore.library_file.schema == "IFC2X3" + or not IfcStore.library_file.by_type("IfcProjectLibrary") + ): + return + if item.is_declared: + op = row.operator("bim.unassign_library_declaration", text="", icon="KEYFRAME_HLT", emboss=False) + op.definition = item.ifc_definition_id + else: + op = row.operator("bim.assign_library_declaration", text="", icon="KEYFRAME", emboss=False) + op.definition = item.ifc_definition_id From 544d6ee695714f489bd9c6121e3fee5033850421 Mon Sep 17 00:00:00 2001 From: bosonprojets Date: Tue, 20 Apr 2021 05:12:13 +0000 Subject: [PATCH 45/64] Add IfcTaskTime to IfckTask --- .../bim/module/sequence/__init__.py | 3 + .../bim/module/sequence/operator.py | 73 ++++++++++++++++++- .../blenderbim/bim/module/sequence/prop.py | 20 ++++- .../blenderbim/bim/module/sequence/ui.py | 5 ++ .../api/sequence/add_task_time.py | 37 ++++++++++ .../ifcopenshell/api/sequence/data.py | 21 ++++++ .../api/sequence/edit_task_time.py | 10 +++ 7 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 3d3a176149..c588617c4d 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -36,6 +36,9 @@ classes = ( operator.AssignSuccessor, operator.UnassignPredecessor, operator.UnassignSuccessor, + operator.AddTaskTime, + operator.EnableEditingTaskTime, + operator.DisableEditingTaskTime, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 23969b1fc0..091da38c15 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -473,6 +473,49 @@ class RemoveTask(bpy.types.Operator): return {"FINISHED"} +class EnableEditingTaskTime(bpy.types.Operator): + bl_idname = "bim.enable_editing_task_time" + bl_label = "Enable Editing Task" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + while len(props.task_time_attributes) > 0: + props.task_time_attributes.remove(0) + + if self.file.by_id(self.task).TaskTime: + task_time_id = self.file.by_id(self.task).TaskTime.id() + else: + task_time = ifcopenshell.api.run("sequence.add_task_time", self.file) + self.file.by_id(self.task).TaskTime = task_time + task_time_id = task_time.id() + Data.load(self.file) + data = Data.task_times[task_time_id] + + for attribute in IfcStore.get_schema().declaration_by_name("IfcTaskTime").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = props.task_time_attributes.add() + new.name = attribute.name() + new.is_null = data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if data_type == "string": + new.string_value = "" if new.is_null else data[attribute.name()] + elif data_type == "boolean": + new.bool_value = False if new.is_null else data[attribute.name()] + elif data_type == "integer": + new.int_value = 0 if new.is_null else data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if data[attribute.name()]: + new.enum_value = data[attribute.name()] + props.active_task_time_id = IfcStore.get_file().by_id(self.task).TaskTime.id() + return {"FINISHED"} + + class EnableEditingTask(bpy.types.Operator): bl_idname = "bim.enable_editing_task" bl_label = "Enable Editing Task" @@ -508,6 +551,15 @@ class EnableEditingTask(bpy.types.Operator): return {"FINISHED"} +class DisableEditingTaskTime(bpy.types.Operator): + bl_idname = "bim.disable_editing_task_time" + bl_label = "Disable Editing Task Time" + + def execute(self, context): + context.scene.BIMWorkScheduleProperties.active_task_time_id = 0 + return {"FINISHED"} + + class DisableEditingTask(bpy.types.Operator): bl_idname = "bim.disable_editing_task" bl_label = "Disable Editing Task" @@ -613,8 +665,25 @@ class UnassignSuccessor(bpy.types.Operator): ifcopenshell.api.run( "sequence.unassign_sequence", self.file, - relating_process=IfcStore.get_file().by_id(props.active_task_id), - related_process=IfcStore.get_file().by_id(self.task), + relating_process=self.file.by_id(props.active_task_id), + related_process=self.file.by_id(self.task), + ) + Data.load(self.file) + return {"FINISHED"} + + +class AddTaskTime(bpy.types.Operator): + bl_idname = "bim.add_task_time" + bl_label = "Add Task Time" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.add_task_time", + self.file, + task = self.file.by_id(self.task), ) Data.load(self.file) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index a4d87d6016..b3bf062ca5 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -47,6 +47,20 @@ def updateTaskIdentification(self, context): attribute = context.scene.BIMWorkScheduleProperties.task_attributes.get("Identification") attribute.string_value = self.identification +def updateTaskTimeScheduleStart(self, context): + if self.schedule_start == "X": + return + self.file = IfcStore.get_file() + props = context.scene.BIMWorkScheduleProperties + ifcopenshell.api.run( + "sequence.edit_task_time", + self.file, + **{"task": self.file.by_id(self.ifc_definition_id), "attributes": {"ScheduleStart": self.schedule_start}} + ) + Data.load(IfcStore.get_file()) + if props.active_task_id == self.ifc_definition_id: + attribute = context.scene.BIMWorkScheduleProperties.task_attributes.get("ScheduleStart") + attribute.string_value = self.schedule_start class Task(PropertyGroup): name: StringProperty(name="Name", update=updateTaskName) @@ -55,7 +69,9 @@ class Task(PropertyGroup): has_children: BoolProperty(name="Has Children") is_expanded: BoolProperty(name="Is Expanded") level_index: IntProperty(name="Level Index") - + schedule_duration: StringProperty(name="Duration") + schedule_start: StringProperty(name="Schedule Start ", update=updateTaskTimeScheduleStart) + schedule_finish: StringProperty(name="Schedule Finish ") class WorkPlan(PropertyGroup): name: StringProperty(name="Name") @@ -79,6 +95,8 @@ class BIMWorkScheduleProperties(PropertyGroup): active_task_index: IntProperty(name="Active Task Index") active_task_id: IntProperty(name="Active Task Id") task_attributes: CollectionProperty(name="Task Attributes", type=Attribute) + active_task_time_id: IntProperty(name="Active Task Id") + task_time_attributes: CollectionProperty(name="Task Time Attributes", type=Attribute) contracted_tasks: StringProperty(name="Contracted Task Items", default="[]") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 25d90b949f..b3274bd4fc 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -170,6 +170,10 @@ class BIM_UL_tasks(UIList): row.label(text="", icon="DOT") row.prop(item, "identification", emboss=False, text="") row.prop(item, "name", emboss=False, text="") + row.prop(item, "schedule_start", emboss=False, text="Start Time") + row.prop(item, "schedule_finish", emboss=False, text="Finish Time") + row.prop(item, "schedule_duration", emboss=False, text="Duration") + if props.active_task_id == item.ifc_definition_id: row.operator("bim.edit_task", text="", icon="CHECKMARK") row.operator("bim.disable_editing_task", text="", icon="CANCEL") @@ -187,6 +191,7 @@ class BIM_UL_tasks(UIList): row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id else: + row.operator("bim.enable_editing_task_time", text="", icon="TIME").task = item.ifc_definition_id row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = item.ifc_definition_id row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py new file mode 100644 index 0000000000..e37f60ebc4 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py @@ -0,0 +1,37 @@ +import ifcopenshell.util.date +from datetime import datetime +from datetime import timedelta + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "task": None, + "name": "Unnamed", + "duration_type": "NOTDEFINED", + "schedule_duration": None, + "schedule_start_time": datetime.now(), + "schedule_finish_time": datetime.now() + timedelta(days=5), + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + task_time = self.file.create_entity("IfcTaskTime", **{"Name": self.settings["name"]}) + task_time.DurationType = self.settings["duration_type"] + task_time.ScheduleStart = ifcopenshell.util.date.datetime2ifc(self.settings["schedule_start_time"], "IfcDateTime") + duration = self.settings["schedule_duration"] + if duration: + task_time.ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration, "IfcTime") + task_time.ScheduleFinish = ifcopenshell.util.date.datetime2ifc( + self.settings["schedule_start"] + duration, + "IfcDateTime" + ) + else: + duration = self.settings["schedule_finish_time"] - self.settings["schedule_start_time"] + # task_time.ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration.days, "IfcTime") + task_time.ScheduleFinish = ifcopenshell.util.date.datetime2ifc(self.settings["schedule_finish_time"], "IfcDateTime") + task = self.settings["task"] + if task: + task.TaskTime = task_time + return task_time diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py index 3aa298bafc..07f923b10a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py @@ -6,6 +6,7 @@ class Data: work_plans = {} work_schedules = {} tasks = {} + task_times = {} @classmethod def purge(cls): @@ -24,6 +25,7 @@ class Data: cls.load_work_schedules() cls.load_work_calendars() cls.load_tasks() + cls.load_task_times() cls.is_loaded = True @classmethod @@ -78,8 +80,27 @@ class Data: data["RelatedObjects"] = [] data["IsPredecessorTo"] = [] data["IsSuccessorFrom"] = [] + if task.TaskTime: + data["TaskTime"] = task.TaskTime + data["ScheduleStart"] = task.TaskTime.ScheduleStart + data["ScheduleFinish"] = task.TaskTime.ScheduleFinish + data["ScheduleDuration"] = task.TaskTime.ScheduleDuration for rel in task.IsNestedBy: [data["RelatedObjects"].append(o.id()) for o in rel.RelatedObjects if o.is_a("IfcTask")] [data["IsPredecessorTo"].append(rel.RelatedProcess.id()) for rel in task.IsPredecessorTo or []] [data["IsSuccessorFrom"].append(rel.RelatingProcess.id()) for rel in task.IsSuccessorFrom or []] cls.tasks[task.id()] = data + + @classmethod + def load_task_times(cls): + cls.task_times = {} + for task_time in cls._file.by_type("IfcTaskTime"): + data = task_time.get_info() + data["ScheduleStart"] = ifcopenshell.util.date.ifc2datetime(data["ScheduleStart"]) + data["ScheduleFinish"] = ifcopenshell.util.date.ifc2datetime(data["ScheduleFinish"]) + data["EarlyStart"] = ifcopenshell.util.date.ifc2datetime(data["EarlyStart"]) + data["EarlyFinish"] = ifcopenshell.util.date.ifc2datetime(data["EarlyFinish"]) + data["LateStart"] = ifcopenshell.util.date.ifc2datetime(data["LateStart"]) + data["LateFinish"] = ifcopenshell.util.date.ifc2datetime(data["LateFinish"]) + data["EarlyFinish"] = ifcopenshell.util.date.ifc2datetime(data["EarlyFinish"]) + cls.task_times[task_time.id()] = data diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py new file mode 100644 index 0000000000..140dff656e --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py @@ -0,0 +1,10 @@ +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"task": None, "attributes": {}} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + for name, value in self.settings["attributes"].items(): + setattr(self.settings["task"].TaskTime, name, value) From 6d9770542f3bcbfb1f37c136d33cca32e575e825 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 20 Apr 2021 17:11:06 +1000 Subject: [PATCH 46/64] Fixes to editing task times --- .../bim/module/sequence/__init__.py | 2 +- .../bim/module/sequence/operator.py | 125 ++++++++++----- .../blenderbim/bim/module/sequence/prop.py | 53 +++++-- .../blenderbim/bim/module/sequence/ui.py | 147 +++++++++++------- .../api/sequence/add_task_time.py | 24 +-- .../ifcopenshell/api/sequence/data.py | 19 +-- .../api/sequence/edit_task_time.py | 10 +- 7 files changed, 241 insertions(+), 139 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index c588617c4d..6ba11ff1e0 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -36,9 +36,9 @@ classes = ( operator.AssignSuccessor, operator.UnassignPredecessor, operator.UnassignSuccessor, - operator.AddTaskTime, operator.EnableEditingTaskTime, operator.DisableEditingTaskTime, + operator.EditTaskTime, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 091da38c15..5ebffd575c 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1,6 +1,8 @@ import bpy import json import ifcopenshell.api +from datetime import datetime +from dateutil.parser import parse from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.sequence.data import Data @@ -232,6 +234,16 @@ class EnableEditingTasks(bpy.types.Operator): new.ifc_definition_id = related_object_id new.name = task["Name"] or "Unnamed" new.identification = task["Identification"] or "X" + if task["TaskTime"]: + task_time = Data.task_times[task["TaskTime"]] + new.start = self.canonicalise_time(task_time["ScheduleStart"]) + new.finish = self.canonicalise_time(task_time["ScheduleFinish"]) + # TODO: duration + new.duration = "-" + else: + new.start = "-" + new.finish = "-" + new.duration = "-" new.is_expanded = related_object_id not in self.contracted_tasks new.level_index = level_index if task["RelatedObjects"]: @@ -240,6 +252,11 @@ class EnableEditingTasks(bpy.types.Operator): for related_object_id in task["RelatedObjects"]: self.create_new_task_li(related_object_id, level_index + 1) + def canonicalise_time(self, time): + if not time: + return "-" + return time.strftime("%d/%m/%y") + class DisableEditingWorkSchedule(bpy.types.Operator): bl_idname = "bim.disable_editing_work_schedule" @@ -481,16 +498,12 @@ class EnableEditingTaskTime(bpy.types.Operator): def execute(self, context): props = context.scene.BIMWorkScheduleProperties self.file = IfcStore.get_file() + + task_time_id = Data.tasks[self.task]["TaskTime"] or self.add_task_time().id() + while len(props.task_time_attributes) > 0: props.task_time_attributes.remove(0) - if self.file.by_id(self.task).TaskTime: - task_time_id = self.file.by_id(self.task).TaskTime.id() - else: - task_time = ifcopenshell.api.run("sequence.add_task_time", self.file) - self.file.by_id(self.task).TaskTime = task_time - task_time_id = task_time.id() - Data.load(self.file) data = Data.task_times[task_time_id] for attribute in IfcStore.get_schema().declaration_by_name("IfcTaskTime").all_attributes(): @@ -503,18 +516,82 @@ class EnableEditingTaskTime(bpy.types.Operator): new.is_optional = attribute.optional() new.data_type = data_type if data_type == "string": - new.string_value = "" if new.is_null else data[attribute.name()] + if isinstance(data[attribute.name()], datetime): + new.string_value = "" if new.is_null else data[attribute.name()].isoformat() + else: + new.string_value = "" if new.is_null else data[attribute.name()] elif data_type == "boolean": new.bool_value = False if new.is_null else data[attribute.name()] - elif data_type == "integer": - new.int_value = 0 if new.is_null else data[attribute.name()] + elif data_type == "float": + new.float_value = 0.0 if new.is_null else data[attribute.name()] elif data_type == "enum": new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) if data[attribute.name()]: new.enum_value = data[attribute.name()] - props.active_task_time_id = IfcStore.get_file().by_id(self.task).TaskTime.id() + props.active_task_time_id = task_time_id + props.active_task_id = self.task return {"FINISHED"} + def add_task_time(self): + task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=self.file.by_id(self.task)) + Data.load(IfcStore.get_file()) + return task_time + + +class DisableEditingTaskTime(bpy.types.Operator): + bl_idname = "bim.disable_editing_task_time" + bl_label = "Disable Editing Task Time" + + def execute(self, context): + context.scene.BIMWorkScheduleProperties.active_task_time_id = 0 + bpy.ops.bim.disable_editing_task() + return {"FINISHED"} + + +class EditTaskTime(bpy.types.Operator): + bl_idname = "bim.edit_task_time" + bl_label = "Edit Task Time" + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + attributes = {} + for attribute in props.task_time_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "boolean": + attributes[attribute.name] = attribute.bool_value + elif attribute.data_type == "float": + attributes[attribute.name] = attribute.float_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + + attributes = self.convert_strings_to_date_times(attributes) + + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.edit_task_time", + self.file, + **{"task_time": self.file.by_id(props.active_task_time_id), "attributes": attributes} + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_task_time() + bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) + return {"FINISHED"} + + def convert_strings_to_date_times(self, attributes): + for key, value in attributes.items(): + if not value: + continue + if "Start" in key or "Finish" in key or key == "StatusTime": + try: + attributes[key] = parse(value) + except: + attributes[key] = None + return attributes + class EnableEditingTask(bpy.types.Operator): bl_idname = "bim.enable_editing_task" @@ -551,15 +628,6 @@ class EnableEditingTask(bpy.types.Operator): return {"FINISHED"} -class DisableEditingTaskTime(bpy.types.Operator): - bl_idname = "bim.disable_editing_task_time" - bl_label = "Disable Editing Task Time" - - def execute(self, context): - context.scene.BIMWorkScheduleProperties.active_task_time_id = 0 - return {"FINISHED"} - - class DisableEditingTask(bpy.types.Operator): bl_idname = "bim.disable_editing_task" bl_label = "Disable Editing Task" @@ -670,20 +738,3 @@ class UnassignSuccessor(bpy.types.Operator): ) Data.load(self.file) return {"FINISHED"} - - -class AddTaskTime(bpy.types.Operator): - bl_idname = "bim.add_task_time" - bl_label = "Add Task Time" - task: bpy.props.IntProperty() - - def execute(self, context): - props = context.scene.BIMWorkScheduleProperties - self.file = IfcStore.get_file() - ifcopenshell.api.run( - "sequence.add_task_time", - self.file, - task = self.file.by_id(self.task), - ) - Data.load(self.file) - return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index b3bf062ca5..d45798b9ac 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -3,6 +3,7 @@ import ifcopenshell.api from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.sequence.data import Data from blenderbim.bim.prop import StrProperty, Attribute +from dateutil.parser import parse from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -47,20 +48,53 @@ def updateTaskIdentification(self, context): attribute = context.scene.BIMWorkScheduleProperties.task_attributes.get("Identification") attribute.string_value = self.identification -def updateTaskTimeScheduleStart(self, context): - if self.schedule_start == "X": + +def updateTaskTimeStart(self, context): + updateTaskTimeDateTime(self, context, "start") + + +def updateTaskTimeFinish(self, context): + updateTaskTimeDateTime(self, context, "finish") + + +def updateTaskTimeDateTime(self, context, startfinish): + def canonicalise_time(time): + if not time: + return "-" + return time.strftime("%d/%m/%y") + + startfinish_key = "Schedule" + startfinish.capitalize() + startfinish_value = getattr(self, startfinish) + + if startfinish_value == "-": return self.file = IfcStore.get_file() props = context.scene.BIMWorkScheduleProperties + + try: + startfinish_datetime = parse(startfinish_value) + except: + setattr(self, startfinish, "-") + return + + task = self.file.by_id(self.ifc_definition_id) + if task.TaskTime: + task_time = task.TaskTime + else: + task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=task) + Data.load(IfcStore.get_file()) + + if Data.task_times[task_time.id()][startfinish_key] == startfinish_datetime: + return + ifcopenshell.api.run( "sequence.edit_task_time", self.file, - **{"task": self.file.by_id(self.ifc_definition_id), "attributes": {"ScheduleStart": self.schedule_start}} + **{"task_time": task_time, "attributes": {startfinish_key: startfinish_datetime}} ) Data.load(IfcStore.get_file()) - if props.active_task_id == self.ifc_definition_id: - attribute = context.scene.BIMWorkScheduleProperties.task_attributes.get("ScheduleStart") - attribute.string_value = self.schedule_start + setattr(self, startfinish, canonicalise_time(startfinish_datetime)) + class Task(PropertyGroup): name: StringProperty(name="Name", update=updateTaskName) @@ -69,9 +103,10 @@ class Task(PropertyGroup): has_children: BoolProperty(name="Has Children") is_expanded: BoolProperty(name="Is Expanded") level_index: IntProperty(name="Level Index") - schedule_duration: StringProperty(name="Duration") - schedule_start: StringProperty(name="Schedule Start ", update=updateTaskTimeScheduleStart) - schedule_finish: StringProperty(name="Schedule Finish ") + duration: StringProperty(name="Duration") + start: StringProperty(name="Start", update=updateTaskTimeStart) + finish: StringProperty(name="Finish", update=updateTaskTimeFinish) + class WorkPlan(PropertyGroup): name: StringProperty(name="Name") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index b3274bd4fc..5e2aa6c028 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -136,65 +136,39 @@ class BIM_PT_work_schedules(Panel): "active_task_index", ) if self.props.active_task_id: - for attribute in self.props.task_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + self.draw_editable_task_attributes_ui() + if self.props.active_task_time_id: + self.draw_editable_task_time_attributes_ui() + def draw_editable_task_attributes_ui(self): + for attribute in self.props.task_attributes: + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "boolean": + row.prop(attribute, "bool_value", text=attribute.name) + elif attribute.data_type == "integer": + row.prop(attribute, "int_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") -class BIM_UL_tasks(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - if item: - props = context.scene.BIMWorkScheduleProperties - row = layout.row(align=True) - for i in range(0, item.level_index): - row.label(text="", icon="BLANK1") - if item.has_children: - if item.is_expanded: - row.operator( - "bim.contract_task", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN" - ).task = item.ifc_definition_id - else: - row.operator( - "bim.expand_task", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT" - ).task = item.ifc_definition_id - else: - row.label(text="", icon="DOT") - row.prop(item, "identification", emboss=False, text="") - row.prop(item, "name", emboss=False, text="") - row.prop(item, "schedule_start", emboss=False, text="Start Time") - row.prop(item, "schedule_finish", emboss=False, text="Finish Time") - row.prop(item, "schedule_duration", emboss=False, text="Duration") - - if props.active_task_id == item.ifc_definition_id: - row.operator("bim.edit_task", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_task", text="", icon="CANCEL") - elif props.active_task_id: - if props.active_task_id in Data.tasks[item.ifc_definition_id]["IsPredecessorTo"]: - row.operator("bim.unassign_predecessor", text="", icon="BACK", emboss=False).task = item.ifc_definition_id - else: - row.operator("bim.assign_predecessor", text="", icon="TRACKING_BACKWARDS", emboss=False).task = item.ifc_definition_id - - if props.active_task_id in Data.tasks[item.ifc_definition_id]["IsSuccessorFrom"]: - row.operator("bim.unassign_successor", text="", icon="FORWARD", emboss=False).task = item.ifc_definition_id - else: - row.operator("bim.assign_successor", text="", icon="TRACKING_FORWARDS", emboss=False).task = item.ifc_definition_id - - row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id - row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id - else: - row.operator("bim.enable_editing_task_time", text="", icon="TIME").task = item.ifc_definition_id - row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id - row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = item.ifc_definition_id - row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id + def draw_editable_task_time_attributes_ui(self): + for attribute in self.props.task_time_attributes: + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "boolean": + row.prop(attribute, "bool_value", text=attribute.name) + elif attribute.data_type == "integer": + row.prop(attribute, "int_value", text=attribute.name) + elif attribute.data_type == "float": + row.prop(attribute, "float_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") class BIM_PT_work_calendars(Panel): @@ -259,3 +233,62 @@ class BIM_UL_work_calendars(UIList): op = row.operator("bim.enable_editing_work_calendar", text="", icon="GREASEPENCIL") op.work_calendar = item.ifc_definition_id row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = item.ifc_definition_id + + +class BIM_UL_tasks(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + props = context.scene.BIMWorkScheduleProperties + row = layout.row(align=True) + for i in range(0, item.level_index): + row.label(text="", icon="BLANK1") + if item.has_children: + if item.is_expanded: + row.operator( + "bim.contract_task", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN" + ).task = item.ifc_definition_id + else: + row.operator( + "bim.expand_task", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT" + ).task = item.ifc_definition_id + else: + row.label(text="", icon="DOT") + row.prop(item, "identification", emboss=False, text="") + row.prop(item, "name", emboss=False, text="") + + row.prop(item, "start", emboss=False, text="") + row.prop(item, "finish", emboss=False, text="") + row.prop(item, "duration", emboss=False, text="") + + if props.active_task_id == item.ifc_definition_id: + if props.active_task_time_id: + row.operator("bim.edit_task_time", text="", icon="CHECKMARK") + else: + row.operator("bim.edit_task", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_task", text="", icon="CANCEL") + elif props.active_task_id: + if props.active_task_id in Data.tasks[item.ifc_definition_id]["IsPredecessorTo"]: + row.operator( + "bim.unassign_predecessor", text="", icon="BACK", emboss=False + ).task = item.ifc_definition_id + else: + row.operator( + "bim.assign_predecessor", text="", icon="TRACKING_BACKWARDS", emboss=False + ).task = item.ifc_definition_id + + if props.active_task_id in Data.tasks[item.ifc_definition_id]["IsSuccessorFrom"]: + row.operator( + "bim.unassign_successor", text="", icon="FORWARD", emboss=False + ).task = item.ifc_definition_id + else: + row.operator( + "bim.assign_successor", text="", icon="TRACKING_FORWARDS", emboss=False + ).task = item.ifc_definition_id + + row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id + row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id + else: + row.operator("bim.enable_editing_task_time", text="", icon="TIME").task = item.ifc_definition_id + row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id + row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = item.ifc_definition_id + row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py index e37f60ebc4..a79eb12664 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py @@ -7,31 +7,11 @@ class Usecase: self.file = file self.settings = { "task": None, - "name": "Unnamed", - "duration_type": "NOTDEFINED", - "schedule_duration": None, - "schedule_start_time": datetime.now(), - "schedule_finish_time": datetime.now() + timedelta(days=5), } for key, value in settings.items(): self.settings[key] = value def execute(self): - task_time = self.file.create_entity("IfcTaskTime", **{"Name": self.settings["name"]}) - task_time.DurationType = self.settings["duration_type"] - task_time.ScheduleStart = ifcopenshell.util.date.datetime2ifc(self.settings["schedule_start_time"], "IfcDateTime") - duration = self.settings["schedule_duration"] - if duration: - task_time.ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration, "IfcTime") - task_time.ScheduleFinish = ifcopenshell.util.date.datetime2ifc( - self.settings["schedule_start"] + duration, - "IfcDateTime" - ) - else: - duration = self.settings["schedule_finish_time"] - self.settings["schedule_start_time"] - # task_time.ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration.days, "IfcTime") - task_time.ScheduleFinish = ifcopenshell.util.date.datetime2ifc(self.settings["schedule_finish_time"], "IfcDateTime") - task = self.settings["task"] - if task: - task.TaskTime = task_time + task_time = self.file.create_entity("IfcTaskTime") + self.settings["task"].TaskTime = task_time return task_time diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py index 07f923b10a..b2289fc178 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py @@ -15,6 +15,7 @@ class Data: cls.work_schedules = {} cls.work_calendars = {} cls.tasks = {} + cls.task_times = {} @classmethod def load(cls, file): @@ -81,10 +82,7 @@ class Data: data["IsPredecessorTo"] = [] data["IsSuccessorFrom"] = [] if task.TaskTime: - data["TaskTime"] = task.TaskTime - data["ScheduleStart"] = task.TaskTime.ScheduleStart - data["ScheduleFinish"] = task.TaskTime.ScheduleFinish - data["ScheduleDuration"] = task.TaskTime.ScheduleDuration + data["TaskTime"] = data["TaskTime"].id() for rel in task.IsNestedBy: [data["RelatedObjects"].append(o.id()) for o in rel.RelatedObjects if o.is_a("IfcTask")] [data["IsPredecessorTo"].append(rel.RelatedProcess.id()) for rel in task.IsPredecessorTo or []] @@ -96,11 +94,10 @@ class Data: cls.task_times = {} for task_time in cls._file.by_type("IfcTaskTime"): data = task_time.get_info() - data["ScheduleStart"] = ifcopenshell.util.date.ifc2datetime(data["ScheduleStart"]) - data["ScheduleFinish"] = ifcopenshell.util.date.ifc2datetime(data["ScheduleFinish"]) - data["EarlyStart"] = ifcopenshell.util.date.ifc2datetime(data["EarlyStart"]) - data["EarlyFinish"] = ifcopenshell.util.date.ifc2datetime(data["EarlyFinish"]) - data["LateStart"] = ifcopenshell.util.date.ifc2datetime(data["LateStart"]) - data["LateFinish"] = ifcopenshell.util.date.ifc2datetime(data["LateFinish"]) - data["EarlyFinish"] = ifcopenshell.util.date.ifc2datetime(data["EarlyFinish"]) + for key, value in data.items(): + if not value: + continue + if "Start" in key or "Finish" in key or key == "StatusTime": + data[key] = ifcopenshell.util.date.ifc2datetime(value) + # TODO parse duration cls.task_times[task_time.id()] = data 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 140dff656e..5859e48221 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py @@ -1,10 +1,16 @@ +import ifcopenshell.util.date + + class Usecase: def __init__(self, file, **settings): self.file = file - self.settings = {"task": None, "attributes": {}} + self.settings = {"task_time": None, "attributes": {}} for key, value in settings.items(): self.settings[key] = value def execute(self): for name, value in self.settings["attributes"].items(): - setattr(self.settings["task"].TaskTime, name, value) + if "Start" in name or "Finish" in name or name == "StatusTime": + if value: + value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime") + setattr(self.settings["task_time"], name, value) From 9beddca2f75b3a026ca36d31a1757d4ec6952f9f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 20 Apr 2021 23:34:10 +1000 Subject: [PATCH 47/64] You can now toggle showing time columns in work schedule, minor fixes and ui cleanup --- .../blenderbim/bim/module/sequence/operator.py | 9 ++++++--- .../blenderbim/bim/module/sequence/prop.py | 17 ++++++++++++----- .../blenderbim/bim/module/sequence/ui.py | 15 ++++++++------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 5ebffd575c..3b80089d32 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -2,7 +2,7 @@ import bpy import json import ifcopenshell.api from datetime import datetime -from dateutil.parser import parse +from dateutil import parser from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.sequence.data import Data @@ -587,9 +587,12 @@ class EditTaskTime(bpy.types.Operator): continue if "Start" in key or "Finish" in key or key == "StatusTime": try: - attributes[key] = parse(value) + attributes[key] = parser.isoparse(value) except: - attributes[key] = None + try: + attributes[key] = parser.parse(value, dayfirst=True, fuzzy=True) + except: + attributes[key] = None return attributes diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index d45798b9ac..39075fe847 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -3,7 +3,7 @@ import ifcopenshell.api from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.sequence.data import Data from blenderbim.bim.prop import StrProperty, Attribute -from dateutil.parser import parse +from dateutil import parser from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -68,14 +68,17 @@ def updateTaskTimeDateTime(self, context, startfinish): if startfinish_value == "-": return + self.file = IfcStore.get_file() - props = context.scene.BIMWorkScheduleProperties try: - startfinish_datetime = parse(startfinish_value) + startfinish_datetime = parser.isoparse(startfinish_value) except: - setattr(self, startfinish, "-") - return + try: + startfinish_datetime = parser.parse(startfinish_value, dayfirst=True, fuzzy=True) + except: + setattr(self, startfinish, "-") + return task = self.file.by_id(self.ifc_definition_id) if task.TaskTime: @@ -85,6 +88,9 @@ def updateTaskTimeDateTime(self, context, startfinish): Data.load(IfcStore.get_file()) if Data.task_times[task_time.id()][startfinish_key] == startfinish_datetime: + canonical_startfinish_value = canonicalise_time(startfinish_datetime) + if startfinish_value != canonical_startfinish_value: + setattr(self, startfinish, canonical_startfinish_value) return ifcopenshell.api.run( @@ -130,6 +136,7 @@ class BIMWorkScheduleProperties(PropertyGroup): active_task_index: IntProperty(name="Active Task Index") active_task_id: IntProperty(name="Active Task Id") task_attributes: CollectionProperty(name="Task Attributes", type=Attribute) + should_show_times: BoolProperty(name="Should Show Times", default=False) active_task_time_id: IntProperty(name="Active Task Id") task_time_attributes: CollectionProperty(name="Task Time Attributes", type=Attribute) contracted_tasks: StringProperty(name="Contracted Task Items", default="[]") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 5e2aa6c028..8e5930c40c 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -98,6 +98,9 @@ class BIM_PT_work_schedules(Panel): if self.props.active_work_schedule_id and self.props.active_work_schedule_id == work_schedule_id: if self.props.is_editing == "WORK_SCHEDULE": row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK") + elif self.props.is_editing == "TASKS": + row.prop(self.props, "should_show_times", text="", icon="TIME") + row.operator("bim.add_summary_task", text="", icon="ADD").work_schedule = work_schedule_id row.operator("bim.disable_editing_work_schedule", text="", icon="CANCEL") elif self.props.active_work_schedule_id: row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id @@ -124,9 +127,6 @@ class BIM_PT_work_schedules(Panel): row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") def draw_editable_task_ui(self, work_schedule_id): - row = self.layout.row(align=True) - row.label(text="X Summary Tasks") - row.operator("bim.add_summary_task", text="", icon="ADD").work_schedule = work_schedule_id self.layout.template_list( "BIM_UL_tasks", "", @@ -256,9 +256,10 @@ class BIM_UL_tasks(UIList): row.prop(item, "identification", emboss=False, text="") row.prop(item, "name", emboss=False, text="") - row.prop(item, "start", emboss=False, text="") - row.prop(item, "finish", emboss=False, text="") - row.prop(item, "duration", emboss=False, text="") + if props.should_show_times: + row.prop(item, "start", emboss=False, text="") + row.prop(item, "finish", emboss=False, text="") + row.prop(item, "duration", emboss=False, text="") if props.active_task_id == item.ifc_definition_id: if props.active_task_time_id: @@ -289,6 +290,6 @@ class BIM_UL_tasks(UIList): row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id else: row.operator("bim.enable_editing_task_time", text="", icon="TIME").task = item.ifc_definition_id - row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = item.ifc_definition_id + row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id From ac092fb6f8f66695ed0e3cb50abd13b85983db56 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 21 Apr 2021 09:14:57 +1000 Subject: [PATCH 48/64] Minor fix - add dateutil as dependency --- src/blenderbim/Makefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 4ee574bbe5..894f8297d6 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -177,6 +177,13 @@ endif cp -r dist/working/svgwrite-1.3.1/svgwrite dist/blenderbim/libs/site/packages/ rm -rf dist/working + # Provides fuzzy date parsing for construction sequencing + mkdir dist/working + cd dist/working && wget https://files.pythonhosted.org/packages/be/ed/5bbc91f03fa4c839c4c7360375da77f9659af5f7086b7a7bdda65771c8e0/python-dateutil-2.8.1.tar.gz + cd dist/working && tar -xzvf python-dateutil* + cp -r dist/working/python-dateutil-2.8.1/dateutil dist/blenderbim/libs/site/packages/ + rm -rf dist/working + # Required by IFCDiff mkdir dist/working cd dist/working && wget https://github.com/Moult/deepdiff/archive/master.zip From 0506475f265d11d24dfe5e8169cff6b8eb1cb921 Mon Sep 17 00:00:00 2001 From: bosonprojets Date: Tue, 20 Apr 2021 23:51:55 +0000 Subject: [PATCH 49/64] Feature to add building elements to construction scheduling tasks in blenderBIM and assign product to process with the ifcopenshell.api --- .../bim/module/sequence/__init__.py | 1 + .../bim/module/sequence/operator.py | 21 +++++++ .../blenderbim/bim/module/sequence/prop.py | 1 + .../blenderbim/bim/module/sequence/ui.py | 5 ++ .../api/sequence/assign_product.py | 56 +++++++++++++++++++ 5 files changed, 84 insertions(+) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 6ba11ff1e0..6e3658388f 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -39,6 +39,7 @@ classes = ( operator.EnableEditingTaskTime, operator.DisableEditingTaskTime, operator.EditTaskTime, + operator.AssignProduct, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 3b80089d32..0f7f714e8a 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -628,6 +628,7 @@ class EnableEditingTask(bpy.types.Operator): if data[attribute.name()]: new.enum_value = data[attribute.name()] props.active_task_id = self.task + props.should_show_times = True return {"FINISHED"} @@ -741,3 +742,23 @@ class UnassignSuccessor(bpy.types.Operator): ) Data.load(self.file) return {"FINISHED"} + + +class AssignProduct(bpy.types.Operator): + bl_idname = "bim.assign_product" + bl_label = "Assign Product" + task: bpy.props.IntProperty() + + def execute(self, context): + obj = bpy.context.active_object.BIMObjectProperties.ifc_definition_id + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.assign_product", + self.file, + relating_product = self.file.by_id(obj), + related_process = self.file.by_id(self.task), + ) + props.has_assignment = True + Data.load(self.file) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 39075fe847..e59a744cd8 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -107,6 +107,7 @@ class Task(PropertyGroup): identification: StringProperty(name="Identification", update=updateTaskIdentification) ifc_definition_id: IntProperty(name="IFC Definition ID") has_children: BoolProperty(name="Has Children") + has_assignment: BoolProperty(name="Has Assignement") is_expanded: BoolProperty(name="Is Expanded") level_index: IntProperty(name="Level Index") duration: StringProperty(name="Duration") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 8e5930c40c..917076dc3b 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -293,3 +293,8 @@ class BIM_UL_tasks(UIList): row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = item.ifc_definition_id row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id + if context.selected_objects: + obj = context.selected_objects[0] + pass + row = layout.row(align=True) + row.operator("bim.assign_product", text="ADD", icon="OUTLINER_COLLECTION").task = item.ifc_definition_id diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py new file mode 100644 index 0000000000..cd4f39176c --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py @@ -0,0 +1,56 @@ +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "relating_product": None, + "related_process": None, + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + referenced_by = None + + if self.settings["relating_product"].ReferencedBy: + for rel in self.settings["relating_product"].ReferencedBy: + if rel.is_a("IfcRelAssignsToProduct"): + referenced_by = rel + + assignment = None + for rel in self.settings["related_process"].HasAssignments: + if rel.is_a('IfcRelAssignsToProduct'): + assignment = rel + break + if referenced_by and referenced_by == assignment: + return + + if assignment: + related_objects = set(assignment.RelatedObjects) + related_objects.add(self.settings["relating_product"]) + assignment.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": assignment}) + else: + rel = self.file.create_entity( + "IfcRelAssignsToProduct", + **{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatingProduct": self.settings["relating_product"], + "RelatedObjects": [self.settings["related_process"]], + }) + return rel + + if referenced_by: + related_objects = list(referenced_by.RelatedObjects) + related_objects.remove(self.settings["relating_product"]) + if related_objects: + referenced_by.RelatedObjects = related_objects + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": referenced_by}) + else: + self.file.remove(referenced_by) + + return rel From 8dd781aea2ccf9b0d8617b2e99e243896944e6ce Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 21 Apr 2021 11:03:36 +1000 Subject: [PATCH 50/64] Fix bug where you couldn't assign objects to a fresh layer --- src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py b/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py index 66fb65a748..0faf5573bd 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py @@ -12,6 +12,6 @@ class Usecase: self.settings[key] = value def execute(self): - assigned_items = set(self.settings["layer"].AssignedItems) or set() + assigned_items = set(self.settings["layer"].AssignedItems or []) assigned_items.add(self.settings["item"]) self.settings["layer"].AssignedItems = list(assigned_items) From 3143f8c9a5728aa3a209ff6161985b93d5dca412 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 21 Apr 2021 11:11:08 +1000 Subject: [PATCH 51/64] Code review for assign product with myoualid --- .../bim/module/sequence/operator.py | 25 ++++---- .../blenderbim/bim/module/sequence/prop.py | 3 +- .../blenderbim/bim/module/sequence/ui.py | 3 - .../api/control/assign_control.py | 27 +++----- .../ifcopenshell/api/nest/assign_object.py | 12 ++-- .../api/sequence/assign_product.py | 61 ++++++++----------- 6 files changed, 52 insertions(+), 79 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 0f7f714e8a..c07d659323 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -628,7 +628,6 @@ class EnableEditingTask(bpy.types.Operator): if data[attribute.name()]: new.enum_value = data[attribute.name()] props.active_task_id = self.task - props.should_show_times = True return {"FINISHED"} @@ -662,9 +661,7 @@ class EditTask(bpy.types.Operator): attributes[attribute.name] = attribute.enum_value self.file = IfcStore.get_file() ifcopenshell.api.run( - "sequence.edit_task", - self.file, - **{"task": self.file.by_id(props.active_task_id), "attributes": attributes} + "sequence.edit_task", self.file, **{"task": self.file.by_id(props.active_task_id), "attributes": attributes} ) Data.load(IfcStore.get_file()) bpy.ops.bim.disable_editing_task() @@ -748,17 +745,19 @@ class AssignProduct(bpy.types.Operator): bl_idname = "bim.assign_product" bl_label = "Assign Product" task: bpy.props.IntProperty() + related_product: bpy.props.StringProperty() def execute(self, context): - obj = bpy.context.active_object.BIMObjectProperties.ifc_definition_id - props = context.scene.BIMWorkScheduleProperties - self.file = IfcStore.get_file() - ifcopenshell.api.run( - "sequence.assign_product", - self.file, - relating_product = self.file.by_id(obj), - related_process = self.file.by_id(self.task), + related_products = ( + [bpy.data.objects.get(self.related_product)] if self.related_product else bpy.context.selected_objects ) - props.has_assignment = True + for related_product in related_products: + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.assign_product", + self.file, + relating_product=self.file.by_id(related_product.BIMObjectProperties.ifc_definition_id), + related_object=self.file.by_id(self.task), + ) Data.load(self.file) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index e59a744cd8..ea599001c4 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -107,7 +107,6 @@ class Task(PropertyGroup): identification: StringProperty(name="Identification", update=updateTaskIdentification) ifc_definition_id: IntProperty(name="IFC Definition ID") has_children: BoolProperty(name="Has Children") - has_assignment: BoolProperty(name="Has Assignement") is_expanded: BoolProperty(name="Is Expanded") level_index: IntProperty(name="Level Index") duration: StringProperty(name="Duration") @@ -137,7 +136,7 @@ class BIMWorkScheduleProperties(PropertyGroup): active_task_index: IntProperty(name="Active Task Index") active_task_id: IntProperty(name="Active Task Id") task_attributes: CollectionProperty(name="Task Attributes", type=Attribute) - should_show_times: BoolProperty(name="Should Show Times", default=False) + should_show_times: BoolProperty(name="Should Show Times", default=True) active_task_time_id: IntProperty(name="Active Task Id") task_time_attributes: CollectionProperty(name="Task Time Attributes", type=Attribute) contracted_tasks: StringProperty(name="Contracted Task Items", default="[]") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 917076dc3b..0831afbf90 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -293,8 +293,5 @@ class BIM_UL_tasks(UIList): row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = item.ifc_definition_id row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id - if context.selected_objects: - obj = context.selected_objects[0] - pass row = layout.row(align=True) row.operator("bim.assign_product", text="ADD", icon="OUTLINER_COLLECTION").task = item.ifc_definition_id diff --git a/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py b/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py index d684f63f44..f45c93e80e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py +++ b/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py @@ -13,27 +13,18 @@ class Usecase: self.settings[key] = value def execute(self): - has_assignments = None if self.settings["related_object"].HasAssignments: - for assignement in self.settings["related_object"].HasAssignments: - if assignement.is_a("IfclRelAssignsToControl"): - has_assignments = assignement + for assignment in self.settings["related_object"].HasAssignments: + if ( + assignment.is_a("IfclRelAssignsToControl") + and assignment.RelatingControl == self.settings["relating_control"] + ): + return controls = None - for rel in self.settings["relating_control"].Controls: - if rel.is_a("IfcRelAssignsToControl"): - controls = rel - break - if has_assignments and has_assignments == controls: - return - if has_assignments: - related_objects = list(has_assignments.RelatedObjects) - related_objects.remove(self.settings["related_object"]) - if related_objects: - has_assignments.RelatedObjects = related_objects - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": has_assignments}) - else: - self.file.remove(has_assignments) + if self.settings["relating_control"].Controls: + controls = self.settings["relating_control"].Controls[0] + if controls: related_objects = list(controls.RelatedObjects) related_objects.append(self.settings["related_object"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py b/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py index 2c948e3f80..f0d79efaf9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py +++ b/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py @@ -6,7 +6,7 @@ class Usecase: def __init__(self, file, **settings): self.file = file self.settings = { - "object": None, + "related_object": None, "relating_object": None, } for key, value in settings.items(): @@ -14,8 +14,8 @@ class Usecase: def execute(self): nests = None - if self.settings["object"].Nests: - nests = self.settings["object"].Nests[0] + if self.settings["related_object"].Nests: + nests = self.settings["related_object"].Nests[0] is_nested_by = None for rel in self.settings["relating_object"].IsNestedBy: @@ -28,7 +28,7 @@ class Usecase: if nests: related_objects = list(nests.RelatedObjects) - related_objects.remove(self.settings["object"]) + related_objects.remove(self.settings["related_object"]) if related_objects: nests.RelatedObjects = related_objects ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": nests}) @@ -37,7 +37,7 @@ class Usecase: if is_nested_by: related_objects = list(is_nested_by.RelatedObjects) - related_objects.append(self.settings["object"]) + related_objects.append(self.settings["related_object"]) is_nested_by.RelatedObjects = related_objects ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_nested_by}) else: @@ -46,7 +46,7 @@ class Usecase: **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatedObjects": [self.settings["object"]], + "RelatedObjects": [self.settings["related_object"]], "RelatingObject": self.settings["relating_object"], } ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py index cd4f39176c..afc29f5d64 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py @@ -7,50 +7,37 @@ class Usecase: self.file = file self.settings = { "relating_product": None, - "related_process": None, + "related_object": None, } for key, value in settings.items(): self.settings[key] = value def execute(self): + if self.settings["related_object"].HasAssignments: + for assignment in self.settings["related_object"].HasAssignments: + if ( + assignment.is_a("IfclRelAssignsToProduct") + and assignment.RelatingProduct == self.settings["relating_product"] + ): + return + referenced_by = None - if self.settings["relating_product"].ReferencedBy: - for rel in self.settings["relating_product"].ReferencedBy: - if rel.is_a("IfcRelAssignsToProduct"): - referenced_by = rel - - assignment = None - for rel in self.settings["related_process"].HasAssignments: - if rel.is_a('IfcRelAssignsToProduct'): - assignment = rel - break - if referenced_by and referenced_by == assignment: - return - - if assignment: - related_objects = set(assignment.RelatedObjects) - related_objects.add(self.settings["relating_product"]) - assignment.RelatedObjects = list(related_objects) - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": assignment}) - else: - rel = self.file.create_entity( - "IfcRelAssignsToProduct", - **{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatingProduct": self.settings["relating_product"], - "RelatedObjects": [self.settings["related_process"]], - }) - return rel + referenced_by = self.settings["relating_product"].ReferencedBy[0] if referenced_by: related_objects = list(referenced_by.RelatedObjects) - related_objects.remove(self.settings["relating_product"]) - if related_objects: - referenced_by.RelatedObjects = related_objects - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": referenced_by}) - else: - self.file.remove(referenced_by) - - return rel + related_objects.append(self.settings["related_object"]) + referenced_by.RelatedObjects = related_objects + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": referenced_by}) + else: + referenced_by = self.file.create_entity( + "IfcRelAssignsToProduct", + **{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatedObjects": [self.settings["related_object"]], + "RelatingProduct": self.settings["relating_product"], + } + ) + return referenced_by From 9b855bed2d55206754c88267d0208fc62da6f948 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 21 Apr 2021 11:53:33 +1000 Subject: [PATCH 52/64] Implement unassigning products to tasks --- .../bim/module/sequence/__init__.py | 1 + .../bim/module/sequence/operator.py | 22 ++++++++++++++++ .../blenderbim/bim/module/sequence/ui.py | 12 +++++++-- .../ifcopenshell/api/sequence/add_task.py | 9 ++++--- .../ifcopenshell/api/sequence/data.py | 6 +++++ .../api/sequence/unassign_product.py | 25 +++++++++++++++++++ 6 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_product.py diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 6e3658388f..b205cce8bb 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -40,6 +40,7 @@ classes = ( operator.DisableEditingTaskTime, operator.EditTaskTime, operator.AssignProduct, + operator.UnassignProduct, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index c07d659323..3984198796 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -761,3 +761,25 @@ class AssignProduct(bpy.types.Operator): ) Data.load(self.file) return {"FINISHED"} + + +class UnassignProduct(bpy.types.Operator): + bl_idname = "bim.unassign_product" + bl_label = "Unassign Product" + task: bpy.props.IntProperty() + related_product: bpy.props.StringProperty() + + def execute(self, context): + related_products = ( + [bpy.data.objects.get(self.related_product)] if self.related_product else bpy.context.selected_objects + ) + for related_product in related_products: + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.unassign_product", + self.file, + relating_product=self.file.by_id(related_product.BIMObjectProperties.ifc_definition_id), + related_object=self.file.by_id(self.task), + ) + Data.load(self.file) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 0831afbf90..492fbda7a9 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -261,6 +261,16 @@ class BIM_UL_tasks(UIList): row.prop(item, "finish", emboss=False, text="") row.prop(item, "duration", emboss=False, text="") + if context.active_object: + oprops = context.active_object.BIMObjectProperties + row = layout.row(align=True) + if oprops.ifc_definition_id in Data.tasks[item.ifc_definition_id]["RelatingProducts"]: + op = row.operator("bim.unassign_product", text="", icon="KEYFRAME_HLT", emboss=False) + op.task = item.ifc_definition_id + else: + op = row.operator("bim.assign_product", text="", icon="KEYFRAME", emboss=False) + op.task = item.ifc_definition_id + if props.active_task_id == item.ifc_definition_id: if props.active_task_time_id: row.operator("bim.edit_task_time", text="", icon="CHECKMARK") @@ -293,5 +303,3 @@ class BIM_UL_tasks(UIList): row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = item.ifc_definition_id row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id - row = layout.row(align=True) - row.operator("bim.assign_product", text="ADD", icon="OUTLINER_COLLECTION").task = item.ifc_definition_id diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py index 1102606064..98ce8bc33a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py @@ -1,6 +1,7 @@ import ifcopenshell.api import ifcopenshell + class Usecase: def __init__(self, file, **settings): self.file = file @@ -16,9 +17,9 @@ class Usecase: "root.create_entity", self.file, ifc_class="IfcTask", - name= None, - predefined_type= "NOTDEFINED", - identification= "none", + name=None, + predefined_type="NOTDEFINED", + identification="none", ) task.IsMilestone = False if self.settings["work_schedule"]: @@ -33,6 +34,6 @@ class Usecase: ) elif self.settings["parent_task"]: ifcopenshell.api.run( - "nest.assign_object", self.file, object=task, relating_object=self.settings["parent_task"] + "nest.assign_object", self.file, related_object=task, relating_object=self.settings["parent_task"] ) return task diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py index b2289fc178..c193551f23 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py @@ -79,12 +79,18 @@ class Data: data = task.get_info() del data["OwnerHistory"] data["RelatedObjects"] = [] + data["RelatingProducts"] = [] data["IsPredecessorTo"] = [] data["IsSuccessorFrom"] = [] if task.TaskTime: data["TaskTime"] = data["TaskTime"].id() for rel in task.IsNestedBy: [data["RelatedObjects"].append(o.id()) for o in rel.RelatedObjects if o.is_a("IfcTask")] + [ + data["RelatingProducts"].append(r.RelatingProduct.id()) + for r in task.HasAssignments + if r.is_a("IfcRelAssignsToProduct") + ] [data["IsPredecessorTo"].append(rel.RelatedProcess.id()) for rel in task.IsPredecessorTo or []] [data["IsSuccessorFrom"].append(rel.RelatingProcess.id()) for rel in task.IsSuccessorFrom or []] cls.tasks[task.id()] = data diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_product.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_product.py new file mode 100644 index 0000000000..5d28bc43d0 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_product.py @@ -0,0 +1,25 @@ +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "relating_product": None, + "related_object": None, + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + for rel in self.settings["related_object"].HasAssignments or []: + if not rel.is_a("IfcRelAssignsToProduct") or rel.RelatingProduct != self.settings["relating_product"]: + continue + if len(rel.RelatedObjects) == 1: + return self.file.remove(rel) + related_objects = list(rel.RelatedObjects) + related_objects.remove(self.settings["related_object"]) + rel.RelatedObjects = related_objects + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + return rel From e7b153cbf67655cb86f8226bf6f997246bb16bbe Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 21 Apr 2021 12:57:31 +1000 Subject: [PATCH 53/64] You can now generate gantt charts from work schedules --- src/blenderbim/Makefile | 7 ++++ .../blenderbim/bim/data/gantt/index.mustache | 11 +++++ .../bim/module/sequence/__init__.py | 1 + .../bim/module/sequence/operator.py | 42 +++++++++++++++++++ .../blenderbim/bim/module/sequence/ui.py | 1 + src/blenderbim/ifc_to_gantt.py | 33 --------------- 6 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 src/blenderbim/blenderbim/bim/data/gantt/index.mustache delete mode 100644 src/blenderbim/ifc_to_gantt.py diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 894f8297d6..f90728a02e 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -184,6 +184,13 @@ endif cp -r dist/working/python-dateutil-2.8.1/dateutil dist/blenderbim/libs/site/packages/ rm -rf dist/working + # Provides jsgantt-improved supports for web-based construction sequencing gantt charts + mkdir dist/working + cd dist/working && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js + cd dist/working && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css + cd dist/working && mv jsgantt* dist/blenderbim/bim/data/gantt/ + rm -rf dist/working + # Required by IFCDiff mkdir dist/working cd dist/working && wget https://github.com/Moult/deepdiff/archive/master.zip diff --git a/src/blenderbim/blenderbim/bim/data/gantt/index.mustache b/src/blenderbim/blenderbim/bim/data/gantt/index.mustache new file mode 100644 index 0000000000..832f8e4a8b --- /dev/null +++ b/src/blenderbim/blenderbim/bim/data/gantt/index.mustache @@ -0,0 +1,11 @@ + + +
+ diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index b205cce8bb..6dd9b2c6b9 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -41,6 +41,7 @@ classes = ( operator.EditTaskTime, operator.AssignProduct, operator.UnassignProduct, + operator.GenerateGanttChart, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 3984198796..66fb5232e7 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1,5 +1,8 @@ +import os import bpy import json +import pystache +import webbrowser import ifcopenshell.api from datetime import datetime from dateutil import parser @@ -783,3 +786,42 @@ class UnassignProduct(bpy.types.Operator): ) Data.load(self.file) return {"FINISHED"} + + +class GenerateGanttChart(bpy.types.Operator): + bl_idname = "bim.generate_gantt_chart" + bl_label = "Generate Gantt Chart" + work_schedule: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + self.json = [] + for task_id in Data.work_schedules[self.work_schedule]["RelatedObjects"]: + self.create_new_task_json(task_id) + with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"), "w") as f: + with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.mustache"), "r") as t: + f.write(pystache.render(t.read(), {"json_data": json.dumps(self.json)})) + webbrowser.open("file://" + os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html")) + return {"FINISHED"} + + def create_new_task_json(self, task_id): + task = self.file.by_id(task_id) + self.json.append( + { + "pID": task.id(), + "pName": task.Name, + "pStart": task.TaskTime.ScheduleStart if task.TaskTime else "", + "pEnd": task.TaskTime.ScheduleFinish if task.TaskTime else "", + "pPlanStart": task.TaskTime.ScheduleStart if task.TaskTime else "", + "pPlanEnd": task.TaskTime.ScheduleFinish if task.TaskTime else "", + "pClass": "ggroupblack", + "pMile": 1 if task.IsMilestone else 0, + "pComp": 0, + "pGroup": 1, + "pParent": task.Nests[0].RelatingObject.id() if task.Nests else 0, + "pOpen": 1, + "pCost": 1, + } + ) + for task_id in Data.tasks[task_id]["RelatedObjects"]: + self.create_new_task_json(task_id) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 492fbda7a9..e1bb49fe11 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -100,6 +100,7 @@ class BIM_PT_work_schedules(Panel): row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK") elif self.props.is_editing == "TASKS": row.prop(self.props, "should_show_times", text="", icon="TIME") + row.operator("bim.generate_gantt_chart", text="", icon="NLA").work_schedule = work_schedule_id row.operator("bim.add_summary_task", text="", icon="ADD").work_schedule = work_schedule_id row.operator("bim.disable_editing_work_schedule", text="", icon="CANCEL") elif self.props.active_work_schedule_id: diff --git a/src/blenderbim/ifc_to_gantt.py b/src/blenderbim/ifc_to_gantt.py deleted file mode 100644 index 1359805a02..0000000000 --- a/src/blenderbim/ifc_to_gantt.py +++ /dev/null @@ -1,33 +0,0 @@ -import json -import ifcopenshell - -class IfcToGantt: - def __init__(self): - self.json = [] - - def execute(self): - self.file = ifcopenshell.open("p6.ifc") - self.root = self.file.by_type("IfcTask")[0] - task = self.root - for task in self.file.by_type("IfcTask"): - self.json.append({ - "pID": task.id(), - "pName": task.Name, - "pStart": task.TaskTime.ScheduleStart if task.TaskTime else "", - "pEnd": task.TaskTime.ScheduleFinish if task.TaskTime else "", - "pPlanStart": task.TaskTime.ScheduleStart if task.TaskTime else "", - "pPlanEnd": task.TaskTime.ScheduleFinish if task.TaskTime else "", - "pClass": "ggroupblack", - "pMile": 1 if task.IsMilestone else 0, - "pComp": 0, - "pGroup": 1, - "pParent": task.Nests[0].RelatingObject.id() if task.Nests else 0, - "pOpen": 1, - "pCost": 1 - }) - with open("p6.json", "w") as f: - json.dump(self.json, f) - -ifc_to_gantt = IfcToGantt() -ifc_to_gantt.execute() - From 5e815e783226d890197bbca0f7163150ac86b059 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 21 Apr 2021 16:45:11 +1000 Subject: [PATCH 54/64] You can now append object types from an IFC project library to your active project --- src/blenderbim/Makefile | 2 +- .../blenderbim/bim/module/project/__init__.py | 1 + .../blenderbim/bim/module/project/operator.py | 35 +++++++++++++++++++ .../blenderbim/bim/module/project/ui.py | 21 ++++++----- .../ifcopenshell/api/cost/add_cost_item.py | 2 +- 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index f90728a02e..973b705fc2 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -188,7 +188,7 @@ endif mkdir dist/working cd dist/working && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js cd dist/working && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css - cd dist/working && mv jsgantt* dist/blenderbim/bim/data/gantt/ + cp dist/working/jsgantt* dist/blenderbim/bim/data/gantt/ rm -rf dist/working # Required by IFCDiff diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index 576a026de3..d05306f3ce 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -12,6 +12,7 @@ classes = ( operator.AssignLibraryDeclaration, operator.UnassignLibraryDeclaration, operator.SaveLibraryFile, + operator.AppendLibraryElement, prop.LibraryElement, prop.BIMProjectProperties, ui.BIM_PT_project, diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 36eebc2549..a1c279c71c 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -4,6 +4,7 @@ import ifcopenshell import ifcopenshell.api import bpy from blenderbim.bim.ifc import IfcStore +from blenderbim.bim import import_ifc class CreateProject(bpy.types.Operator): @@ -220,3 +221,37 @@ class SaveLibraryFile(bpy.types.Operator): def execute(self, context): IfcStore.library_file.write(IfcStore.library_path) return {"FINISHED"} + + +class AppendLibraryElement(bpy.types.Operator): + bl_idname = "bim.append_library_element" + bl_label = "Append Library Element" + definition: bpy.props.IntProperty() + + def execute(self, context): + element = ifcopenshell.api.run( + "project.append_asset", + IfcStore.get_file(), + element=IfcStore.library_file.by_id(self.definition), + ) + self.import_type_from_ifc(element) + return {"FINISHED"} + + def import_type_from_ifc(self, element): + self.file = IfcStore.get_file() + logger = logging.getLogger("ImportIFC") + ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, logger) + + type_collection = bpy.data.collections.get("Types") + if not type_collection: + type_collection = bpy.data.collections.new("Types") + for collection in bpy.data.collections: + if "IfcProject/" in collection.name: + collection.children.link(type_collection) + break + + ifc_importer = import_ifc.IfcImporter(ifc_import_settings) + ifc_importer.file = self.file + ifc_importer.type_collection = type_collection + ifc_importer.create_type_product(element) + ifc_importer.place_objects_in_spatial_tree() diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 4c62ad5c8d..82006ba3e1 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -71,6 +71,7 @@ class BIM_PT_project(Panel): class BIM_PT_project_library(Panel): bl_label = "IFC Project Library" bl_idname = "BIM_PT_project_library" + bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" @@ -117,14 +118,16 @@ class BIM_UL_library(UIList): op.element_name = item.name row.label(text=item.name) if ( - not item.ifc_definition_id - or IfcStore.library_file.schema == "IFC2X3" - or not IfcStore.library_file.by_type("IfcProjectLibrary") + item.ifc_definition_id + and IfcStore.library_file.schema != "IFC2X3" + and IfcStore.library_file.by_type("IfcProjectLibrary") ): - return - if item.is_declared: - op = row.operator("bim.unassign_library_declaration", text="", icon="KEYFRAME_HLT", emboss=False) - op.definition = item.ifc_definition_id - else: - op = row.operator("bim.assign_library_declaration", text="", icon="KEYFRAME", emboss=False) + if item.is_declared: + op = row.operator("bim.unassign_library_declaration", text="", icon="KEYFRAME_HLT", emboss=False) + op.definition = item.ifc_definition_id + else: + op = row.operator("bim.assign_library_declaration", text="", icon="KEYFRAME", emboss=False) + op.definition = item.ifc_definition_id + if item.ifc_definition_id: + op = row.operator("bim.append_library_element", text="", icon="APPEND_BLEND") op.definition = item.ifc_definition_id diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py index 9d9b017f18..dc1aa1e29c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py @@ -23,6 +23,6 @@ class Usecase: ) elif self.settings["cost_item"]: ifcopenshell.api.run( - "nest.assign_object", self.file, object=cost_item, relating_object=self.settings["cost_item"] + "nest.assign_object", self.file, related_object=cost_item, relating_object=self.settings["cost_item"] ) return cost_item From ef88b30639b2e55650c2cdbf79586bce49ffa13d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 21 Apr 2021 20:40:45 +1000 Subject: [PATCH 55/64] Fix bug where import mesh cleaning would inadvertently mark meshes as edited --- src/blenderbim/blenderbim/bim/handler.py | 2 +- src/blenderbim/blenderbim/bim/import_ifc.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 2626d5335d..fd87192b35 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -11,7 +11,7 @@ from ifcopenshell.api.attribute.data import Data as AttributeData def mode_callback(obj, data): for obj in bpy.context.selected_objects: if ( - obj.mode != "OBJECT" + obj.mode != "EDIT" or not obj.data or not isinstance(obj.data, bpy.types.Mesh) or not obj.data.BIMMeshProperties.ifc_definition_id diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index f98f0f5082..9d1b5a5ae9 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -1000,6 +1000,7 @@ class IfcImporter: bpy.ops.mesh.tris_convert_to_quads(context_override) bpy.ops.mesh.normals_make_consistent(context_override) bpy.ops.object.editmode_toggle(context_override) + IfcStore.edited_objs.clear() def add_opening_relation(self, element, obj): if not element.is_a("IfcOpeningElement"): From 78314b1ec78f0a7045273d70571eee4b6853189f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 21 Apr 2021 20:49:58 +1000 Subject: [PATCH 56/64] Forgot to commit append_asset usecase, also minor fixes --- src/blenderbim/blenderbim/bim/export_ifc.py | 13 ++--- .../ifcopenshell/api/project/append_asset.py | 54 +++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index d3cbdbda11..030ad57329 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -80,9 +80,9 @@ class IfcExporter: for guid, obj in IfcStore.guid_map.items(): try: self.sync_object_placement(obj) - except: - pass - self.sync_object_container(guid, obj) + self.sync_object_container(guid, obj) + except ReferenceError: + pass # The object is likely deleted if self.should_delete(obj): to_delete.append(guid) @@ -105,9 +105,10 @@ class IfcExporter: def sync_object_placement(self, obj): blender_matrix = np.matrix(obj.matrix_world) - ifc_matrix = ifcopenshell.util.placement.get_local_placement( - self.file.by_id(obj.BIMObjectProperties.ifc_definition_id).ObjectPlacement - ) + element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + if not hasattr(element, "ObjectPlacement"): + return + ifc_matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) ifc_matrix[0][3] *= self.unit_scale ifc_matrix[1][3] *= self.unit_scale ifc_matrix[2][3] *= self.unit_scale diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py new file mode 100644 index 0000000000..a3409a0bf6 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -0,0 +1,54 @@ +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"element": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + element = self.file.add(self.settings["element"]) + self.existing_contexts = self.file.by_type("IfcGeometricRepresentationContext") + added_contexts = [e for e in self.file.traverse(element) if e.is_a("IfcGeometricRepresentationContext")] + for added_context in added_contexts: + equivalent_existing_context = self.get_equivalent_existing_context(added_context) + if not equivalent_existing_context: + equivalent_existing_context = self.create_equivalent_context(added_context) + for inverse in self.file.get_inverse(added_context): + ifcopenshell.util.element.replace_attribute(inverse, added_context, equivalent_existing_context) + for added_context in added_contexts: + if added_context.is_a() == "IfcGeometricRepresentationContext": + ifcopenshell.util.element.remove_deep(self.file, added_context) + return element + + def get_equivalent_existing_context(self, added_context): + for context in self.existing_contexts: + if context.is_a() != added_context.is_a(): + continue + if context.is_a("IfcGeometricRepresentationSubContext"): + if ( + context.ContextType == added_context.ContextType + and context.ContextIdentifier == added_context.ContextIdentifier + and context.TargetView == added_context.TargetView + ): + return context + elif ( + context.ContextType == added_context.ContextType + and context.ContextIdentifier == added_context.ContextIdentifier + ): + return context + + def create_equivalent_context(self, added_context): + if added_context.is_a("IfcGeometricRepresentationSubContext"): + return ifcopenshell.api.run( + "context.add_context", + context=added_context.ContextType, + subcontext=added_context.ContextIdentifier, + target_view=added_context.TargetView, + ) + return ifcopenshell.api.run( + "context.add_context", context=added_context.ContextType, subcontext=added_context.ContextIdentifier + ) From 8b35a3aadd8c0bf3a9f11e6a11c02d5ae7558a9e Mon Sep 17 00:00:00 2001 From: bosonprojets Date: Thu, 22 Apr 2021 00:18:22 +0000 Subject: [PATCH 57/64] Assign control to cost items and UX Workschedules and Costchedules cleanup --- .../blenderbim/bim/module/cost/__init__.py | 4 + .../blenderbim/bim/module/cost/operator.py | 204 +++++++++++--- .../blenderbim/bim/module/cost/prop.py | 24 +- .../blenderbim/bim/module/cost/ui.py | 93 +++++-- .../bim/module/sequence/__init__.py | 3 +- .../bim/module/sequence/operator.py | 263 ++++++++---------- .../blenderbim/bim/module/sequence/prop.py | 8 +- .../blenderbim/bim/module/sequence/ui.py | 133 +++++---- .../api/control/assign_control.py | 4 +- .../api/control/unassign_control.py | 25 ++ .../ifcopenshell/api/cost/data.py | 3 + .../ifcopenshell/api/cost/edit_cost_item.py | 10 + 12 files changed, 502 insertions(+), 272 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/control/unassign_control.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index 7a8a31b1d3..8d420e367d 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -6,12 +6,16 @@ classes = ( operator.RemoveCostSchedule, operator.EditCostSchedule, operator.EnableEditingCostSchedule, + operator.EnableEditingCostItems, + operator.EnableEditingCostItem, operator.DisableEditingCostSchedule, operator.AddCostItem, operator.AddSummaryCostItem, operator.ExpandCostItem, operator.ContractCostItem, operator.RemoveCostItem, + operator.AssignControl, + operator.UnassignControl, prop.CostItem, prop.BIMCostProperties, ui.BIM_PT_cost_schedules, diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index 80b6df245b..95e1b89008 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -15,6 +15,32 @@ class AddCostSchedule(bpy.types.Operator): return {"FINISHED"} +class EditCostSchedule(bpy.types.Operator): + bl_idname = "bim.edit_cost_schedule" + bl_label = "Edit Cost Schedule" + + def execute(self, context): + props = context.scene.BIMCostProperties + attributes = {} + for attribute in props.cost_schedule_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.edit_cost_schedule", + self.file, + **{"cost_schedule": self.file.by_id(props.active_cost_schedule_id), "attributes": attributes}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_cost_schedule() + return {"FINISHED"} + + class RemoveCostSchedule(bpy.types.Operator): bl_idname = "bim.remove_cost_schedule" bl_label = "Remove Cost Schedule" @@ -38,10 +64,13 @@ class EnableEditingCostSchedule(bpy.types.Operator): def execute(self, context): self.props = context.scene.BIMCostProperties self.props.active_cost_schedule_id = self.cost_schedule - while len(self.props.cost_schedule_attributes) > 0: self.props.cost_schedule_attributes.remove(0) + self.enable_editing_cost_schedule() + self.props.is_editing = "COST_SCHEDULE" + return {"FINISHED"} + def enable_editing_cost_schedule(self): data = Data.cost_schedules[self.cost_schedule] for attribute in IfcStore.get_schema().declaration_by_name("IfcCostSchedule").all_attributes(): @@ -62,12 +91,22 @@ class EnableEditingCostSchedule(bpy.types.Operator): if data[attribute.name()]: new.enum_value = data[attribute.name()] + +class EnableEditingCostItems(bpy.types.Operator): + bl_idname = "bim.enable_editing_cost_items" + bl_label = "Enable Editing Cost Items" + cost_schedule: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMCostProperties + self.props.active_cost_schedule_id = self.cost_schedule while len(self.props.cost_items) > 0: self.props.cost_items.remove(0) self.contracted_cost_items = json.loads(self.props.contracted_cost_items) for related_object_id in Data.cost_schedules[self.cost_schedule]["RelatedObjects"]: self.create_new_cost_item_li(related_object_id, 0) + self.props.is_editing = "COST_ITEMS" return {"FINISHED"} def create_new_cost_item_li(self, related_object_id, level_index): @@ -83,40 +122,15 @@ class EnableEditingCostSchedule(bpy.types.Operator): for related_object_id in cost_item["RelatedObjects"]: self.create_new_cost_item_li(related_object_id, level_index + 1) + return {"FINISHED"} + class DisableEditingCostSchedule(bpy.types.Operator): bl_idname = "bim.disable_editing_cost_schedule" bl_label = "Disable Editing Cost Schedule" def execute(self, context): - props = context.scene.BIMCostProperties - props.active_cost_schedule_id = 0 - return {"FINISHED"} - - -class EditCostSchedule(bpy.types.Operator): - bl_idname = "bim.edit_cost_schedule" - bl_label = "Edit Cost Schedule" - - def execute(self, context): - props = context.scene.BIMCostProperties - attributes = {} - for attribute in props.cost_schedule_attributes: - if attribute.is_null: - attributes[attribute.name] = None - else: - if attribute.data_type == "string": - attributes[attribute.name] = attribute.string_value - elif attribute.data_type == "enum": - attributes[attribute.name] = attribute.enum_value - self.file = IfcStore.get_file() - ifcopenshell.api.run( - "cost.edit_cost_schedule", - self.file, - **{"cost_schedule": self.file.by_id(props.active_cost_schedule_id), "attributes": attributes} - ) - Data.load(IfcStore.get_file()) - bpy.ops.bim.disable_editing_cost_schedule() + context.scene.BIMCostProperties.active_cost_schedule_id = 0 return {"FINISHED"} @@ -130,7 +144,7 @@ class AddSummaryCostItem(bpy.types.Operator): self.file = IfcStore.get_file() ifcopenshell.api.run("cost.add_cost_item", self.file, **{"cost_schedule": self.file.by_id(self.cost_schedule)}) Data.load(self.file) - bpy.ops.bim.enable_editing_cost_schedule(cost_schedule=self.cost_schedule) + bpy.ops.bim.enable_editing_cost_items(cost_schedule=self.cost_schedule) return {"FINISHED"} @@ -142,10 +156,9 @@ class AddCostItem(bpy.types.Operator): def execute(self, context): props = context.scene.BIMCostProperties self.file = IfcStore.get_file() - data = {"cost_item": self.file.by_id(self.cost_item)} - ifcopenshell.api.run("cost.add_cost_item", self.file, **data) + ifcopenshell.api.run("cost.add_cost_item", self.file, **{"cost_item": self.file.by_id(self.cost_item)}) Data.load(self.file) - bpy.ops.bim.enable_editing_cost_schedule(cost_schedule=props.active_cost_schedule_id) + bpy.ops.bim.enable_editing_cost_items(cost_schedule=props.active_cost_schedule_id) return {"FINISHED"} @@ -160,7 +173,7 @@ class ExpandCostItem(bpy.types.Operator): contracted_cost_items = json.loads(props.contracted_cost_items) contracted_cost_items.remove(self.cost_item) props.contracted_cost_items = json.dumps(contracted_cost_items) - bpy.ops.bim.enable_editing_cost_schedule(cost_schedule=props.active_cost_schedule_id) + bpy.ops.bim.enable_editing_cost_items(cost_schedule=props.active_cost_schedule_id) return {"FINISHED"} @@ -175,7 +188,7 @@ class ContractCostItem(bpy.types.Operator): contracted_cost_items = json.loads(props.contracted_cost_items) contracted_cost_items.append(self.cost_item) props.contracted_cost_items = json.dumps(contracted_cost_items) - bpy.ops.bim.enable_editing_cost_schedule(cost_schedule=props.active_cost_schedule_id) + bpy.ops.bim.enable_editing_cost_items(cost_schedule=props.active_cost_schedule_id) return {"FINISHED"} @@ -197,5 +210,124 @@ class RemoveCostItem(bpy.types.Operator): contracted_cost_items.remove(props.active_cost_item_index) props.contracted_cost_items = json.dumps(contracted_cost_items) Data.load(self.file) - bpy.ops.bim.enable_editing_cost_schedule(cost_schedule=props.active_cost_schedule_id) + bpy.ops.bim.enable_editing_cost_items(cost_schedule=props.active_cost_schedule_id) + return {"FINISHED"} + + +class EnableEditingCostItem(bpy.types.Operator): + bl_idname = "bim.enable_editing_cost_item" + bl_label = "Enable Editing Cost Item" + cost_item: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + while len(props.cost_item_attributes) > 0: + props.cost_item_attributes.remove(0) + + data = Data.cost_items[self.cost_item] + + for attribute in IfcStore.get_schema().declaration_by_name("IfcCostItem").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = props.cost_item_attributes.add() + new.name = attribute.name() + new.is_null = data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if data_type == "string": + new.string_value = "" if new.is_null else data[attribute.name()] + elif data_type == "boolean": + new.bool_value = False if new.is_null else data[attribute.name()] + elif data_type == "integer": + new.int_value = 0 if new.is_null else data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if data[attribute.name()]: + new.enum_value = data[attribute.name()] + props.active_cost_item_id = self.cost_item + return {"FINISHED"} + + +class DisableEditingCostItem(bpy.types.Operator): + bl_idname = "bim.disable_editing_cost_item" + bl_label = "Disable Editing Cost Item" + + def execute(self, context): + context.scene.BIMCostProperties.active_cost_item_id = 0 + return {"FINISHED"} + + +class EditCostItem(bpy.types.Operator): + bl_idname = "bim.edit_cost_item" + bl_label = "Edit Cost Item" + + def execute(self, context): + props = context.scene.BIMCostProperties + attributes = {} + for attribute in props.cost_item_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "boolean": + attributes[attribute.name] = attribute.bool_value + elif attribute.data_type == "integer": + attributes[attribute.name] = attribute.int_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.edit_cost_item", + self.file, + **{"cost_item": self.file.by_id(props.active_cost_item_id), "attributes": attributes}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_cost_item() + bpy.ops.bim.enable_editing_cost_items(cost_schedule=props.active_cost_schedule_id) + return {"FINISHED"} + + +class AssignControl(bpy.types.Operator): + bl_idname = "bim.assign_control" + bl_label = "Assign Control" + cost_item: bpy.props.IntProperty() + related_object: bpy.props.StringProperty() + + def execute(self, context): + related_objects = ( + [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects + ) + for related_object in related_objects: + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "control.assign_control", + self.file, + related_object=self.file.by_id(related_object.BIMObjectProperties.ifc_definition_id), + relating_control=self.file.by_id(self.cost_item), + ) + Data.load(self.file) + return {"FINISHED"} + + +class UnassignControl(bpy.types.Operator): + bl_idname = "bim.unassign_control" + bl_label = "Unassign Control" + cost_item: bpy.props.IntProperty() + related_object: bpy.props.StringProperty() + + def execute(self, context): + related_objects = ( + [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects + ) + for related_object in related_objects: + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "control.unassign_control", + self.file, + related_object=self.file.by_id(related_object.BIMObjectProperties.ifc_definition_id), + relating_control=self.file.by_id(self.cost_item), + ) + Data.load(self.file) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cost/prop.py b/src/blenderbim/blenderbim/bim/module/cost/prop.py index b2d2f26832..4be8e8399e 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/prop.py +++ b/src/blenderbim/blenderbim/bim/module/cost/prop.py @@ -1,4 +1,7 @@ import bpy +import ifcopenshell.api +from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.cost.data import Data from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -13,8 +16,24 @@ from bpy.props import ( ) +def updateCostItemName(self, context): + if self.name == "Unnamed": + return + self.file = IfcStore.get_file() + props = context.scene.BIMCostProperties + ifcopenshell.api.run( + "cost.edit_cost_item", + self.file, + **{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}}, + ) + Data.load(IfcStore.get_file()) + if props.active_cost_item_id == self.ifc_definition_id: + attribute = props.cost_item_attributes.get("Name") + attribute.string_value = self.name + + class CostItem(PropertyGroup): - name: StringProperty(name="Name") + name: StringProperty(name="Name", update=updateCostItemName) ifc_definition_id: IntProperty(name="IFC Definition ID") has_children: BoolProperty(name="Has Children") is_expanded: BoolProperty(name="Is Expanded") @@ -23,7 +42,10 @@ class CostItem(PropertyGroup): class BIMCostProperties(PropertyGroup): cost_schedule_attributes: CollectionProperty(name="Cost Schedule Attributes", type=Attribute) + is_editing: StringProperty(name="Is Editing") active_cost_schedule_id: IntProperty(name="Active Cost Schedule Id") cost_items: CollectionProperty(name="Work Calendar", type=CostItem) + active_cost_item_id: IntProperty(name="Active Cost Id") active_cost_item_index: IntProperty(name="Active Cost Item Index") + cost_item_attributes: CollectionProperty(name="Task Attributes", type=Attribute) contracted_cost_items: StringProperty(name="Contracted Cost Items", default="[]") diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 7c9edc329e..22a85127b1 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -25,22 +25,34 @@ class BIM_PT_cost_schedules(Panel): row.operator("bim.add_cost_schedule", icon="ADD") for cost_schedule_id, cost_schedule in Data.cost_schedules.items(): - row = self.layout.row(align=True) - row.label(text=cost_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") + self.draw_cost_schedule_ui(cost_schedule_id, cost_schedule) - if self.props.active_cost_schedule_id and self.props.active_cost_schedule_id == cost_schedule_id: + def draw_cost_schedule_ui(self, cost_schedule_id, cost_schedule): + row = self.layout.row(align=True) + row.label(text=cost_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") + + if self.props.active_cost_schedule_id and self.props.active_cost_schedule_id == cost_schedule_id: + if self.props.is_editing == "COST_SCHEDULE": row.operator("bim.edit_cost_schedule", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_cost_schedule", text="", icon="X") - elif self.props.active_cost_schedule_id: - row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule_id - else: - row.operator("bim.enable_editing_cost_schedule", text="", icon="GREASEPENCIL").cost_schedule = cost_schedule_id - row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule_id + elif self.props.is_editing == "COST_ITEMS": + row.operator("bim.add_summary_cost_item", text="", icon="ADD").cost_schedule = cost_schedule_id + row.operator("bim.disable_editing_cost_schedule", text="", icon="CANCEL") + elif self.props.active_cost_schedule_id: + row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule_id + else: + row.operator("bim.enable_editing_cost_items", text="", icon="OUTLINER").cost_schedule = cost_schedule_id + row.operator( + "bim.enable_editing_cost_schedule", text="", icon="GREASEPENCIL" + ).cost_schedule = cost_schedule_id + row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule_id - if self.props.active_cost_schedule_id == cost_schedule_id: - self.draw_editable_cost_schedule_ui(cost_schedule_id, cost_schedule) + if self.props.active_cost_schedule_id == cost_schedule_id: + if self.props.is_editing == "COST_SCHEDULE": + self.draw_editable_cost_schedule_ui() + elif self.props.is_editing == "COST_ITEMS": + self.draw_editable_cost_item_ui(cost_schedule_id) - def draw_editable_cost_schedule_ui(self, cost_schedule_id, cost_schedule): + def draw_editable_cost_schedule_ui(self): for attribute in self.props.cost_schedule_attributes: row = self.layout.row(align=True) if attribute.data_type == "string": @@ -50,9 +62,10 @@ class BIM_PT_cost_schedules(Panel): if attribute.is_optional: row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") - row = self.layout.row(align=True) - row.label(text="X Summary Cost Items") - row.operator("bim.add_summary_cost_item", text="", icon="ADD").cost_schedule = cost_schedule_id + # row = self.layout.row(align=True) + # row.label(text="X Summary Cost Items") + + def draw_editable_cost_item_ui(self, cost_schedule_id): self.layout.template_list( "BIM_UL_cost_items", "", @@ -61,22 +74,60 @@ class BIM_PT_cost_schedules(Panel): self.props, "active_cost_item_index", ) + if self.props.active_cost_item_id: + self.draw_editable_cost_item_attributes_ui() + + def draw_editable_cost_item_attributes_ui(self): + for attribute in self.props.cost_item_attributes: + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "boolean": + row.prop(attribute, "bool_value", text=attribute.name) + elif attribute.data_type == "integer": + row.prop(attribute, "int_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") class BIM_UL_cost_items(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: + props = context.scene.BIMCostProperties row = layout.row(align=True) for i in range(0, item.level_index): row.label(text="", icon="BLANK1") if item.has_children: if item.is_expanded: - row.operator("bim.contract_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN").cost_item = item.ifc_definition_id + row.operator( + "bim.contract_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN" + ).cost_item = item.ifc_definition_id else: - row.operator("bim.expand_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT").cost_item = item.ifc_definition_id + row.operator( + "bim.expand_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT" + ).cost_item = item.ifc_definition_id else: row.label(text="", icon="DOT") - row.label(text=item.name) - row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = item.ifc_definition_id - op = row.operator("bim.remove_cost_item", text="", icon="X") - op.cost_item = item.ifc_definition_id + row.prop(item, "name", emboss=False, text="") + + if context.active_object: + oprops = context.active_object.BIMObjectProperties + row = layout.row(align=True) + if oprops.ifc_definition_id in Data.cost_items[item.ifc_definition_id]["Controls"]: + op = row.operator("bim.unassign_control", text="", icon="KEYFRAME_HLT", emboss=False) + op.cost_item = item.ifc_definition_id + else: + op = row.operator("bim.assign_control", text="", icon="KEYFRAME", emboss=False) + op.cost_item = item.ifc_definition_id + + if props.active_cost_item_id == item.ifc_definition_id: + row.operator("bim.edit_cost_item", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL") + else: + row.operator( + "bim.enable_editing_cost_item", text="", icon="GREASEPENCIL" + ).cost_item = item.ifc_definition_id + row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = item.ifc_definition_id + row.operator("bim.remove_cost_item", text="", icon="X").cost_item = item.ifc_definition_id diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 6dd9b2c6b9..df151b8ef5 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -15,7 +15,6 @@ classes = ( operator.EnableEditingWorkSchedule, operator.EnableEditingTasks, operator.DisableEditingWorkSchedule, - operator.LoadTasks, operator.DisableTaskEditingUI, operator.LoadWorkCalendars, operator.DisableWorkCalendarEditingUI, @@ -56,11 +55,13 @@ classes = ( ui.BIM_UL_tasks, ) + def register(): bpy.types.Scene.BIMWorkPlanProperties = bpy.props.PointerProperty(type=prop.BIMWorkPlanProperties) bpy.types.Scene.BIMWorkScheduleProperties = bpy.props.PointerProperty(type=prop.BIMWorkScheduleProperties) bpy.types.Scene.BIMWorkCalendarProperties = bpy.props.PointerProperty(type=prop.BIMWorkCalendarProperties) + def unregister(): del bpy.types.Scene.BIMWorkPlanProperties del bpy.types.Scene.BIMWorkScheduleProperties diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 66fb5232e7..dcb00ca14c 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -66,7 +66,7 @@ class EditWorkPlan(bpy.types.Operator): ifcopenshell.api.run( "sequence.edit_work_plan", self.file, - **{"work_plan": self.file.by_id(props.active_work_plan_id), "attributes": attributes} + **{"work_plan": self.file.by_id(props.active_work_plan_id), "attributes": attributes}, ) Data.load(IfcStore.get_file()) bpy.ops.bim.load_work_plans() @@ -157,7 +157,7 @@ class EditWorkSchedule(bpy.types.Operator): ifcopenshell.api.run( "sequence.edit_work_schedule", self.file, - **{"work_schedule": self.file.by_id(props.active_work_schedule_id), "attributes": attributes} + **{"work_schedule": self.file.by_id(props.active_work_schedule_id), "attributes": attributes}, ) Data.load(IfcStore.get_file()) bpy.ops.bim.disable_editing_work_schedule() @@ -236,7 +236,7 @@ class EnableEditingTasks(bpy.types.Operator): new = self.props.tasks.add() new.ifc_definition_id = related_object_id new.name = task["Name"] or "Unnamed" - new.identification = task["Identification"] or "X" + new.identification = task["Identification"] or "XXX" if task["TaskTime"]: task_time = Data.task_times[task["TaskTime"]] new.start = self.canonicalise_time(task_time["ScheduleStart"]) @@ -254,6 +254,7 @@ class EnableEditingTasks(bpy.types.Operator): if new.is_expanded: for related_object_id in task["RelatedObjects"]: self.create_new_task_li(related_object_id, level_index + 1) + return {"FINISHED"} def canonicalise_time(self, time): if not time: @@ -270,142 +271,6 @@ class DisableEditingWorkSchedule(bpy.types.Operator): return {"FINISHED"} -class LoadWorkCalendars(bpy.types.Operator): - bl_idname = "bim.load_work_calendars" - bl_label = "Load Work Calendars" - - def execute(self, context): - props = context.scene.BIMWorkCalendarProperties - while len(props.work_calendars) > 0: - props.work_calendars.remove(0) - for ifc_definition_id, work_calendar in Data.work_calendars.items(): - new = props.work_calendars.add() - new.ifc_definition_id = ifc_definition_id - new.name = work_calendar["Name"] or "Unnamed" - props.is_editing = True - bpy.ops.bim.disable_editing_work_calendar() - return {"FINISHED"} - - -class DisableWorkCalendarEditingUI(bpy.types.Operator): - bl_idname = "bim.disable_work_calendar_editing_ui" - bl_label = "Disable WorkCalendar Editing UI" - - def execute(self, context): - context.scene.BIMWorkCalendarProperties.is_editing = False - return {"FINISHED"} - - -class AddWorkCalendar(bpy.types.Operator): - bl_idname = "bim.add_work_calendar" - bl_label = "Add Work Calendar" - - def execute(self, context): - ifcopenshell.api.run("sequence.add_work_calendar", IfcStore.get_file()) - Data.load(IfcStore.get_file()) - bpy.ops.bim.load_work_calendars() - return {"FINISHED"} - - -class EditWorkCalendar(bpy.types.Operator): - bl_idname = "bim.edit_work_calendar" - bl_label = "Edit Work Calendar" - - def execute(self, context): - props = context.scene.BIMWorkCalendarProperties - attributes = {} - for attribute in props.work_calendar_attributes: - if attribute.is_null: - attributes[attribute.name] = None - else: - if attribute.data_type == "string": - attributes[attribute.name] = attribute.string_value - elif attribute.data_type == "enum": - attributes[attribute.name] = attribute.enum_value - self.file = IfcStore.get_file() - ifcopenshell.api.run( - "sequence.edit_work_calendar", - self.file, - **{"work_calendar": self.file.by_id(props.active_work_calendar_id), "attributes": attributes} - ) - Data.load(IfcStore.get_file()) - bpy.ops.bim.load_work_calendars() - return {"FINISHED"} - - -class RemoveWorkCalendar(bpy.types.Operator): - bl_idname = "bim.remove_work_calendar" - bl_label = "Remove Work Plan" - work_calendar: bpy.props.IntProperty() - - def execute(self, context): - self.file = IfcStore.get_file() - ifcopenshell.api.run( - "sequence.remove_work_calendar", self.file, **{"work_calendar": self.file.by_id(self.work_calendar)} - ) - Data.load(self.file) - bpy.ops.bim.load_work_calendars() - return {"FINISHED"} - - -class EnableEditingWorkCalendar(bpy.types.Operator): - bl_idname = "bim.enable_editing_work_calendar" - bl_label = "Enable Editing Work Plan" - work_calendar: bpy.props.IntProperty() - - def execute(self, context): - props = context.scene.BIMWorkCalendarProperties - while len(props.work_calendar_attributes) > 0: - props.work_calendar_attributes.remove(0) - - data = Data.work_calendars[self.work_calendar] - - for attribute in IfcStore.get_schema().declaration_by_name("IfcWorkCalendar").all_attributes(): - data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity": - continue - new = props.work_calendar_attributes.add() - new.name = attribute.name() - new.is_null = data[attribute.name()] is None - new.is_optional = attribute.optional() - new.data_type = data_type - if data_type == "string": - new.string_value = "" if new.is_null else data[attribute.name()] - elif data_type == "enum": - new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) - if data[attribute.name()]: - new.enum_value = data[attribute.name()] - props.active_work_calendar_id = self.work_calendar - return {"FINISHED"} - - -class DisableEditingWorkCalendar(bpy.types.Operator): - bl_idname = "bim.disable_editing_work_calendar" - bl_label = "Disable Editing Work Calendar" - - def execute(self, context): - context.scene.BIMWorkCalendarProperties.active_work_calendar_id = 0 - return {"FINISHED"} - - -class LoadTasks(bpy.types.Operator): - bl_idname = "bim.load_tasks" - bl_label = "Load Tasks" - work_schedule: bpy.props.IntProperty() - - def execute(self, context): - props = context.scene.BIMWorkScheduleProperties - while len(props.tasks) > 0: - props.tasks.remove(0) - for ifc_definition_id in Data.work_schedules[self.work_schedule]["RelatedObjects"]: - task = Data.tasks[ifc_definition_id] - new = props.tasks.add() - new.ifc_definition_id = ifc_definition_id - new.name = task["Name"] or "Unnamed" - new.identification = task["Identification"] - return {"FINISHED"} - - class DisableTaskEditingUI(bpy.types.Operator): bl_idname = "bim.disable_task_editing_ui" bl_label = "Disable Task Editing UI" @@ -577,7 +442,7 @@ class EditTaskTime(bpy.types.Operator): ifcopenshell.api.run( "sequence.edit_task_time", self.file, - **{"task_time": self.file.by_id(props.active_task_time_id), "attributes": attributes} + **{"task_time": self.file.by_id(props.active_task_time_id), "attributes": attributes}, ) Data.load(IfcStore.get_file()) bpy.ops.bim.disable_editing_task_time() @@ -825,3 +690,121 @@ class GenerateGanttChart(bpy.types.Operator): ) for task_id in Data.tasks[task_id]["RelatedObjects"]: self.create_new_task_json(task_id) + + +class LoadWorkCalendars(bpy.types.Operator): + bl_idname = "bim.load_work_calendars" + bl_label = "Load Work Calendars" + + def execute(self, context): + props = context.scene.BIMWorkCalendarProperties + while len(props.work_calendars) > 0: + props.work_calendars.remove(0) + for ifc_definition_id, work_calendar in Data.work_calendars.items(): + new = props.work_calendars.add() + new.ifc_definition_id = ifc_definition_id + new.name = work_calendar["Name"] or "Unnamed" + props.is_editing = True + bpy.ops.bim.disable_editing_work_calendar() + return {"FINISHED"} + + +class DisableWorkCalendarEditingUI(bpy.types.Operator): + bl_idname = "bim.disable_work_calendar_editing_ui" + bl_label = "Disable WorkCalendar Editing UI" + + def execute(self, context): + context.scene.BIMWorkCalendarProperties.is_editing = False + return {"FINISHED"} + + +class AddWorkCalendar(bpy.types.Operator): + bl_idname = "bim.add_work_calendar" + bl_label = "Add Work Calendar" + + def execute(self, context): + ifcopenshell.api.run("sequence.add_work_calendar", IfcStore.get_file()) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_work_calendars() + return {"FINISHED"} + + +class EditWorkCalendar(bpy.types.Operator): + bl_idname = "bim.edit_work_calendar" + bl_label = "Edit Work Calendar" + + def execute(self, context): + props = context.scene.BIMWorkCalendarProperties + attributes = {} + for attribute in props.work_calendar_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.edit_work_calendar", + self.file, + **{"work_calendar": self.file.by_id(props.active_work_calendar_id), "attributes": attributes}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_work_calendars() + return {"FINISHED"} + + +class RemoveWorkCalendar(bpy.types.Operator): + bl_idname = "bim.remove_work_calendar" + bl_label = "Remove Work Plan" + work_calendar: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.remove_work_calendar", self.file, **{"work_calendar": self.file.by_id(self.work_calendar)} + ) + Data.load(self.file) + bpy.ops.bim.load_work_calendars() + return {"FINISHED"} + + +class EnableEditingWorkCalendar(bpy.types.Operator): + bl_idname = "bim.enable_editing_work_calendar" + bl_label = "Enable Editing Work Plan" + work_calendar: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkCalendarProperties + while len(props.work_calendar_attributes) > 0: + props.work_calendar_attributes.remove(0) + + data = Data.work_calendars[self.work_calendar] + + for attribute in IfcStore.get_schema().declaration_by_name("IfcWorkCalendar").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = props.work_calendar_attributes.add() + new.name = attribute.name() + new.is_null = data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if data_type == "string": + new.string_value = "" if new.is_null else data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if data[attribute.name()]: + new.enum_value = data[attribute.name()] + props.active_work_calendar_id = self.work_calendar + return {"FINISHED"} + + +class DisableEditingWorkCalendar(bpy.types.Operator): + bl_idname = "bim.disable_editing_work_calendar" + bl_label = "Disable Editing Work Calendar" + + def execute(self, context): + context.scene.BIMWorkCalendarProperties.active_work_calendar_id = 0 + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index ea599001c4..ebf065ac39 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -25,11 +25,11 @@ def updateTaskName(self, context): ifcopenshell.api.run( "sequence.edit_task", self.file, - **{"task": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}} + **{"task": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}}, ) Data.load(IfcStore.get_file()) if props.active_task_id == self.ifc_definition_id: - attribute = context.scene.BIMWorkScheduleProperties.task_attributes.get("Name") + attribute = props.task_attributes.get("Name") attribute.string_value = self.name @@ -41,7 +41,7 @@ def updateTaskIdentification(self, context): ifcopenshell.api.run( "sequence.edit_task", self.file, - **{"task": self.file.by_id(self.ifc_definition_id), "attributes": {"Identification": self.identification}} + **{"task": self.file.by_id(self.ifc_definition_id), "attributes": {"Identification": self.identification}}, ) Data.load(IfcStore.get_file()) if props.active_task_id == self.ifc_definition_id: @@ -96,7 +96,7 @@ def updateTaskTimeDateTime(self, context, startfinish): ifcopenshell.api.run( "sequence.edit_task_time", self.file, - **{"task_time": task_time, "attributes": {startfinish_key: startfinish_datetime}} + **{"task_time": task_time, "attributes": {startfinish_key: startfinish_datetime}}, ) Data.load(IfcStore.get_file()) setattr(self, startfinish, canonicalise_time(startfinish_datetime)) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index e1bb49fe11..1cc813390d 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -93,7 +93,7 @@ class BIM_PT_work_schedules(Panel): def draw_work_schedule_ui(self, work_schedule_id, work_schedule): row = self.layout.row(align=True) - row.label(text=work_schedule["Name"] or "Unnamed", icon="TEXT") + row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") if self.props.active_work_schedule_id and self.props.active_work_schedule_id == work_schedule_id: if self.props.is_editing == "WORK_SCHEDULE": @@ -107,8 +107,7 @@ class BIM_PT_work_schedules(Panel): row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id else: row.operator("bim.enable_editing_tasks", text="", icon="ACTION").work_schedule = work_schedule_id - op = row.operator("bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL") - op.work_schedule = work_schedule_id + row.operator("bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL").work_schedule = work_schedule_id row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id if self.props.active_work_schedule_id == work_schedule_id: @@ -172,70 +171,6 @@ class BIM_PT_work_schedules(Panel): row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") -class BIM_PT_work_calendars(Panel): - bl_label = "IFC Work Calendars" - bl_idname = "BIM_PT_work_calendars" - bl_options = {"DEFAULT_CLOSED"} - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - - @classmethod - def poll(cls, context): - return IfcStore.get_file() - - def draw(self, context): - if not Data.is_loaded: - Data.load(IfcStore.get_file()) - self.props = context.scene.BIMWorkCalendarProperties - row = self.layout.row(align=True) - row.label(text="{} Work Calendar Found".format(len(Data.work_calendars)), icon="TEXT") - if self.props.is_editing: - row.operator("bim.add_work_calendar", text="", icon="ADD") - row.operator("bim.disable_work_calendar_editing_ui", text="", icon="CHECKMARK") - else: - row.operator("bim.load_work_calendars", text="", icon="GREASEPENCIL") - - if self.props.is_editing: - self.layout.template_list( - "BIM_UL_work_calendars", - "", - self.props, - "work_calendars", - self.props, - "active_work_calendar_index", - ) - - if self.props.active_work_calendar_id: - self.draw_editable_ui(context) - - def draw_editable_ui(self, context): - for attribute in self.props.work_calendar_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") - - -class BIM_UL_work_calendars(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - if item: - row = layout.row(align=True) - row.label(text=item.name) - if context.scene.BIMWorkCalendarProperties.active_work_calendar_id == item.ifc_definition_id: - row.operator("bim.edit_work_calendar", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_work_calendar", text="", icon="X") - elif context.scene.BIMWorkCalendarProperties.active_work_calendar_id: - row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = item.ifc_definition_id - else: - op = row.operator("bim.enable_editing_work_calendar", text="", icon="GREASEPENCIL") - op.work_calendar = item.ifc_definition_id - row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = item.ifc_definition_id - - class BIM_UL_tasks(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: @@ -304,3 +239,67 @@ class BIM_UL_tasks(UIList): row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = item.ifc_definition_id row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id + + +class BIM_PT_work_calendars(Panel): + bl_label = "IFC Work Calendars" + bl_idname = "BIM_PT_work_calendars" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + return IfcStore.get_file() + + def draw(self, context): + if not Data.is_loaded: + Data.load(IfcStore.get_file()) + self.props = context.scene.BIMWorkCalendarProperties + row = self.layout.row(align=True) + row.label(text="{} Work Calendar Found".format(len(Data.work_calendars)), icon="TEXT") + if self.props.is_editing: + row.operator("bim.add_work_calendar", text="", icon="ADD") + row.operator("bim.disable_work_calendar_editing_ui", text="", icon="CHECKMARK") + else: + row.operator("bim.load_work_calendars", text="", icon="GREASEPENCIL") + + if self.props.is_editing: + self.layout.template_list( + "BIM_UL_work_calendars", + "", + self.props, + "work_calendars", + self.props, + "active_work_calendar_index", + ) + + if self.props.active_work_calendar_id: + self.draw_editable_ui(context) + + def draw_editable_ui(self, context): + for attribute in self.props.work_calendar_attributes: + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + + +class BIM_UL_work_calendars(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=item.name) + if context.scene.BIMWorkCalendarProperties.active_work_calendar_id == item.ifc_definition_id: + row.operator("bim.edit_work_calendar", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_work_calendar", text="", icon="X") + elif context.scene.BIMWorkCalendarProperties.active_work_calendar_id: + row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = item.ifc_definition_id + else: + op = row.operator("bim.enable_editing_work_calendar", text="", icon="GREASEPENCIL") + op.work_calendar = item.ifc_definition_id + row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = item.ifc_definition_id diff --git a/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py b/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py index f45c93e80e..d06224ab40 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py +++ b/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py @@ -6,8 +6,8 @@ class Usecase: def __init__(self, file, **settings): self.file = file self.settings = { - "related_object": None, "relating_control": None, + "related_object": None, } for key, value in settings.items(): self.settings[key] = value @@ -38,6 +38,6 @@ class Usecase: "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), "RelatedObjects": [self.settings["related_object"]], "RelatingControl": self.settings["relating_control"], - } + }, ) return controls diff --git a/src/ifcopenshell-python/ifcopenshell/api/control/unassign_control.py b/src/ifcopenshell-python/ifcopenshell/api/control/unassign_control.py new file mode 100644 index 0000000000..68f6ab673d --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/control/unassign_control.py @@ -0,0 +1,25 @@ +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "relating_control": None, + "related_object": None, + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + for rel in self.settings["related_object"].HasAssignments or []: + if not rel.is_a("IfcRelAssignsToControl") or rel.RelatingControl != self.settings["relating_control"]: + continue + if len(rel.RelatedObjects) == 1: + return self.file.remove(rel) + related_objects = list(rel.RelatedObjects) + related_objects.remove(self.settings["related_object"]) + rel.RelatedObjects = related_objects + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/data.py b/src/ifcopenshell-python/ifcopenshell/api/cost/data.py index 6a25361dea..ba83cebdf0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/data.py @@ -38,7 +38,10 @@ class Data: del data["CostValues"] del data["CostQuantities"] data["RelatedObjects"] = [] + data["Controls"] = [] for rel in cost_item.IsNestedBy: [data["RelatedObjects"].append(o.id()) for o in rel.RelatedObjects if o.is_a("IfcCostItem")] + for rel in cost_item.Controls: + [data["Controls"].append(o.id()) for o in rel.RelatedObjects or []] cls.cost_items[cost_item.id()] = data cls.is_loaded=True diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py new file mode 100644 index 0000000000..c2e409bdb9 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py @@ -0,0 +1,10 @@ +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"cost_item": None, "attributes": {}} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + for name, value in self.settings["attributes"].items(): + setattr(self.settings["cost_item"], name, value) From d3ca647cd5cc47bc1dfffcff8250f1ca73234d28 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 22 Apr 2021 10:53:25 +1000 Subject: [PATCH 58/64] Minor code review fix --- src/blenderbim/blenderbim/bim/module/cost/__init__.py | 2 ++ src/blenderbim/blenderbim/bim/module/cost/operator.py | 6 +----- src/blenderbim/blenderbim/bim/module/cost/ui.py | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index 8d420e367d..f5fb62433b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -5,9 +5,11 @@ classes = ( operator.AddCostSchedule, operator.RemoveCostSchedule, operator.EditCostSchedule, + operator.EditCostItem, operator.EnableEditingCostSchedule, operator.EnableEditingCostItems, operator.EnableEditingCostItem, + operator.DisableEditingCostItem, operator.DisableEditingCostSchedule, operator.AddCostItem, operator.AddSummaryCostItem, diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index 95e1b89008..4a4b9597e3 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -228,7 +228,7 @@ class EnableEditingCostItem(bpy.types.Operator): for attribute in IfcStore.get_schema().declaration_by_name("IfcCostItem").all_attributes(): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity": + if data_type == "entity" or isinstance(data_type, tuple): continue new = props.cost_item_attributes.add() new.name = attribute.name() @@ -237,10 +237,6 @@ class EnableEditingCostItem(bpy.types.Operator): new.data_type = data_type if data_type == "string": new.string_value = "" if new.is_null else data[attribute.name()] - elif data_type == "boolean": - new.bool_value = False if new.is_null else data[attribute.name()] - elif data_type == "integer": - new.int_value = 0 if new.is_null else data[attribute.name()] elif data_type == "enum": new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) if data[attribute.name()]: diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 22a85127b1..8523ba70ed 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -62,9 +62,6 @@ class BIM_PT_cost_schedules(Panel): if attribute.is_optional: row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") - # row = self.layout.row(align=True) - # row.label(text="X Summary Cost Items") - def draw_editable_cost_item_ui(self, cost_schedule_id): self.layout.template_list( "BIM_UL_cost_items", @@ -125,6 +122,9 @@ class BIM_UL_cost_items(UIList): if props.active_cost_item_id == item.ifc_definition_id: row.operator("bim.edit_cost_item", text="", icon="CHECKMARK") row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL") + elif props.active_cost_item_id: + row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = item.ifc_definition_id + row.operator("bim.remove_cost_item", text="", icon="X").cost_item = item.ifc_definition_id else: row.operator( "bim.enable_editing_cost_item", text="", icon="GREASEPENCIL" From 3810255f04c363bda611ad8466c4ddc0c70b6005 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 22 Apr 2021 13:56:01 +1000 Subject: [PATCH 59/64] Fix bug where syncing was not maintained across file saves and collection names were not synced --- src/blenderbim/blenderbim/bim/__init__.py | 2 - src/blenderbim/blenderbim/bim/handler.py | 40 ++++++------------- .../blenderbim/bim/module/cost/operator.py | 2 +- 3 files changed, 13 insertions(+), 31 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 121342189f..b7d1db49ac 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -173,7 +173,6 @@ if bpy is not None: bpy.app.handlers.load_post.append(handler.setDefaultProperties) bpy.app.handlers.load_post.append(handler.loadIfcStore) bpy.app.handlers.save_pre.append(handler.ensureIfcExported) - bpy.app.handlers.save_pre.append(handler.storeIdMap) bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties) @@ -200,7 +199,6 @@ if bpy is not None: bpy.utils.unregister_class(cls) bpy.app.handlers.load_post.remove(handler.setDefaultProperties) bpy.app.handlers.load_post.remove(handler.loadIfcStore) - bpy.app.handlers.save_pre.remove(handler.storeIdMap) bpy.app.handlers.save_pre.remove(handler.ensureIfcExported) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index fd87192b35..6e4f64b880 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -30,6 +30,9 @@ def name_callback(obj, data): element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) if not element.is_a("IfcRoot"): return + if element.is_a("IfcSpatialStructureElement") or (hasattr(element, "IsDecomposedBy") and element.IsDecomposedBy): + collection = obj.users_collection[0] + collection.name = obj.name element.Name = "/".join(obj.name.split("/")[1:]) AttributeData.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) @@ -69,13 +72,13 @@ def purge_module_data(): def loadIfcStore(scene): IfcStore.file = None IfcStore.schema = None - props = bpy.context.scene.BIMProperties - IfcStore.id_map = ( - {int(k): bpy.data.objects.get(v) for k, v in json.loads(props.id_map).items()} if props.id_map else {} - ) - IfcStore.guid_map = ( - {k: bpy.data.objects.get(v) for k, v in json.loads(props.guid_map).items()} if props.id_map else {} - ) + ifc_file = IfcStore.get_file() + IfcStore.get_schema() + [ + IfcStore.link_element(ifc_file.by_id(o.BIMObjectProperties.ifc_definition_id), o) + for o in bpy.data.objects + if o.BIMObjectProperties.ifc_definition_id + ] purge_module_data() @@ -85,25 +88,6 @@ def ensureIfcExported(scene): bpy.ops.export_ifc.bim("INVOKE_DEFAULT") -@persistent -def storeIdMap(scene): - try: - bpy.context.scene.BIMProperties.id_map = json.dumps({k: v.name for k, v in IfcStore.id_map.items()}) - bpy.context.scene.BIMProperties.guid_map = json.dumps({k: v.name for k, v in IfcStore.guid_map.items()}) - except: - # Regenerate maps. Is there a better solution for this? It seems fragile. - file = IfcStore.get_file() - IfcStore.id_map = { - o.ifc_definition_id: o.name for o in bpy.data.objects if o.BIMObjectProperties.ifc_definition_id - } - IfcStore.guid_map = { - file.by_id(i).GlobalId: n for i, n in IfcStore.id_map.items() if file.by_id(i).is_a("IfcRoot") - } - # Then attempt to store it again - bpy.context.scene.BIMProperties.id_map = json.dumps({k: v.name for k, v in IfcStore.id_map.items()}) - bpy.context.scene.BIMProperties.guid_map = json.dumps({k: v.name for k, v in IfcStore.guid_map.items()}) - - def get_application(ifc): version = get_application_version() for element in ifc.by_type("IfcApplication"): @@ -176,12 +160,12 @@ def create_application_organisation(ifc): @persistent def setDefaultProperties(scene): ifcopenshell.api.owner.settings.get_person = ( - lambda ifc : ifc.by_id(int(bpy.context.scene.BIMOwnerProperties.user_person)) + lambda ifc: ifc.by_id(int(bpy.context.scene.BIMOwnerProperties.user_person)) if bpy.context.scene.BIMOwnerProperties.user_person else None ) ifcopenshell.api.owner.settings.get_organisation = ( - lambda ifc : ifc.by_id(int(bpy.context.scene.BIMOwnerProperties.user_organisation)) + lambda ifc: ifc.by_id(int(bpy.context.scene.BIMOwnerProperties.user_organisation)) if bpy.context.scene.BIMOwnerProperties.user_organisation else None ) diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index 4a4b9597e3..26c31f0c03 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -112,8 +112,8 @@ class EnableEditingCostItems(bpy.types.Operator): def create_new_cost_item_li(self, related_object_id, level_index): cost_item = Data.cost_items[related_object_id] new = self.props.cost_items.add() - new.name = cost_item["Name"] or "Unnamed" new.ifc_definition_id = related_object_id + new.name = cost_item["Name"] or "Unnamed" new.is_expanded = related_object_id not in self.contracted_cost_items new.level_index = level_index if cost_item["RelatedObjects"]: From dca2e58586a356ebf87f0adbf0a8fe54f8a74cdd Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 22 Apr 2021 14:00:18 +1000 Subject: [PATCH 60/64] Fix bug where you shouldn't be able to reassign classes of non-rooted elements --- src/blenderbim/blenderbim/bim/module/root/ui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/root/ui.py b/src/blenderbim/blenderbim/bim/module/root/ui.py index 6991dbca58..bad4a43fc6 100644 --- a/src/blenderbim/blenderbim/bim/module/root/ui.py +++ b/src/blenderbim/blenderbim/bim/module/root/ui.py @@ -41,7 +41,8 @@ class BIM_PT_class(Panel): row.label(text=name) row.operator("bim.copy_class", icon="DUPLICATE", text="") row.operator("bim.unlink_object", icon="UNLINKED", text="") - row.operator("bim.enable_reassign_class", icon="GREASEPENCIL", text="") + if IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcRoot"): + row.operator("bim.enable_reassign_class", icon="GREASEPENCIL", text="") if context.selected_objects: row.operator("bim.unassign_class", icon="X", text="") else: From ee0f7f16773d74d2ff612f956a8b37b7bb488c96 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 22 Apr 2021 15:44:58 +1000 Subject: [PATCH 61/64] You can now import an IFC work schedule from a P6 XML export --- .../bim/module/sequence/__init__.py | 7 + .../bim/module/sequence/operator.py | 22 +++ src/blenderbim/p6_to_ifc.py | 129 ------------------ src/ifcp6/ifcp6/p62ifc.py | 114 ++++++++++++++++ 4 files changed, 143 insertions(+), 129 deletions(-) delete mode 100644 src/blenderbim/p6_to_ifc.py create mode 100644 src/ifcp6/ifcp6/p62ifc.py diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index df151b8ef5..9184ae5ab8 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -41,6 +41,7 @@ classes = ( operator.AssignProduct, operator.UnassignProduct, operator.GenerateGanttChart, + operator.ImportP6, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, @@ -56,13 +57,19 @@ classes = ( ) +def menu_func_import(self, context): + self.layout.operator(operator.ImportP6.bl_idname, text="P6 (.xml)") + + def register(): bpy.types.Scene.BIMWorkPlanProperties = bpy.props.PointerProperty(type=prop.BIMWorkPlanProperties) bpy.types.Scene.BIMWorkScheduleProperties = bpy.props.PointerProperty(type=prop.BIMWorkScheduleProperties) bpy.types.Scene.BIMWorkCalendarProperties = bpy.props.PointerProperty(type=prop.BIMWorkCalendarProperties) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) def unregister(): del bpy.types.Scene.BIMWorkPlanProperties del bpy.types.Scene.BIMWorkScheduleProperties del bpy.types.Scene.BIMWorkCalendarProperties + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index dcb00ca14c..3e49d5a28e 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1,12 +1,14 @@ import os import bpy import json +import time import pystache import webbrowser import ifcopenshell.api from datetime import datetime from dateutil import parser from blenderbim.bim.ifc import IfcStore +from bpy_extras.io_utils import ImportHelper from ifcopenshell.api.sequence.data import Data @@ -808,3 +810,23 @@ class DisableEditingWorkCalendar(bpy.types.Operator): def execute(self, context): context.scene.BIMWorkCalendarProperties.active_work_calendar_id = 0 return {"FINISHED"} + + +class ImportP6(bpy.types.Operator, ImportHelper): + bl_idname = "import_p6.bim" + bl_label = "Import P6" + filename_ext = ".xml" + filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"}) + + def execute(self, context): + from ifcp6.p62ifc import P62Ifc + self.file = IfcStore.get_file() + start = time.time() + p62ifc = P62Ifc() + p62ifc.xml = self.filepath + p62ifc.file = self.file + p62ifc.work_plan = self.file.by_type("IfcWorkPlan")[0] + p62ifc.execute() + Data.load(IfcStore.get_file()) + print("Import finished in {:.2f} seconds".format(time.time() - start)) + return {"FINISHED"} diff --git a/src/blenderbim/p6_to_ifc.py b/src/blenderbim/p6_to_ifc.py deleted file mode 100644 index 68ab8bd74f..0000000000 --- a/src/blenderbim/p6_to_ifc.py +++ /dev/null @@ -1,129 +0,0 @@ -import ifcopenshell -import ifcopenshell.util.date -import xml.etree.ElementTree as ET - - -class P6ToIfc: - def __init__(self): - self.project = {} - self.wbs = {} - - def execute(self): - self.parse_xml() - self.create_ifc() - - def parse_xml(self): - tree = ET.parse("p6.xml") - ns = {"pr": "http://xmlns.oracle.com/Primavera/P6/V19.12/API/BusinessObjects"} - root = tree.getroot() - project = root.find("pr:Project", ns) - self.project["Name"] = project.find("pr:Name", ns).text - - for wbs in project.findall("pr:WBS", ns): - self.wbs[wbs.find("pr:ObjectId", ns).text] = { - "Name": wbs.find("pr:Name", ns).text, - "ParentObjectId": wbs.find("pr:ParentObjectId", ns).text, - "ifc": None, - "rel": None, - "activities": [], - } - - for activity in project.findall("pr:Activity", ns): - self.wbs[activity.find("pr:WBSObjectId", ns).text]["activities"].append( - { - "Name": activity.find("pr:Name", ns).text, - "Identification": activity.find("pr:Id", ns).text, - "StartDate": activity.find("pr:StartDate", ns).text, - "FinishDate": activity.find("pr:FinishDate", ns).text, - "ifc": None, - } - ) - - def get_wbs(self, wbs): - return {"Name": wbs.find("pr:Name", ns).text, "subtasks": []} - - def create_ifc(self): - self.file = ifcopenshell.file(schema="IFC4") - # self.file = ifcopenshell.file(schema="IFC2X3") - self.root = self.file.create_entity( - "IfcTask", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.project["Name"]} - ) - self.root_rel = self.create_rel_nests(self.root) - - for wbs in self.wbs.values(): - wbs["ifc"] = self.file.create_entity( - "IfcTask", **{"GlobalId": ifcopenshell.guid.new(), "Name": wbs["Name"]} - ) - if wbs["ParentObjectId"]: - parent_wbs = self.wbs[wbs["ParentObjectId"]] - if not parent_wbs["rel"]: - parent_wbs["rel"] = self.create_rel_nests(parent_wbs["ifc"]) - rel = parent_wbs["rel"] - else: - rel = self.root_rel - self.append_to_rel(rel, wbs["ifc"]) - - for activity in wbs["activities"]: - if not wbs["rel"]: - wbs["rel"] = self.create_rel_nests(wbs["ifc"]) - - if self.file.schema == "IFC2X3": - activity["TimeForTask"] = self.file.create_entity( - "IfcScheduleTimeControl", - **{ - "ScheduleStart": self.file.create_entity( - "IfcCalendarDate", - **ifcopenshell.util.date.datetime2ifc(activity["StartDate"], "IfcCalendarDate") - ), - "ScheduleFinish": self.file.create_entity( - "IfcCalendarDate", - **ifcopenshell.util.date.datetime2ifc(activity["FinishDate"], "IfcCalendarDate") - ), - } - ) - else: - activity["TaskTime"] = self.file.create_entity( - "IfcTaskTime", - **{"ScheduleStart": activity["StartDate"], "ScheduleFinish": activity["FinishDate"]} - ) - - is_milestone = activity["StartDate"] == activity["FinishDate"] - activity["ifc"] = self.file.create_entity( - "IfcTask", - **{ - "GlobalId": ifcopenshell.guid.new(), - "Name": activity["Name"], - "Identification": activity["Identification"], - "IsMilestone": is_milestone, - } - ) - - if self.file.schema == "IFC2X3": - # Invalid, but reading the IFC2X3 docs gives me a headache - self.file.create_entity( - "IfcRelAssignsTasks", - **{ - "GlobalId": ifcopenshell.guid.new(), - "RelatedObjects": [activity["ifc"]], - "TimeForTask": activity["TimeForTask"], - } - ) - else: - activity["ifc"].TaskTime = activity["TaskTime"] - - self.append_to_rel(wbs["rel"], activity["ifc"]) - self.file.write("p6.ifc") - - def create_rel_nests(self, relating_object): - return self.file.create_entity( - "IfcRelNests", **{"GlobalId": ifcopenshell.guid.new(), "RelatingObject": relating_object} - ) - - def append_to_rel(self, rel, element): - related_objects = list(rel.RelatedObjects or []) - related_objects.append(element) - rel.RelatedObjects = related_objects - - -p6_to_ifc = P6ToIfc() -p6_to_ifc.execute() diff --git a/src/ifcp6/ifcp6/p62ifc.py b/src/ifcp6/ifcp6/p62ifc.py new file mode 100644 index 0000000000..8e9a2deddd --- /dev/null +++ b/src/ifcp6/ifcp6/p62ifc.py @@ -0,0 +1,114 @@ +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.date +import xml.etree.ElementTree as ET +from datetime import datetime + + +class P62Ifc: + def __init__(self): + self.xml = None + self.file = None + self.work_plan = None + self.project = {} + self.wbs = {} + self.activity = {} + + def execute(self): + self.parse_xml() + self.create_ifc() + + def parse_xml(self): + tree = ET.parse(self.xml) + ns = {"pr": "http://xmlns.oracle.com/Primavera/P6/V19.12/API/BusinessObjects"} + root = tree.getroot() + project = root.find("pr:Project", ns) + self.project["Name"] = project.find("pr:Name", ns).text + + for wbs in project.findall("pr:WBS", ns): + self.wbs[wbs.find("pr:ObjectId", ns).text] = { + "Name": wbs.find("pr:Name", ns).text, + "Code": wbs.find("pr:Code", ns).text, + "ParentObjectId": wbs.find("pr:ParentObjectId", ns).text, + "ifc": None, + "rel": None, + "activities": [], + } + + for activity in project.findall("pr:Activity", ns): + self.wbs[activity.find("pr:WBSObjectId", ns).text]["activities"].append( + { + "Name": activity.find("pr:Name", ns).text, + "Identification": activity.find("pr:Id", ns).text, + "StartDate": datetime.fromisoformat(activity.find("pr:StartDate", ns).text), + "FinishDate": datetime.fromisoformat(activity.find("pr:FinishDate", ns).text), + "Status": activity.find("pr:Status", ns).text, + "ifc": None, + } + ) + + def get_wbs(self, wbs): + return {"Name": wbs.find("pr:Name", ns).text, "subtasks": []} + + def create_ifc(self): + if not self.file: + self.file = self.create_boilerplate_ifc() + work_schedule = self.create_work_schedule() + self.create_tasks(work_schedule) + + def create_work_schedule(self): + return ifcopenshell.api.run( + "sequence.add_work_schedule", self.file, name=self.project["Name"], work_plan=self.work_plan + ) + + def create_tasks(self, work_schedule): + for wbs in self.wbs.values(): + self.create_task_from_wbs(wbs, work_schedule) + + def create_task_from_wbs(self, wbs, work_schedule): + wbs["ifc"] = ifcopenshell.api.run( + "sequence.add_task", + self.file, + work_schedule=None if wbs["ParentObjectId"] else work_schedule, + parent_task=self.wbs[wbs["ParentObjectId"]]["ifc"] if wbs["ParentObjectId"] else None, + ) + ifcopenshell.api.run( + "sequence.edit_task", + self.file, + task=wbs["ifc"], + attributes={"Name": wbs["Name"], "Identification": wbs["Code"]}, + ) + for activity in wbs["activities"]: + self.create_task_from_activity(activity, wbs, work_schedule) + + def create_task_from_activity(self, activity, wbs, work_schedule): + activity["ifc"] = ifcopenshell.api.run( + "sequence.add_task", + self.file, + parent_task=wbs["ifc"], + ) + ifcopenshell.api.run( + "sequence.edit_task", + self.file, + task=activity["ifc"], + attributes={ + "Name": activity["Name"], + "Identification": activity["Identification"], + "Status": activity["Status"], + "IsMilestone": activity["StartDate"] == activity["FinishDate"], + }, + ) + task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=activity["ifc"]) + ifcopenshell.api.run( + "sequence.edit_task_time", + self.file, + task_time=task_time, + attributes={ + "ScheduleStart": activity["StartDate"], + "ScheduleFinish": activity["FinishDate"], + }, + ) + + def create_boilerplate_ifc(self): + self.file = ifcopenshell.file(schema="IFC4") + self.work_plan = self.file.create_entity("IfcWorkPlan") From 761b4c035f83668bb45fe998063572e3cb2f5505 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 22 Apr 2021 18:34:45 +1000 Subject: [PATCH 62/64] Optimisations for construction sequencing task trees where there are thousands of tasks --- .../bim/module/sequence/__init__.py | 4 ++ .../bim/module/sequence/operator.py | 62 ++++++++++++++----- .../blenderbim/bim/module/sequence/prop.py | 23 +++++-- .../blenderbim/bim/module/sequence/ui.py | 7 ++- 4 files changed, 71 insertions(+), 25 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 9184ae5ab8..83777c9476 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -42,10 +42,12 @@ classes = ( operator.UnassignProduct, operator.GenerateGanttChart, operator.ImportP6, + operator.LoadTaskProperties, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, prop.BIMWorkScheduleProperties, + prop.BIMTaskTreeProperties, prop.WorkCalendar, prop.BIMWorkCalendarProperties, ui.BIM_PT_work_plans, @@ -64,6 +66,7 @@ def menu_func_import(self, context): def register(): 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) bpy.types.Scene.BIMWorkCalendarProperties = bpy.props.PointerProperty(type=prop.BIMWorkCalendarProperties) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) @@ -71,5 +74,6 @@ def register(): def unregister(): del bpy.types.Scene.BIMWorkPlanProperties del bpy.types.Scene.BIMWorkScheduleProperties + del bpy.types.Scene.BIMTaskTreeProperties del bpy.types.Scene.BIMWorkCalendarProperties bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 3e49d5a28e..68e5659948 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -223,32 +223,22 @@ class EnableEditingTasks(bpy.types.Operator): def execute(self, context): self.props = context.scene.BIMWorkScheduleProperties + self.tprops = context.scene.BIMTaskTreeProperties self.props.active_work_schedule_id = self.work_schedule - while len(self.props.tasks) > 0: - self.props.tasks.remove(0) + while len(self.tprops.tasks) > 0: + self.tprops.tasks.remove(0) self.contracted_tasks = json.loads(self.props.contracted_tasks) for related_object_id in Data.work_schedules[self.work_schedule]["RelatedObjects"]: self.create_new_task_li(related_object_id, 0) + bpy.ops.bim.load_task_properties() self.props.is_editing = "TASKS" return {"FINISHED"} def create_new_task_li(self, related_object_id, level_index): task = Data.tasks[related_object_id] - new = self.props.tasks.add() + new = self.tprops.tasks.add() new.ifc_definition_id = related_object_id - new.name = task["Name"] or "Unnamed" - new.identification = task["Identification"] or "XXX" - if task["TaskTime"]: - task_time = Data.task_times[task["TaskTime"]] - new.start = self.canonicalise_time(task_time["ScheduleStart"]) - new.finish = self.canonicalise_time(task_time["ScheduleFinish"]) - # TODO: duration - new.duration = "-" - else: - new.start = "-" - new.finish = "-" - new.duration = "-" new.is_expanded = related_object_id not in self.contracted_tasks new.level_index = level_index if task["RelatedObjects"]: @@ -258,6 +248,38 @@ class EnableEditingTasks(bpy.types.Operator): self.create_new_task_li(related_object_id, level_index + 1) return {"FINISHED"} + +class LoadTaskProperties(bpy.types.Operator): + bl_idname = "bim.load_task_properties" + bl_label = "Load Task Properties" + task: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMWorkScheduleProperties + self.tprops = context.scene.BIMTaskTreeProperties + self.props.is_task_update_enabled = False + for item in self.tprops.tasks: + if self.task and item.ifc_definition_id != self.task: + continue + task = Data.tasks[item.ifc_definition_id] + item.name = task["Name"] or "Unnamed" + item.identification = task["Identification"] or "XXX" + if self.props.active_task_id: + item.is_predecessor = self.props.active_task_id in task["IsPredecessorTo"] + item.is_successor = self.props.active_task_id in task["IsSuccessorFrom"] + if task["TaskTime"]: + task_time = Data.task_times[task["TaskTime"]] + item.start = self.canonicalise_time(task_time["ScheduleStart"]) + item.finish = self.canonicalise_time(task_time["ScheduleFinish"]) + # TODO: duration + item.duration = "-" + else: + item.start = "-" + item.finish = "-" + item.duration = "-" + self.props.is_task_update_enabled = True + return {"FINISHED"} + def canonicalise_time(self, time): if not time: return "-" @@ -400,6 +422,7 @@ class EnableEditingTaskTime(bpy.types.Operator): new.enum_value = data[attribute.name()] props.active_task_time_id = task_time_id props.active_task_id = self.task + bpy.ops.bim.load_task_properties() return {"FINISHED"} def add_task_time(self): @@ -448,7 +471,7 @@ class EditTaskTime(bpy.types.Operator): ) Data.load(IfcStore.get_file()) bpy.ops.bim.disable_editing_task_time() - bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) + bpy.ops.bim.load_task_properties(task=props.active_task_id) return {"FINISHED"} def convert_strings_to_date_times(self, attributes): @@ -498,6 +521,7 @@ class EnableEditingTask(bpy.types.Operator): if data[attribute.name()]: new.enum_value = data[attribute.name()] props.active_task_id = self.task + bpy.ops.bim.load_task_properties() return {"FINISHED"} @@ -535,7 +559,7 @@ class EditTask(bpy.types.Operator): ) Data.load(IfcStore.get_file()) bpy.ops.bim.disable_editing_task() - bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) + bpy.ops.bim.load_task_properties(task=props.active_task_id) return {"FINISHED"} @@ -554,6 +578,7 @@ class AssignPredecessor(bpy.types.Operator): related_process=IfcStore.get_file().by_id(props.active_task_id), ) Data.load(self.file) + bpy.ops.bim.load_task_properties(task=self.task) return {"FINISHED"} @@ -572,6 +597,7 @@ class AssignSuccessor(bpy.types.Operator): related_process=IfcStore.get_file().by_id(self.task), ) Data.load(self.file) + bpy.ops.bim.load_task_properties(task=self.task) return {"FINISHED"} @@ -590,6 +616,7 @@ class UnassignPredecessor(bpy.types.Operator): related_process=IfcStore.get_file().by_id(props.active_task_id), ) Data.load(self.file) + bpy.ops.bim.load_task_properties(task=self.task) return {"FINISHED"} @@ -608,6 +635,7 @@ class UnassignSuccessor(bpy.types.Operator): related_process=self.file.by_id(self.task), ) Data.load(self.file) + bpy.ops.bim.load_task_properties(task=self.task) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index ebf065ac39..7416ae88a7 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -18,10 +18,10 @@ from bpy.props import ( def updateTaskName(self, context): - if self.name == "Unnamed": + props = context.scene.BIMWorkScheduleProperties + if not props.is_task_update_enabled or self.name == "Unnamed": return self.file = IfcStore.get_file() - props = context.scene.BIMWorkScheduleProperties ifcopenshell.api.run( "sequence.edit_task", self.file, @@ -34,10 +34,10 @@ def updateTaskName(self, context): def updateTaskIdentification(self, context): - if self.identification == "X": + props = context.scene.BIMWorkScheduleProperties + if not props.is_task_update_enabled or self.identification == "XXX": return self.file = IfcStore.get_file() - props = context.scene.BIMWorkScheduleProperties ifcopenshell.api.run( "sequence.edit_task", self.file, @@ -58,6 +58,11 @@ def updateTaskTimeFinish(self, context): def updateTaskTimeDateTime(self, context, startfinish): + props = context.scene.BIMWorkScheduleProperties + + if not props.is_task_update_enabled: + return + def canonicalise_time(time): if not time: return "-" @@ -112,6 +117,8 @@ class Task(PropertyGroup): duration: StringProperty(name="Duration") start: StringProperty(name="Start", update=updateTaskTimeStart) finish: StringProperty(name="Finish", update=updateTaskTimeFinish) + is_predecessor: BoolProperty(name="Is Predecessor") + is_successor: BoolProperty(name="Is Successor") class WorkPlan(PropertyGroup): @@ -132,7 +139,6 @@ class BIMWorkScheduleProperties(PropertyGroup): is_editing: StringProperty(name="Is Editing") active_work_schedule_index: IntProperty(name="Active Work Schedules Index") active_work_schedule_id: IntProperty(name="Active Work Schedules Id") - tasks: CollectionProperty(name="Tasks", type=Task) active_task_index: IntProperty(name="Active Task Index") active_task_id: IntProperty(name="Active Task Id") task_attributes: CollectionProperty(name="Task Attributes", type=Attribute) @@ -140,6 +146,13 @@ class BIMWorkScheduleProperties(PropertyGroup): active_task_time_id: IntProperty(name="Active Task Id") task_time_attributes: CollectionProperty(name="Task Time Attributes", type=Attribute) contracted_tasks: StringProperty(name="Contracted Task Items", default="[]") + is_task_update_enabled: BoolProperty(name="Is Task Update Enabled", default=True) + + +class BIMTaskTreeProperties(PropertyGroup): + # This belongs by itself for performance reasons. + # In Blender if you add thousands of tasks it makes other property access in the same group really slow. + tasks: CollectionProperty(name="Tasks", type=Task) class WorkCalendar(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 1cc813390d..e09e356ec1 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -81,6 +81,7 @@ class BIM_PT_work_schedules(Panel): def draw(self, context): self.props = context.scene.BIMWorkScheduleProperties + self.tprops = context.scene.BIMTaskTreeProperties if not Data.is_loaded: Data.load(IfcStore.get_file()) @@ -130,7 +131,7 @@ class BIM_PT_work_schedules(Panel): self.layout.template_list( "BIM_UL_tasks", "", - self.props, + self.tprops, "tasks", self.props, "active_task_index", @@ -214,7 +215,7 @@ class BIM_UL_tasks(UIList): row.operator("bim.edit_task", text="", icon="CHECKMARK") row.operator("bim.disable_editing_task", text="", icon="CANCEL") elif props.active_task_id: - if props.active_task_id in Data.tasks[item.ifc_definition_id]["IsPredecessorTo"]: + if item.is_predecessor: row.operator( "bim.unassign_predecessor", text="", icon="BACK", emboss=False ).task = item.ifc_definition_id @@ -223,7 +224,7 @@ class BIM_UL_tasks(UIList): "bim.assign_predecessor", text="", icon="TRACKING_BACKWARDS", emboss=False ).task = item.ifc_definition_id - if props.active_task_id in Data.tasks[item.ifc_definition_id]["IsSuccessorFrom"]: + if item.is_successor: row.operator( "bim.unassign_successor", text="", icon="FORWARD", emboss=False ).task = item.ifc_definition_id From 8da92557cca0f8d69560f01912a2465d4b385223 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 22 Apr 2021 18:37:03 +1000 Subject: [PATCH 63/64] Fix bug where deletion syncing may fail on freshly created objects --- src/blenderbim/blenderbim/bim/export_ifc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index 030ad57329..c0de745658 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -77,19 +77,19 @@ class IfcExporter: self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) to_delete = [] - for guid, obj in IfcStore.guid_map.items(): + for ifc_definition_id, obj in IfcStore.id_map.items(): try: self.sync_object_placement(obj) - self.sync_object_container(guid, obj) + self.sync_object_container(ifc_definition_id, obj) except ReferenceError: pass # The object is likely deleted if self.should_delete(obj): - to_delete.append(guid) + to_delete.append(ifc_definition_id) SpatialData.purge() - for guid in to_delete: - product = self.file.by_id(guid) + for ifc_definition_id in to_delete: + product = self.file.by_id(ifc_definition_id) IfcStore.unlink_element(product) ifcopenshell.api.run("root.remove_product", self.file, **{"product": product}) From 18d7193efc821e1d76592975ae505a5ec80844a8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 22 Apr 2021 18:37:41 +1000 Subject: [PATCH 64/64] Fix bug where a new Blender file would remember old pset templates and libraries --- src/blenderbim/blenderbim/bim/handler.py | 3 +-- src/blenderbim/blenderbim/bim/ifc.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 6e4f64b880..bc3f805109 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -70,8 +70,7 @@ def purge_module_data(): @persistent def loadIfcStore(scene): - IfcStore.file = None - IfcStore.schema = None + IfcStore.purge() ifc_file = IfcStore.get_file() IfcStore.get_schema() [ diff --git a/src/blenderbim/blenderbim/bim/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py index 7a5833a852..9e1e4ec54b 100644 --- a/src/blenderbim/blenderbim/bim/ifc.py +++ b/src/blenderbim/blenderbim/bim/ifc.py @@ -15,6 +15,19 @@ class IfcStore: library_path = "" library_file = None + @staticmethod + def purge(): + IfcStore.path = "" + IfcStore.file = None + IfcStore.schema = None + IfcStore.id_map = {} + IfcStore.guid_map = {} + IfcStore.edited_objs = set() + IfcStore.pset_template_path = "" + IfcStore.pset_template_file = None + IfcStore.library_path = "" + IfcStore.library_file = None + @staticmethod def get_file(): if IfcStore.file is None: