diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index 39bc82df35..729786b7a7 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -28,6 +28,7 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.placement import blenderbim.tool as tool +import blenderbim.core.geometry import blenderbim.core.aggregate import blenderbim.core.spatial import blenderbim.core.style @@ -179,6 +180,7 @@ class IfcExporter: self.sync_object_placement(grid_obj) if grid_obj.matrix_world != obj.matrix_world: bpy.ops.bim.update_representation(obj=obj.name) + tool.Geometry.record_object_position(obj) def get_application_name(self): return "BlenderBIM" diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index b15cfc425d..27fa0688fa 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -678,6 +678,7 @@ class ActivateView(bpy.types.Operator): project_collection.children["Views"].children[camera.users_collection[0].name].hide_viewport = False bpy.data.collections.get(camera.users_collection[0].name).hide_render = False bpy.ops.bim.activate_drawing_style() + core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing)) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index 754cf053cd..f9c058cbea 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -158,3 +158,42 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None) ifc.run("group.assign_group", group=drawing_tool.get_drawing_group(drawing), product=element) collector.assign(obj) drawing_tool.enable_editing(obj) + + +def sync_references(ifc, collector, drawing_tool, drawing=None): + context = drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing)) + if not context: + return + + group = drawing_tool.get_drawing_group(drawing) + for reference_element in drawing_tool.get_potential_reference_elements(drawing): + reference_obj = ifc.get_object(reference_element) + annotation = drawing_tool.get_drawing_reference_annotation(drawing, reference_element) + + should_delete_existing_annotation = False + should_create_annotation = False + + if annotation and (not reference_obj or ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)): + should_delete_existing_annotation = True + + if reference_obj and (should_delete_existing_annotation or not annotation): + should_create_annotation = True + + if should_delete_existing_annotation: + annotation_obj = ifc.get_object(annotation) + if annotation_obj: + drawing_tool.delete_object(annotation_obj) + ifc.run("root.remove_product", product=annotation) + + if should_create_annotation: + annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context) + if annotation: + ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation) + ifc.run("group.assign_group", group=group, product=annotation) + collector.assign(ifc.get_object(annotation)) + + if reference_obj and ifc.is_moved(reference_obj): + drawing_tool.sync_object_placement(reference_obj) + + if reference_obj and ifc.is_edited(reference_obj): + drawing_tool.sync_object_representation(reference_obj) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index dd50034085..5d4471091e 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -159,6 +159,7 @@ class Drawing: def create_svg_sheet(cls, document, titleblock): pass def delete_collection(cls, collection): pass def delete_drawing_elements(cls, elements): pass + def delete_object(cls, obj): pass def disable_editing_drawings(cls): pass def disable_editing_sheets(cls): pass def disable_editing_text(cls, obj): pass @@ -193,6 +194,7 @@ class Drawing: def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): pass def set_drawing_collection_name(cls, group, collection): pass def show_decorations(cls): pass + def sync_object_placement(cls, obj): pass def update_text_value(cls, obj): pass @@ -247,6 +249,9 @@ class Ifc: def get_entity(cls, obj): pass def get_object(cls, entity): pass def get_schema(cls): pass + def is_deleted(cls, element): pass + def is_edited(cls, obj): pass + def is_moved(cls, obj): pass def link(cls, element, obj): pass def run(cls, command, **kwargs): pass def set(cls, ifc): pass diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 173730e817..95b8fd6d3b 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -19,13 +19,17 @@ import os import re import bpy +import bmesh import mathutils import webbrowser +import numpy as np import blenderbim.core.tool +import blenderbim.core.geometry import blenderbim.tool as tool import ifcopenshell.util.representation import blenderbim.bim.module.drawing.sheeter as sheeter import blenderbim.bim.module.drawing.annotation as annotation +import blenderbim.bim.module.drawing.helper as helper class Drawing(blenderbim.core.tool.Drawing): @@ -83,6 +87,10 @@ class Drawing(blenderbim.core.tool.Drawing): if obj: bpy.data.objects.remove(obj) + @classmethod + def delete_object(cls, obj): + bpy.data.objects.remove(obj) + @classmethod def disable_editing_drawings(cls): bpy.context.scene.DocProperties.is_editing_drawings = False @@ -363,3 +371,154 @@ class Drawing(blenderbim.core.tool.Drawing): elif target_view in ("SECTION_VIEW", "ELEVATION_VIEW") and location_hint: return location_hint + " " + target_view.split("_")[0] return target_view + + @classmethod + def get_potential_reference_elements(cls, drawing): + elements = [] + existing_references = cls.get_group_elements(cls.get_drawing_group(drawing)) + for element in tool.Ifc.get().by_type("IfcAnnotation"): + if element in existing_references or element == drawing: + continue + if element.ObjectType == "DRAWING": + psets = ifcopenshell.util.element.get_psets(element) + if psets.get("EPset_Drawing", {}).get("TargetView", None) in ("SECTION_VIEW", "ELEVATION_VIEW"): + elements.append(element) + for element in tool.Ifc.get().by_type("IfcGridAxis"): + elements.append(element) + return elements + + @classmethod + def get_drawing_reference_annotation(cls, drawing, reference_element): + if drawing == reference_element: + return True + for element in cls.get_group_elements(cls.get_drawing_group(drawing)): + if element.is_a("IfcAnnotation"): + for rel in element.HasAssignments: + if rel.is_a("IfcRelAssignsToProduct"): + if rel.RelatingProduct == reference_element: + return element + # We cannot associate IfcGridAxis directly, so we establish a convention: + # IfcRelAssignsToProduct.RelatingProduct = IfcGrid + # IfcRelAssignsToProduct.Name = IfcGridAxis.AxisTag + elif reference_element.is_a("IfcGridAxis") and rel.Name == reference_element.AxisTag: + return element + + @classmethod + def generate_reference_annotation(cls, drawing, reference_element, context): + if reference_element.is_a("IfcGridAxis"): + return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context) + + @classmethod + def generate_grid_axis_reference_annotation(cls, drawing, reference_element, context): + target_view = tool.Drawing.get_drawing_target_view(drawing) + + camera = tool.Ifc.get_object(drawing) + + is_ortho = camera.data.type == "ORTHO" + bounds = helper.ortho_view_frame(camera.data) if is_ortho else None + clipping = is_ortho and target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") + elevating = is_ortho and target_view in ("ELEVATION_VIEW", "SECTION_VIEW") + + def clone(src): + dst = src.copy() + dst.data = dst.data.copy() + dst.name = dst.name.replace("IfcGridAxis/", "") + dst.BIMObjectProperties.ifc_definition_id = 0 + dst.data.BIMMeshProperties.ifc_definition_id = 0 + return dst + + def disassemble(obj): + mesh = bmesh.new() + mesh.verts.ensure_lookup_table() + mesh.from_mesh(obj.data) + return obj, mesh + + def assemble(obj, mesh): + mesh.to_mesh(obj.data) + return obj + + def to_camera_coords(obj, mesh): + mesh.transform(camera.matrix_world.inverted() @ obj.matrix_world) + obj.matrix_world = camera.matrix_world + annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start)) + annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset + obj.matrix_world[0][3] += annotation_offset[0] + obj.matrix_world[1][3] += annotation_offset[1] + obj.matrix_world[2][3] += annotation_offset[2] + return obj, mesh + + def clip_to_camera_boundary(mesh): + mesh.verts.ensure_lookup_table() + points = [v.co for v in mesh.verts[0:2]] + points = helper.clip_segment(bounds, points) + if points is None: + return None + mesh.verts[0].co = points[0] + mesh.verts[1].co = points[1] + return mesh + + def draw_grids_vertically(mesh): + mesh.verts.ensure_lookup_table() + points = [v.co for v in mesh.verts[0:2]] + points = helper.elevate_segment(bounds, points) + if points is None: + return None + points = helper.clip_segment(bounds, points) + if points is None: + return None + mesh.verts[0].co = points[0] + mesh.verts[1].co = points[1] + return mesh + + obj = tool.Ifc.get_object(reference_element) + if not obj: + return + obj, mesh = to_camera_coords(*disassemble(clone(obj))) + + if clipping: + mesh = clip_to_camera_boundary(mesh) + elif elevating: + mesh = draw_grids_vertically(mesh) + + if mesh is None: + return + + assemble(obj, mesh) + + element = cls.run_root_assign_class( + obj=obj, + ifc_class="IfcAnnotation", + predefined_type="GRID", + should_add_representation=True, + context=context, + ifc_representation_class=None, + ) + return element + + @classmethod + def sync_object_representation(cls, obj): + bpy.ops.bim.update_representation(obj=obj.name) + + @classmethod + def sync_object_placement(cls, obj): + blender_matrix = np.array(obj.matrix_world) + element = tool.Ifc.get_entity(obj) + if (obj.scale - mathutils.Vector((1.0, 1.0, 1.0))).length > 1e-4: + bpy.ops.bim.update_representation(obj=obj.name) + return element + if element.is_a("IfcGridAxis"): + return cls.sync_grid_axis_object_placement(obj, element) + if not hasattr(element, "ObjectPlacement"): + return + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + return element + + @classmethod + def sync_grid_axis_object_placement(cls, obj, element): + grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0] + grid_obj = tool.Ifc.get_object(grid) + if grid_obj: + cls.sync_object_placement(grid_obj) + if grid_obj.matrix_world != obj.matrix_world: + bpy.ops.bim.update_representation(obj=obj.name) + tool.Geometry.record_object_position(obj) diff --git a/src/blenderbim/blenderbim/tool/ifc.py b/src/blenderbim/blenderbim/tool/ifc.py index ad719f9c55..d487aafbcb 100644 --- a/src/blenderbim/blenderbim/tool/ifc.py +++ b/src/blenderbim/blenderbim/tool/ifc.py @@ -17,6 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import bpy +import numpy as np import ifcopenshell.api import blenderbim.core.tool from blenderbim.bim.ifc import IfcStore @@ -44,6 +45,26 @@ class Ifc(blenderbim.core.tool.Ifc): if IfcStore.get_file(): return IfcStore.get_file().schema + @classmethod + def is_deleted(cls, element): + return element.id() in IfcStore.deleted_ids + + @classmethod + def is_edited(cls, obj): + return list(obj.scale) != [1.0, 1.0, 1.0] or obj in IfcStore.edited_objs + + @classmethod + def is_moved(cls, obj): + if not obj.BIMObjectProperties.location_checksum: + return True # Let's be conservative + loc_check = np.frombuffer(eval(obj.BIMObjectProperties.location_checksum)) + rot_check = np.frombuffer(eval(obj.BIMObjectProperties.rotation_checksum)) + loc_real = np.array(obj.matrix_world.translation).flatten() + rot_real = np.array(obj.matrix_world.to_3x3()).flatten() + if np.allclose(loc_check, loc_real, atol=1e-4) and np.allclose(rot_check, rot_real, atol=1e-2): + return False + return True + @classmethod def schema(cls): return IfcStore.get_schema() diff --git a/src/ifcopenshell-python/ifcopenshell/api/drawing/assign_product.py b/src/ifcopenshell-python/ifcopenshell/api/drawing/assign_product.py index 913a82e8e5..b41ce3865b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/drawing/assign_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/drawing/assign_product.py @@ -31,16 +31,32 @@ class Usecase: self.settings[key] = value def execute(self): - if self.settings["related_object"].HasAssignments: - for assignment in self.settings["related_object"].HasAssignments: - if ( - assignment.is_a("IfcRelAssignsToProduct") - and assignment.RelatingProduct == self.settings["relating_product"] - ): + is_grid_axis = self.settings["relating_product"].is_a("IfcGridAxis") + + if is_grid_axis: + if self.settings["related_object"].HasAssignments: + for rel in self.settings["related_object"].HasAssignments: + if rel.is_a("IfcRelAssignsToProduct") and rel.Name == self.settings["relating_product"].AxisTag: + return + elif self.settings["related_object"].HasAssignments: + for rel in self.settings["related_object"].HasAssignments: + if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == self.settings["relating_product"]: return referenced_by = None - if self.settings["relating_product"].ReferencedBy: + + if is_grid_axis: + axis = self.settings["relating_product"] + grid = None + for attribute in ("PartOfW", "PartOfV", "PartOfU"): + if getattr(axis, attribute, None): + grid = getattr(axis, attribute)[0] + self.settings["relating_product"] = grid + for rel in grid.ReferencedBy: + if rel.Name == axis.AxisTag: + referenced_by = rel + break + elif self.settings["relating_product"].ReferencedBy: referenced_by = self.settings["relating_product"].ReferencedBy[0] if referenced_by: @@ -58,4 +74,7 @@ class Usecase: "RelatingProduct": self.settings["relating_product"], } ) + + if is_grid_axis: + referenced_by.Name = axis.AxisTag return referenced_by