From 008014a89bc9a3107438cb6308d74dbff86f39ed Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 2 Jan 2021 15:39:24 +1100 Subject: [PATCH] WIP refactor ability to partially write a new shape representation. See #1222. --- .../blenderbim/bim/__init__.py | 1 - .../blenderbim/bim/import_ifc.py | 8 +- .../geometry/add_shape_representation.py | 150 ++++++++++++++++++ .../blenderbim/bim/operator.py | 138 ++-------------- src/ifcblenderexport/blenderbim/bim/ui.py | 3 - 5 files changed, 173 insertions(+), 127 deletions(-) create mode 100644 src/ifcblenderexport/blenderbim/bim/module/geometry/add_shape_representation.py diff --git a/src/ifcblenderexport/blenderbim/bim/__init__.py b/src/ifcblenderexport/blenderbim/bim/__init__.py index 312babafa2..1386dd14af 100644 --- a/src/ifcblenderexport/blenderbim/bim/__init__.py +++ b/src/ifcblenderexport/blenderbim/bim/__init__.py @@ -162,7 +162,6 @@ if bpy is not None: operator.AddVariable, operator.RemoveVariable, operator.PropagateTextData, - operator.PushRepresentation, operator.ConvertLocalToGlobal, operator.ConvertGlobalToLocal, operator.GuessQuantity, diff --git a/src/ifcblenderexport/blenderbim/bim/import_ifc.py b/src/ifcblenderexport/blenderbim/bim/import_ifc.py index 3243c5e96a..86ec311310 100644 --- a/src/ifcblenderexport/blenderbim/bim/import_ifc.py +++ b/src/ifcblenderexport/blenderbim/bim/import_ifc.py @@ -3,6 +3,7 @@ import ifcopenshell.geom import ifcopenshell.util.geolocation import ifcopenshell.util.selector import ifcopenshell.util.element +import ifcopenshell.util.unit import bpy import bmesh import os @@ -19,7 +20,6 @@ import tempfile from pathlib import Path from itertools import cycle from datetime import datetime -from . import helper from . import ifc from . import schema @@ -953,6 +953,10 @@ class IfcImporter: mesh = self.create_native_mesh(element, shape) if mesh is None: mesh = self.create_mesh(element, shape) + if "-" in shape.geometry.id: + mesh.BIMMeshProperties.ifc_definition_id = int(shape.geometry.id.split("-")[0]) + else: + mesh.BIMMeshProperties.ifc_definition_id = int(shape.geometry.id) self.meshes[mesh_name] = mesh else: mesh = None @@ -1503,7 +1507,7 @@ class IfcImporter: self.unit_scale *= unit.ConversionFactor.ValueComponent.wrappedValue unit = unit.ConversionFactor.UnitComponent if unit.is_a("IfcSIUnit"): - self.unit_scale *= helper.SIUnitHelper.get_prefix_multiplier(unit.Prefix) + self.unit_scale *= ifcopenshell.util.unit.get_prefix_multiplier(unit.Prefix) def set_units(self): units = self.file.by_type("IfcUnitAssignment")[0] diff --git a/src/ifcblenderexport/blenderbim/bim/module/geometry/add_shape_representation.py b/src/ifcblenderexport/blenderbim/bim/module/geometry/add_shape_representation.py new file mode 100644 index 0000000000..8d440d6e55 --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/geometry/add_shape_representation.py @@ -0,0 +1,150 @@ +import ifcopenshell.util.unit + + +class Usecase: + def __init__(self, file, settings=None): + # TODO: This usecase currently depends on Blender's data model + self.file = file + self.settings = { + "context": None, # IfcGeometricRepresentationContext + "geometry": None, # This is (currently) a Blender data object, hence this depends on Blender now + "total_items": 1, # How many representation items to create + "unit_scale": None, # A scale factor to apply for all vectors in case the unit is different + "should_force_faceted_brep": False, # If we should force faceted breps for meshes + "is_wireframe": False, # If the geometry is a wireframe + "is_curve": False, # If the geometry is a Blender curve + "is_point_cloud": False, # If the geometry is a point cloud + } + self.ifc_vertices = [] + for key, value in settings.items(): + self.settings[key] = value + self.context_of_items = None + + def execute(self): + if self.settings["unit_scale"] is None: + self.settings["unit_scale"] = self.calculate_unit_scale() + if self.settings["context"].ContextType == "Model": + return self.create_model_representation() + elif self.settings["context"].ContextType == "Plan": + return self.create_plan_representation() + return self.create_variable_representation() + + def calculate_unit_scale(self): + units = self.file.by_type("IfcUnitAssignment")[0] + unit_scale = 1 + for unit in units.Units: + if not hasattr(unit, "UnitType") or unit.UnitType != "LENGTHUNIT": + continue + while unit.is_a("IfcConversionBasedUnit"): + unit_scale *= unit.ConversionFactor.ValueComponent.wrappedValue + unit = unit.ConversionFactor.UnitComponent + if unit.is_a("IfcSIUnit"): + unit_scale *= ifcopenshell.util.unit.get_prefix_multiplier(unit.Prefix) + return unit_scale + + def create_model_representation(self): + if self.settings["context"].is_a() == "IfcGeometricRepresentationContext": + return self.create_variable_representation() + if self.settings["context"].ContextIdentifier == "Annotation": + return self.create_geometric_set_representation() + elif self.settings["context"].ContextIdentifier == "Axis": + return self.create_curve3d_representation() + elif self.settings["context"].ContextIdentifier == "Body": + return self.create_variable_representation() + elif self.settings["context"].ContextIdentifier == "Box": + return self.create_box_representation() + elif self.settings["context"].ContextIdentifier == "Clearance": + return self.create_variable_representation() + elif self.settings["context"].ContextIdentifier == "CoG": + return self.create_cog_representation() + elif self.settings["context"].ContextIdentifier == "FootPrint": + return self.create_variable_representation() + elif self.settings["context"].ContextIdentifier == "Reference": + if self.settings["context"].TargetView == "GRAPH_VIEW": + return self.create_structural_reference_representation() + elif self.settings["context"].ContextIdentifier == "Profile": + return self.create_curve3d_representation() + elif self.settings["context"].ContextIdentifier == "SurveyPoints": + return self.create_geometric_curve_set_representation() + + def create_plan_representation(self): + if self.settings["context"].ContextIdentifier == "Annotation": + if self.settings["is_text"]: + return self.create_text_representation() + shape_representation = self.create_geometric_curve_set_representation(is_2d=True) + shape_representation.RepresentationType = "Annotation2D" + return shape_representation + elif self.settings["context"].ContextIdentifier == "Axis": + return self.create_curve2d_representation() + elif self.settings["context"].ContextIdentifier == "Body": + pass + elif self.settings["context"].ContextIdentifier == "Box": + pass + elif self.settings["context"].ContextIdentifier == "Clearance": + pass + elif self.settings["context"].ContextIdentifier == "CoG": + pass + elif self.settings["context"].ContextIdentifier == "FootPrint": + if self.settings["context"].TargetView in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]: + return self.create_geometric_curve_set_representation(is_2d=True) + elif self.settings["context"].ContextIdentifier == "Reference": + pass + elif self.settings["context"].ContextIdentifier == "Profile": + pass + elif self.settings["context"].ContextIdentifier == "SurveyPoints": + pass + + def create_variable_representation(self): + if self.settings["is_wireframe"]: + return self.create_wireframe_representation() + elif self.settings["is_curve"]: + return self.create_curve_representation() + elif self.settings["is_point_cloud"]: + return self.create_point_cloud_representation() + return self.create_mesh_representation() + + def create_mesh_representation(self): + if self.file.schema == "IFC2X3" or self.settings["should_force_faceted_brep"]: + return self.create_faceted_brep() + return self.create_polygonal_face_set() + + def create_faceted_brep(self): + self.create_vertices() + ifc_raw_items = [None] * self.settings["total_items"] + for i, value in enumerate(ifc_raw_items): + ifc_raw_items[i] = [] + for polygon in self.settings["geometry"].polygons: + ifc_raw_items[polygon.material_index % self.settings["total_items"]].append( + self.file.createIfcFace( + [ + self.file.createIfcFaceOuterBound( + self.file.createIfcPolyLoop([self.ifc_vertices[vertice] for vertice in polygon.vertices]), + True, + ) + ] + ) + ) + # TODO: May not actually be a closed shell, but who checks anyway? + items = [self.file.createIfcFacetedBrep(self.file.createIfcClosedShell(i)) for i in ifc_raw_items if i] + return self.file.createIfcShapeRepresentation( + self.settings["context"], + self.settings["context"].ContextIdentifier, + "Brep", + items, + ) + + def create_vertices(self, is_2d=False): + if is_2d: + for v in self.settings["geometry"].vertices: + co = self.convert_si_to_unit(v.co) + self.ifc_vertices.append(self.file.createIfcCartesianPoint((co[0], co[1]))) + return + self.ifc_vertices.extend( + [ + self.file.createIfcCartesianPoint(self.convert_si_to_unit(v.co)) + for v in self.settings["geometry"].vertices + ] + ) + + def convert_si_to_unit(self, co): + return co / self.settings["unit_scale"] diff --git a/src/ifcblenderexport/blenderbim/bim/operator.py b/src/ifcblenderexport/blenderbim/bim/operator.py index 08df3d915e..938201067d 100644 --- a/src/ifcblenderexport/blenderbim/bim/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/operator.py @@ -3362,117 +3362,6 @@ class PropagateTextData(bpy.types.Operator): return {"FINISHED"} -class PushRepresentation(bpy.types.Operator): - bl_idname = "bim.push_representation" - bl_label = "Push Representation" - - # Warning: This is an incredibly experimental operator. - def execute(self, context): - self.file = ifc.IfcStore.get_file() - - logger = logging.getLogger("ExportIFC") - output_file = "tmp.ifc" - ifc_export_settings = export_ifc.IfcExportSettings.factory(context, output_file, logger) - qto_calculator = qto.QtoCalculator() - ifc_parser = export_ifc.IfcParser(ifc_export_settings, qto_calculator) - ifc_parser.parse([bpy.context.active_object]) - self.ifc_exporter = export_ifc.IfcExporter(ifc_export_settings, ifc_parser) - self.ifc_exporter.file = ifcopenshell.file(schema=self.file.schema) - self.ifc_exporter.create_origin() - self.ifc_exporter.create_rep_context() - self.ifc_exporter.create_representations() - - self.context, self.subcontext, self.target_view, self.mesh_name = bpy.context.active_object.data.name.split("/") - rep_context = self.get_geometric_representation_context() - - for key, rep in self.ifc_exporter.ifc_parser.representations.items(): - if key != bpy.context.active_object.data.name: - continue - if rep_context: - self.ifc_exporter.file.add(rep_context) - rep["ifc"].MappedRepresentation.ContextOfItems = rep_context - self.push_representation(rep["ifc"]) - break - self.file.write(bpy.context.scene.BIMProperties.ifc_file[0:-4] + "-patch.ifc") - return {"FINISHED"} - - def get_geometric_representation_context(self): - for element in self.file.by_type("IfcGeometricRepresentationSubContext"): - if self.is_current_context(element): - return element - - def push_representation(self, new_representation): - element = self.file.by_guid( - bpy.context.active_object.BIMObjectProperties.attributes.get("GlobalId").string_value - ) - old_shape = None - new_shape = self.file.add(new_representation.MappedRepresentation) - if element.is_a("IfcProduct"): - representations = element.Representation.Representations - else: - representations = [rm.MappedRepresentation for rm in element.RepresentationMaps] - for representation in representations: - if self.is_current_context(representation.ContextOfItems): - old_shape = self.resolve_mapped_representation(representation) - break - if old_shape: - self.swap_old_representation(old_shape, new_shape) - else: - self.add_new_representation(element, new_shape) - - def resolve_mapped_representation(self, representation): - if representation.RepresentationType == "MappedRepresentation": - if representation.Items: - return representation.Items[0].MappingSource.MappedRepresentation - return representation - - def swap_old_representation(self, old, new): - inverse_elements = self.file.get_inverse(old) - for element in inverse_elements: - for i, attribute in enumerate(element): - if (isinstance(attribute, list) or isinstance(attribute, tuple)) and old in attribute: - items = list(attribute) - for j, item in enumerate(items): - if item == old: - del items[j] - items.append(new) - element[i] = items - elif attribute == old: - element[i] = new - - def add_new_representation(self, element, new): - if element.is_a("IfcProduct"): - self.add_new_representation_to_product(element, new) - return - - if element.RepresentationMaps: - representation_maps = list(element.RepresentationMaps) - representation_maps.append(self.file.createIfcRepresentationMap(self.ifc_exporter.origin, new)) - element.RepresentationMaps = representation_maps - else: - element.RepresentationMaps = self.file.createIfcRepresentationMap(self.ifc_exporter.origin, new) - - if hasattr(element, "Types"): - related_objects = element.Types[0].RelatedObjects - elif hasattr(element, "ObjectTypeOf"): # IFC2X3 - related_objects = element.ObjectTypeOf[0].RelatedObjects - - for related_object in related_objects: - self.add_new_representation_to_product(related_object, new) - - def add_new_representation_to_product(self, element, new): - representations = list(element.Representation.Representations) - representations.append(new) - element.Representation.Representations = representations - - def is_current_context(self, element): - return ( - element.ContextType == self.context - and element.ContextIdentifier == self.subcontext - and element.TargetView == self.target_view - ) - - class ConvertLocalToGlobal(bpy.types.Operator): bl_idname = "bim.convert_local_to_global" bl_label = "Convert Local To Global" @@ -4458,16 +4347,23 @@ class BakeParametricGeometry(bpy.types.Operator): bl_label = "Bake Parametric Geometry" def execute(self, context): - # TODO rewrite, see #1222 - # obj = bpy.context.active_object - # if "/" not in obj.data.name: - # context, subcontext, target_view = ("Model", "Body", "MODEL_VIEW") - # else: - # context, subcontext, target_view = obj.data.name.split("/")[0:3] - # for representation in obj.BIMObjectProperties.representations: - # element = self.file.by_id(representation.ifc_definition_id) - # if ifcopenshell.util.element.is_representation_of_context(element, context, subcontext, target_view): - # representation.ifc_definition_id = 0 + import blenderbim.bim.module.geometry.add_shape_representation as add_shape_representation + obj = bpy.context.active_object + self.file = ifc.IfcStore.get_file() + element = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id) + usecase = add_shape_representation.Usecase(self.file, { + "context": element.ContextOfItems, + "geometry": obj.data, + "total_items": max(1, len(obj.material_slots)), + }) + result = usecase.execute() + if not result: + print("Failed to write shape representation") + return {"FINISHED"} + for inverse in self.file.get_inverse(element): + ifcopenshell.util.element.replace_attribute(inverse, element, result) + obj.data.BIMMeshProperties.ifc_definition_id = int(result.id()) + print(result) return {"FINISHED"} diff --git a/src/ifcblenderexport/blenderbim/bim/ui.py b/src/ifcblenderexport/blenderbim/bim/ui.py index 244b94eb0d..0d1731eaff 100644 --- a/src/ifcblenderexport/blenderbim/bim/ui.py +++ b/src/ifcblenderexport/blenderbim/bim/ui.py @@ -809,9 +809,6 @@ class BIM_PT_mesh(Panel): layout = self.layout props = context.active_object.data.BIMMeshProperties - row = layout.row(align=True) - row.operator("bim.push_representation") - row = layout.row() row.prop(props, "geometry_type") layout.label(text="IFC Parameters:")