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/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..973b705fc2 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -177,6 +177,20 @@ 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 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..b7d1db49ac 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -173,7 +173,6 @@ if bpy is not None: bpy.app.handlers.load_post.append(handler.setDefaultProperties) bpy.app.handlers.load_post.append(handler.loadIfcStore) bpy.app.handlers.save_pre.append(handler.ensureIfcExported) - bpy.app.handlers.save_pre.append(handler.storeIdMap) bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties) @@ -200,7 +199,6 @@ if bpy is not None: bpy.utils.unregister_class(cls) bpy.app.handlers.load_post.remove(handler.setDefaultProperties) bpy.app.handlers.load_post.remove(handler.loadIfcStore) - bpy.app.handlers.save_pre.remove(handler.storeIdMap) bpy.app.handlers.save_pre.remove(handler.ensureIfcExported) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) diff --git a/src/blenderbim/blenderbim/bim/data/gantt/index.mustache b/src/blenderbim/blenderbim/bim/data/gantt/index.mustache new file mode 100644 index 0000000000..832f8e4a8b --- /dev/null +++ b/src/blenderbim/blenderbim/bim/data/gantt/index.mustache @@ -0,0 +1,11 @@ + + +
+ diff --git a/src/blenderbim/blenderbim/bim/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..bc3f805109 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -9,17 +9,18 @@ from ifcopenshell.api.attribute.data import Data as AttributeData def mode_callback(obj, data): - if ( - obj.mode != "OBJECT" - or not obj.data - or not isinstance(obj.data, bpy.types.Mesh) - or not obj.data.BIMMeshProperties.ifc_definition_id - or not bpy.context.scene.BIMProjectProperties.is_authoring - ): - return - representation = IfcStore.get_file().by_id(obj.data.BIMMeshProperties.ifc_definition_id) - if representation.RepresentationType == "Tessellation" or representation.RepresentationType == "Brep": - IfcStore.edited_objs.add(obj.name) + for obj in bpy.context.selected_objects: + if ( + obj.mode != "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 +30,9 @@ def name_callback(obj, data): element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) if not element.is_a("IfcRoot"): return + if element.is_a("IfcSpatialStructureElement") or (hasattr(element, "IsDecomposedBy") and element.IsDecomposedBy): + collection = obj.users_collection[0] + collection.name = obj.name element.Name = "/".join(obj.name.split("/")[1:]) AttributeData.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) @@ -52,52 +56,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 +159,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 4a3ffa2d75..9d1b5a5ae9 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -414,7 +414,11 @@ class IfcImporter: self.exclude_elements |= self.native_elements def is_native(self, element): - if not element.Representation or not element.Representation.Representations or element.HasOpenings: + if ( + not element.Representation + or not element.Representation.Representations + or getattr(element, "HasOpenings", None) + ): return representations = self.get_transformed_body_representations(element.Representation.Representations) @@ -776,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: @@ -999,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"): @@ -1080,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} @@ -1125,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): @@ -1175,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) @@ -1189,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: @@ -1428,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/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index 2605c0910c..f5fb62433b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -1,16 +1,33 @@ import bpy -from . import ui, operator +from . import ui, prop, operator classes = ( operator.AddCostSchedule, operator.RemoveCostSchedule, + operator.EditCostSchedule, + operator.EditCostItem, + operator.EnableEditingCostSchedule, + operator.EnableEditingCostItems, + operator.EnableEditingCostItem, + operator.DisableEditingCostItem, + operator.DisableEditingCostSchedule, + operator.AddCostItem, + operator.AddSummaryCostItem, + operator.ExpandCostItem, + operator.ContractCostItem, + operator.RemoveCostItem, + operator.AssignControl, + operator.UnassignControl, + prop.CostItem, + prop.BIMCostProperties, ui.BIM_PT_cost_schedules, + ui.BIM_UL_cost_items, ) def register(): - pass + bpy.types.Scene.BIMCostProperties = bpy.props.PointerProperty(type=prop.BIMCostProperties) def unregister(): - pass + del bpy.types.Scene.BIMCostProperties diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index ef6997b9ad..26c31f0c03 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -1,4 +1,5 @@ import bpy +import json import ifcopenshell.api from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.cost.data import Data @@ -14,6 +15,32 @@ class AddCostSchedule(bpy.types.Operator): return {"FINISHED"} +class EditCostSchedule(bpy.types.Operator): + bl_idname = "bim.edit_cost_schedule" + bl_label = "Edit Cost Schedule" + + def execute(self, context): + props = context.scene.BIMCostProperties + attributes = {} + for attribute in props.cost_schedule_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.edit_cost_schedule", + self.file, + **{"cost_schedule": self.file.by_id(props.active_cost_schedule_id), "attributes": attributes}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_cost_schedule() + return {"FINISHED"} + + class RemoveCostSchedule(bpy.types.Operator): bl_idname = "bim.remove_cost_schedule" bl_label = "Remove Cost Schedule" @@ -27,3 +54,276 @@ class RemoveCostSchedule(bpy.types.Operator): ) Data.load(IfcStore.get_file()) return {"FINISHED"} + + +class EnableEditingCostSchedule(bpy.types.Operator): + bl_idname = "bim.enable_editing_cost_schedule" + bl_label = "Enable Editing Cost Schedule" + cost_schedule: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMCostProperties + self.props.active_cost_schedule_id = self.cost_schedule + while len(self.props.cost_schedule_attributes) > 0: + self.props.cost_schedule_attributes.remove(0) + self.enable_editing_cost_schedule() + self.props.is_editing = "COST_SCHEDULE" + return {"FINISHED"} + + def enable_editing_cost_schedule(self): + data = Data.cost_schedules[self.cost_schedule] + + for attribute in IfcStore.get_schema().declaration_by_name("IfcCostSchedule").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + new = self.props.cost_schedule_attributes.add() + new.name = attribute.name() + new.is_null = data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if attribute.name() in ["SubmittedOn", "UpdateDate"]: + new.string_value = "" if new.is_null else data[attribute.name()].isoformat() + elif data_type == "string": + new.string_value = "" if new.is_null else data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if data[attribute.name()]: + new.enum_value = data[attribute.name()] + + +class EnableEditingCostItems(bpy.types.Operator): + bl_idname = "bim.enable_editing_cost_items" + bl_label = "Enable Editing Cost Items" + cost_schedule: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMCostProperties + self.props.active_cost_schedule_id = self.cost_schedule + while len(self.props.cost_items) > 0: + self.props.cost_items.remove(0) + + self.contracted_cost_items = json.loads(self.props.contracted_cost_items) + for related_object_id in Data.cost_schedules[self.cost_schedule]["RelatedObjects"]: + self.create_new_cost_item_li(related_object_id, 0) + self.props.is_editing = "COST_ITEMS" + return {"FINISHED"} + + def create_new_cost_item_li(self, related_object_id, level_index): + cost_item = Data.cost_items[related_object_id] + new = self.props.cost_items.add() + new.ifc_definition_id = related_object_id + new.name = cost_item["Name"] or "Unnamed" + new.is_expanded = related_object_id not in self.contracted_cost_items + new.level_index = level_index + if cost_item["RelatedObjects"]: + new.has_children = True + if new.is_expanded: + for related_object_id in cost_item["RelatedObjects"]: + self.create_new_cost_item_li(related_object_id, level_index + 1) + + return {"FINISHED"} + + +class DisableEditingCostSchedule(bpy.types.Operator): + bl_idname = "bim.disable_editing_cost_schedule" + bl_label = "Disable Editing Cost Schedule" + + def execute(self, context): + context.scene.BIMCostProperties.active_cost_schedule_id = 0 + return {"FINISHED"} + + +class AddSummaryCostItem(bpy.types.Operator): + bl_idname = "bim.add_summary_cost_item" + bl_label = "Add Cost Item" + cost_schedule: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run("cost.add_cost_item", self.file, **{"cost_schedule": self.file.by_id(self.cost_schedule)}) + Data.load(self.file) + bpy.ops.bim.enable_editing_cost_items(cost_schedule=self.cost_schedule) + return {"FINISHED"} + + +class AddCostItem(bpy.types.Operator): + bl_idname = "bim.add_cost_item" + bl_label = "Add Cost Item" + cost_item: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run("cost.add_cost_item", self.file, **{"cost_item": self.file.by_id(self.cost_item)}) + Data.load(self.file) + bpy.ops.bim.enable_editing_cost_items(cost_schedule=props.active_cost_schedule_id) + return {"FINISHED"} + + +class ExpandCostItem(bpy.types.Operator): + bl_idname = "bim.expand_cost_item" + bl_label = "Expand Cost Item" + cost_item: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() + contracted_cost_items = json.loads(props.contracted_cost_items) + contracted_cost_items.remove(self.cost_item) + props.contracted_cost_items = json.dumps(contracted_cost_items) + bpy.ops.bim.enable_editing_cost_items(cost_schedule=props.active_cost_schedule_id) + return {"FINISHED"} + + +class ContractCostItem(bpy.types.Operator): + bl_idname = "bim.contract_cost_item" + bl_label = "Contract Cost Item" + cost_item: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() + contracted_cost_items = json.loads(props.contracted_cost_items) + contracted_cost_items.append(self.cost_item) + props.contracted_cost_items = json.dumps(contracted_cost_items) + bpy.ops.bim.enable_editing_cost_items(cost_schedule=props.active_cost_schedule_id) + return {"FINISHED"} + + +class RemoveCostItem(bpy.types.Operator): + bl_idname = "bim.remove_cost_item" + bl_label = "Remove Cost item" + cost_item: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.remove_cost_item", + self.file, + cost_item=self.file.by_id(self.cost_item), + ) + contracted_cost_items = json.loads(props.contracted_cost_items) + if props.active_cost_item_index in contracted_cost_items: + contracted_cost_items.remove(props.active_cost_item_index) + props.contracted_cost_items = json.dumps(contracted_cost_items) + Data.load(self.file) + bpy.ops.bim.enable_editing_cost_items(cost_schedule=props.active_cost_schedule_id) + return {"FINISHED"} + + +class EnableEditingCostItem(bpy.types.Operator): + bl_idname = "bim.enable_editing_cost_item" + bl_label = "Enable Editing Cost Item" + cost_item: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + while len(props.cost_item_attributes) > 0: + props.cost_item_attributes.remove(0) + + data = Data.cost_items[self.cost_item] + + for attribute in IfcStore.get_schema().declaration_by_name("IfcCostItem").all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity" or isinstance(data_type, tuple): + continue + new = props.cost_item_attributes.add() + new.name = attribute.name() + new.is_null = data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if data_type == "string": + new.string_value = "" if new.is_null else data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if data[attribute.name()]: + new.enum_value = data[attribute.name()] + props.active_cost_item_id = self.cost_item + return {"FINISHED"} + + +class DisableEditingCostItem(bpy.types.Operator): + bl_idname = "bim.disable_editing_cost_item" + bl_label = "Disable Editing Cost Item" + + def execute(self, context): + context.scene.BIMCostProperties.active_cost_item_id = 0 + return {"FINISHED"} + + +class EditCostItem(bpy.types.Operator): + bl_idname = "bim.edit_cost_item" + bl_label = "Edit Cost Item" + + def execute(self, context): + props = context.scene.BIMCostProperties + attributes = {} + for attribute in props.cost_item_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "boolean": + attributes[attribute.name] = attribute.bool_value + elif attribute.data_type == "integer": + attributes[attribute.name] = attribute.int_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.edit_cost_item", + self.file, + **{"cost_item": self.file.by_id(props.active_cost_item_id), "attributes": attributes}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_cost_item() + bpy.ops.bim.enable_editing_cost_items(cost_schedule=props.active_cost_schedule_id) + return {"FINISHED"} + + +class AssignControl(bpy.types.Operator): + bl_idname = "bim.assign_control" + bl_label = "Assign Control" + cost_item: bpy.props.IntProperty() + related_object: bpy.props.StringProperty() + + def execute(self, context): + related_objects = ( + [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects + ) + for related_object in related_objects: + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "control.assign_control", + self.file, + related_object=self.file.by_id(related_object.BIMObjectProperties.ifc_definition_id), + relating_control=self.file.by_id(self.cost_item), + ) + Data.load(self.file) + return {"FINISHED"} + + +class UnassignControl(bpy.types.Operator): + bl_idname = "bim.unassign_control" + bl_label = "Unassign Control" + cost_item: bpy.props.IntProperty() + related_object: bpy.props.StringProperty() + + def execute(self, context): + related_objects = ( + [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects + ) + for related_object in related_objects: + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "control.unassign_control", + self.file, + related_object=self.file.by_id(related_object.BIMObjectProperties.ifc_definition_id), + relating_control=self.file.by_id(self.cost_item), + ) + Data.load(self.file) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cost/prop.py b/src/blenderbim/blenderbim/bim/module/cost/prop.py new file mode 100644 index 0000000000..4be8e8399e --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/cost/prop.py @@ -0,0 +1,51 @@ +import bpy +import ifcopenshell.api +from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.cost.data import Data +from blenderbim.bim.prop import StrProperty, Attribute +from bpy.types import PropertyGroup +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) + + +def updateCostItemName(self, context): + if self.name == "Unnamed": + return + self.file = IfcStore.get_file() + props = context.scene.BIMCostProperties + ifcopenshell.api.run( + "cost.edit_cost_item", + self.file, + **{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}}, + ) + Data.load(IfcStore.get_file()) + if props.active_cost_item_id == self.ifc_definition_id: + attribute = props.cost_item_attributes.get("Name") + attribute.string_value = self.name + + +class CostItem(PropertyGroup): + name: StringProperty(name="Name", update=updateCostItemName) + ifc_definition_id: IntProperty(name="IFC Definition ID") + has_children: BoolProperty(name="Has Children") + is_expanded: BoolProperty(name="Is Expanded") + level_index: IntProperty(name="Level Index") + + +class BIMCostProperties(PropertyGroup): + cost_schedule_attributes: CollectionProperty(name="Cost Schedule Attributes", type=Attribute) + is_editing: StringProperty(name="Is Editing") + active_cost_schedule_id: IntProperty(name="Active Cost Schedule Id") + cost_items: CollectionProperty(name="Work Calendar", type=CostItem) + active_cost_item_id: IntProperty(name="Active Cost Id") + active_cost_item_index: IntProperty(name="Active Cost Item Index") + cost_item_attributes: CollectionProperty(name="Task Attributes", type=Attribute) + contracted_cost_items: StringProperty(name="Contracted Cost Items", default="[]") diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 43ecd1bf19..8523ba70ed 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -16,6 +16,8 @@ class BIM_PT_cost_schedules(Panel): return IfcStore.get_file() def draw(self, context): + self.props = context.scene.BIMCostProperties + if not Data.is_loaded: Data.load(IfcStore.get_file()) @@ -23,7 +25,109 @@ class BIM_PT_cost_schedules(Panel): row.operator("bim.add_cost_schedule", icon="ADD") for cost_schedule_id, cost_schedule in Data.cost_schedules.items(): - row = self.layout.row(align=True) - row.label(text=cost_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") - row.operator("bim.add_cost_schedule", text="", icon="GREASEPENCIL") + self.draw_cost_schedule_ui(cost_schedule_id, cost_schedule) + + def draw_cost_schedule_ui(self, cost_schedule_id, cost_schedule): + row = self.layout.row(align=True) + row.label(text=cost_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") + + if self.props.active_cost_schedule_id and self.props.active_cost_schedule_id == cost_schedule_id: + if self.props.is_editing == "COST_SCHEDULE": + row.operator("bim.edit_cost_schedule", text="", icon="CHECKMARK") + elif self.props.is_editing == "COST_ITEMS": + row.operator("bim.add_summary_cost_item", text="", icon="ADD").cost_schedule = cost_schedule_id + row.operator("bim.disable_editing_cost_schedule", text="", icon="CANCEL") + elif self.props.active_cost_schedule_id: row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule_id + else: + row.operator("bim.enable_editing_cost_items", text="", icon="OUTLINER").cost_schedule = cost_schedule_id + row.operator( + "bim.enable_editing_cost_schedule", text="", icon="GREASEPENCIL" + ).cost_schedule = cost_schedule_id + row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule_id + + if self.props.active_cost_schedule_id == cost_schedule_id: + if self.props.is_editing == "COST_SCHEDULE": + self.draw_editable_cost_schedule_ui() + elif self.props.is_editing == "COST_ITEMS": + self.draw_editable_cost_item_ui(cost_schedule_id) + + def draw_editable_cost_schedule_ui(self): + for attribute in self.props.cost_schedule_attributes: + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + + def draw_editable_cost_item_ui(self, cost_schedule_id): + self.layout.template_list( + "BIM_UL_cost_items", + "", + self.props, + "cost_items", + self.props, + "active_cost_item_index", + ) + if self.props.active_cost_item_id: + self.draw_editable_cost_item_attributes_ui() + + def draw_editable_cost_item_attributes_ui(self): + for attribute in self.props.cost_item_attributes: + row = self.layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "boolean": + row.prop(attribute, "bool_value", text=attribute.name) + elif attribute.data_type == "integer": + row.prop(attribute, "int_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + + +class BIM_UL_cost_items(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + props = context.scene.BIMCostProperties + row = layout.row(align=True) + for i in range(0, item.level_index): + row.label(text="", icon="BLANK1") + if item.has_children: + if item.is_expanded: + row.operator( + "bim.contract_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN" + ).cost_item = item.ifc_definition_id + else: + row.operator( + "bim.expand_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT" + ).cost_item = item.ifc_definition_id + else: + row.label(text="", icon="DOT") + row.prop(item, "name", emboss=False, text="") + + if context.active_object: + oprops = context.active_object.BIMObjectProperties + row = layout.row(align=True) + if oprops.ifc_definition_id in Data.cost_items[item.ifc_definition_id]["Controls"]: + op = row.operator("bim.unassign_control", text="", icon="KEYFRAME_HLT", emboss=False) + op.cost_item = item.ifc_definition_id + else: + op = row.operator("bim.assign_control", text="", icon="KEYFRAME", emboss=False) + op.cost_item = item.ifc_definition_id + + if props.active_cost_item_id == item.ifc_definition_id: + row.operator("bim.edit_cost_item", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL") + elif props.active_cost_item_id: + row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = item.ifc_definition_id + row.operator("bim.remove_cost_item", text="", icon="X").cost_item = item.ifc_definition_id + else: + row.operator( + "bim.enable_editing_cost_item", text="", icon="GREASEPENCIL" + ).cost_item = item.ifc_definition_id + row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = item.ifc_definition_id + row.operator("bim.remove_cost_item", text="", icon="X").cost_item = item.ifc_definition_id diff --git a/src/blenderbim/blenderbim/bim/module/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/layer/operator.py b/src/blenderbim/blenderbim/bim/module/layer/operator.py index 7c930823e9..52e5a9760d 100644 --- a/src/blenderbim/blenderbim/bim/module/layer/operator.py +++ b/src/blenderbim/blenderbim/bim/module/layer/operator.py @@ -47,7 +47,7 @@ class EnableEditingLayer(bpy.types.Operator): for attribute in IfcStore.get_schema().declaration_by_name("IfcPresentationLayerAssignment").all_attributes(): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity": + if data_type == "entity" or data_type == "select": continue new = props.layer_attributes.add() new.name = attribute.name() diff --git a/src/blenderbim/blenderbim/bim/module/material/__init__.py b/src/blenderbim/blenderbim/bim/module/material/__init__.py index 7680e62122..83ef17c184 100644 --- a/src/blenderbim/blenderbim/bim/module/material/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/material/__init__.py @@ -10,6 +10,7 @@ classes = ( operator.RemoveConstituent, operator.AddProfile, operator.RemoveProfile, + operator.AssignParameterizedProfile, operator.AddLayer, operator.RemoveLayer, operator.ReorderMaterialSetItem, diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index c77e96048b..11745bdf7a 100644 --- a/src/blenderbim/blenderbim/bim/module/material/operator.py +++ b/src/blenderbim/blenderbim/bim/module/material/operator.py @@ -1,8 +1,37 @@ 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 + + +class AssignParameterizedProfile(bpy.types.Operator): + bl_idname = "bim.assign_parameterized_profile" + bl_label = "Assign Parameterized Profile" + ifc_class: bpy.props.StringProperty() + material_profile: bpy.props.IntProperty() + obj: bpy.props.StringProperty() + + def execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + self.file = IfcStore.get_file() + profile = ifcopenshell.api.run( + "profile.add_parameterized_profile", + self.file, + **{"ifc_class": self.ifc_class}, + ) + ifcopenshell.api.run( + "material.assign_profile", + self.file, + **{"material_profile": self.file.by_id(self.material_profile), "profile": profile} + ) + Data.load_profiles() + ProfileData.load(self.file) + bpy.ops.bim.enable_editing_material_set_item(obj=obj.name, material_set_item=self.material_profile) + return {"FINISHED"} class AddMaterial(bpy.types.Operator): @@ -16,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"} @@ -288,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: @@ -370,8 +404,8 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object - props = obj.BIMObjectMaterialProperties - props.active_material_set_item_id = self.material_set_item + self.props = obj.BIMObjectMaterialProperties + self.props.active_material_set_item_id = self.material_set_item product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id] material_set_item = self.file.by_id(self.material_set_item) @@ -379,22 +413,29 @@ 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 = {} - props.material_set_item_material = str(material_set_item_data["Material"]) + self.props.material_set_item_material = str(material_set_item_data["Material"]) - while len(props.material_set_item_attributes) > 0: - props.material_set_item_attributes.remove(0) + self.load_set_item_attributes(material_set_item, material_set_item_data) + if material_set_item.is_a("IfcMaterialProfile"): + self.load_profile_attributes(material_set_item, material_set_item_data) + + return {"FINISHED"} + + def load_set_item_attributes(self, material_set_item, material_set_item_data): + while len(self.props.material_set_item_attributes) > 0: + self.props.material_set_item_attributes.remove(0) for attribute in IfcStore.get_schema().declaration_by_name(material_set_item.is_a()).all_attributes(): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) if data_type == "entity": continue if attribute.name() in material_set_item_data: - new = props.material_set_item_attributes.add() + new = self.props.material_set_item_attributes.add() new.name = attribute.name() new.is_null = material_set_item_data[attribute.name()] is None new.data_type = data_type @@ -406,7 +447,45 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): new.int_value = 0 if new.is_null else material_set_item_data[attribute.name()] elif data_type == "boolean": new.bool_value = False if new.is_null else material_set_item_data[attribute.name()] - return {"FINISHED"} + + def load_profile_attributes(self, material_set_item, material_set_item_data): + while len(self.props.material_set_item_profile_attributes) > 0: + self.props.material_set_item_profile_attributes.remove(0) + + if not material_set_item_data["Profile"]: + return + + profile = self.file.by_id(material_set_item_data["Profile"]) + profile_data = ProfileData.profiles[material_set_item_data["Profile"]] + + for attribute in IfcStore.get_schema().declaration_by_name(profile.is_a()).all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + if attribute.name() in profile_data: + new = self.props.material_set_item_profile_attributes.add() + new.name = attribute.name() + new.is_null = profile_data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if data_type == "string": + new.string_value = "" if new.is_null else profile_data[attribute.name()] + elif data_type == "float": + new.float_value = 0.0 if new.is_null else profile_data[attribute.name()] + elif data_type == "integer": + new.int_value = 0 if new.is_null else profile_data[attribute.name()] + elif data_type == "boolean": + new.bool_value = False if new.is_null else profile_data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if profile_data[attribute.name()]: + new.enum_value = profile_data[attribute.name()] + + # Force null to be false if the attribute is mandatory because when we first assign a profile, all of + # its fields are null (which is illegal). + # TODO: find a better solution. + if not new.is_optional: + new.is_null = False class DisableEditingMaterialSetItem(bpy.types.Operator): @@ -467,17 +546,32 @@ 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": + value = attribute.string_value + elif attribute.data_type == "float": + value = attribute.float_value + elif attribute.data_type == "integer": + value = attribute.int_value + elif attribute.data_type == "boolean": + value = attribute.bool_value + elif attribute.data_type == "enum": + value = attribute.enum_value + profile_attributes[attribute.name] = None if attribute.is_null else value ifcopenshell.api.run( "material.edit_profile", self.file, **{ "profile": self.file.by_id(self.material_set_item), "attributes": attributes, + "profile_attributes": profile_attributes, "material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material_set_item_material)), }, ) Data.load_profiles() + ProfileData.load(self.file) else: pass diff --git a/src/blenderbim/blenderbim/bim/module/material/prop.py b/src/blenderbim/blenderbim/bim/module/material/prop.py index 4795832bbc..4bd7093d69 100644 --- a/src/blenderbim/blenderbim/bim/module/material/prop.py +++ b/src/blenderbim/blenderbim/bim/module/material/prop.py @@ -1,5 +1,5 @@ import bpy -import blenderbim.bim.schema # refactor +import blenderbim.bim.schema # refactor from ifcopenshell.api.material.data import Data from blenderbim.bim.ifc import IfcStore from blenderbim.bim.prop import StrProperty, Attribute @@ -17,6 +17,40 @@ from bpy.props import ( materials_enum = [] materialtypes_enum = [] +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(): + profileclasses_enum.clear() + profileclasses_enum = [ + (t.name(), t.name(), "") for t in IfcStore.get_schema().declaration_by_name("IfcProfileDef").subtypes() + ] + return profileclasses_enum + + +def getParameterizedProfileClasses(self, context): + global parameterizedprofileclasses_enum + if len(parameterizedprofileclasses_enum) == 0 and IfcStore.get_schema(): + parameterizedprofileclasses_enum.clear() + parameterizedprofileclasses_enum = [ + (t.name(), t.name(), "") + for t in IfcStore.get_schema().declaration_by_name("IfcParameterizedProfileDef").subtypes() + ] + return parameterizedprofileclasses_enum def getMaterials(self, context): @@ -36,6 +70,7 @@ def getMaterialTypes(self, context): "IfcMaterialLayerSet", "IfcMaterialLayerSetUsage", "IfcMaterialProfileSet", + "IfcMaterialProfileSetUsage", "IfcMaterialList", ] if IfcStore.get_file().schema == "IFC2X3": @@ -52,4 +87,9 @@ class BIMObjectMaterialProperties(PropertyGroup): material_set_attributes: CollectionProperty(name="Material Set Attributes", type=Attribute) active_material_set_item_id: IntProperty(name="Active Material Set ID") material_set_item_attributes: CollectionProperty(name="Material Set Item Attributes", type=Attribute) + material_set_item_profile_attributes: CollectionProperty(name="Material Set Item Profile Attributes", type=Attribute) material_set_item_material: EnumProperty(items=getMaterials, name="Material") + profile_classes: EnumProperty(items=getProfileClasses, name="Profile Classes") + parameterized_profile_classes: EnumProperty( + items=getParameterizedProfileClasses, name="Parameterized Profile Classes" + ) diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index 6d432f673a..262a920ef3 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -1,5 +1,6 @@ from bpy.types import Panel from ifcopenshell.api.material.data import Data +from ifcopenshell.api.profile.data import Data as ProfileData from blenderbim.bim.ifc import IfcStore @@ -46,6 +47,8 @@ class BIM_PT_object_material(Panel): Data.load(IfcStore.get_file()) if self.oprops.ifc_definition_id not in Data.products: Data.load(IfcStore.get_file(), self.oprops.ifc_definition_id) + if not ProfileData.is_loaded: + ProfileData.load(self.file) self.product_data = Data.products[self.oprops.ifc_definition_id] if not Data.materials: @@ -79,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] @@ -172,6 +182,39 @@ class BIM_PT_object_material(Panel): row.prop(attribute, "bool_value", text=attribute.name) row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + if self.set_item_name == "profile": + self.draw_assign_profile_ui(box, item) + self.draw_editable_profile_ui(box, item) + + def draw_assign_profile_ui(self, layout, item): + row = layout.row(align=True) + row.prop(self.props, "profile_classes", text="") + if self.props.profile_classes == "IfcParameterizedProfileDef": + row.prop(self.props, "parameterized_profile_classes", text="") + op = row.operator("bim.assign_parameterized_profile", icon="GREASEPENCIL" if item["Profile"] else "ADD", text="") + op.ifc_class = self.props.parameterized_profile_classes + op.material_profile = item["id"] + else: + # TODO: support non parametric profiles by showing a list of named profiles to select from, or an + # eyedropper to pick profile geometry from the scene + row.operator("bim.disable_editing_material_set_item", icon="X", text="") + + def draw_editable_profile_ui(self, layout, item): + for attribute in self.props.material_set_item_profile_attributes: + row = layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "integer": + row.prop(attribute, "int_value", text=attribute.name) + elif attribute.data_type == "float": + row.prop(attribute, "float_value", text=attribute.name) + elif attribute.data_type == "boolean": + row.prop(attribute, "bool_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + def draw_read_only_set_item_ui(self, set_item_id, index, is_first=False, is_last=False): if self.product_data["type"] == "IfcMaterialList": item = Data.materials[set_item_id] diff --git a/src/blenderbim/blenderbim/bim/module/model/pie.py b/src/blenderbim/blenderbim/bim/module/model/pie.py index a93010bd47..b19a258f4c 100644 --- a/src/blenderbim/blenderbim/bim/module/model/pie.py +++ b/src/blenderbim/blenderbim/bim/module/model/pie.py @@ -84,6 +84,8 @@ class PieAddOpening(bpy.types.Operator): for obj in context.selected_objects: if "IfcOpeningElement" in obj.name or not obj.BIMObjectProperties.ifc_definition_id: opening_name = obj.name + elif len(obj.children) == 1 and not obj.children[0].BIMObjectProperties.ifc_definition_id: + opening_name = obj.children[0].name else: opj_name = obj.name bpy.ops.bim.add_opening(obj=opj_name, opening=opening_name) 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 846f082a61..d05306f3ce 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -3,9 +3,21 @@ from . import ui, prop, operator 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 6b56cd45cf..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): @@ -53,6 +52,30 @@ class CreateProject(bpy.types.Operator): return {"FINISHED"} +class CreateProjectLibrary(bpy.types.Operator): + bl_idname = "bim.create_project_library" + bl_label = "Create Project Library" + + def execute(self, context): + self.file = IfcStore.get_file() + if self.file: + return {"FINISHED"} + + IfcStore.file = ifcopenshell.api.run( + "project.create_file", **{"version": bpy.context.scene.BIMProperties.export_schema} + ) + self.file = IfcStore.get_file() + + if self.file.schema == "IFC2X3": + bpy.ops.bim.add_person() + bpy.ops.bim.add_organisation() + + project_library = bpy.data.objects.new("My Project Library", None) + bpy.ops.bim.assign_class(obj=project_library.name, ifc_class="IfcProjectLibrary") + bpy.ops.bim.assign_unit() + return {"FINISHED"} + + class ValidateIfcFile(bpy.types.Operator): bl_idname = "bim.validate_ifc_file" bl_label = "Validate IFC File" @@ -64,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 1da1f0f64d..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 @@ -63,3 +63,71 @@ class BIM_PT_project(Panel): row.prop(props, "volume_unit", text="Volume Unit") row = self.layout.row() row.operator("bim.create_project") + if props.export_schema != "IFC2X3": + row = self.layout.row() + row.operator("bim.create_project_library") + + +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..a785741d60 100644 --- a/src/blenderbim/blenderbim/bim/module/root/operator.py +++ b/src/blenderbim/blenderbim/bim/module/root/operator.py @@ -153,6 +153,7 @@ class AssignClass(bpy.types.Operator): collection.objects.link(obj) if parent_collection: parent_collection.children.link(collection) + bpy.ops.bim.assign_object(related_object=obj.name, relating_object=parent_collection.name) else: bpy.context.scene.collection.children.link(collection) 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..83777c9476 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -9,14 +9,12 @@ classes = ( operator.RemoveWorkPlan, operator.EnableEditingWorkPlan, operator.DisableEditingWorkPlan, - operator.LoadWorkSchedules, - operator.DisableWorkScheduleEditingUI, operator.AddWorkSchedule, operator.EditWorkSchedule, operator.RemoveWorkSchedule, operator.EnableEditingWorkSchedule, + operator.EnableEditingTasks, operator.DisableEditingWorkSchedule, - operator.LoadTasks, operator.DisableTaskEditingUI, operator.LoadWorkCalendars, operator.DisableWorkCalendarEditingUI, @@ -25,32 +23,57 @@ classes = ( operator.RemoveWorkCalendar, operator.EnableEditingWorkCalendar, operator.DisableEditingWorkCalendar, + operator.AddTask, + operator.AddSummaryTask, + operator.ExpandTask, + operator.ContractTask, + operator.RemoveTask, + operator.EnableEditingTask, + operator.DisableEditingTask, + operator.EditTask, + operator.AssignPredecessor, + operator.AssignSuccessor, + operator.UnassignPredecessor, + operator.UnassignSuccessor, + operator.EnableEditingTaskTime, + operator.DisableEditingTaskTime, + operator.EditTaskTime, + operator.AssignProduct, + operator.UnassignProduct, + operator.GenerateGanttChart, + operator.ImportP6, + operator.LoadTaskProperties, prop.WorkPlan, prop.BIMWorkPlanProperties, - prop.WorkSchedule, + prop.Task, prop.BIMWorkScheduleProperties, + prop.BIMTaskTreeProperties, prop.WorkCalendar, prop.BIMWorkCalendarProperties, - prop.Task, - prop.BIMTaskProperties, ui.BIM_PT_work_plans, ui.BIM_UL_work_plans, ui.BIM_PT_work_schedules, - ui.BIM_UL_work_schedules, ui.BIM_PT_work_calendars, ui.BIM_UL_work_calendars, - ui.BIM_PT_tasks, ui.BIM_UL_tasks, ) + +def 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..68e5659948 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1,7 +1,14 @@ +import os import bpy import json +import time +import pystache +import webbrowser import ifcopenshell.api +from datetime import datetime +from dateutil import parser from blenderbim.bim.ifc import IfcStore +from bpy_extras.io_utils import ImportHelper from ifcopenshell.api.sequence.data import Data @@ -61,7 +68,7 @@ class EditWorkPlan(bpy.types.Operator): ifcopenshell.api.run( "sequence.edit_work_plan", self.file, - **{"work_plan": self.file.by_id(props.active_work_plan_id), "attributes": attributes} + **{"work_plan": self.file.by_id(props.active_work_plan_id), "attributes": attributes}, ) Data.load(IfcStore.get_file()) bpy.ops.bim.load_work_plans() @@ -123,32 +130,6 @@ class DisableEditingWorkPlan(bpy.types.Operator): return {"FINISHED"} -class LoadWorkSchedules(bpy.types.Operator): - bl_idname = "bim.load_work_schedules" - bl_label = "Load Work Schedules" - - def execute(self, context): - props = context.scene.BIMWorkScheduleProperties - while len(props.work_schedules) > 0: - props.work_schedules.remove(0) - for ifc_definition_id, work_schedule in Data.work_schedules.items(): - new = props.work_schedules.add() - new.ifc_definition_id = ifc_definition_id - new.name = work_schedule["Name"] or "Unnamed" - props.is_editing = True - bpy.ops.bim.disable_editing_work_schedule() - return {"FINISHED"} - - -class DisableWorkScheduleEditingUI(bpy.types.Operator): - bl_idname = "bim.disable_work_schedule_editing_ui" - bl_label = "Disable WorkSchedule Editing UI" - - def execute(self, context): - context.scene.BIMWorkScheduleProperties.is_editing = False - return {"FINISHED"} - - class AddWorkSchedule(bpy.types.Operator): bl_idname = "bim.add_work_schedule" bl_label = "Add Work Schedule" @@ -156,7 +137,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 +159,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 +174,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 +186,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.is_editing = "WORK_SCHEDULE" + return {"FINISHED"} + def enable_editing_work_schedule(self): data = Data.work_schedules[self.work_schedule] for attribute in IfcStore.get_schema().declaration_by_name("IfcWorkSchedule").all_attributes(): 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 +214,77 @@ 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) + for related_object_id in Data.work_schedules[self.work_schedule]["RelatedObjects"]: + self.create_new_task_li(related_object_id, 0) + bpy.ops.bim.load_task_properties() + self.props.is_editing = "TASKS" return {"FINISHED"} + def create_new_task_li(self, related_object_id, level_index): + task = Data.tasks[related_object_id] + new = self.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: + for related_object_id in task["RelatedObjects"]: + self.create_new_task_li(related_object_id, level_index + 1) + return {"FINISHED"} + + +class LoadTaskProperties(bpy.types.Operator): + bl_idname = "bim.load_task_properties" + bl_label = "Load Task Properties" + task: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMWorkScheduleProperties + self.tprops = context.scene.BIMTaskTreeProperties + self.props.is_task_update_enabled = False + for item in self.tprops.tasks: + if self.task and item.ifc_definition_id != self.task: + continue + task = Data.tasks[item.ifc_definition_id] + item.name = task["Name"] or "Unnamed" + item.identification = task["Identification"] or "XXX" + if self.props.active_task_id: + item.is_predecessor = self.props.active_task_id in task["IsPredecessorTo"] + item.is_successor = self.props.active_task_id in task["IsSuccessorFrom"] + if task["TaskTime"]: + task_time = Data.task_times[task["TaskTime"]] + item.start = self.canonicalise_time(task_time["ScheduleStart"]) + item.finish = self.canonicalise_time(task_time["ScheduleFinish"]) + # TODO: duration + item.duration = "-" + else: + item.start = "-" + item.finish = "-" + item.duration = "-" + self.props.is_task_update_enabled = True + return {"FINISHED"} + + def canonicalise_time(self, time): + if not time: + return "-" + return time.strftime("%d/%m/%y") + class DisableEditingWorkSchedule(bpy.types.Operator): bl_idname = "bim.disable_editing_work_schedule" @@ -243,6 +295,433 @@ class DisableEditingWorkSchedule(bpy.types.Operator): return {"FINISHED"} +class DisableTaskEditingUI(bpy.types.Operator): + bl_idname = "bim.disable_task_editing_ui" + bl_label = "Disable Task Editing UI" + + def execute(self, context): + context.scene.BIMTaskProperties.is_editing = False + return {"FINISHED"} + + +class AddTask(bpy.types.Operator): + bl_idname = "bim.add_task" + bl_label = "Add Task" + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run("sequence.add_task", self.file, **{"parent_task": self.file.by_id(self.task)}) + Data.load(self.file) + bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) + return {"FINISHED"} + + +class AddSummaryTask(bpy.types.Operator): + bl_idname = "bim.add_summary_task" + bl_label = "Add Task" + work_schedule: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + 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() + 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 + bpy.ops.bim.load_task_properties() + return {"FINISHED"} + + def add_task_time(self): + task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=self.file.by_id(self.task)) + Data.load(IfcStore.get_file()) + return task_time + + +class DisableEditingTaskTime(bpy.types.Operator): + bl_idname = "bim.disable_editing_task_time" + bl_label = "Disable Editing Task Time" + + def execute(self, context): + context.scene.BIMWorkScheduleProperties.active_task_time_id = 0 + bpy.ops.bim.disable_editing_task() + return {"FINISHED"} + + +class EditTaskTime(bpy.types.Operator): + bl_idname = "bim.edit_task_time" + bl_label = "Edit Task Time" + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + attributes = {} + for attribute in props.task_time_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "boolean": + attributes[attribute.name] = attribute.bool_value + elif attribute.data_type == "float": + attributes[attribute.name] = attribute.float_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + + attributes = self.convert_strings_to_date_times(attributes) + + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.edit_task_time", + self.file, + **{"task_time": self.file.by_id(props.active_task_time_id), "attributes": attributes}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_task_time() + bpy.ops.bim.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 + 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 + bpy.ops.bim.load_task_properties() + return {"FINISHED"} + + +class DisableEditingTask(bpy.types.Operator): + bl_idname = "bim.disable_editing_task" + bl_label = "Disable Editing Task" + + def execute(self, context): + context.scene.BIMWorkScheduleProperties.active_task_id = 0 + return {"FINISHED"} + + +class EditTask(bpy.types.Operator): + bl_idname = "bim.edit_task" + bl_label = "Edit Task" + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + attributes = {} + for attribute in props.task_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + elif attribute.data_type == "boolean": + attributes[attribute.name] = attribute.bool_value + elif attribute.data_type == "integer": + attributes[attribute.name] = attribute.int_value + elif attribute.data_type == "enum": + attributes[attribute.name] = attribute.enum_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.edit_task", self.file, **{"task": self.file.by_id(props.active_task_id), "attributes": attributes} + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_task() + bpy.ops.bim.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 LoadWorkCalendars(bpy.types.Operator): bl_idname = "bim.load_work_calendars" bl_label = "Load Work Calendars" @@ -299,7 +778,7 @@ 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() @@ -361,27 +840,21 @@ 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 - return {"FINISHED"} - - -class DisableTaskEditingUI(bpy.types.Operator): - bl_idname = "bim.disable_task_editing_ui" - bl_label = "Disable Task Editing UI" - - def execute(self, context): - context.scene.BIMTaskProperties.is_editing = False + from ifcp6.p62ifc import P62Ifc + self.file = IfcStore.get_file() + start = time.time() + p62ifc = P62Ifc() + p62ifc.xml = self.filepath + p62ifc.file = self.file + p62ifc.work_plan = self.file.by_type("IfcWorkPlan")[0] + p62ifc.execute() + Data.load(IfcStore.get_file()) + print("Import finished in {:.2f} seconds".format(time.time() - start)) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 29a1af6b14..7416ae88a7 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,108 @@ 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)) + + 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) + is_predecessor: BoolProperty(name="Is Predecessor") + is_successor: BoolProperty(name="Is Successor") class WorkPlan(PropertyGroup): @@ -38,17 +134,25 @@ class BIMWorkPlanProperties(PropertyGroup): active_work_plan_id: IntProperty(name="Active Work Plan Id") -class WorkSchedule(PropertyGroup): - name: StringProperty(name="Name") - ifc_definition_id: IntProperty(name="IFC Definition ID") - - class BIMWorkScheduleProperties(PropertyGroup): work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute) - is_editing: BoolProperty(name="Is Editing", default=False) - work_schedules: CollectionProperty(name="Work Schedules", type=WorkSchedule) + is_editing: StringProperty(name="Is Editing") active_work_schedule_index: IntProperty(name="Active Work Schedules Index") active_work_schedule_id: IntProperty(name="Active Work Schedules Id") + active_task_index: IntProperty(name="Active Task Index") + active_task_id: IntProperty(name="Active Task Id") + task_attributes: CollectionProperty(name="Task Attributes", type=Attribute) + should_show_times: BoolProperty(name="Should Show Times", default=True) + active_task_time_id: IntProperty(name="Active Task Id") + task_time_attributes: CollectionProperty(name="Task Time Attributes", type=Attribute) + contracted_tasks: StringProperty(name="Contracted Task Items", default="[]") + is_task_update_enabled: BoolProperty(name="Is Task Update Enabled", default=True) + + +class BIMTaskTreeProperties(PropertyGroup): + # This belongs by itself for performance reasons. + # In Blender if you add thousands of tasks it makes other property access in the same group really slow. + tasks: CollectionProperty(name="Tasks", type=Task) class WorkCalendar(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 9ed96cef3c..e09e356ec1 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -80,31 +80,44 @@ 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.is_editing == "WORK_SCHEDULE": + row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK") + elif self.props.is_editing == "TASKS": + row.prop(self.props, "should_show_times", text="", icon="TIME") + row.operator("bim.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.is_editing == "WORK_SCHEDULE": + self.draw_editable_work_schedule_ui() + elif self.props.is_editing == "TASKS": + self.draw_editable_task_ui(work_schedule_id) - if self.props.active_work_schedule_id: - self.draw_editable_ui(context) - - def draw_editable_ui(self, context): + 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 +127,119 @@ 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: + self.draw_editable_task_attributes_ui() + if self.props.active_task_time_id: + self.draw_editable_task_time_attributes_ui() -class BIM_UL_work_schedules(UIList): + 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: + row.prop(item, "start", emboss=False, text="") + row.prop(item, "finish", emboss=False, text="") + row.prop(item, "duration", emboss=False, text="") + + if context.active_object: + oprops = context.active_object.BIMObjectProperties + row = layout.row(align=True) + if oprops.ifc_definition_id in Data.tasks[item.ifc_definition_id]["RelatingProducts"]: + op = row.operator("bim.unassign_product", text="", icon="KEYFRAME_HLT", emboss=False) + op.task = item.ifc_definition_id + else: + op = row.operator("bim.assign_product", text="", icon="KEYFRAME", emboss=False) + op.task = item.ifc_definition_id + + if props.active_task_id == item.ifc_definition_id: + if props.active_task_time_id: + row.operator("bim.edit_task_time", text="", icon="CHECKMARK") + else: + row.operator("bim.edit_task", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_task", text="", icon="CANCEL") + elif props.active_task_id: + 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_time", text="", icon="TIME").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): @@ -193,53 +304,3 @@ class BIM_UL_work_calendars(UIList): op = row.operator("bim.enable_editing_work_calendar", text="", icon="GREASEPENCIL") op.work_calendar = item.ifc_definition_id row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = item.ifc_definition_id - - -class BIM_PT_tasks(Panel): - bl_label = "IFC Tasks" - bl_idname = "BIM_PT_tasks" - bl_options = {"DEFAULT_CLOSED"} - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - - @classmethod - def poll(cls, context): - return IfcStore.get_file() - - def draw(self, context): - if not Data.is_loaded: - Data.load(IfcStore.get_file()) - self.props = context.scene.BIMTaskProperties - - row = self.layout.row(align=True) - row.label(text="{} Tasks Found".format(len(Data.tasks)), icon="ACTION") - if self.props.is_editing: - row.operator("bim.disable_task_editing_ui", text="", icon="CHECKMARK") - else: - row.operator("bim.load_tasks", text="", icon="GREASEPENCIL") - - if self.props.is_editing: - self.layout.template_list( - "BIM_UL_tasks", - "", - self.props, - "tasks", - self.props, - "active_task_index", - ) - - if self.props.active_task_index: - self.draw_editable_ui(context) - - def draw_editable_ui(self, context): - pass - - -class BIM_UL_tasks(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - if item: - row = layout.row(align=True) - if item.identification: - layout.label(text=item.identification) - layout.label(text=item.name) 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..a361a238ad 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/structural/__init__.py @@ -20,12 +20,16 @@ classes = ( operator.EnableEditingStructuralConnectionCondition, operator.DisableEditingStructuralConnectionCondition, operator.RemoveStructuralConnectionCondition, + operator.EnableEditingStructuralMemberAxis, + operator.DisableEditingStructuralMemberAxis, + operator.EditStructuralMemberAxis, prop.StructuralAnalysisModel, prop.BIMStructuralProperties, prop.BIMObjectStructuralProperties, ui.BIM_PT_structural_analysis_models, ui.BIM_PT_structural_boundary_conditions, ui.BIM_PT_connected_structural_members, + ui.BIM_PT_structural_member, ui.BIM_UL_structural_analysis_models, ) diff --git a/src/blenderbim/blenderbim/bim/module/structural/operator.py b/src/blenderbim/blenderbim/bim/module/structural/operator.py index a997d2e91f..5d735ef84c 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/operator.py +++ b/src/blenderbim/blenderbim/bim/module/structural/operator.py @@ -2,6 +2,8 @@ import bpy import json import ifcopenshell import ifcopenshell.api +from math import degrees +from mathutils import Vector, Matrix from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.structural.data import Data from ifcopenshell.api.context.data import Data as ContextData @@ -363,3 +365,74 @@ class UnassignStructuralAnalysisModel(bpy.types.Operator): ) Data.load(IfcStore.get_file()) return {"FINISHED"} + + +class EnableEditingStructuralMemberAxis(bpy.types.Operator): + bl_idname = "bim.enable_editing_structural_member_axis" + bl_label = "Enable Editing Structural Member Axis" + + def execute(self, context): + obj = bpy.context.active_object + oprops = obj.BIMObjectProperties + props = obj.BIMStructuralProperties + + self.file = IfcStore.get_file() + member = self.file.by_id(oprops.ifc_definition_id) + z_axis = Vector(member.Axis.DirectionRatios).normalized() @ obj.matrix_world if member.Axis else None + x_axis = (obj.data.vertices[1].co - obj.data.vertices[0].co).normalized() + location = obj.data.vertices[0].co + empty = bpy.data.objects.new("Member Axis", None) + empty.empty_display_type = "ARROWS" + if z_axis: + y_axis = (z_axis.cross(x_axis)).normalized() + empty.matrix_world = Matrix(( + (x_axis[0], y_axis[0], z_axis[0], location[0]), + (x_axis[1], y_axis[1], z_axis[1], location[1]), + (x_axis[2], y_axis[2], z_axis[2], location[2]), + (0, 0, 0, 1), + )) + else: + empty.location = location + empty.rotation_mode = "QUATERNION" + empty.rotation_quaternion = x_axis.to_track_quat("X", "Z") + + props.axis_angle = degrees(empty.rotation_euler[0]) + props.axis_empty = empty + context.scene.collection.objects.link(empty) + + props.is_editing_axis = True + return {"FINISHED"} + + +class DisableEditingStructuralMemberAxis(bpy.types.Operator): + bl_idname = "bim.disable_editing_structural_member_axis" + bl_label = "Disable Editing Structural Member Axis" + + def execute(self, context): + obj = bpy.context.active_object + props = obj.BIMStructuralProperties + props.is_editing_axis = False + if props.axis_empty: + bpy.data.objects.remove(props.axis_empty) + return {"FINISHED"} + + +class EditStructuralMemberAxis(bpy.types.Operator): + bl_idname = "bim.edit_structural_member_axis" + bl_label = "Edit Structural Member Axis" + + def execute(self, context): + obj = bpy.context.active_object + oprops = obj.BIMObjectProperties + props = obj.BIMStructuralProperties + relative_matrix = props.axis_empty.matrix_world @ obj.matrix_world.inverted() + z_axis = relative_matrix.col[2][0:3] + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "structural.edit_structural_member_axis", + self.file, + structural_member=self.file.by_id(oprops.ifc_definition_id), + axis=z_axis, + ) + bpy.ops.bim.disable_editing_structural_member_axis() + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/structural/prop.py b/src/blenderbim/blenderbim/bim/module/structural/prop.py index f8cc972b87..71709c470d 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/prop.py +++ b/src/blenderbim/blenderbim/bim/module/structural/prop.py @@ -1,4 +1,5 @@ import bpy +from math import radians from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -13,6 +14,19 @@ from bpy.props import ( ) +def updateAxisAngle(self, context): + if not self.axis_empty: + return + obj = context.active_object + empty = self.axis_empty + x_axis = obj.data.vertices[1].co - obj.data.vertices[0].co + empty.location = obj.data.vertices[0].co + empty.rotation_mode = "QUATERNION" + empty.rotation_quaternion = x_axis.to_track_quat("X", "Z") + empty.rotation_mode = "XYZ" + empty.rotation_euler[0] = radians(self.axis_angle) + + class StructuralAnalysisModel(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") @@ -33,3 +47,6 @@ class BIMObjectStructuralProperties(PropertyGroup): active_boundary_condition: IntProperty(name="Active Boundary Condition") active_connects_structural_member: IntProperty(name="Active Connects Structural Member") relating_structural_member: PointerProperty(name="Relating Structural Member", type=bpy.types.Object) + is_editing_axis: BoolProperty(name="Is Editing Axis", default=False) + axis_angle: FloatProperty(name="Axis Angle", update=updateAxisAngle) + axis_empty: PointerProperty(name="Axis Empty", type=bpy.types.Object) diff --git a/src/blenderbim/blenderbim/bim/module/structural/ui.py b/src/blenderbim/blenderbim/bim/module/structural/ui.py index 87aff9b060..b1b51edd4d 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/ui.py +++ b/src/blenderbim/blenderbim/bim/module/structural/ui.py @@ -141,6 +141,44 @@ class BIM_PT_connected_structural_members(Panel): draw_boundary_condition_ui(box, data["AppliedCondition"], data["id"], self.props) +class BIM_PT_structural_member(Panel): + bl_label = "IFC Structural Member" + bl_idname = "BIM_PT_structural_member" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + @classmethod + def poll(cls, context): + if not context.active_object: + return False + props = context.active_object.BIMObjectProperties + if not props.ifc_definition_id: + return False + if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralMember"): + return False + return True + + def draw(self, context): + self.oprops = context.active_object.BIMObjectProperties + self.props = context.active_object.BIMStructuralProperties + self.file = IfcStore.get_file() + + if self.file.by_id(self.oprops.ifc_definition_id).is_a("IfcStructuralCurveMember"): + if self.props.is_editing_axis: + row = self.layout.row(align=True) + row.prop(self.props, "axis_angle") + row.operator("bim.edit_structural_member_axis", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_structural_member_axis", text="", icon="CANCEL") + else: + row = self.layout.row() + row.operator("bim.enable_editing_structural_member_axis", text="Edit Axis", icon="GREASEPENCIL") + else: + row = self.layout.row() + row.label(text="TODO") + + class BIM_PT_structural_analysis_models(Panel): bl_label = "IFC Structural Analysis Models" bl_idname = "BIM_PT_structural_analysis_models" diff --git a/src/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/module/void/ui.py b/src/blenderbim/blenderbim/bim/module/void/ui.py index 548ff2f096..20ba1fdb99 100644 --- a/src/blenderbim/blenderbim/bim/module/void/ui.py +++ b/src/blenderbim/blenderbim/bim/module/void/ui.py @@ -27,6 +27,8 @@ class BIM_PT_voids(Panel): for obj in context.selected_objects: if "IfcOpeningElement" in obj.name or not obj.BIMObjectProperties.ifc_definition_id: op.opening = obj.name + elif len(obj.children) == 1 and not obj.children[0].BIMObjectProperties.ifc_definition_id: + op.opening = obj.children[0].name else: op.obj = obj.name 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/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 0a15bc0a20..536881401b 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