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/IfcOpenShell-fork.zip b/IfcOpenShell-fork.zip deleted file mode 100644 index 6ecc329e75..0000000000 Binary files a/IfcOpenShell-fork.zip and /dev/null differ diff --git a/README.md b/README.md index 87eba9c96a..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 @@ -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: diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 4ee574bbe5..4806b2e195 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -177,6 +177,27 @@ 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 + + # Provides duration parsing for construction sequencing + mkdir dist/working + cd dist/working && wget https://files.pythonhosted.org/packages/b1/80/fb8c13a4cd38eb5021dc3741a9e588e4d1de88d895c1910c6fc8a08b7a70/isodate-0.6.0.tar.gz + cd dist/working && tar -xzvf isodate* + cp -r dist/working/isodate-0.6.0/src/isodate 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 + cp dist/working/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/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 121342189f..65618371a3 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -24,8 +24,9 @@ if bpy is not None: "aggregate": None, "geometry": None, "cobie": None, - "sequence": None, + "resource": None, "cost": None, + "sequence": None, "group": None, "structural": None, "material": None, @@ -173,7 +174,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 +200,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/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index 5290b1215f..c0de745658 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 @@ -46,9 +47,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 @@ -79,16 +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) - except: - pass - if self.should_delete(guid, obj): - to_delete.append(guid) + self.sync_object_container(ifc_definition_id, obj) + except ReferenceError: + pass # The object is likely deleted + if self.should_delete(obj): + to_delete.append(ifc_definition_id) - for guid in to_delete: - product = self.file.by_id(guid) + SpatialData.purge() + + 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}) @@ -104,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 @@ -124,7 +126,36 @@ 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 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"): + 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/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 5f462f8a5e..9de63c1916 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -6,20 +6,22 @@ import ifcopenshell.api.owner.settings from bpy.app.handlers import persistent from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.attribute.data import Data as AttributeData +from ifcopenshell.api.type.data import Data as TypeData 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 != "EDIT" + 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): @@ -29,6 +31,11 @@ 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 + if element.is_a("IfcTypeProduct"): + TypeData.purge() element.Name = "/".join(obj.name.split("/")[1:]) AttributeData.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) @@ -52,52 +59,35 @@ 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): - 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 {} - ) + IfcStore.purge() + 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() @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) - - -@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()}) + bpy.ops.export_ifc.bim("INVOKE_DEFAULT") def get_application(ifc): @@ -172,12 +162,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/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py index 371e05e51b..9e1e4ec54b 100644 --- a/src/blenderbim/blenderbim/bim/ifc.py +++ b/src/blenderbim/blenderbim/bim/ifc.py @@ -12,6 +12,21 @@ class IfcStore: edited_objs = set() pset_template_path = "" pset_template_file = None + 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(): diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index eb7a298c4f..9d1b5a5ae9 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: @@ -1003,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"): @@ -1084,8 +1082,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 +1125,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 +1173,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 +1185,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 +1417,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/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index ef943fad44..27a62fd4d2 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -12,37 +12,41 @@ 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: + if not collection.children.get(spatial_collection.name): + 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..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="") + 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/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/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, diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index b5ff702d02..11745bdf7a 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"} @@ -316,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: @@ -407,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 = {} @@ -540,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 ffa7c5d87a..4bd7093d69 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(): @@ -59,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/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/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index fb41b6e9ed..d05306f3ce 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -5,8 +5,19 @@ classes = ( operator.CreateProject, operator.CreateProjectLibrary, operator.ValidateIfcFile, + operator.SelectLibraryFile, + operator.ChangeLibraryElement, + operator.RefreshLibrary, + operator.RewindLibrary, + operator.AssignLibraryDeclaration, + operator.UnassignLibraryDeclaration, + operator.SaveLibraryFile, + operator.AppendLibraryElement, + 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..a1c279c71c 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -4,8 +4,7 @@ import ifcopenshell import ifcopenshell.api import bpy from blenderbim.bim.ifc import IfcStore - -# from ifcopenshell.api.project.data import Data +from blenderbim.bim import import_ifc class CreateProject(bpy.types.Operator): @@ -88,3 +87,171 @@ 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() + 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() + 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"} + + +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"} + + +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/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py index d90ceea623..3cb43b6128 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,15 @@ 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): 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..82006ba3e1 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,68 @@ 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_options = {"DEFAULT_CLOSED"} + 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") + 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() + + 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) + if ( + item.ifc_definition_id + and IfcStore.library_file.schema != "IFC2X3" + and IfcStore.library_file.by_type("IfcProjectLibrary") + ): + 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/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..83b134c399 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( @@ -58,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 diff --git a/src/blenderbim/blenderbim/bim/module/root/operator.py b/src/blenderbim/blenderbim/bim/module/root/operator.py index 0af5b8bddb..19e6f07a7d 100644 --- a/src/blenderbim/blenderbim/bim/module/root/operator.py +++ b/src/blenderbim/blenderbim/bim/module/root/operator.py @@ -76,6 +76,7 @@ class ReassignClass(bpy.types.Operator): class AssignClass(bpy.types.Operator): bl_idname = "bim.assign_class" bl_label = "Assign IFC Class" + bl_options = {'REGISTER', 'UNDO'} obj: bpy.props.StringProperty() ifc_class: bpy.props.StringProperty() predefined_type: bpy.props.StringProperty() @@ -153,6 +154,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) 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/root/ui.py b/src/blenderbim/blenderbim/bim/module/root/ui.py index d30a7ffe40..bad4a43fc6 100644 --- a/src/blenderbim/blenderbim/bim/module/root/ui.py +++ b/src/blenderbim/blenderbim/bim/module/root/ui.py @@ -39,9 +39,10 @@ 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 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: diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 16af753c26..379532fa0c 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -2,55 +2,99 @@ import bpy from . import ui, prop, operator classes = ( - operator.LoadWorkPlans, - operator.DisableWorkPlanEditingUI, operator.AddWorkPlan, operator.EditWorkPlan, operator.RemoveWorkPlan, operator.EnableEditingWorkPlan, operator.DisableEditingWorkPlan, - operator.LoadWorkSchedules, - operator.DisableWorkScheduleEditingUI, + operator.EnableEditingWorkPlanSchedules, + operator.AssignWorkSchedule, + operator.UnassignWorkSchedule, operator.AddWorkSchedule, operator.EditWorkSchedule, operator.RemoveWorkSchedule, operator.EnableEditingWorkSchedule, + operator.EnableEditingTasks, operator.DisableEditingWorkSchedule, - operator.LoadTasks, - operator.DisableTaskEditingUI, - operator.LoadWorkCalendars, - operator.DisableWorkCalendarEditingUI, + operator.DisableEditingSequence, + operator.EditSequenceAttributes, + operator.EditSequenceTimeLag, + operator.EnableEditingSequenceAttributes, + operator.EnableEditingSequenceTimeLag, operator.AddWorkCalendar, operator.EditWorkCalendar, + operator.EditWorkTime, operator.RemoveWorkCalendar, + operator.RemoveWorkTime, + operator.UnassignRecurrencePattern, + operator.UnassignLagTime, + operator.RemoveTimePeriod, operator.EnableEditingWorkCalendar, + operator.EnableEditingWorkTime, + operator.EnableEditingWorkCalendarTimes, operator.DisableEditingWorkCalendar, + operator.DisableEditingWorkTime, + operator.AddWorkTime, + operator.AssignLagTime, + operator.AssignRecurrencePattern, + operator.AddTimePeriod, + operator.AddTask, + operator.AddSummaryTask, + operator.ExpandTask, + operator.ContractTask, + operator.RemoveTask, + operator.EnableEditingTask, + operator.DisableEditingTask, + operator.DisableEditingTaskTime, + operator.EditTask, + operator.AssignPredecessor, + operator.AssignSuccessor, + operator.UnassignPredecessor, + operator.UnassignSuccessor, + operator.EnableEditingTaskTime, + operator.EnableEditingTaskCalendar, + operator.EnableEditingTaskSequence, + operator.EditTaskTime, + operator.EditTaskCalendar, + operator.RemoveTaskCalendar, + operator.AssignProduct, + operator.UnassignProduct, + operator.GenerateGanttChart, + operator.ImportP6, + operator.LoadTaskProperties, + operator.SelectTaskRelatedProducts, + operator.VisualiseWorkScheduleDate, + operator.VisualiseWorkScheduleDateRange, prop.WorkPlan, prop.BIMWorkPlanProperties, - prop.WorkSchedule, - prop.BIMWorkScheduleProperties, - prop.WorkCalendar, - prop.BIMWorkCalendarProperties, prop.Task, - prop.BIMTaskProperties, + prop.BIMWorkScheduleProperties, + prop.BIMTaskTreeProperties, + prop.WorkCalendar, + prop.RecurrenceComponent, + 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_PT_tasks, ui.BIM_UL_tasks, ) + +def menu_func_import(self, context): + self.layout.operator(operator.ImportP6.bl_idname, text="P6 (.xml)") + + 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.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) + def unregister(): - del bpy.types.Scene.BIMTaskProperties 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 f78fbf5513..88cdece0e5 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1,36 +1,21 @@ +import os import bpy import json +import time +import isodate +import pystache +import webbrowser import ifcopenshell.api +import ifcopenshell.util.date +import blenderbim.bim.module.sequence.helper as helper +from datetime import datetime +from datetime import timedelta +from dateutil import parser from blenderbim.bim.ifc import IfcStore +from bpy_extras.io_utils import ImportHelper from ifcopenshell.api.sequence.data import Data -class LoadWorkPlans(bpy.types.Operator): - bl_idname = "bim.load_work_plans" - bl_label = "Load Work Plans" - - def execute(self, context): - props = context.scene.BIMWorkPlanProperties - while len(props.work_plans) > 0: - props.work_plans.remove(0) - for ifc_definition_id, work_plan in Data.work_plans.items(): - new = props.work_plans.add() - new.ifc_definition_id = ifc_definition_id - new.name = work_plan["Name"] or "Unnamed" - props.is_editing = True - bpy.ops.bim.disable_editing_work_plan() - return {"FINISHED"} - - -class DisableWorkPlanEditingUI(bpy.types.Operator): - bl_idname = "bim.disable_work_plan_editing_ui" - bl_label = "Disable WorkPlan Editing UI" - - def execute(self, context): - context.scene.BIMWorkPlanProperties.is_editing = False - return {"FINISHED"} - - class AddWorkPlan(bpy.types.Operator): bl_idname = "bim.add_work_plan" bl_label = "Add Work Plan" @@ -38,7 +23,6 @@ class AddWorkPlan(bpy.types.Operator): def execute(self, context): ifcopenshell.api.run("sequence.add_work_plan", IfcStore.get_file()) Data.load(IfcStore.get_file()) - bpy.ops.bim.load_work_plans() return {"FINISHED"} @@ -61,10 +45,10 @@ 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() + bpy.ops.bim.disable_editing_work_plan() return {"FINISHED"} @@ -77,7 +61,6 @@ class RemoveWorkPlan(bpy.types.Operator): self.file = IfcStore.get_file() ifcopenshell.api.run("sequence.remove_work_plan", self.file, **{"work_plan": self.file.by_id(self.work_plan)}) Data.load(IfcStore.get_file()) - bpy.ops.bim.load_work_plans() return {"FINISHED"} @@ -111,6 +94,7 @@ class EnableEditingWorkPlan(bpy.types.Operator): if data[attribute.name()]: new.enum_value = data[attribute.name()] props.active_work_plan_id = self.work_plan + props.editing_type = "ATTRIBUTES" return {"FINISHED"} @@ -123,29 +107,55 @@ class DisableEditingWorkPlan(bpy.types.Operator): return {"FINISHED"} -class LoadWorkSchedules(bpy.types.Operator): - bl_idname = "bim.load_work_schedules" - bl_label = "Load Work Schedules" +class EnableEditingWorkPlanSchedules(bpy.types.Operator): + bl_idname = "bim.enable_editing_work_plan_schedules" + bl_label = "Enable Editing Work Plan Schedules" + work_plan: bpy.props.IntProperty() 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() + props = context.scene.BIMWorkPlanProperties + props.active_work_plan_id = self.work_plan + props.editing_type = "SCHEDULES" return {"FINISHED"} -class DisableWorkScheduleEditingUI(bpy.types.Operator): - bl_idname = "bim.disable_work_schedule_editing_ui" - bl_label = "Disable WorkSchedule Editing UI" +class AssignWorkSchedule(bpy.types.Operator): + bl_idname = "bim.assign_work_schedule" + bl_label = "Assign Work Schedule" + work_plan: bpy.props.IntProperty() + work_schedule: bpy.props.IntProperty() def execute(self, context): - context.scene.BIMWorkScheduleProperties.is_editing = False + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "aggregate.assign_object", + self.file, + **{ + "relating_object": self.file.by_id(self.work_plan), + "product": self.file.by_id(self.work_schedule), + }, + ) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class UnassignWorkSchedule(bpy.types.Operator): + bl_idname = "bim.unassign_work_schedule" + bl_label = "Unassign Work Schedule" + work_plan: bpy.props.IntProperty() + work_schedule: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "aggregate.unassign_object", + self.file, + **{ + "relating_object": self.file.by_id(self.work_plan), + "product": self.file.by_id(self.work_schedule), + }, + ) + Data.load(IfcStore.get_file()) return {"FINISHED"} @@ -156,7 +166,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"} @@ -179,10 +188,10 @@ 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.load_work_schedules() + bpy.ops.bim.disable_editing_work_schedule() return {"FINISHED"} @@ -194,10 +203,9 @@ 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,17 +215,22 @@ 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) + self.enable_editing_work_schedule() + self.props.editing_type = "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(): 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,9 +243,93 @@ 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 + + +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.tprops = context.scene.BIMTaskTreeProperties + self.props.active_work_schedule_id = self.work_schedule + while len(self.tprops.tasks) > 0: + self.tprops.tasks.remove(0) + + self.contracted_tasks = json.loads(self.props.contracted_tasks) + sort_keys = { + i: Data.tasks[i]["Identification"] for i in Data.work_schedules[self.work_schedule]["RelatedObjects"] + } + for related_object_id in sorted(sort_keys, key=sort_keys.__getitem__): + self.create_new_task_li(related_object_id, 0) + bpy.ops.bim.load_task_properties() + self.props.editing_type = "TASKS" return {"FINISHED"} + def create_new_task_li(self, related_object_id, level_index): + task = Data.tasks[related_object_id] + new = self.tprops.tasks.add() + 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: + sort_keys = {i: Data.tasks[i]["Identification"] for i in task["RelatedObjects"]} + for related_object_id in sorted(sort_keys, key=sort_keys.__getitem__): + 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 [ + Data.sequences[r]["RelatedProcess"] for r in task["IsPredecessorTo"] + ] + item.is_successor = self.props.active_task_id in [ + Data.sequences[r]["RelatingProcess"] for r 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"]) + item.duration = ( + isodate.duration_isoformat(task_time["ScheduleDuration"]) if task_time["ScheduleDuration"] else "-" + ) + else: + derived_start = helper.derive_date(item.ifc_definition_id, "ScheduleStart", is_earliest=True) + derived_finish = helper.derive_date(item.ifc_definition_id, "ScheduleFinish", is_latest=True) + item.derived_start = self.canonicalise_time(derived_start) if derived_start else "" + item.derived_finish = self.canonicalise_time(derived_finish) if derived_finish else "" + if derived_start and derived_finish: + derived_duration = ifcopenshell.util.date.timedelta2duration(derived_finish - derived_start) + item.derived_duration = isodate.duration_isoformat(derived_duration) + item.start = "-" + item.finish = "-" + item.duration = "-" + self.props.is_task_update_enabled = True + return {"FINISHED"} + + 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" @@ -243,32 +340,423 @@ class DisableEditingWorkSchedule(bpy.types.Operator): return {"FINISHED"} -class LoadWorkCalendars(bpy.types.Operator): - bl_idname = "bim.load_work_calendars" - bl_label = "Load Work Calendars" +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.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() + 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_tasks(work_schedule=props.active_work_schedule_id) return {"FINISHED"} -class DisableWorkCalendarEditingUI(bpy.types.Operator): - bl_idname = "bim.disable_work_calendar_editing_ui" - bl_label = "Disable WorkCalendar Editing UI" +class AddSummaryTask(bpy.types.Operator): + bl_idname = "bim.add_summary_task" + bl_label = "Add Task" + work_schedule: bpy.props.IntProperty() def execute(self, context): - context.scene.BIMWorkCalendarProperties.is_editing = False + 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)}) + Data.load(self.file) + bpy.ops.bim.enable_editing_tasks(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_tasks(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_tasks(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_tasks(work_schedule=props.active_work_schedule_id) + 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() + + 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) + + 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": + if isinstance(data[attribute.name()], datetime): + new.string_value = "" if new.is_null else data[attribute.name()].isoformat() + elif isinstance(data[attribute.name()], isodate.Duration): + new.string_value = ( + "" + if new.is_null + else ifcopenshell.util.date.datetime2ifc(data[attribute.name()], "IfcDuration") + ) + 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 == "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 = task_time_id + props.active_task_id = self.task + props.editing_task_type = "TASKTIME" + 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 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.load_task_properties(task=props.active_task_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] = parser.isoparse(value) + except: + try: + attributes[key] = parser.parse(value, dayfirst=True, fuzzy=True) + except: + attributes[key] = None + elif key == "ScheduleDuration": + attributes[key] = isodate.parse_duration(value) + return attributes + + +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 + props.editing_task_type = "ATTRIBUTES" + 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 + context.scene.BIMWorkScheduleProperties.active_task_time_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.load_task_properties(task=props.active_task_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) + bpy.ops.bim.load_task_properties(task=self.task) + 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) + bpy.ops.bim.load_task_properties(task=self.task) + 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) + bpy.ops.bim.load_task_properties(task=self.task) + 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=self.file.by_id(props.active_task_id), + related_process=self.file.by_id(self.task), + ) + Data.load(self.file) + bpy.ops.bim.load_task_properties(task=self.task) + return {"FINISHED"} + + +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): + 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.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"} + + +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"} + + +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) + + class AddWorkCalendar(bpy.types.Operator): bl_idname = "bim.add_work_calendar" bl_label = "Add Work Calendar" @@ -276,7 +764,6 @@ class AddWorkCalendar(bpy.types.Operator): 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"} @@ -299,10 +786,10 @@ class EditWorkCalendar(bpy.types.Operator): ifcopenshell.api.run( "sequence.edit_work_calendar", self.file, - **{"work_calendar": self.file.by_id(props.active_work_calendar_id), "attributes": attributes} + **{"work_calendar": self.file.by_id(props.active_work_calendar_id), "attributes": attributes}, ) Data.load(IfcStore.get_file()) - bpy.ops.bim.load_work_calendars() + bpy.ops.bim.disable_editing_work_calendar() return {"FINISHED"} @@ -317,19 +804,18 @@ class RemoveWorkCalendar(bpy.types.Operator): "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" + bl_label = "Enable Editing Work Calendar" 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) + self.props = context.scene.BIMWorkCalendarProperties + while len(self.props.work_calendar_attributes) > 0: + self.props.work_calendar_attributes.remove(0) data = Data.work_calendars[self.work_calendar] @@ -337,7 +823,7 @@ class EnableEditingWorkCalendar(bpy.types.Operator): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) if data_type == "entity": continue - new = props.work_calendar_attributes.add() + new = self.props.work_calendar_attributes.add() new.name = attribute.name() new.is_null = data[attribute.name()] is None new.is_optional = attribute.optional() @@ -348,7 +834,8 @@ class EnableEditingWorkCalendar(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_calendar_id = self.work_calendar + self.props.active_work_calendar_id = self.work_calendar + self.props.editing_type = "ATTRIBUTES" return {"FINISHED"} @@ -361,27 +848,699 @@ class DisableEditingWorkCalendar(bpy.types.Operator): return {"FINISHED"} -class LoadTasks(bpy.types.Operator): - bl_idname = "bim.load_tasks" - bl_label = "Load Tasks" +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): - props = context.scene.BIMTaskProperties - while len(props.tasks) > 0: - props.tasks.remove(0) - for ifc_definition_id, task in Data.tasks.items(): - new = props.tasks.add() - new.ifc_definition_id = ifc_definition_id - new.name = task["Name"] - new.identification = task["Identification"] - props.is_editing = True + 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] if self.file.by_type("IfcWorkPlan") else None + p62ifc.execute() + Data.load(IfcStore.get_file()) + print("Import finished in {:.2f} seconds".format(time.time() - start)) return {"FINISHED"} -class DisableTaskEditingUI(bpy.types.Operator): - bl_idname = "bim.disable_task_editing_ui" - bl_label = "Disable Task Editing UI" +class EnableEditingWorkCalendarTimes(bpy.types.Operator): + bl_idname = "bim.enable_editing_work_calendar_times" + bl_label = "Enable Editing Work Calendar Times" + work_calendar: bpy.props.IntProperty() def execute(self, context): - context.scene.BIMTaskProperties.is_editing = False + props = context.scene.BIMWorkCalendarProperties + props.active_work_calendar_id = self.work_calendar + props.editing_type = "WORKTIMES" return {"FINISHED"} + + +class AddWorkTime(bpy.types.Operator): + bl_idname = "bim.add_work_time" + bl_label = "Add Work Time" + work_calendar: bpy.props.IntProperty() + time_type: bpy.props.StringProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.add_work_time", + self.file, + **{"work_calendar": self.file.by_id(self.work_calendar), "time_type": self.time_type}, + ) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class EnableEditingWorkTime(bpy.types.Operator): + bl_idname = "bim.enable_editing_work_time" + bl_label = "Enable Editing Work Time" + work_time: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMWorkCalendarProperties + while len(self.props.work_time_attributes) > 0: + self.props.work_time_attributes.remove(0) + + data = Data.work_times[self.work_time] + + for attribute in IfcStore.get_schema().declaration_by_name("IfcWorkTime").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = self.props.work_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 attribute.name() in ["Start", "Finish"]: + 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()] + + self.initialise_recurrence_components() + self.load_recurrence_pattern_data(data) + self.props.active_work_time_id = self.work_time + return {"FINISHED"} + + def initialise_recurrence_components(self): + if len(self.props.day_components) == 0: + for i in range(0, 31): + new = self.props.day_components.add() + new.name = str(i + 1) + if len(self.props.weekday_components) == 0: + for d in ["M", "T", "W", "T", "F", "S", "S"]: + new = self.props.weekday_components.add() + new.name = d + if len(self.props.month_components) == 0: + for m in ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]: + new = self.props.month_components.add() + new.name = m + + def load_recurrence_pattern_data(self, work_time): + self.props.position = 0 + self.props.interval = 0 + self.props.occurrences = 0 + self.props.start_time = "" + self.props.end_time = "" + for component in self.props.day_components: + component.is_specified = False + for component in self.props.weekday_components: + component.is_specified = False + for component in self.props.month_components: + component.is_specified = False + if not work_time["RecurrencePattern"]: + return + recurrence_pattern = Data.recurrence_patterns[work_time["RecurrencePattern"]] + for attribute in ["Position", "Interval", "Occurrences"]: + if recurrence_pattern[attribute]: + setattr(self.props, attribute.lower(), recurrence_pattern[attribute]) + for component in recurrence_pattern["DayComponent"] or []: + self.props.day_components[component - 1].is_specified = True + for component in recurrence_pattern["WeekdayComponent"] or []: + self.props.weekday_components[component - 1].is_specified = True + for component in recurrence_pattern["MonthComponent"] or []: + self.props.month_components[component - 1].is_specified = True + + +class DisableEditingWorkTime(bpy.types.Operator): + bl_idname = "bim.disable_editing_work_time" + bl_label = "Disable Editing Work Time" + + def execute(self, context): + context.scene.BIMWorkCalendarProperties.active_work_time_id = 0 + return {"FINISHED"} + + +class EditWorkTime(bpy.types.Operator): + bl_idname = "bim.edit_work_time" + bl_label = "Edit Work Time" + + def execute(self, context): + self.props = context.scene.BIMWorkCalendarProperties + attributes = {} + for attribute in self.props.work_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 == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.edit_work_time", + self.file, + **{"work_time": self.file.by_id(self.props.active_work_time_id), "attributes": attributes}, + ) + + work_time = Data.work_times[self.props.active_work_time_id] + if work_time["RecurrencePattern"]: + self.edit_recurrence_pattern(work_time["RecurrencePattern"]) + + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_work_time() + return {"FINISHED"} + + def edit_recurrence_pattern(self, recurrence_pattern_id): + recurrence_pattern = self.file.by_id(recurrence_pattern_id) + attributes = { + "Interval": self.props.interval if self.props.interval > 0 else None, + "Occurrences": self.props.occurrences if self.props.occurrences > 0 else None, + } + applicable_data = { + "DAILY": ["Interval", "Occurrences"], + "WEEKLY": ["WeekdayComponent", "Interval", "Occurrences"], + "MONTHLY_BY_DAY_OF_MONTH": ["DayComponent", "Interval", "Occurrences"], + "MONTHLY_BY_POSITION": ["WeekdayComponent", "Position", "Interval", "Occurrences"], + "BY_DAY_COUNT": ["Interval", "Occurrences"], + "BY_WEEKDAY_COUNT": ["WeekdayComponent", "Interval", "Occurrences"], + "YEARLY_BY_DAY_OF_MONTH": ["DayComponent", "MonthComponent", "Interval", "Occurrences"], + "YEARLY_BY_POSITION": ["WeekdayComponent", "MonthComponent", "Position", "Interval", "Occurrences"], + } + if "Position" in applicable_data[recurrence_pattern.RecurrenceType]: + attributes["Position"] = self.props.position if self.props.position != 0 else None + if "DayComponent" in applicable_data[recurrence_pattern.RecurrenceType]: + attributes["DayComponent"] = [i + 1 for i, c in enumerate(self.props.day_components) if c.is_specified] + if "WeekdayComponent" in applicable_data[recurrence_pattern.RecurrenceType]: + attributes["WeekdayComponent"] = [ + i + 1 for i, c in enumerate(self.props.weekday_components) if c.is_specified + ] + if "MonthComponent" in applicable_data[recurrence_pattern.RecurrenceType]: + attributes["MonthComponent"] = [i + 1 for i, c in enumerate(self.props.month_components) if c.is_specified] + ifcopenshell.api.run( + "sequence.edit_recurrence_pattern", + self.file, + **{"recurrence_pattern": recurrence_pattern, "attributes": attributes}, + ) + + +class RemoveWorkTime(bpy.types.Operator): + bl_idname = "bim.remove_work_time" + bl_label = "Remove Work Plan" + work_time: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run("sequence.remove_work_time", self.file, **{"work_time": self.file.by_id(self.work_time)}) + Data.load(self.file) + return {"FINISHED"} + + +class AssignRecurrencePattern(bpy.types.Operator): + bl_idname = "bim.assign_recurrence_pattern" + bl_label = "Assign Recurrence Pattern" + work_time: bpy.props.IntProperty() + recurrence_type: bpy.props.StringProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.assign_recurrence_pattern", + self.file, + **{"parent": self.file.by_id(self.work_time), "recurrence_type": self.recurrence_type}, + ) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class UnassignRecurrencePattern(bpy.types.Operator): + bl_idname = "bim.unassign_recurrence_pattern" + bl_label = "Unassign Recurrence Pattern" + recurrence_pattern: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.unassign_recurrence_pattern", + self.file, + **{"recurrence_pattern": self.file.by_id(self.recurrence_pattern)}, + ) + Data.load(self.file) + return {"FINISHED"} + + +class AddTimePeriod(bpy.types.Operator): + bl_idname = "bim.add_time_period" + bl_label = "Add Time Period" + recurrence_pattern: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMWorkCalendarProperties + self.file = IfcStore.get_file() + try: + start_time = parser.parse(self.props.start_time) + end_time = parser.parse(self.props.end_time) + except: + return {"FINISHED"} + ifcopenshell.api.run( + "sequence.add_time_period", + self.file, + **{ + "recurrence_pattern": self.file.by_id(self.recurrence_pattern), + "start_time": start_time, + "end_time": end_time, + }, + ) + self.props.start_time = "" + self.props.end_time = "" + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class RemoveTimePeriod(bpy.types.Operator): + bl_idname = "bim.remove_time_period" + bl_label = "Remove Time Period" + time_period: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.remove_time_period", + self.file, + **{"time_period": self.file.by_id(self.time_period)}, + ) + Data.load(self.file) + return {"FINISHED"} + + +class EnableEditingTaskCalendar(bpy.types.Operator): + bl_idname = "bim.enable_editing_task_calendar" + bl_label = "Enable Editing Task Calendar" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + props.active_task_id = self.task + props.editing_task_type = "CALENDAR" + return {"FINISHED"} + + +class EditTaskCalendar(bpy.types.Operator): + bl_idname = "bim.edit_task_calendar" + bl_label = "Edit Task Calendar" + work_calendar: bpy.props.IntProperty() + task: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "control.assign_control", + self.file, + **{ + "relating_control": self.file.by_id(self.work_calendar), + "related_object": self.file.by_id(self.task), + }, + ) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class RemoveTaskCalendar(bpy.types.Operator): + bl_idname = "bim.remove_task_calendar" + bl_label = "Remove Task Calendar" + work_calendar: bpy.props.IntProperty() + task: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "control.unassign_control", + self.file, + **{ + "relating_control": self.file.by_id(self.work_calendar), + "related_object": self.file.by_id(self.task), + }, + ) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class EnableEditingTaskSequence(bpy.types.Operator): + bl_idname = "bim.enable_editing_task_sequence" + bl_label = "Enable Editing Task Sequence" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + props.active_task_id = self.task + props.editing_task_type = "SEQUENCE" + bpy.ops.bim.load_task_properties() + 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 + bpy.ops.bim.disable_editing_task() + return {"FINISHED"} + + +class EnableEditingSequenceAttributes(bpy.types.Operator): + bl_idname = "bim.enable_editing_sequence_attributes" + bl_label = "Enable Editing Sequence Attributes" + sequence: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMWorkScheduleProperties + self.props.active_sequence_id = self.sequence + self.props.editing_sequence_type = "ATTRIBUTES" + while len(self.props.sequence_attributes) > 0: + self.props.sequence_attributes.remove(0) + self.enable_editing_sequence_attributes() + return {"FINISHED"} + + def enable_editing_sequence_attributes(self): + data = Data.sequences[self.sequence] + for attribute in IfcStore.get_schema().declaration_by_name("IfcRelSequence").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = self.props.sequence_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()] + + +class EnableEditingSequenceTimeLag(bpy.types.Operator): + bl_idname = "bim.enable_editing_sequence_time_lag" + bl_label = "Enable Editing Sequence Time Lag" + sequence: bpy.props.IntProperty() + lag_time: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMWorkScheduleProperties + self.props.active_sequence_id = self.sequence + self.props.editing_sequence_type = "TIME_LAG" + while len(self.props.time_lag_attributes) > 0: + self.props.time_lag_attributes.remove(0) + self.enable_editing_attributes() + return {"FINISHED"} + + def enable_editing_attributes(self): + data = Data.lag_times[self.lag_time] + for attribute in IfcStore.get_schema().declaration_by_name("IfcLagTime").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = self.props.time_lag_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() == "LagValue": + if isinstance(data[attribute.name()], isodate.Duration): + new.data_type = "string" + new.string_value = ( + "" + if new.is_null + else ifcopenshell.util.date.datetime2ifc(data[attribute.name()], "IfcDuration") + ) + else: + new.data_type = "float" + new.float_value = 0.0 if new.is_null else data[attribute.name()] + elif data_type == "string": + new.string_value = "" 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()] + + +class UnassignLagTime(bpy.types.Operator): + bl_idname = "bim.unassign_lag_time" + bl_label = "Unassign Time Lag" + sequence: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.unassign_lag_time", self.file, **{"rel_sequence": self.file.by_id(self.sequence)} + ) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class AssignLagTime(bpy.types.Operator): + bl_idname = "bim.assign_lag_time" + bl_label = "Assign Time Lag" + sequence: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.assign_lag_time", + self.file, + **{"rel_sequence": self.file.by_id(self.sequence), "lag_value": "P0D"}, + ) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class EditSequenceAttributes(bpy.types.Operator): + bl_idname = "bim.edit_sequence_attributes" + bl_label = "Edit Sequence" + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + attributes = {} + for attribute in props.sequence_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_sequence", + self.file, + **{"rel_sequence": self.file.by_id(props.active_sequence_id), "attributes": attributes}, + ) + Data.load(self.file) + bpy.ops.bim.disable_editing_sequence() + return {"FINISHED"} + + +class EditSequenceTimeLag(bpy.types.Operator): + bl_idname = "bim.edit_sequence_time_lag" + bl_label = "Edit Time Lag" + lag_time: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + attributes = {} + for attribute in props.time_lag_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 == "float": + attributes[attribute.name] = attribute.float_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.edit_lag_time", + self.file, + **{"lag_time": self.file.by_id(self.lag_time), "attributes": attributes}, + ) + Data.load(self.file) + bpy.ops.bim.disable_editing_sequence() + return {"FINISHED"} + + +class DisableEditingSequence(bpy.types.Operator): + bl_idname = "bim.disable_editing_sequence" + bl_label = "Disable Editing Sequence Attributes" + + def execute(self, context): + context.scene.BIMWorkScheduleProperties.active_sequence_id = 0 + return {"FINISHED"} + + +class SelectTaskRelatedProducts(bpy.types.Operator): + bl_idname = "bim.select_task_related_products" + bl_label = "Select Similar Type" + task: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + related_products = ifcopenshell.api.run( + "sequence.get_related_products", self.file, **{"related_object": self.file.by_id(self.task)} + ) + for obj in bpy.context.visible_objects: + obj.select_set(False) + if obj.BIMObjectProperties.ifc_definition_id in related_products: + obj.select_set(True) + return {"FINISHED"} + + +class VisualiseWorkScheduleDate(bpy.types.Operator): + bl_idname = "bim.visualise_work_schedule_date" + bl_label = "Visualise Work Schedule Date" + work_schedule: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + self.date = parser.parse(props.visualisation_start, dayfirst=True, fuzzy=True) + self.preprocess_tasks() + for obj in bpy.data.objects: + if not obj.BIMObjectProperties.ifc_definition_id: + continue + obj.color = (1.0, 1.0, 1.0, 1) + obj.hide_set(False) + if obj.BIMObjectProperties.ifc_definition_id in self.not_yet_started: + obj.hide_set(True) + elif obj.BIMObjectProperties.ifc_definition_id in self.started: + obj.color = (0.0, 1.0, 0.0, 1) + elif obj.BIMObjectProperties.ifc_definition_id in self.completed: + pass + else: + obj.color = (1.0, 0.0, 0.0, 1) + area = next(area for area in context.screen.areas if area.type == "VIEW_3D") + area.spaces[0].shading.color_type = "OBJECT" + return {"FINISHED"} + + def preprocess_tasks(self): + self.not_yet_started = set() + self.started = set() + self.completed = set() + for rel in self.file.by_id(self.work_schedule).Controls or []: + for related_object in rel.RelatedObjects: + if related_object.is_a("IfcTask"): + self.preprocess_task(related_object) + + def preprocess_task(self, task): + for rel in task.IsNestedBy or []: + for related_object in rel.RelatedObjects: + self.preprocess_task(related_object) + start = helper.derive_date(task.id(), "ScheduleStart", is_earliest=True) + finish = helper.derive_date(task.id(), "ScheduleFinish", is_latest=True) + if not start or not finish: + return + products = [r.RelatingProduct.id() for r in task.HasAssignments or [] if r.is_a("IfcRelAssignsToProduct")] + if not products: + return + if self.date < start: + self.not_yet_started.update(products) + elif self.date < finish: + self.started.update(products) + else: + self.completed.update(products) + + +class VisualiseWorkScheduleDateRange(bpy.types.Operator): + bl_idname = "bim.visualise_work_schedule_date_range" + bl_label = "Visualise Work Schedule Date Range" + work_schedule: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + self.props = context.scene.BIMWorkScheduleProperties + self.start = parser.parse(self.props.visualisation_start, dayfirst=True, fuzzy=True) + self.finish = parser.parse(self.props.visualisation_finish, dayfirst=True, fuzzy=True) + self.duration = self.finish - self.start + self.start_frame = 1 + self.total_frames = self.calculate_total_frames() + self.preprocess_tasks() + for obj in bpy.data.objects: + if not obj.BIMObjectProperties.ifc_definition_id: + continue + product_frames = self.product_frames.get(obj.BIMObjectProperties.ifc_definition_id) + if not product_frames: + continue + obj.hide_viewport = True + obj.keyframe_insert(data_path="hide_viewport", frame=self.start_frame) + obj.hide_viewport = False + obj.color = (0.0, 1.0, 0.0, 1) + obj.keyframe_insert(data_path="hide_viewport", frame=product_frames["STARTED"]) + obj.keyframe_insert(data_path="color", frame=product_frames["STARTED"]) + obj.color = (1.0, 1.0, 1.0, 1) + obj.keyframe_insert(data_path="color", frame=product_frames["COMPLETED"]) + area = next(area for area in context.screen.areas if area.type == "VIEW_3D") + area.spaces[0].shading.color_type = "OBJECT" + context.scene.frame_start = self.start_frame + context.scene.frame_end = self.start_frame + self.total_frames + return {"FINISHED"} + + def calculate_total_frames(self): + if self.props.speed_types == "FRAME_SPEED": + return self.calculate_using_frames( + self.start, + self.finish, + self.props.speed_animation_frames, + isodate.parse_duration(self.props.speed_real_duration), + ) + elif self.props.speed_types == "DURATION_SPEED": + return self.calculate_using_duration( + self.start, + self.finish, + bpy.context.scene.render.fps, + isodate.parse_duration(self.props.speed_animation_duration), + isodate.parse_duration(self.props.speed_real_duration), + ) + elif self.props.speed_types == "MULTIPLIER_SPEED": + return self.calculate_using_multiplier( + self.start, + self.finish, + bpy.context.scene.render.fps, + self.props.speed_multiplier, + ) + + def calculate_using_multiplier(self, start, finish, fps, multiplier): + animation_time = (finish - start) / multiplier + return animation_time.total_seconds() * fps + + def calculate_using_duration(self, start, finish, fps, animation_duration, real_duration): + return self.calculate_using_multiplier(start, finish, fps, real_duration / animation_duration) + + def calculate_using_frames(self, start, finish, animation_frames, real_duration): + return ((finish - start) / real_duration) * animation_frames + + def preprocess_tasks(self): + self.product_frames = {} + for rel in self.file.by_id(self.work_schedule).Controls or []: + for related_object in rel.RelatedObjects: + if related_object.is_a("IfcTask"): + self.preprocess_task(related_object) + + def preprocess_task(self, task): + for rel in task.IsNestedBy or []: + for related_object in rel.RelatedObjects: + self.preprocess_task(related_object) + start = helper.derive_date(task.id(), "ScheduleStart", is_earliest=True) + finish = helper.derive_date(task.id(), "ScheduleFinish", is_latest=True) + if not start or not finish: + return + product_ids = [r.RelatingProduct.id() for r in task.HasAssignments or [] if r.is_a("IfcRelAssignsToProduct")] + for product_id in product_ids: + self.product_frames[product_id] = { + "STARTED": round(self.start_frame + (((start - self.start) / self.duration) * self.total_frames)), + "COMPLETED": round(self.start_frame + (((finish - self.start) / self.duration) * self.total_frames)), + } diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 29a1af6b14..ad51aff00d 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -1,5 +1,9 @@ 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 dateutil import parser from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -13,16 +17,150 @@ from bpy.props import ( ) +def updateTaskName(self, context): + props = context.scene.BIMWorkScheduleProperties + if not props.is_task_update_enabled or self.name == "Unnamed": + return + self.file = IfcStore.get_file() + 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 = props.task_attributes.get("Name") + attribute.string_value = self.name + + +def updateTaskIdentification(self, context): + props = context.scene.BIMWorkScheduleProperties + if not props.is_task_update_enabled or self.identification == "XXX": + return + self.file = IfcStore.get_file() + 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 + + +def updateTaskTimeStart(self, context): + updateTaskTimeDateTime(self, context, "start") + + +def updateTaskTimeFinish(self, context): + updateTaskTimeDateTime(self, context, "finish") + + +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 "-" + 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() + + try: + startfinish_datetime = parser.isoparse(startfinish_value) + except: + 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: + 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: + canonical_startfinish_value = canonicalise_time(startfinish_datetime) + if startfinish_value != canonical_startfinish_value: + setattr(self, startfinish, canonical_startfinish_value) + return + + ifcopenshell.api.run( + "sequence.edit_task_time", + self.file, + **{"task_time": task_time, "attributes": {startfinish_key: startfinish_datetime}}, + ) + Data.load(IfcStore.get_file()) + setattr(self, startfinish, canonicalise_time(startfinish_datetime)) + + +def updateVisualisationStart(self, context): + updateVisualisationStartFinish(self, context, "visualisation_start") + + +def updateVisualisationFinish(self, context): + updateVisualisationStartFinish(self, context, "visualisation_finish") + + +def updateVisualisationStartFinish(self, context, startfinish): + def canonicalise_time(time): + if not time: + return "-" + return time.strftime("%d/%m/%y") + + startfinish_value = getattr(self, startfinish) + try: + startfinish_datetime = parser.isoparse(startfinish_value) + except: + try: + startfinish_datetime = parser.parse(startfinish_value, dayfirst=True, fuzzy=True) + except: + setattr(self, startfinish, "-") + return + canonical_value = canonicalise_time(startfinish_datetime) + if startfinish_value != canonical_value: + setattr(self, startfinish, canonical_value) + + +workschedule_enum = [] + + +def getWorkSchedules(self, context): + return [(str(k), v["Name"], "") for k, v in Data.work_schedules.items()] + + +def getWorkCalendars(self, context): + return [(str(k), v["Name"], "") for k, v in Data.work_calendars.items()] + + 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") - - -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") + has_children: BoolProperty(name="Has Children") + is_expanded: BoolProperty(name="Is Expanded") + level_index: IntProperty(name="Level Index") + duration: StringProperty(name="Duration") + start: StringProperty(name="Start", update=updateTaskTimeStart) + finish: StringProperty(name="Finish", update=updateTaskTimeFinish) + derived_start: StringProperty(name="Derived Start") + derived_finish: StringProperty(name="Derived Finish") + derived_duration: StringProperty(name="Derived Duration") + is_predecessor: BoolProperty(name="Is Predecessor") + is_successor: BoolProperty(name="Is Successor") class WorkPlan(PropertyGroup): @@ -32,23 +170,54 @@ class WorkPlan(PropertyGroup): class BIMWorkPlanProperties(PropertyGroup): work_plan_attributes: CollectionProperty(name="Work Plan Attributes", type=Attribute) - is_editing: BoolProperty(name="Is Editing", default=False) + editing_type: StringProperty(name="Editing Type") work_plans: CollectionProperty(name="Work Plans", type=WorkPlan) active_work_plan_index: IntProperty(name="Active Work Plan Index") 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") + work_schedules: EnumProperty(items=getWorkSchedules, name="Work Schedules") class BIMWorkScheduleProperties(PropertyGroup): + work_calendars: EnumProperty(items=getWorkCalendars, name="Work Calendars") 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) + editing_type: StringProperty(name="Editing Type") + editing_task_type: StringProperty(name="Editing Task Type") active_work_schedule_index: IntProperty(name="Active Work Schedules Index") active_work_schedule_id: IntProperty(name="Active Work Schedules Id") + 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_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=False) + 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="[]") + is_task_update_enabled: BoolProperty(name="Is Task Update Enabled", default=True) + editing_sequence_type: StringProperty(name="Editing Sequence Type") + active_sequence_id: IntProperty(name="Active Sequence Id") + sequence_attributes: CollectionProperty(name="Sequence Attributes", type=Attribute) + time_lag_attributes: CollectionProperty(name="Time Lag Attributes", type=Attribute) + visualisation_start: StringProperty(name="Visualisation Start", update=updateVisualisationStart) + visualisation_finish: StringProperty(name="Visualisation Finish", update=updateVisualisationFinish) + speed_multiplier: FloatProperty(name="Speed Multiplier", default=10000) + speed_animation_duration: StringProperty(name="Speed Animation Duration", default="PT1S") + speed_animation_frames: IntProperty(name="Speed Animation Frames", default=24) + speed_real_duration: StringProperty(name="Speed Real Duration", default="P1W") + speed_types: EnumProperty( + items=[ + ("FRAME_SPEED", "Frame-based", "e.g. 25 frames = 1 real week"), + ("DURATION_SPEED", "Duration-based", "e.g. 1 video second = 1 real week"), + ("MULTIPLIER_SPEED", "Multiplier", "e.g. 1000 x real life speed"), + ], + name="Speed Type", + default="FRAME_SPEED", + ) + + +class BIMTaskTreeProperties(PropertyGroup): + # This belongs by itself for performance reasons. https://developer.blender.org/T87737 + # 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): @@ -56,9 +225,36 @@ class WorkCalendar(PropertyGroup): ifc_definition_id: IntProperty(name="IFC Definition ID") +class RecurrenceComponent(PropertyGroup): + name: StringProperty(name="Name") + is_specified: BoolProperty(name="Is Specified") + + class BIMWorkCalendarProperties(PropertyGroup): work_calendar_attributes: CollectionProperty(name="Work Calendar Attributes", type=Attribute) - is_editing: BoolProperty(name="Is Editing", default=False) - work_calendars: CollectionProperty(name="Work Calendar", type=WorkCalendar) - active_work_calendar_index: IntProperty(name="Active Work Calendar Index") + work_time_attributes: CollectionProperty(name="Work Time Attributes", type=Attribute) + editing_type: StringProperty(name="Editing Type") active_work_calendar_id: IntProperty(name="Active Work Calendar Id") + active_work_time_id: IntProperty(name="Active Work Time Id") + day_components: CollectionProperty(name="Day Components", type=RecurrenceComponent) + weekday_components: CollectionProperty(name="Weekday Components", type=RecurrenceComponent) + month_components: CollectionProperty(name="Month Components", type=RecurrenceComponent) + position: IntProperty(name="Position") + interval: IntProperty(name="Recurrence Interval") + occurrences: IntProperty(name="Occurs N Times") + recurrence_types: EnumProperty( + items=[ + ("DAILY", "Daily", "e.g. Every day"), + ("WEEKLY", "Weekly", "e.g. Every Friday"), + ("MONTHLY_BY_DAY_OF_MONTH", "Monthly on Specified Date", "e.g. Every 2nd of each Month"), + ("MONTHLY_BY_POSITION", "Monthly on Specified Weekday", "e.g. Every 1st Friday of each Month"), + # https://forums.buildingsmart.org/t/what-does-by-day-count-and-by-weekday-count-mean-in-ifcrecurrencetypeenum/3571 + # ("BY_DAY_COUNT", "", ""), + # ("BY_WEEKDAY_COUNT", "", ""), + ("YEARLY_BY_DAY_OF_MONTH", "Yearly on Specified Date", "e.g. Every 2nd of October"), + ("YEARLY_BY_POSITION", "Yearly on Specified Weekday", "e.g. Every 1st Friday of October"), + ], + name="Recurrence Types", + ) + start_time: StringProperty(name="Start Time") + end_time: StringProperty(name="End Time") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 9ed96cef3c..ea5aab4a13 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -1,3 +1,4 @@ +import isodate from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.sequence.data import Data @@ -19,28 +20,36 @@ class BIM_PT_work_plans(Panel): if not Data.is_loaded: Data.load(IfcStore.get_file()) self.props = context.scene.BIMWorkPlanProperties + + row = self.layout.row() + row.operator("bim.add_work_plan", icon="ADD") + + for work_plan_id, work_plan in Data.work_plans.items(): + self.draw_work_plan_ui(work_plan_id, work_plan) + + def draw_work_plan_ui(self, work_plan_id, work_plan): row = self.layout.row(align=True) - row.label(text="{} Work Plans Found".format(len(Data.work_plans)), icon="TEXT") - if self.props.is_editing: - row.operator("bim.add_work_plan", text="", icon="ADD") - row.operator("bim.disable_work_plan_editing_ui", text="", icon="CHECKMARK") + row.label(text=work_plan["Name"] or "Unnamed", icon="TEXT") + + if self.props.active_work_plan_id == work_plan_id: + row.operator("bim.edit_work_plan", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_work_plan", text="", icon="CANCEL") + elif self.props.active_work_plan_id: + row.operator("bim.remove_work_plan", text="", icon="X").work_plan = work_plan_id else: - row.operator("bim.load_work_plans", text="", icon="GREASEPENCIL") + op = row.operator("bim.enable_editing_work_plan_schedules", text="", icon="LINENUMBERS_ON") + op.work_plan = work_plan_id + op = row.operator("bim.enable_editing_work_plan", text="", icon="GREASEPENCIL") + op.work_plan = work_plan_id + row.operator("bim.remove_work_plan", text="", icon="X").work_plan = work_plan_id - if self.props.is_editing: - self.layout.template_list( - "BIM_UL_work_plans", - "", - self.props, - "work_plans", - self.props, - "active_work_plan_index", - ) + if self.props.active_work_plan_id == work_plan_id: + if self.props.editing_type == "ATTRIBUTES": + self.draw_editable_ui() + elif self.props.editing_type == "SCHEDULES": + self.draw_work_schedule_ui() - if self.props.active_work_plan_id: - self.draw_editable_ui(context) - - def draw_editable_ui(self, context): + def draw_editable_ui(self): for attribute in self.props.work_plan_attributes: row = self.layout.row(align=True) if attribute.data_type == "string": @@ -50,21 +59,20 @@ class BIM_PT_work_plans(Panel): if attribute.is_optional: row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + def draw_work_schedule_ui(self): + row = self.layout.row(align=True) + row.prop(self.props, "work_schedules", text="") + op = row.operator("bim.assign_work_schedule", text="", icon="ADD") + op.work_plan = self.props.active_work_plan_id + op.work_schedule = int(self.props.work_schedules) -class BIM_UL_work_plans(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.BIMWorkPlanProperties.active_work_plan_id == item.ifc_definition_id: - row.operator("bim.edit_work_plan", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_work_plan", text="", icon="X") - elif context.scene.BIMWorkPlanProperties.active_work_plan_id: - row.operator("bim.remove_work_plan", text="", icon="X").work_plan = item.ifc_definition_id - else: - op = row.operator("bim.enable_editing_work_plan", text="", icon="GREASEPENCIL") - op.work_plan = item.ifc_definition_id - row.operator("bim.remove_work_plan", text="", icon="X").work_plan = item.ifc_definition_id + for work_schedule_id in Data.work_plans[self.props.active_work_plan_id]["IsDecomposedBy"]: + work_schedule = Data.work_schedules[work_schedule_id] + row = self.layout.row(align=True) + row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") + op = row.operator("bim.unassign_work_schedule", text="", icon="X") + op.work_plan = self.props.active_work_plan_id + op.work_schedule = int(self.props.work_schedules) class BIM_PT_work_schedules(Panel): @@ -80,31 +88,69 @@ class BIM_PT_work_schedules(Panel): return IfcStore.get_file() def draw(self, context): + self.props = context.scene.BIMWorkScheduleProperties + self.tprops = context.scene.BIMTaskTreeProperties + if not Data.is_loaded: Data.load(IfcStore.get_file()) - self.props = context.scene.BIMWorkScheduleProperties + + row = self.layout.row() + row.operator("bim.add_work_schedule", icon="ADD") + + for work_schedule_id, work_schedule in Data.work_schedules.items(): + self.draw_work_schedule_ui(work_schedule_id, work_schedule) + + def draw_work_schedule_ui(self, work_schedule_id, work_schedule): 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") + 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.editing_type == "WORK_SCHEDULE": + row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK") + elif self.props.editing_type == "TASKS": + row.prop(self.props, "should_show_times", text="", icon="TIME") + row.prop(self.props, "should_show_visualisation_ui", text="", icon="CAMERA_STEREO") + 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: + row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id else: - row.operator("bim.load_work_schedules", text="", icon="GREASEPENCIL") + row.operator("bim.enable_editing_tasks", text="", icon="ACTION").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.is_editing: - self.layout.template_list( - "BIM_UL_work_schedules", - "", - self.props, - "work_schedules", - self.props, - "active_work_schedule_index", - ) + if self.props.active_work_schedule_id == work_schedule_id: + if self.props.should_show_visualisation_ui: + self.draw_visualisation_ui() + if self.props.editing_type == "WORK_SCHEDULE": + self.draw_editable_work_schedule_ui() + elif self.props.editing_type == "TASKS": + self.draw_editable_task_ui(work_schedule_id) - if self.props.active_work_schedule_id: - self.draw_editable_ui(context) + def draw_visualisation_ui(self): + row = self.layout.row(align=True) + row.prop(self.props, "visualisation_start", text="", icon="REW") + row.prop(self.props, "visualisation_finish", text="", icon="FF") + op = row.operator("bim.visualise_work_schedule_date", text="", icon="RESTRICT_RENDER_OFF") + op.work_schedule = self.props.active_work_schedule_id + op = row.operator("bim.visualise_work_schedule_date_range", text="", icon="OUTLINER_OB_CAMERA") + op.work_schedule = self.props.active_work_schedule_id - def draw_editable_ui(self, context): + row = self.layout.row(align=True) + row.prop(self.props, "speed_types", text="") + if self.props.speed_types == "FRAME_SPEED": + row.prop(self.props, "speed_animation_frames", text="") + row.prop(self.props, "speed_real_duration", text="") + elif self.props.speed_types == "DURATION_SPEED": + row.prop(self.props, "speed_animation_duration", text="") + row.prop(self.props, "speed_real_duration", text="") + elif self.props.speed_types == "MULTIPLIER_SPEED": + row.prop(self.props, "speed_multiplier", text="") + + 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": @@ -114,21 +160,236 @@ 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): + self.layout.template_list( + "BIM_UL_tasks", + "", + self.tprops, + "tasks", + self.props, + "active_task_index", + ) + if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES": + self.draw_editable_task_attributes_ui() + elif self.props.active_task_id and self.props.editing_task_type == "CALENDAR": + self.draw_editable_task_calendar_ui() + elif self.props.active_task_id and self.props.editing_task_type == "SEQUENCE": + self.draw_editable_task_sequence_ui() + elif self.props.active_task_time_id and self.props.editing_task_type == "TASKTIME": + self.draw_editable_task_time_attributes_ui() -class BIM_UL_work_schedules(UIList): + def draw_editable_task_sequence_ui(self): + task = Data.tasks[self.props.active_task_id] + row = self.layout.row() + row.label(text="{} Predecessors".format(len(task["IsSuccessorFrom"])), icon="BACK") + for sequence_id in task["IsSuccessorFrom"]: + self.draw_editable_sequence_ui(Data.sequences[sequence_id], "RelatingProcess") + + row = self.layout.row() + row.label(text="{} Successors".format(len(task["IsPredecessorTo"])), icon="FORWARD") + for sequence_id in task["IsPredecessorTo"]: + self.draw_editable_sequence_ui(Data.sequences[sequence_id], "RelatedProcess") + + def draw_editable_sequence_ui(self, sequence, process_type): + task = Data.tasks[sequence[process_type]] + row = self.layout.row(align=True) + row.label(text=task["Identification"] or "XXX") + row.label(text=task["Name"] or "Unnamed") + row.label(text=sequence["SequenceType"] or "N/A") + if sequence["TimeLag"]: + row.operator("bim.unassign_lag_time", text="", icon="X").sequence = sequence["id"] + row.label(text=isodate.duration_isoformat(Data.lag_times[sequence["TimeLag"]]["LagValue"])) + else: + row.operator("bim.assign_lag_time", text="", icon="ADD").sequence = sequence["id"] + row.label(text="N/A") + if self.props.active_sequence_id == sequence["id"]: + if self.props.editing_sequence_type == "ATTRIBUTES": + row.operator("bim.edit_sequence_attributes", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_sequence", text="", icon="X") + self.draw_editable_sequence_attributes_ui() + elif self.props.editing_sequence_type == "TIME_LAG": + op = row.operator("bim.edit_sequence_time_lag", text="", icon="CHECKMARK") + op.lag_time = sequence["TimeLag"] + row.operator("bim.disable_editing_sequence", text="", icon="X") + self.draw_editable_sequence_time_lag_ui() + else: + if sequence["TimeLag"]: + op = row.operator("bim.enable_editing_sequence_time_lag", text="", icon="CON_LOCKTRACK") + op.sequence = sequence["id"] + op.lag_time = sequence["TimeLag"] + op = row.operator("bim.enable_editing_sequence_attributes", text="", icon="GREASEPENCIL") + op.sequence = sequence["id"] + + def draw_editable_sequence_attributes_ui(self): + for attribute in self.props.sequence_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="") + + def draw_editable_sequence_time_lag_ui(self): + for attribute in self.props.time_lag_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="") + + def draw_editable_task_calendar_ui(self): + task = Data.tasks[self.props.active_task_id] + if task["HasAssignmentsWorkCalendar"]: + row = self.layout.row(align=True) + calendar = Data.work_calendars[task["HasAssignmentsWorkCalendar"][0]] + row.label(text=calendar["Name"] or "Unnamed") + op = row.operator("bim.remove_task_calendar", text="", icon="X") + op.work_calendar = task["HasAssignmentsWorkCalendar"][0] + op.task = self.props.active_task_id + else: + row = self.layout.row(align=True) + row.prop(self.props, "work_calendars", text="") + op = row.operator("bim.edit_task_calendar", text="", icon="ADD") + op.work_calendar = int(self.props.work_calendars) + op.task = self.props.active_task_id + + 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="") + + 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_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) - 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.prop(item, "identification", emboss=False, text="") + row.prop(item, "name", emboss=False, text="") + + if props.should_show_times: + if item.derived_start: + row.label(text=item.derived_start + "*") + else: + row.prop(item, "start", emboss=False, text="") + + if item.derived_finish: + row.label(text=item.derived_finish + "*") + else: + row.prop(item, "finish", emboss=False, text="") + + if item.derived_duration: + row.label(text=item.derived_duration + "*") + else: + 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.editing_task_type == "TASKTIME": + row.operator("bim.edit_task_time", text="", icon="CHECKMARK") + elif props.editing_task_type == "CALENDAR": + row.operator("bim.disable_editing_task", text="", icon="CHECKMARK") + elif props.editing_task_type == "SEQUENCE": + row.operator("bim.disable_editing_task", text="", icon="CHECKMARK") + elif props.editing_task_type == "ATTRIBUTES": + row.operator("bim.edit_task", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_task", text="", icon="CANCEL") + elif props.active_task_id: + if props.editing_task_type == "SEQUENCE": + if item.is_predecessor: + 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 item.is_successor: + 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_sequence", text="", icon="TRACKING").task = item.ifc_definition_id + row.operator( + "bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="" + ).task = item.ifc_definition_id + row.operator("bim.enable_editing_task_time", text="", icon="TIME").task = item.ifc_definition_id + row.operator( + "bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO" + ).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 class BIM_PT_work_calendars(Panel): @@ -147,29 +408,72 @@ class BIM_PT_work_calendars(Panel): if not Data.is_loaded: Data.load(IfcStore.get_file()) self.props = context.scene.BIMWorkCalendarProperties + + row = self.layout.row() + row.operator("bim.add_work_calendar", icon="ADD") + + for work_calendar_id, work_calendar in Data.work_calendars.items(): + self.draw_work_calendar_ui(work_calendar_id, work_calendar) + + def draw_work_calendar_ui(self, work_calendar_id, work_calendar): 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") + row.label(text=work_calendar["Name"] or "Unnamed", icon="VIEW_ORTHO") + if self.props.active_work_calendar_id == work_calendar_id: + if self.props.editing_type == "ATTRIBUTES": + row.operator("bim.edit_work_calendar", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_work_calendar", text="", icon="CANCEL") + elif self.props.active_work_calendar_id: + row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = work_calendar_id else: - row.operator("bim.load_work_calendars", text="", icon="GREASEPENCIL") + op = row.operator("bim.enable_editing_work_calendar_times", text="", icon="MESH_GRID") + op.work_calendar = work_calendar_id + op = row.operator("bim.enable_editing_work_calendar", text="", icon="GREASEPENCIL") + op.work_calendar = work_calendar_id + row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = work_calendar_id - 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 == work_calendar_id: + if self.props.editing_type == "ATTRIBUTES": + self.draw_editable_ui() + elif self.props.editing_type == "WORKTIMES": + self.draw_work_times_ui(work_calendar_id, work_calendar) - if self.props.active_work_calendar_id: - self.draw_editable_ui(context) + def draw_work_times_ui(self, work_calendar_id, work_calendar): + row = self.layout.row(align=True) + op = row.operator("bim.add_work_time", text="Add Work Time", icon="ADD") + op.work_calendar = work_calendar_id + op.time_type = "WorkingTimes" + op = row.operator("bim.add_work_time", text="Add Exception Time", icon="ADD") + op.work_calendar = work_calendar_id + op.time_type = "ExceptionTimes" - def draw_editable_ui(self, context): - for attribute in self.props.work_calendar_attributes: + for work_time_id in work_calendar["WorkingTimes"]: + self.draw_work_time_ui(Data.work_times[work_time_id], time_type="WorkingTimes") + + for work_time_id in work_calendar["ExceptionTimes"]: + self.draw_work_time_ui(Data.work_times[work_time_id], time_type="ExceptionTimes") + + def draw_work_time_ui(self, work_time, time_type): + row = self.layout.row(align=True) + row.label(text=work_time["Name"] or "Unnamed", icon="AUTO" if time_type == "WorkingTimes" else "HOME") + if work_time["Start"] or work_time["Finish"]: + row.label(text="{} - {}".format(work_time["Start"] or "*", work_time["Finish"] or "*")) + if self.props.active_work_time_id == work_time["id"]: + row.operator("bim.edit_work_time", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_work_time", text="", icon="CANCEL") + elif self.props.active_work_time_id: + op = row.operator("bim.remove_work_time", text="", icon="X") + op.work_time = work_time["id"] + else: + op = row.operator("bim.enable_editing_work_time", text="", icon="GREASEPENCIL") + op.work_time = work_time["id"] + op = row.operator("bim.remove_work_time", text="", icon="X") + op.work_time = work_time["id"] + + if self.props.active_work_time_id == work_time["id"]: + self.draw_editable_work_time_ui(work_time) + + def draw_editable_work_time_ui(self, work_time): + for attribute in self.props.work_time_attributes: row = self.layout.row(align=True) if attribute.data_type == "string": row.prop(attribute, "string_value", text=attribute.name) @@ -178,68 +482,78 @@ class BIM_PT_work_calendars(Panel): 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_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") + if work_time["RecurrencePattern"]: + self.draw_editable_recurrence_pattern_ui(Data.recurrence_patterns[work_time["RecurrencePattern"]]) else: - row.operator("bim.load_tasks", text="", icon="GREASEPENCIL") + row = self.layout.row(align=True) + row.prop(self.props, "recurrence_types", icon="RECOVER_LAST", text="") + op = row.operator("bim.assign_recurrence_pattern", icon="ADD", text="") + op.work_time = work_time["id"] + op.recurrence_type = self.props.recurrence_types - if self.props.is_editing: - self.layout.template_list( - "BIM_UL_tasks", - "", - self.props, - "tasks", - self.props, - "active_task_index", - ) + def draw_editable_recurrence_pattern_ui(self, recurrence_pattern): + box = self.layout.box() + row = box.row(align=True) + row.label(text=recurrence_pattern["RecurrenceType"], icon="RECOVER_LAST") + op = row.operator("bim.unassign_recurrence_pattern", text="", icon="X") + op.recurrence_pattern = recurrence_pattern["id"] - if self.props.active_task_index: - self.draw_editable_ui(context) + row = box.row(align=True) + row.prop(self.props, "start_time", text="") + row.prop(self.props, "end_time", text="") + op = row.operator("bim.add_time_period", text="", icon="ADD") + op.recurrence_pattern = recurrence_pattern["id"] - def draw_editable_ui(self, context): - pass + for time_period_id in recurrence_pattern["TimePeriods"]: + time_period = Data.time_periods[time_period_id] + row = box.row(align=True) + row.label(text="{} - {}".format(time_period["StartTime"], time_period["EndTime"]), icon="TIME") + op = row.operator("bim.remove_time_period", text="", icon="X") + op.time_period = time_period_id + applicable_data = { + "DAILY": ["Interval", "Occurrences"], + "WEEKLY": ["WeekdayComponent", "Interval", "Occurrences"], + "MONTHLY_BY_DAY_OF_MONTH": ["DayComponent", "Interval", "Occurrences"], + "MONTHLY_BY_POSITION": ["WeekdayComponent", "Position", "Interval", "Occurrences"], + "BY_DAY_COUNT": ["Interval", "Occurrences"], + "BY_WEEKDAY_COUNT": ["WeekdayComponent", "Interval", "Occurrences"], + "YEARLY_BY_DAY_OF_MONTH": ["DayComponent", "MonthComponent", "Interval", "Occurrences"], + "YEARLY_BY_POSITION": ["WeekdayComponent", "MonthComponent", "Position", "Interval", "Occurrences"], + } -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) + if "Position" in applicable_data[recurrence_pattern["RecurrenceType"]]: + row = box.row() + row.prop(self.props, "position") + + if "DayComponent" in applicable_data[recurrence_pattern["RecurrenceType"]]: + for i, component in enumerate(self.props.day_components): + if i % 7 == 0: + row = box.row(align=True) + row.prop(component, "is_specified", text=component.name) + + if "WeekdayComponent" in applicable_data[recurrence_pattern["RecurrenceType"]]: + row = box.row(align=True) + for component in self.props.weekday_components: + row.prop(component, "is_specified", text=component.name) + + if "MonthComponent" in applicable_data[recurrence_pattern["RecurrenceType"]]: + for i, component in enumerate(self.props.month_components): + if i % 4 == 0: + row = box.row(align=True) + row.prop(component, "is_specified", text=component.name) + + row = box.row() + row.prop(self.props, "interval") + row = box.row() + row.prop(self.props, "occurrences") + + def draw_editable_ui(self): + 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="") 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): diff --git a/src/blenderbim/blenderbim/bim/module/structural/__init__.py b/src/blenderbim/blenderbim/bim/module/structural/__init__.py index 816b5b374e..23c81445d0 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/structural/__init__.py @@ -20,13 +20,23 @@ classes = ( operator.EnableEditingStructuralConnectionCondition, operator.DisableEditingStructuralConnectionCondition, operator.RemoveStructuralConnectionCondition, + operator.EnableEditingStructuralMemberAxis, + operator.DisableEditingStructuralMemberAxis, + operator.EditStructuralMemberAxis, + operator.AddStructuralLoadCase, + operator.EditStructuralLoadCase, + operator.RemoveStructuralLoadCase, + operator.EnableEditingStructuralLoadCase, + operator.DisableEditingStructuralLoadCase, 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, + ui.BIM_PT_structural_load_cases, ) diff --git a/src/blenderbim/blenderbim/bim/module/structural/operator.py b/src/blenderbim/blenderbim/bim/module/structural/operator.py index a997d2e91f..4fdb84203b 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,210 @@ 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"} + + +class AssignStructuralLoadCase(bpy.types.Operator): + bl_idname = "bim.assign_structural_load_case" + bl_label = "Assign Structural Load Case" + work_plan: bpy.props.IntProperty() + load_case: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "aggregate.assign_object", + self.file, + **{ + "relating_object": self.file.by_id(self.work_plan), + "product": self.file.by_id(self.load_case), + }, + ) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class UnassignStructuralLoadCase(bpy.types.Operator): + bl_idname = "bim.unassign_structural_load_case" + bl_label = "Unassign Structural Load Case" + work_plan: bpy.props.IntProperty() + load_case: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "aggregate.unassign_object", + self.file, + **{ + "relating_object": self.file.by_id(self.work_plan), + "product": self.file.by_id(self.load_case), + }, + ) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class AddStructuralLoadCase(bpy.types.Operator): + bl_idname = "bim.add_structural_load_case" + bl_label = "Add Structural Load Case" + + def execute(self, context): + ifcopenshell.api.run("structural.add_structural_load_case", IfcStore.get_file()) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class EditStructuralLoadCase(bpy.types.Operator): + bl_idname = "bim.edit_structural_load_case" + bl_label = "Edit Structural Load Case" + + def execute(self, context): + props = context.scene.BIMStructuralProperties + attributes = {} + for attribute in props.load_case_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( + "structural.edit_structural_load_case", + self.file, + **{"load_case": self.file.by_id(props.active_load_case_id), "attributes": attributes}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_structural_load_case() + return {"FINISHED"} + + +class RemoveStructuralLoadCase(bpy.types.Operator): + bl_idname = "bim.remove_structural_load_case" + bl_label = "Remove Structural Load Case" + load_case: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "structural.remove_structural_load_case", self.file, load_case=self.file.by_id(self.load_case) + ) + Data.load(self.file) + return {"FINISHED"} + + +class EnableEditingStructuralLoadCase(bpy.types.Operator): + bl_idname = "bim.enable_editing_structural_load_case" + bl_label = "Enable Editing Structural Load Case" + load_case: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMStructuralProperties + self.props.active_load_case_id = self.load_case + while len(self.props.load_case_attributes) > 0: + self.props.load_case_attributes.remove(0) + self.enable_editing_structural_load_case() + return {"FINISHED"} + + def enable_editing_structural_load_case(self): + data = Data.load_cases[self.load_case] + print(data) + + for attribute in IfcStore.get_schema().declaration_by_name("IfcStructuralLoadCase").all_attributes(): + if attribute.name() in ["SelfWeightCoefficients", "Coefficient"]: + continue + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = self.props.load_case_attributes.add() + new.name = attribute.name() + new.is_null = data[attribute.name()] is None + new.is_optional = attribute.optional() + print(data_type) + 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()] + + +class DisableEditingStructuralLoadCase(bpy.types.Operator): + bl_idname = "bim.disable_editing_structural_load_case" + bl_label = "Disable Editing Structural Load Case" + + def execute(self, context): + context.scene.BIMStructuralProperties.active_load_case_id = 0 + return {"FINISHED"} \ No newline at end of file diff --git a/src/blenderbim/blenderbim/bim/module/structural/prop.py b/src/blenderbim/blenderbim/bim/module/structural/prop.py index f8cc972b87..c05101ea84 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") @@ -27,9 +41,20 @@ class BIMStructuralProperties(PropertyGroup): active_structural_analysis_model_index: IntProperty(name="Active Structural Analysis Model Index") active_structural_analysis_model_id: IntProperty(name="Active Structural Analysis Model Id") + # editing_type: StringProperty(name="Editing Type") + # active_load_case_index: IntProperty(name="Active Work Schedules Index") + + load_case_attributes: CollectionProperty(name="Load Case Attributes", type=Attribute) + active_load_case_id: IntProperty(name="Active Load Case Id") + class BIMObjectStructuralProperties(PropertyGroup): boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute) 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) + + # relating_structural_activity: PointerProperty(name="Relating Structural Activity", 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..4f33f899d1 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" @@ -223,3 +261,56 @@ class BIM_UL_structural_analysis_models(UIList): op.structural_analysis_model = item.ifc_definition_id op = row.operator("bim.remove_structural_analysis_model", text="", icon="X") op.structural_analysis_model = item.ifc_definition_id + + +class BIM_PT_structural_load_cases(Panel): + bl_label = "IFC Structural Load Cases" + bl_idname = "BIM_PT_structural_load_cases" + 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): + self.props = context.scene.BIMStructuralProperties + + if not Data.is_loaded: + Data.load(IfcStore.get_file()) + + row = self.layout.row() + row.operator("bim.add_structural_load_case", icon="ADD") + + for load_case_id, load_case in Data.load_cases.items(): + self.draw_load_case_ui(load_case_id, load_case) + + def draw_load_case_ui(self, load_case_id, load_case): + row = self.layout.row(align=True) + row.label(text=load_case["Name"] or "Unnamed", icon="CON_CLAMPTO") + + if self.props.active_load_case_id and self.props.active_load_case_id == load_case_id: + row.operator("bim.edit_structural_load_case", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_structural_load_case", text="", icon="CANCEL") + elif self.props.active_load_case_id: + row.operator("bim.remove_structural_load_case", text="", icon="X").load_case = load_case_id + else: + row.operator( + "bim.enable_editing_structural_load_case", text="", icon="GREASEPENCIL" + ).load_case = load_case_id + row.operator("bim.remove_structural_load_case", text="", icon="X").load_case = load_case_id + + if self.props.active_load_case_id == load_case_id: + self.draw_editable_load_case_ui() + + def draw_editable_load_case_ui(self): + for attribute in self.props.load_case_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/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() diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index 3851f1aff2..6514d2bc54 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -67,9 +67,11 @@ 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 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 @@ -104,8 +106,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"} @@ -115,7 +117,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 +141,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 @@ -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 diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 903f1788c4..c156781431 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -190,13 +190,22 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): svg_command: StringProperty(name="SVG Command", description="E.g. [['firefox-bin', path]]") pdf_command: StringProperty(name="PDF Command", description="E.g. [['firefox-bin', path]]") should_hide_empty_props: BoolProperty(name="Should Hide Empty Properties", default=True) + should_play_chaching_sound: BoolProperty( + name="Should Make A Cha-Ching Sound When Project Costs Updates", default=False + ) def draw(self, context): layout = self.layout row = layout.row() - row.label(text="To upgrade, first uninstall your current BlenderBIM Add-on, then install the new version.", icon="ERROR") + row.label( + text="To upgrade, first uninstall your current BlenderBIM Add-on, then install the new version.", + icon="ERROR", + ) row = layout.row() - row.label(text="To uninstall, first disable the add-on. Then restart Blender before pressing the 'Remove' button.", icon="ERROR") + row.label( + text="To uninstall, first disable the add-on. Then restart Blender before pressing the 'Remove' button.", + icon="ERROR", + ) row = layout.row() row.operator("bim.open_upstream", text="Visit Homepage").page = "home" row.operator("bim.open_upstream", text="Visit Documentation").page = "docs" 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() - 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/ifcconvert/IfcConvert.cpp b/src/ifcconvert/IfcConvert.cpp index 3359ce5052..e83cfea3ae 100644 --- a/src/ifcconvert/IfcConvert.cpp +++ b/src/ifcconvert/IfcConvert.cpp @@ -392,8 +392,11 @@ 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") + ("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.") @@ -1010,7 +1013,9 @@ 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); + 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/ifcgeom/IfcGeom.h b/src/ifcgeom/IfcGeom.h index b3731ff072..5f6689ee4b 100644 --- a/src/ifcgeom/IfcGeom.h +++ b/src/ifcgeom/IfcGeom.h @@ -352,7 +352,8 @@ public: IfcSchema::IfcSurfaceStyleShading* get_surface_style(IfcSchema::IfcRepresentationItem* item); const IfcSchema::IfcRepresentationItem* find_item_carrying_style(const IfcSchema::IfcRepresentationItem* item); bool create_solid_from_compound(const TopoDS_Shape& compound, TopoDS_Shape& solid); - bool create_solid_from_faces(const TopTools_ListOfShape& face_list, TopoDS_Shape& solid); + bool shape_to_face_list(const TopoDS_Shape& s, TopTools_ListOfShape& li); + bool create_solid_from_faces(const TopTools_ListOfShape& face_list, TopoDS_Shape& solid, bool force_sewing=false); bool is_compound(const TopoDS_Shape& shape); bool is_convex(const TopoDS_Wire& wire); TopoDS_Shape halfspace_from_plane(const gp_Pln& pln,const gp_Pnt& cent); diff --git a/src/ifcgeom/IfcGeomFunctions.cpp b/src/ifcgeom/IfcGeomFunctions.cpp index c2cfa9712b..2b98875e6e 100644 --- a/src/ifcgeom/IfcGeomFunctions.cpp +++ b/src/ifcgeom/IfcGeomFunctions.cpp @@ -529,22 +529,25 @@ void IfcGeom::Kernel::set_rotation(const std::array &p_rotation) { offset_and_rotation = combine_offset_and_rotation(offset, rotation); } -bool IfcGeom::Kernel::create_solid_from_compound(const TopoDS_Shape& compound, TopoDS_Shape& shape) { - TopTools_ListOfShape face_list; - TopExp_Explorer exp(compound, TopAbs_FACE); +bool IfcGeom::Kernel::shape_to_face_list(const TopoDS_Shape& s, TopTools_ListOfShape& li) { + TopExp_Explorer exp(s, TopAbs_FACE); for (; exp.More(); exp.Next()) { TopoDS_Face face = TopoDS::Face(exp.Current()); - face_list.Append(face); + li.Append(face); } + return true; +} +bool IfcGeom::Kernel::create_solid_from_compound(const TopoDS_Shape& compound, TopoDS_Shape& shape) { + TopTools_ListOfShape face_list; + shape_to_face_list(compound, face_list); if (face_list.Extent() == 0) { return false; } - return create_solid_from_faces(face_list, shape); } -bool IfcGeom::Kernel::create_solid_from_faces(const TopTools_ListOfShape& face_list, TopoDS_Shape& shape) { +bool IfcGeom::Kernel::create_solid_from_faces(const TopTools_ListOfShape& face_list, TopoDS_Shape& shape, bool force_sewing) { bool valid_shell = false; if (face_list.Extent() == 1) { @@ -565,7 +568,7 @@ bool IfcGeom::Kernel::create_solid_from_faces(const TopTools_ListOfShape& face_l // found a case where this actually improves boolean ops later on. // if (!faceset_helper_ || !faceset_helper_->non_manifold()) { - for (face_iterator.Initialize(face_list); face_iterator.More(); face_iterator.Next()) { + for (face_iterator.Initialize(face_list); !force_sewing && face_iterator.More(); face_iterator.Next()) { // As soon as is detected one of the edges is shared, the assumption is made no // additional sewing is necessary. if (!has_shared_edges) { diff --git a/src/ifcgeom/IfcGeomShapes.cpp b/src/ifcgeom/IfcGeomShapes.cpp index 81dca71450..5dd6187ed3 100644 --- a/src/ifcgeom/IfcGeomShapes.cpp +++ b/src/ifcgeom/IfcGeomShapes.cpp @@ -240,18 +240,12 @@ bool IfcGeom::Kernel::convert(const IfcSchema::IfcExtrudedAreaSolidTapered* l, T TopoDS_Shape result = builder.Shape(); - BRepOffsetAPI_Sewing sewer; - sewer.SetTolerance(getValue(GV_PRECISION)); - sewer.SetMaxTolerance(getValue(GV_PRECISION)); - sewer.SetMinTolerance(getValue(GV_PRECISION)); - - sewer.Add(result); - sewer.Add(BRepBuilderAPI_MakeFace(w1).Face()); - sewer.Add(BRepBuilderAPI_MakeFace(w2).Face().Moved(end_profile)); - - sewer.Perform(); - - result = sewer.SewedShape(); + TopTools_ListOfShape li; + shape_to_face_list(result, li); + li.Append(BRepBuilderAPI_MakeFace(w1).Face().Reversed()); + li.Append(BRepBuilderAPI_MakeFace(w2).Face().Moved(end_profile)); + + create_solid_from_faces(li, result, true); // @todo ugly hack diff --git a/src/ifcgeom/IfcGeomTree.h b/src/ifcgeom/IfcGeomTree.h index 8911183b58..049066261b 100644 --- a/src/ifcgeom/IfcGeomTree.h +++ b/src/ifcgeom/IfcGeomTree.h @@ -30,6 +30,7 @@ #include #include #include +#include #include namespace IfcGeom { @@ -38,6 +39,30 @@ namespace IfcGeom { template class tree { + bool test(const TopoDS_Shape& A, const TopoDS_Shape& B, bool completely_within, double extend) const { + if (extend > 0.) { + BRepExtrema_DistShapeShape dss(A, B); + if (dss.Perform() && dss.NbSolution() >= 1) { + return dss.Value() <= extend; + } + } else if (completely_within) { + BRepAlgoAPI_Cut cut(B, A); + if (cut.IsDone()) { + if (IfcGeom::Kernel::count(cut.Shape(), TopAbs_SHELL) == 0) { + return true; + } + } + } else { + BRepAlgoAPI_Common common(A, B); + if (common.IsDone()) { + if (IfcGeom::Kernel::count(common.Shape(), TopAbs_SHELL) > 0) { + return true; + } + } + } + return false; + } + public: void add(const T& t, const Bnd_Box& b) { @@ -104,8 +129,8 @@ namespace IfcGeom { } } - std::vector select(const T& t, bool completely_within = false) const { - std::vector ts = select_box(t); + std::vector select(const T& t, bool completely_within = false, double extend = 0.0) const { + std::vector ts = select_box(t, completely_within, extend); if (ts.empty()) { return ts; } @@ -126,29 +151,18 @@ namespace IfcGeom { continue; } - if (completely_within) { - BRepAlgoAPI_Cut cut(B, A); - if (cut.IsDone()) { - if (IfcGeom::Kernel::count(cut.Shape(), TopAbs_SHELL) == 0) { - ts_filtered.push_back(*it); - } - } - } else { - BRepAlgoAPI_Common common(A, B); - if (common.IsDone()) { - if (IfcGeom::Kernel::count(common.Shape(), TopAbs_SHELL) > 0) { - ts_filtered.push_back(*it); - } - } + if (test(A, B, completely_within, extend)) { + ts_filtered.push_back(*it); } } return ts_filtered; } - std::vector select(const TopoDS_Shape& s) const { + std::vector select(const TopoDS_Shape& s, bool completely_within = false, double extend = -1.e-5) const { Bnd_Box bb; BRepBndLib::AddClose(s, bb); + bb.SetGap(bb.GetGap() + extend); std::vector ts; @@ -156,7 +170,7 @@ namespace IfcGeom { return ts; } - ts = select_box(bb); + ts = select_box(bb, completely_within); if (ts.empty()) { return ts; @@ -168,16 +182,13 @@ namespace IfcGeom { typename std::vector::const_iterator it = ts.begin(); for (it = ts.begin(); it != ts.end(); ++it) { const TopoDS_Shape& B = shapes_.find(*it)->second; - + if (IfcGeom::Kernel::count(B, TopAbs_SHELL) == 0) { continue; } - BRepAlgoAPI_Common common(s, B); - if (common.IsDone()) { - if (IfcGeom::Kernel::count(common.Shape(), TopAbs_SHELL) > 0) { - ts_filtered.push_back(*it); - } + if (test(s, B, completely_within, extend)) { + ts_filtered.push_back(*it); } } @@ -258,6 +269,10 @@ namespace IfcGeom { add_file(f, settings); } + tree(IfcGeom::Iterator& it) { + add_file(it); + } + void add_file(IfcParse::IfcFile& f, const IfcGeom::IteratorSettings& settings) { IfcGeom::IteratorSettings settings_ = settings; settings_.set(IfcGeom::IteratorSettings::DISABLE_TRIANGULATION, true); @@ -266,10 +281,14 @@ namespace IfcGeom { IfcGeom::Iterator it(settings_, &f); + add_file(it); + } + + void add_file(IfcGeom::Iterator& it) { if (it.initialize()) { do { IfcGeom::BRepElement* elem = (IfcGeom::BRepElement*)it.get(); - add((IfcUtil::IfcBaseEntity*)f.instance_by_id(elem->id()), elem->geometry().as_compound()); + add((IfcUtil::IfcBaseEntity*)it.file()->instance_by_id(elem->id()), elem->geometry().as_compound()); } while (it.next()); } } 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) 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()} diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py index 48b5ef8eec..47e2fd31a4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py @@ -1,5 +1,4 @@ import ifcopenshell -import blenderbim.bim.schema # TODO: refactor class Usecase: diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py index b55522da08..d176e011c9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py @@ -1,5 +1,4 @@ import ifcopenshell -import blenderbim.bim.schema # TODO: refactor class Usecase: diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/data.py b/src/ifcopenshell-python/ifcopenshell/api/pset/data.py index 9b1410fe77..d76a350360 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/data.py @@ -1,6 +1,6 @@ import ifcopenshell import ifcopenshell.util.attribute -import blenderbim.bim.schema # TODO: refactor elsewhere +import ifcopenshell.util.pset class Data: @@ -17,6 +17,7 @@ class Data: @classmethod def load(cls, file, product_id): cls._file = file + cls._psetqto = ifcopenshell.util.pset.get_template("IFC4") cls._schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(file.schema) if not file: return @@ -150,7 +151,7 @@ class Data: @classmethod def get_properties_from_template(cls, name): - template = blenderbim.bim.schema.ifc.psetqto.get_by_name(name) + template = cls._psetqto.get_by_name(name) if not template: return properties = [] diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py index dd76185234..adf235b046 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py @@ -1,4 +1,4 @@ -import blenderbim.bim.schema # TODO: refactor +import ifcopenshell class Usecase: @@ -20,7 +20,9 @@ class Usecase: self.settings["pset"].Name = self.settings["Name"] def load_pset_template(self): - self.pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(self.settings["pset"].Name) + # TODO: add IFC2X3 PsetQto template support + self.psetqto = ifcopenshell.util.pset.get_template("IFC4") + self.pset_template = self.psetqto.get_by_name(self.settings["pset"].Name) def update_existing_properties(self): for prop in self.get_properties(): diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/edit_qto.py b/src/ifcopenshell-python/ifcopenshell/api/pset/edit_qto.py index bd43f646af..67650ef10b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/edit_qto.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/edit_qto.py @@ -1,4 +1,4 @@ -import blenderbim.bim.schema # TODO: refactor +import ifcopenshell class Usecase: @@ -20,7 +20,9 @@ class Usecase: self.settings["qto"].Name = self.settings["Name"] def load_qto_template(self): - self.qto_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(self.settings["qto"].Name) + # TODO: add IFC2X3 PsetQto template support + self.psetqto = ifcopenshell.util.pset.get_template("IFC4") + self.qto_template = self.psetqto.get_by_name(self.settings["qto"].Name) def update_existing_properties(self): for prop in self.settings["qto"].Quantities or []: diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py index 2c742a65cb..884be7b8c9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py @@ -1,6 +1,3 @@ -import blenderbim.bim.schema # TODO: refactor - - class Usecase: def __init__(self, file, **settings): self.file = file diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py index 501af373c4..a59b519e94 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/data.py @@ -5,7 +5,14 @@ class Data: is_loaded = False work_plans = {} work_schedules = {} + work_calendars = {} + work_times = {} + recurrence_patterns = {} + time_periods = {} tasks = {} + task_times = {} + lag_times = {} + sequences = {} @classmethod def purge(cls): @@ -13,7 +20,13 @@ class Data: cls.work_plans = {} cls.work_schedules = {} cls.work_calendars = {} + cls.work_times = {} + cls.recurrence_patterns = {} + cls.time_periods = {} cls.tasks = {} + cls.task_times = {} + cls.lag_times = {} + cls.sequences = {} @classmethod def load(cls, file): @@ -23,7 +36,13 @@ class Data: cls.load_work_plans() cls.load_work_schedules() cls.load_work_calendars() + cls.load_work_times() + cls.load_recurrence_patterns() + cls.load_time_periods() cls.load_tasks() + cls.load_task_times() + cls.load_lag_times() + cls.load_sequences() cls.is_loaded = True @classmethod @@ -38,6 +57,9 @@ class Data: data["StartTime"] = ifcopenshell.util.date.ifc2datetime(data["StartTime"]) if data["FinishTime"]: data["FinishTime"] = ifcopenshell.util.date.ifc2datetime(data["FinishTime"]) + data["IsDecomposedBy"] = [] + for rel in work_plan.IsDecomposedBy: + data["IsDecomposedBy"].extend([o.id() for o in rel.RelatedObjects]) cls.work_plans[work_plan.id()] = data @classmethod @@ -52,19 +74,111 @@ 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 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"] - del data["WorkingTimes"] - del data["ExceptionTimes"] + data["WorkingTimes"] = [t.id() for t in work_calendar.WorkingTimes or []] + data["ExceptionTimes"] = [t.id() for t in work_calendar.ExceptionTimes or []] cls.work_calendars[work_calendar.id()] = data + @classmethod + def load_work_times(cls): + cls.work_times = {} + for work_time in cls._file.by_type("IfcWorkTime"): + data = work_time.get_info() + data["Start"] = ifcopenshell.util.date.ifc2datetime(data["Start"]) if data["Start"] else None + data["Finish"] = ifcopenshell.util.date.ifc2datetime(data["Finish"]) if data["Finish"] else None + data["RecurrencePattern"] = work_time.RecurrencePattern.id() if work_time.RecurrencePattern else None + cls.work_times[work_time.id()] = data + + @classmethod + def load_recurrence_patterns(cls): + cls.recurrence_patterns = {} + for recurrence_pattern in cls._file.by_type("IfcRecurrencePattern"): + data = recurrence_pattern.get_info() + data["TimePeriods"] = [t.id() for t in recurrence_pattern.TimePeriods or []] + cls.recurrence_patterns[recurrence_pattern.id()] = data + + @classmethod + def load_time_periods(cls): + cls.time_periods = {} + for time_period in cls._file.by_type("IfcTimePeriod"): + cls.time_periods[time_period.id()] = { + "StartTime": ifcopenshell.util.date.ifc2datetime(time_period.StartTime), + "EndTime": ifcopenshell.util.date.ifc2datetime(time_period.EndTime), + } + @classmethod 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["HasAssignmentsWorkCalendar"] = [] + 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.id()) for rel in task.IsPredecessorTo or []] + [data["IsSuccessorFrom"].append(rel.id()) for rel in task.IsSuccessorFrom or []] + [ + data["HasAssignmentsWorkCalendar"].append(rel.RelatingControl.id()) + for rel in task.HasAssignments or [] + if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkCalendar") + ] + 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() + 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) + elif key == "ScheduleDuration": + data[key] = ifcopenshell.util.date.ifc2datetime(value) + cls.task_times[task_time.id()] = data + + @classmethod + def load_lag_times(cls): + cls.lag_times = {} + for lag_time in cls._file.by_type("IfcLagTime"): + data = lag_time.get_info() + if data["LagValue"]: + if data["LagValue"].is_a("IfcDuration"): + data["LagValue"] = ifcopenshell.util.date.ifc2datetime(data["LagValue"].wrappedValue) + else: + data["LagValue"] = float(data["LagValue"].wrappedValue) + cls.lag_times[lag_time.id()] = data + + @classmethod + def load_sequences(cls): + cls.sequences = {} + for sequence in cls._file.by_type("IfcRelSequence"): + data = sequence.get_info() + data["RelatingProcess"] = sequence.RelatingProcess.id() + data["RelatedProcess"] = sequence.RelatedProcess.id() + data["TimeLag"] = sequence.TimeLag.id() if sequence.TimeLag else None + cls.sequences[sequence.id()] = data diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py index ddd36ad88e..d49dccd7da 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py @@ -1,3 +1,6 @@ +import ifcopenshell + + class Usecase: def __init__(self, file, **settings): self.file = file diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/data.py b/src/ifcopenshell-python/ifcopenshell/api/structural/data.py index 0165ab64f9..8b5a567e1c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/data.py @@ -6,6 +6,12 @@ class Data: boundary_conditions = {} connects_structural_members = {} + load_cases = {} + # load_case_combinations = {} + # load_groups = {} + # structural_activities = {} + # connects_structural_activities = {} + @classmethod def purge(cls): cls.is_loaded = False @@ -14,6 +20,7 @@ class Data: cls.connections = {} cls.boundary_conditions = {} cls.connects_structural_members = {} + cls.load_cases = {} @classmethod def load(cls, file, product_id=None): @@ -23,6 +30,7 @@ class Data: if product_id: return cls.load_structural_connection(product_id) cls.load_structural_analysis_models() + cls.load_structural_load_cases() cls.is_loaded = True @classmethod @@ -53,6 +61,27 @@ class Data: cls.structural_analysis_models[model.id()] = data + @classmethod + def load_structural_load_cases(cls): + cls.load_cases = {} + + for case in cls._file.by_type("IfcStructuralLoadCase"): + # if case.IsGroupedBy: + # for rel in case.IsGroupedBy: + # for product in rel.RelatedObjects: + # cls.products.setdefault(product.id(), []).append(case.id()) + data = case.get_info() + del data["OwnerHistory"] + + is_grouped_by = [] + for load_group in case.IsGroupedBy or []: + is_grouped_by.append(load_group.id()) + data["IsGroupedBy"] = is_grouped_by + + cls.load_cases[case.id()] = data + + print(data) + @classmethod def load_structural_connection(cls, product_id): cls.connections = {} diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index e8040d2814..56bfed239a 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -134,6 +134,9 @@ class tree(ifcopenshell_wrapper.tree): def add_file(self, file, settings): ifcopenshell_wrapper.tree.add_file(self, file.wrapped_data, settings) + + def add_iterator(self, iterator): + ifcopenshell_wrapper.tree.add_file(self, iterator) def select(self, value, **kwargs): def unwrap(value): diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 542793a664..1654b9179b 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -52,12 +52,18 @@ class facet(metaclass=meta_facet): self.node = node def __getattr__(self, k): - v = self.node.getElementsByTagName(k)[0] - elems = [n for n in v.childNodes if n.nodeType == n.ELEMENT_NODE] - if elems: - return restriction(elems[0]) + try: + v = self.node.getElementsByTagName(k)[0] + except IndexError: + v = None + if v: + elems = [n for n in v.childNodes if n.nodeType == n.ELEMENT_NODE] + if elems: + return restriction(elems[0]) + else: + return v.firstChild.nodeValue.strip() else: - return v.firstChild.nodeValue.strip() + return None def __iter__(self): for k in self.parameters: @@ -76,15 +82,18 @@ class entity(facet): The IDS entity facet currently *with* inheritance """ - parameters = ["name"] - message = "an entity name '%(name)s'" - + parameters = ["name", "predefinedtype"] + def __call__(self, inst, logger): - logger.debug("Testing %s == %s", inst.is_a(), self.name) # @nb with inheritance - # return inst.is_a() == self.name - return facet_evaluation(inst.is_a(self.name), self.message % {"name": inst.is_a()}) - + if self.predefinedtype and hasattr(inst, "PredefinedType"): + # logger.debug("Testing if entity predefinedtype '%s' == '%s'", inst.PredefinedType, self.predefinedtype) + self.message = "an entity name '%(name)s' of predefined type '%(predefinedtype)s'" + return facet_evaluation(inst.is_a(self.name) and inst.PredefinedType == self.predefinedtype, self.message % {"name": inst.is_a(), "predefinedtype": inst.PredefinedType}) + else: + self.message = "an entity name '%(name)s'" + return facet_evaluation(inst.is_a(self.name), self.message % {"name": inst.is_a()}) + class classification(facet): """ @@ -122,7 +131,7 @@ class property(facet): props = ifcopenshell.util.element.get_psets(inst) pset = props.get(self.propertyset) val = pset.get(self.name) if pset else None - logger.debug("Testing %s == %s", val, self.value) + logger.debug("Testing if property %s == %s", val, self.value) di = { "name": self.name, @@ -134,7 +143,7 @@ class property(facet): msg = self.message % di else: if pset: - msg = "a set '%(propertyset)s', but no property '%(name)'" % di + msg = "a set '%(propertyset)s', but no property '%(name)s'" % di else: msg = "no set '%(propertyset)s'" % di @@ -152,13 +161,13 @@ class material(facet): material_relations = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] names = [] for rel in material_relations: + # @todo not all subtypes of IfcMaterial handled 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 @@ -215,8 +224,7 @@ class restriction: 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 @@ -250,19 +258,19 @@ 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({'guid':inst.GlobalId, 'result':valid.success,'sentence':str(self) + "\n%s has" % inst + " " + str(valid) + " so is compliant"}) + logger.info({'guid':inst.GlobalId, 'result':valid.success,'sentence':str(self) + "\n'" + inst.Name + "' (id:" + inst.GlobalId + ") has " + str(valid) + " so is compliant"}) else: - logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + "\n%s has" % inst + " " + str(valid) + " so is not compliant"}) + logger.error({'guid':inst.GlobalId, 'result':valid.success, 'sentence':str(self) + "\n'" + inst.Name + "' (id:" + inst.GlobalId + ") has " + 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: @@ -283,7 +291,7 @@ class ids: for spec in self.specifications: for elem in ifc_file.by_type("IfcObject"): spec(elem, logger) - + if __name__ == "__main__": import sys, os import logging @@ -297,4 +305,7 @@ if __name__ == "__main__": ids_file = ids(sys.argv[1]) ifc_file = ifcopenshell.open(sys.argv[2]) + ids_file.validate(ifc_file, logger) + + print("Validated %s IDS requirements on %s IFC elements. Results saved to %s" % (len(ids_file.specifications[0].requirements.terms), len(ifc_file.by_type('IfcProduct')), filename)) \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/util/date.py b/src/ifcopenshell-python/ifcopenshell/util/date.py index d1b0ad5d7e..65a63b75c2 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/date.py +++ b/src/ifcopenshell-python/ifcopenshell/util/date.py @@ -1,23 +1,42 @@ +import datetime from re import findall -from datetime import datetime + +try: + import isodate +except: + pass # Duration parsing not supported -def duration2dict(duration): - results = {} - for number, unit in findall("(?P\d+)(?PS|M|H|D|W|Y)", duration): - results[unit] = number - return results +def timedelta2duration(timedelta): + components = { + "days": getattr(timedelta, "days", 0), + "hours": 0, + "minutes": 0, + "seconds": getattr(timedelta, "seconds", 0), + } + if components["seconds"]: + components["hours"], components["minutes"], components["seconds"] = [ + int(i) for i in str(datetime.timedelta(seconds=components["seconds"])).split(":") + ] + return isodate.Duration(**components) def ifc2datetime(element): if isinstance(element, str) and element[0] == "P": # IfcDuration - return duration2dict(element) - elif isinstance(element, str): # IfcDateTime, IfcDate - return datetime.fromisoformat(element) + duration = isodate.parse_duration(element) + if isinstance(duration, datetime.timedelta): + return timedelta2duration(duration) + return duration + elif isinstance(element, str) and element[2] == ":": # IfcTime + return datetime.time.fromisoformat(element) + elif isinstance(element, str) and ":" in element: # IfcDateTime + return datetime.datetime.fromisoformat(element) + elif isinstance(element, str): # IfcDate + return datetime.date.fromisoformat(element) elif isinstance(element, int): # IfcTimeStamp - return datetime.fromtimestamp(element) + return datetime.datetime.fromtimestamp(element) elif element.is_a("IfcDateAndTime"): - return datetime( + return datetime.datetime( element.DateComponent.YearComponent, element.DateComponent.MonthComponent, element.DateComponent.DayComponent, @@ -27,7 +46,7 @@ def ifc2datetime(element): # TODO: implement TimeComponent timezone ) elif element.is_a("IfcCalendarDate"): - return datetime( + return datetime.date( element.YearComponent, element.MonthComponent, element.DayComponent, @@ -36,15 +55,29 @@ def ifc2datetime(element): def datetime2ifc(dt, ifc_type): if isinstance(dt, str): - dt = datetime.fromisoformat(dt) - if ifc_type == "IfcTimeStamp": + if ifc_type == "IfcDuration": + return dt + dt = datetime.datetime.fromisoformat(dt) + + if ifc_type == "IfcDuration": + return isodate.duration_isoformat(dt) + elif ifc_type == "IfcTimeStamp": return int(dt.timestamp()) elif ifc_type == "IfcDateTime": - return dt.isoformat() + if isinstance(dt, datetime.datetime): + return dt.isoformat() + elif isinstance(dt, datetime.date): + return datetime.datetime.combine(dt, datetime.datetime.min.time()).isoformat() elif ifc_type == "IfcDate": - return dt.date().isoformat() + if isinstance(dt, datetime.datetime): + return dt.date().isoformat() + elif isinstance(dt, datetime.date): + return dt.isoformat() elif ifc_type == "IfcTime": - return dt.time().isoformat() + if isinstance(dt, datetime.datetime): + return dt.time().isoformat() + elif isinstance(dt, datetime.time): + return dt.isoformat() elif ifc_type == "IfcCalendarDate": return {"DayComponent": dt.day, "MonthComponent": dt.month, "YearComponent": dt.year} elif ifc_type == "IfcLocalTime": diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 788acf7f34..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: @@ -119,17 +124,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() diff --git a/src/ifcopenshell-python/ifcopenshell/util/pset.py b/src/ifcopenshell-python/ifcopenshell/util/pset.py index 43e0afc529..1f6bf735bb 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/pset.py +++ b/src/ifcopenshell-python/ifcopenshell/util/pset.py @@ -6,6 +6,15 @@ from typing import List, Generator, Optional import ifcopenshell from ifcopenshell.entity_instance import entity_instance +templates = {} + + +def get_template(schema): + global templates + if schema not in templates: + templates[schema] = PsetQto(schema) + return templates[schema] + class PsetQto: templates_path = { diff --git a/src/ifcparse/Argument.h b/src/ifcparse/Argument.h index ce2da04206..864a991373 100644 --- a/src/ifcparse/Argument.h +++ b/src/ifcparse/Argument.h @@ -45,6 +45,7 @@ namespace IfcUtil { IFC_PARSE_API const char* ArgumentTypeToString(ArgumentType argument_type); + /// Returns false when the string `s` contains character outside of {'0', '1'} IFC_PARSE_API bool valid_binary_string(const std::string& s); } diff --git a/src/ifcparse/IfcCharacterDecoder.cpp b/src/ifcparse/IfcCharacterDecoder.cpp index 1c2fd38228..f4057ac481 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; 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/IfcHierarchyHelper.cpp b/src/ifcparse/IfcHierarchyHelper.cpp index 6e75e4e003..54a5eabf2f 100644 --- a/src/ifcparse/IfcHierarchyHelper.cpp +++ b/src/ifcparse/IfcHierarchyHelper.cpp @@ -484,6 +484,12 @@ Ifc4x3_rc1::IfcStyledItem* create_styled_item(Ifc4x3_rc1::IfcRepresentationItem* return new Ifc4x3_rc1::IfcStyledItem(item, style_assignments, boost::none); } +Ifc4x3_rc2::IfcStyledItem* create_styled_item(Ifc4x3_rc2::IfcRepresentationItem* item, Ifc4x3_rc2::IfcPresentationStyleAssignment* style_assignment) { + IfcEntityList::ptr style_assignments(new IfcEntityList); + style_assignments->push(style_assignment); + return new Ifc4x3_rc2::IfcStyledItem(item, style_assignments, boost::none); +} + template void IfcHierarchyHelper::setSurfaceColour(typename Schema::IfcRepresentation* rep, typename Schema::IfcPresentationStyleAssignment* style_assignment) @@ -581,3 +587,4 @@ template IFC_PARSE_API class IfcHierarchyHelper; template IFC_PARSE_API class IfcHierarchyHelper; template IFC_PARSE_API class IfcHierarchyHelper; template IFC_PARSE_API class IfcHierarchyHelper; +template IFC_PARSE_API class IfcHierarchyHelper; diff --git a/src/ifcparse/IfcHierarchyHelper.h b/src/ifcparse/IfcHierarchyHelper.h index 392e663d18..0ff2514274 100644 --- a/src/ifcparse/IfcHierarchyHelper.h +++ b/src/ifcparse/IfcHierarchyHelper.h @@ -37,6 +37,7 @@ #include "../ifcparse/Ifc4x1.h" #include "../ifcparse/Ifc4x2.h" #include "../ifcparse/Ifc4x3_rc1.h" +#include "../ifcparse/Ifc4x3_rc2.h" #include "../ifcparse/IfcFile.h" diff --git a/src/ifcparse/IfcParse.h b/src/ifcparse/IfcParse.h index 9d537f8a29..0403cd56fb 100644 --- a/src/ifcparse/IfcParse.h +++ b/src/ifcparse/IfcParse.h @@ -178,7 +178,7 @@ namespace IfcParse { public: ArgumentList() : size_(0), list_(0) {} - ArgumentList(size_t n) : size_(n), list_(new Argument*[size_]) {} + ArgumentList(size_t n) : size_(n), list_(new Argument*[size_] {0}) {} ~ArgumentList(); void read(IfcSpfLexer* t, std::vector& ids); 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; - } } } diff --git a/src/ifcpatch/ifcpatch/__init__.py b/src/ifcpatch/ifcpatch/__init__.py index 1af72f9bf0..d8cf8fa5e4 100644 --- a/src/ifcpatch/ifcpatch/__init__.py +++ b/src/ifcpatch/ifcpatch/__init__.py @@ -17,7 +17,7 @@ def execute(args, is_library=None): patcher = recipe.Patcher(args["input"], ifc_file, logger, args["arguments"]) print("# Patching ...") patcher.patch() - ifc_file = patcher.file + ifc_file = getattr(patcher, "file_patched", patcher.file) if is_library is True: return ifc_file print("# Writing patched file ...") diff --git a/src/ifcpatch/ifcpatch/recipes/Migrate.py b/src/ifcpatch/ifcpatch/recipes/Migrate.py index 610cb4934d..7d238e2204 100644 --- a/src/ifcpatch/ifcpatch/recipes/Migrate.py +++ b/src/ifcpatch/ifcpatch/recipes/Migrate.py @@ -10,9 +10,9 @@ class Patcher: self.args = args def patch(self): - self.new = ifcopenshell.file(schema=self.args[0]) + self.file_patched = ifcopenshell.file(schema=self.args[0]) migrator = ifcopenshell.util.schema.Migrator() for element in self.file: + migrator.migrate(element, self.file_patched) print("Migrating", element) - print("Successfully converted to", migrator.migrate(element, self.new)) - self.file = self.new + print("Successfully converted to", migrator.migrate(element, self.file_patched)) diff --git a/src/serializers/SvgSerializer.cpp b/src/serializers/SvgSerializer.cpp index 3b3d8ae8fd..2a5c18dc6e 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; @@ -574,6 +574,11 @@ void SvgSerializer::write(const IfcGeom::BRepElement* brep_obj) { b->second[0] - b->first[0], b->second[1] - b->first[1] ); + +#if OCC_VERSION_HEX >= 0x70300 + view_box_3d_.emplace(); + BRepBndLib::AddOBB(compound_unmirrored, *view_box_3d_, false, false, false); +#endif } std::vector props; @@ -603,14 +608,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 +667,9 @@ void SvgSerializer::write(const IfcGeom::BRepElement* brep_obj) { element_buffer_.push_back(data); } - write(data); + if (emit_building_storeys_) { + write(data); + } } namespace { @@ -687,6 +717,17 @@ void SvgSerializer::write(const geometry_data& data) { // (When determinant < 0, copy is implied and the input is not mutated.) auto compound_unmirrored = make_transform_global.Shape(); +#if OCC_VERSION_HEX >= 0x70300 + if (view_box_3d_) { + Bnd_OBB obb; + BRepBndLib::AddOBB(compound_unmirrored, obb, false, false, false); + if (view_box_3d_->IsOut(obb)) { + Logger::Notice("Not including element due to viewBox", data.product); + return; + } + } +#endif + if (is_floor_plan_) { BRepBndLib::Add(compound_unmirrored, bnd_); } @@ -1015,10 +1056,15 @@ void SvgSerializer::write(const geometry_data& data) { object_type.erase(std::remove_if(object_type.begin(), object_type.end(), [](char c) { return !std::isalnum(c); }), object_type.end()); } + auto z_local = gp::DZ().Transformed(data.trsf.Inverted()); + if (data.product->declaration().is("IfcAnnotation") && // is an Annotation (proj.Magnitude() > 1.e-5) && // when projected onto the view has a length - zmin >= range.first && zmin < (range.second - 1.e-5)) // the Z-coords are within the range of the building storey, - // this excludes the upper bound with a small tolerance + is_floor_plan_ + ? (zmin >= range.first && zmin < (range.second - 1.e-5)) // the Z-coords are within the range of the building storey, + // this excludes the upper bound with a small tolerance + : (projection_direction.Dot(z_local) < -0.99) // For elevations only include annotations that are "facing" the view direction + ) { auto svg_name = data.svg_name; @@ -1035,9 +1081,24 @@ void SvgSerializer::write(const geometry_data& data) { } } + auto subshape_to_use = subshape; + if (variant.which() == 2) { + // @todo remove duplication with code below. + + gp_Trsf trsf; + trsf.SetTransformation(gp::XOY(), pln.Position()); + subshape_to_use.Move(trsf); + + gp_Trsf trsf_mirror; + trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY())); + BRepBuilderAPI_Transform make_transform_mirror(subshape_to_use, trsf_mirror, true); + make_transform_mirror.Build(); + subshape_to_use = make_transform_mirror.Shape(); + } + if (object_type == "Dimension") { - TopExp_Explorer exp(subshape, TopAbs_EDGE, TopAbs_FACE); + TopExp_Explorer exp(subshape_to_use, TopAbs_EDGE, TopAbs_FACE); for (; exp.More(); exp.Next()) { const auto& e = TopoDS::Edge(exp.Current()); TopoDS_Vertex v0, v1; @@ -1110,7 +1171,7 @@ void SvgSerializer::write(const geometry_data& data) { } else if (object_type == "Symbol") { - TopExp_Explorer exp(subshape, TopAbs_WIRE, TopAbs_FACE); + TopExp_Explorer exp(subshape_to_use, TopAbs_WIRE, TopAbs_FACE); for (; exp.More(); exp.Next()) { const auto& W = TopoDS::Wire(exp.Current()); write(*po, W, *dash_it); @@ -1408,16 +1469,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_; } @@ -1618,87 +1685,92 @@ void SvgSerializer::addTextAnnotations(const drawing_key& k) { gp_Trsf trsf; if (kernel.convert_placement(*pl, trsf)) { - auto v = trsf.TranslationPart(); - if (k.first) { - v.ChangeCoord(1) *= -1.; - trsf.SetTranslationPart(v); - } + auto v = gp_Pnt(trsf.TranslationPart()); - if (!range || (v.Z() >= range->first && v.Z() < range->second)) { + auto z_local = gp::DZ().Transformed(trsf); + auto view_dir = z_local.Dot(meta.pln_3d.Axis().Direction()); - if (meta.pln_3d.Position().Direction().Dot(gp_Dir(trsf.HVectorialPart().Column(3))) > 0.99) { - auto svg_name = nameElement(ann); - path_object* po; - if (k.first) { - po = &start_path(meta.pln_3d, k.first, svg_name); - } - else { - po = &start_path(meta.pln_3d, k.second, svg_name); - } + if ((!range || (v.Z() >= range->first && v.Z() < range->second)) && view_dir > 0.99) { - if (object_type.size()) { - // postfix the object_type for CSS matching - boost::replace_all(svg_name, "class=\"IfcAnnotation\"", "class=\"IfcAnnotation " + object_type + "\""); - } + gp_Trsf trsf_view; + trsf_view.SetTransformation(gp::XOY(), meta.pln_3d.Position()); + v.Transform(trsf_view); - boost::optional font_size; - std::vector tokens; - boost::split(tokens, name, boost::is_any_of("_")); - if (tokens.size() == 2) { - try { - font_size = boost::lexical_cast(tokens.back()); - } - catch (...) {} - } - - // @todo column or row? - double z_rotation = gp_Dir(trsf.HVectorialPart().Column(1)).AngleWithRef(gp_Dir(1., 0., 0.), gp_Dir(0., 0., 1.)); - z_rotation *= 180. / M_PI; - - util::string_buffer path; - // dominant-baseline="central" is not well supported in IE. - // so we add a 0.35 offset to the dy of the tspans - path.add(" "); - - std::vector labels{ desc }; - - for (auto lit = labels.begin(); lit != labels.end(); ++lit) { - const auto& l = *lit; - double dy = labels.begin() == lit - ? 0.0 // align bottom - : 1.0; // <- dy is relative to the previous text element, so - // always 1 for successive spans. - path.add("(dy)); - path.add("em\">"); - path.add(l); - path.add(""); - } - - path.add(""); - - po->second.push_back(path); + auto svg_name = nameElement(ann); + path_object* po; + if (k.first) { + po = &start_path(meta.pln_3d, k.first, svg_name); + } else { + po = &start_path(meta.pln_3d, k.second, svg_name); } + + if (object_type.size()) { + // postfix the object_type for CSS matching + boost::replace_all(svg_name, "class=\"IfcAnnotation\"", "class=\"IfcAnnotation " + object_type + "\""); + } + + boost::optional font_size; + std::vector tokens; + boost::split(tokens, name, boost::is_any_of("_")); + if (tokens.size() == 2) { + try { + font_size = boost::lexical_cast(tokens.back()); + } + catch (...) {} + } + + // @todo column or row? + double z_rotation = gp::DX().Transformed(trsf).AngleWithRef( + meta.pln_3d.Position().XDirection(), + meta.pln_3d.Position().Direction() + ); + z_rotation *= 180. / M_PI; + + auto y = -v.Y(); + + util::string_buffer path; + // dominant-baseline="central" is not well supported in IE. + // so we add a 0.35 offset to the dy of the tspans + path.add(" "); + + std::vector labels{ desc }; + + for (auto lit = labels.begin(); lit != labels.end(); ++lit) { + const auto& l = *lit; + double dy = labels.begin() == lit + ? 0.0 // align bottom + : 1.0; // <- dy is relative to the previous text element, so + // always 1 for successive spans. + path.add("(dy)); + path.add("em\">"); + path.add(l); + path.add(""); + } + + path.add(""); + + po->second.push_back(path); } } } @@ -1708,6 +1780,8 @@ void SvgSerializer::addTextAnnotations(const drawing_key& k) { } void SvgSerializer::finalize() { + doWriteHeader(); + for (auto& p : drawing_metadata) { addTextAnnotations(p.first); } @@ -1815,6 +1889,8 @@ void SvgSerializer::finalize() { draw_hlr(ax, { nullptr, drawing_name }); } + addTextAnnotations({ nullptr, drawing_name }); + if (storey_height_display_ != SH_NONE && pln && std::abs(pln->Position().Direction().Z()) < 1.e-5) { auto storeys = this->file->instances_by_type("IfcBuildingStorey"); if (storeys) { @@ -1900,6 +1976,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 << " #include #include +#include + +#if OCC_VERSION_HEX >= 0x70300 +#include +#endif #include #include @@ -136,16 +141,22 @@ protected: double xmin, ymin, xmax, ymax; boost::optional> 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_; +#if OCC_VERSION_HEX >= 0x70300 + boost::optional view_box_3d_; +#endif + + bool with_section_heights_from_storey_, print_space_names_, print_space_areas_; 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_; + bool emit_building_storeys_; IfcParse::IfcFile* file; IfcUtil::IfcBaseEntity* storey_; @@ -188,6 +199,8 @@ public: , use_namespace_(false) , use_hlr_poly_(false) , always_project_(false) + , polygonal_(false) + , emit_building_storeys_(true) , file(0) , storey_(0) , xcoords_begin(0) @@ -200,6 +213,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); @@ -249,10 +263,18 @@ public: use_hlr_poly_ = b; } + void setPolygonal(bool b) { + polygonal_ = b; + } + void setAlwaysProject(bool b) { 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;