From 83084aeb7b118e7ebb184f31019d7842dce3a252 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 30 Apr 2023 20:57:51 +1000 Subject: [PATCH] Add profile-style (like slab editing) enable editing, edit, and disable editing operators for energy boundary surface geometry. --- .../bim/module/boundary/__init__.py | 7 +- .../bim/module/boundary/operator.py | 94 ++++++++++++++++++- src/blenderbim/blenderbim/tool/model.py | 88 +++++++++++++++++ 3 files changed, 186 insertions(+), 3 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/boundary/__init__.py b/src/blenderbim/blenderbim/bim/module/boundary/__init__.py index c23fe17f43..ea79a4aaa7 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/__init__.py @@ -22,13 +22,16 @@ from . import ui, operator, prop classes = ( operator.ColourByRelatedBuildingElement, operator.DisableEditingBoundary, + operator.DisableEditingBoundaryGeometry, operator.EditBoundaryAttributes, + operator.EditBoundaryGeometry, operator.EnableEditingBoundary, + operator.EnableEditingBoundaryGeometry, + operator.LoadBoundary, operator.LoadProjectSpaceBoundaries, operator.LoadSpaceBoundaries, - operator.LoadBoundary, - operator.SelectRelatedElementBoundaries, operator.SelectProjectBoundaries, + operator.SelectRelatedElementBoundaries, operator.SelectRelatedElementTypeBoundaries, operator.SelectSpaceBoundaries, operator.UpdateBoundaryGeometry, diff --git a/src/blenderbim/blenderbim/bim/module/boundary/operator.py b/src/blenderbim/blenderbim/bim/module/boundary/operator.py index 26ee795ca7..4bdf90097f 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/operator.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/operator.py @@ -24,8 +24,9 @@ import ifcopenshell.api import ifcopenshell.util.placement import ifcopenshell.util.unit import blenderbim.tool as tool -from blenderbim.bim.ifc import IfcStore import blenderbim.bim.import_ifc as import_ifc +from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.module.model.decorator import ProfileDecorator def get_boundaries_collection(blender_space): @@ -38,6 +39,20 @@ def get_boundaries_collection(blender_space): return boundaries_collection +def disable_editing_boundary_geometry(context): + ProfileDecorator.uninstall() + bpy.ops.object.mode_set(mode="OBJECT") + + obj = context.active_object + element = tool.Ifc.get_entity(obj) + + old_mesh = obj.data + loader = Loader() + obj.data = loader.create_mesh(element) + tool.Geometry.delete_data(old_mesh) + return {"FINISHED"} + + class Loader: def __init__(self): self.ifc_file = None @@ -376,3 +391,80 @@ class UpdateBoundaryGeometry(bpy.types.Operator): settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object) ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings) return {"FINISHED"} + + +class EnableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_boundary_geometry" + bl_label = "Enable Editing Boundary Geometry" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.selected_objects + + def _execute(self, context): + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + obj = context.active_object + element = tool.Ifc.get_entity(obj) + + if element.ConnectionGeometry.is_a("IfcConnectionSurfaceGeometry"): + surface = element.ConnectionGeometry.SurfaceOnRelatingElement + tool.Model.import_surface(surface, obj) + + bpy.ops.object.mode_set(mode="EDIT") + ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_boundary_geometry(context)) + if not bpy.app.background: + bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool") + return {"FINISHED"} + + +class EditBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.edit_boundary_geometry" + bl_label = "Edit Boundary Geometry" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + ProfileDecorator.uninstall() + bpy.ops.object.mode_set(mode="OBJECT") + + obj = context.active_object + element = tool.Ifc.get_entity(obj) + + if element.ConnectionGeometry.is_a("IfcConnectionSurfaceGeometry"): + surface = tool.Model.export_surface(obj) + + if not surface: + + def msg(self, context): + self.layout.label(text="INVALID PROFILE") + + bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") + ProfileDecorator.install( + context, exit_edit_mode_callback=lambda: disable_editing_boundary_geometry(context) + ) + bpy.ops.object.mode_set(mode="EDIT") + return + + old_surface = element.ConnectionGeometry.SurfaceOnRelatingElement + for inverse in tool.Ifc.get().get_inverse(old_surface): + ifcopenshell.util.element.replace_attribute(inverse, old_surface, surface) + ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_surface) + + old_mesh = obj.data + loader = Loader() + obj.data = loader.create_mesh(element) + tool.Geometry.delete_data(old_mesh) + + +class DisableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.disable_editing_boundary_geometry" + bl_label = "Disable Editing Boundary Geometry" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.selected_objects + + def _execute(self, context): + return disable_editing_boundary_geometry(context) diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 167ce8d9e2..ef475ce569 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -115,6 +115,56 @@ class Model(blenderbim.core.tool.Model): cls.bm.free() return profile + @classmethod + def export_surface(cls, obj): + p1, p2, p3 = [v.co.copy() for v in obj.data.vertices[0:3]] + + edge1 = p2 - p1 + edge2 = p3 - p1 + normal = edge1.cross(edge2) + z_axis = normal.normalized() + x_axis = p2 - p1 + x_axis.normalize() + y_axis = z_axis.cross(x_axis) + + position = Matrix() + position.col[0][:3] = x_axis + position.col[1][:3] = y_axis + position.col[2][:3] = z_axis + position.col[3][:3] = p1 + + cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + + helper = Helper(tool.Ifc.get()) + indices = helper.auto_detect_arbitrary_profile_with_voids(obj, obj.data) + + if isinstance(indices, tuple) and indices[0] is False: # Ugly + return + + cls.bm = bmesh.new() + cls.bm.from_mesh(obj.data) + cls.bm.verts.ensure_lookup_table() + cls.bm.edges.ensure_lookup_table() + + surface = tool.Ifc.get().createIfcCurveBoundedPlane() + surface.BasisSurface = tool.Ifc.get().createIfcPlane(tool.Ifc.get().createIfcAxis2Placement3D( + tool.Ifc.get().createIfcCartesianPoint([o / cls.unit_scale for o in p1]), + tool.Ifc.get().createIfcDirection([float(o) for o in z_axis]), + tool.Ifc.get().createIfcDirection([float(o) for o in x_axis]), + )) + + if tool.Ifc.get().schema != "IFC2X3": + cls.points = cls.export_points(position, indices["points"]) + + surface.OuterBoundary = cls.export_curve(position, indices["profile"]) + results = [] + for inner_curve in indices["inner_curves"]: + results.append(cls.export_curve(position, inner_curve)) + surface.InnerBoundaries = results + + cls.bm.free() + return surface + @classmethod def generate_occurrence_name(cls, element_type, ifc_class): props = bpy.context.scene.BIMModelProperties @@ -218,6 +268,40 @@ class Model(blenderbim.core.tool.Model): return obj + @classmethod + def import_surface(cls, surface, obj=None): + cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + + cls.vertices = [] + cls.edges = [] + cls.arcs = [] + cls.circles = [] + + if surface.is_a("IfcCurveBoundedPlane"): + position = Matrix(ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist()) + cls.import_curve(obj, position, surface.OuterBoundary) + for inner_boundary in surface.InnerBoundaries: + cls.import_curve(obj, position, inner_boundary) + + mesh = bpy.data.meshes.new("Surface") + mesh.from_pydata(cls.vertices, cls.edges, []) + mesh.BIMMeshProperties.subshape_type = "PROFILE" + + if obj is None: + obj = bpy.data.objects.new("Surface", mesh) + else: + obj.data = mesh + + for arc in cls.arcs: + group = obj.vertex_groups.new(name="IFCARCINDEX") + group.add(arc, 1, "REPLACE") + + for circle in cls.circles: + group = obj.vertex_groups.new(name="IFCCIRCLE") + group.add(circle, 1, "REPLACE") + + return obj + @classmethod def import_curve(cls, obj, position, curve): offset = len(cls.vertices) @@ -232,6 +316,10 @@ class Model(blenderbim.core.tool.Model): cls.vertices.append(global_point) cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices))]) cls.edges[-1] = (len(cls.vertices) - 1, offset) # Close the loop + elif curve.is_a("IfcCompositeCurve"): + # This is a first pass incomplete implementation only for simple polylines, and misses many details. + for segment in curve.Segments: + cls.import_curve(obj, position, segment.ParentCurve) elif curve.is_a("IfcIndexedPolyCurve"): is_arc = False is_closed = False