From 08561b11c3fbca7be6ad95221f82aa90bfda02e1 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 3 Mar 2023 17:47:00 +1100 Subject: [PATCH] Fix #2077. You can now edit profiles via their axis line. --- .../blenderbim/bim/module/cad/workspace.py | 42 ++- .../blenderbim/bim/module/model/__init__.py | 3 + .../blenderbim/bim/module/model/decorator.py | 269 ++++++++++++++++++ .../blenderbim/bim/module/model/profile.py | 137 ++++++++- .../blenderbim/bim/module/model/slab.py | 269 +----------------- .../blenderbim/bim/module/model/workspace.py | 15 +- .../blenderbim/bim/module/profile/data.py | 2 +- .../blenderbim/bim/module/profile/operator.py | 12 +- src/blenderbim/blenderbim/bim/prop.py | 2 +- src/blenderbim/blenderbim/tool/model.py | 49 +++- 10 files changed, 511 insertions(+), 289 deletions(-) create mode 100644 src/blenderbim/blenderbim/bim/module/model/decorator.py diff --git a/src/blenderbim/blenderbim/bim/module/cad/workspace.py b/src/blenderbim/blenderbim/bim/module/cad/workspace.py index e5d7ef85e9..a8507e92e7 100644 --- a/src/blenderbim/blenderbim/bim/module/cad/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/cad/workspace.py @@ -48,7 +48,9 @@ class CadTool(WorkSpaceTool): def draw_settings(context, layout, tool): obj = context.active_object - if obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile: + if not obj or not obj.data: + return + if hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "PROFILE": row = layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_Q") @@ -100,6 +102,32 @@ class CadTool(WorkSpaceTool): row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_X") row.operator("bim.reset_vertex", text="Reset Vertex") + elif hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "AXIS": + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="", icon="EVENT_Q") + row.operator("bim.edit_extrusion_axis", text="Save Axis") + row.operator("bim.disable_editing_extrusion_axis", text="", icon="CANCEL") + + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="", icon="EVENT_E") + row.operator("bim.cad_hotkey", text="Extend").hotkey = "S_E" + + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="", icon="EVENT_T") + row.operator("bim.cad_hotkey", text="Mitre").hotkey = "S_T" + + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="", icon="EVENT_F") + row.operator("bim.cad_hotkey", text="Fillet").hotkey = "S_F" + + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="", icon="EVENT_O") + row.operator("bim.cad_hotkey", text="Offset").hotkey = "S_O" else: row = layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") @@ -187,7 +215,10 @@ class CadHotkey(bpy.types.Operator): def hotkey_S_Q(self): if tool.Ifc.get_entity(bpy.context.active_object): - bpy.ops.bim.edit_extrusion_profile() + if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE": + bpy.ops.bim.edit_extrusion_profile() + elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS": + bpy.ops.bim.edit_extrusion_axis() else: bpy.ops.bim.edit_arbitrary_profile() @@ -210,4 +241,9 @@ class CadHotkey(bpy.types.Operator): def is_profile(self): obj = bpy.context.active_object - return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile + return ( + obj + and obj.data + and hasattr(obj.data, "BIMMeshProperties") + and obj.data.BIMMeshProperties.subshape_type == "PROFILE" + ) diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index dcff87044a..14b7494a35 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -74,6 +74,9 @@ classes = ( opening.ShowOpenings, profile.ChangeCardinalPoint, profile.ChangeProfileDepth, + profile.DisableEditingExtrusionAxis, + profile.EditExtrusionAxis, + profile.EnableEditingExtrusionAxis, profile.ExtendProfile, profile.RecalculateProfile, profile.Rotate90, diff --git a/src/blenderbim/blenderbim/bim/module/model/decorator.py b/src/blenderbim/blenderbim/bim/module/model/decorator.py new file mode 100644 index 0000000000..a5379621ad --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/model/decorator.py @@ -0,0 +1,269 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import gpu +import bgl +import bmesh +import blenderbim.tool as tool +from math import pi, degrees, sin, cos, radians +from bpy.types import SpaceView3D +from mathutils import Vector, Matrix +from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat +from gpu_extras.batch import batch_for_shader + + +class ProfileDecorator: + installed = None + + @classmethod + def install(cls, context): + if cls.installed: + cls.uninstall() + handler = cls() + cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW") + + @classmethod + def uninstall(cls): + try: + SpaceView3D.draw_handler_remove(cls.installed, "WINDOW") + except ValueError: + pass + cls.installed = None + + def __call__(self, context): + obj = context.active_object + if obj.mode != "EDIT": + return + + bgl.glLineWidth(2) + bgl.glPointSize(6) + bgl.glEnable(bgl.GL_BLEND) + bgl.glEnable(bgl.GL_LINE_SMOOTH) + + all_vertices = [] + error_vertices = [] + selected_vertices = [] + unselected_vertices = [] + special_vertices = [] + special_vertex_indices = {} + selected_edges = [] + unselected_edges = [] + special_edges = [] # edges that have a circle or an arc associated with them + + arc_groups = [] + circle_groups = [] + for i, group in enumerate(obj.vertex_groups): + if "IFCARCINDEX" in group.name: + arc_groups.append(i) + elif "IFCCIRCLE" in group.name: + circle_groups.append(i) + + arcs = {} + circles = {} + + bm = bmesh.from_edit_mesh(obj.data) + + # https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess + # This is how we access vertex groups via bmesh, apparently, it's not very intuitive + deform_layer = bm.verts.layers.deform.active + + for vertex in bm.verts: + co = tuple(obj.matrix_world @ vertex.co) + all_vertices.append(co) + if vertex.hide: + continue + + is_arc = False + for group_index in arc_groups: + if group_index in vertex[deform_layer]: + is_arc = True + break + if is_arc: + arcs.setdefault(group_index, []).append(vertex) + special_vertex_indices[vertex.index] = group_index + + is_circle = False + for group_index in circle_groups: + if group_index in vertex[deform_layer]: + is_circle = True + break + if is_circle: + circles.setdefault(group_index, []).append(vertex) + special_vertex_indices[vertex.index] = group_index + + if vertex.select: + selected_vertices.append(co) + else: + if len(vertex.link_edges) > 1 and is_circle: + error_vertices.append(co) + elif is_circle: + special_vertices.append(co) + elif len(vertex.link_edges) != 2: + error_vertices.append(co) + elif is_arc: + special_vertices.append(co) + else: + unselected_vertices.append(co) + + for edge in bm.edges: + edge_indices = [v.index for v in edge.verts] + if edge.hide: + continue + if edge.select: + selected_edges.append(edge_indices) + else: + i1, i2 = edge.verts[0].index, edge.verts[1].index + if i1 in special_vertex_indices and special_vertex_indices[i1] == special_vertex_indices.get(i2, None): + special_edges.append(edge_indices) + else: + unselected_edges.append(edge_indices) + indices = [[v.index for v in e.verts] for e in bm.edges] + + white = (1, 1, 1, 1) + green = (0.545, 0.863, 0, 1) + red = (1, 0.2, 0.322, 1) + blue = (0.157, 0.565, 1, 1) + grey = (0.2, 0.2, 0.2, 1) + + self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR") + + batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=unselected_edges) + self.shader.bind() + self.shader.uniform_float("color", white) + batch.draw(self.shader) + + batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=selected_edges) + self.shader.uniform_float("color", green) + batch.draw(self.shader) + + batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=special_edges) + self.shader.uniform_float("color", grey) + batch.draw(self.shader) + + batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices}) + self.shader.uniform_float("color", white) + batch.draw(self.shader) + + batch = batch_for_shader(self.shader, "POINTS", {"pos": error_vertices}) + self.shader.uniform_float("color", red) + batch.draw(self.shader) + + batch = batch_for_shader(self.shader, "POINTS", {"pos": special_vertices}) + self.shader.uniform_float("color", blue) + batch.draw(self.shader) + + batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices}) + self.shader.uniform_float("color", green) + batch.draw(self.shader) + + # Draw arcs + + arc_centroids = [] + arc_segments = [] + for arc in arcs.values(): + if len(arc) != 3: + continue + sorted_arc = [None, None, None] + for v1 in arc: + connections = 0 + for link_edge in v1.link_edges: + v2 = link_edge.other_vert(v1) + if v2 in arc: + connections += 1 + if connections == 2: # Midpoint + sorted_arc[1] = v1 + else: + sorted_arc[2 if sorted_arc[2] is None else 0] = v1 + points = [tuple(obj.matrix_world @ v.co) for v in sorted_arc] + centroid = tool.Cad.get_center_of_arc(points) + if centroid: + arc_centroids.append(tuple(centroid)) + arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True)) + + batch = batch_for_shader(self.shader, "POINTS", {"pos": arc_centroids}) + self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1)) + batch.draw(self.shader) + + for verts, edges in arc_segments: + batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges) + self.shader.uniform_float("color", (0.157, 0.565, 1, 1)) + batch.draw(self.shader) + + # Draw circles + + circle_centroids = [] + circle_segments = [] + for circle in circles.values(): + if len(circle) != 2: + continue + p1 = obj.matrix_world @ circle[0].co + p2 = obj.matrix_world @ circle[1].co + radius = (p2 - p1).length / 2 + centroid = p1.lerp(p2, 0.5) + circle_centroids.append(tuple(centroid)) + segments = self.create_circle_segments(360, 20, radius) + matrix = obj.matrix_world.copy() + matrix.col[3] = centroid.to_4d() + segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]] + circle_segments.append(segments) + + batch = batch_for_shader(self.shader, "POINTS", {"pos": circle_centroids}) + self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1)) + batch.draw(self.shader) + + for verts, edges in circle_segments: + batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges) + self.shader.uniform_float("color", (0.157, 0.565, 1, 1)) + batch.draw(self.shader) + + def create_matrix(self, p, x, y, z): + return Matrix([x, y, z, p]).to_4x4().transposed() + + # https://github.com/nortikin/sverchok/blob/master/nodes/generator/basic_3pt_arc.py + # This function is taken from Sverchok, licensed under GPL v2-or-later. + # This is a combination of the make_verts and make_edges function. + def create_circle_segments(self, Angle, Vertices, Radius): + if Angle < 360: + theta = Angle / (Vertices - 1) + else: + theta = Angle / Vertices + listVertX = [] + listVertY = [] + for i in range(Vertices): + listVertX.append(Radius * cos(radians(theta * i))) + listVertY.append(Radius * sin(radians(theta * i))) + + if Angle < 360 and self.mode_ == 0: + sigma = radians(Angle) + listVertX[-1] = Radius * cos(sigma) + listVertY[-1] = Radius * sin(sigma) + elif Angle < 360 and self.mode_ == 1: + listVertX.append(0.0) + listVertY.append(0.0) + + points = list((x, y, 0) for x, y in zip(listVertX, listVertY)) + + listEdg = [(i, i + 1) for i in range(Vertices - 1)] + + if Angle < 360 and self.mode_ == 1: + listEdg.append((0, Vertices)) + listEdg.append((Vertices - 1, Vertices)) + else: + listEdg.append((Vertices - 1, 0)) + + return points, listEdg diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index 19878e32b8..ca581e11ca 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -18,7 +18,6 @@ import bpy import copy -import math import bmesh import mathutils.geometry import ifcopenshell @@ -29,10 +28,10 @@ import blenderbim.bim.handler import blenderbim.tool as tool import blenderbim.core.type import blenderbim.core.geometry -from blenderbim.bim.ifc import IfcStore from math import pi, degrees, inf -from mathutils import Vector, Matrix +from mathutils import Vector, Matrix, Quaternion from blenderbim.bim.module.geometry.helper import Helper +from blenderbim.bim.module.model.decorator import ProfileDecorator def element_listener(element, obj): @@ -48,12 +47,12 @@ def mode_callback(obj, data): or not bpy.context.scene.BIMProjectProperties.is_authoring ): return - product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) + product = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id) parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile": return if obj.mode == "EDIT": - IfcStore.edited_objs.add(obj) + tool.Ifc.edit(obj) bm = bmesh.from_edit_mesh(obj.data) bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) bmesh.update_edit_mesh(obj.data) @@ -97,8 +96,8 @@ class DumbProfileGenerator: self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) def generate(self): - self.file = IfcStore.get_file() - self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) + self.file = tool.Ifc.get() + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) material = ifcopenshell.util.element.get_material(self.relating_type) if material and material.is_a("IfcMaterialProfileSet"): self.profile_set = material @@ -328,7 +327,7 @@ class DumbProfileJoiner: body = copy.deepcopy(axis1) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) si_length = unit_scale * length - end = profile1.matrix_world @ Vector((si_length, 0, 0)) + end = profile1.matrix_world @ Vector((0, 0, si_length)) axis[1] = end body[1] = end self.recreate_profile(element1, profile1, axis, body) @@ -500,9 +499,7 @@ class DumbProfileJoiner: # Openings should move with the host overall ... # ... except their position should stay the same along the local Z axis of the wall for opening in [r.RelatedOpeningElement for r in element.HasOpenings]: - percent = tool.Cad.edge_percent( - self.body[0], (previous_origin, (previous_matrix @ Vector((0, 0, 1)))) - ) + percent = tool.Cad.edge_percent(self.body[0], (previous_origin, (previous_matrix @ Vector((0, 0, 1))))) is_z_offset_increased = True if percent < 0 else False change_in_z = (self.body[0] - previous_origin).length / self.unit_scale @@ -913,3 +910,121 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator): bpy.context.view_layer.update() DumbProfileRecalculator().recalculate(objs) return {"FINISHED"} + + +class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_extrusion_axis" + bl_label = "Enable Editing Extrusion Axis" + 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) + + axis = ifcopenshell.util.representation.get_representation(element, "Model", "Axis", "GRAPH_VIEW") + if axis: + position = obj.matrix_world.copy() + tool.Model.import_axis(axis.Items[0], obj=obj) + else: + body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + extrusion = tool.Model.get_extrusion(body) + + if extrusion.Position: + position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist()) + position[0][3] *= self.unit_scale + position[1][3] *= self.unit_scale + position[2][3] *= self.unit_scale + else: + position = Matrix() + + direction = Vector(extrusion.ExtrudedDirection.DirectionRatios).normalized() + tool.Model.import_axis([Vector((0, 0, 0)), direction * extrusion.Depth], obj=obj, position=position) + + bpy.ops.object.mode_set(mode="EDIT") + ProfileDecorator.install(context) + if not bpy.app.background: + bpy.ops.wm.tool_set_by_id(name="bim.cad_tool") + return {"FINISHED"} + + +class DisableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.disable_editing_extrusion_axis" + bl_label = "Disable Editing Extrusion Axis" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.selected_objects + + def _execute(self, context): + ProfileDecorator.uninstall() + bpy.ops.object.mode_set(mode="OBJECT") + + obj = context.active_object + element = tool.Ifc.get_entity(obj) + body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=body, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) + return {"FINISHED"} + + +class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.edit_extrusion_axis" + bl_label = "Edit Extrusion Axis" + 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) + + start = obj.matrix_world @ obj.data.vertices[0].co.copy() + end = obj.matrix_world @ obj.data.vertices[1].co.copy() + depth = (end - start).length + z_axis = (end - start).normalized() + y_axis = Vector((0, 0, 1)) + x_axis = y_axis.cross(z_axis).normalized() + y_axis = z_axis.cross(x_axis).normalized() + + body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=body, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) + + matrix = Matrix( + ( + [x_axis[0], y_axis[0], z_axis[0], start.x], + [x_axis[1], y_axis[1], z_axis[1], start.y], + [x_axis[2], y_axis[2], z_axis[2], start.z], + [0, 0, 0, 1], + ) + ) + + obj.matrix_world = matrix + bpy.context.view_layer.update() + + joiner = DumbProfileJoiner() + joiner.set_depth(obj, depth / self.unit_scale) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/slab.py b/src/blenderbim/blenderbim/bim/module/model/slab.py index 15e60d43bc..e376a07c66 100644 --- a/src/blenderbim/blenderbim/bim/module/model/slab.py +++ b/src/blenderbim/blenderbim/bim/module/model/slab.py @@ -16,30 +16,21 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . -import os -import gpu -import blf -import bgl import bpy -import math import json import bmesh import ifcopenshell import ifcopenshell.util.type import ifcopenshell.util.unit import ifcopenshell.util.element -import mathutils.geometry import blenderbim.bim.handler import blenderbim.core.type import blenderbim.core.geometry import blenderbim.tool as tool -from bpy.types import SpaceView3D -from blenderbim.bim.ifc import IfcStore -from math import pi, degrees, sin, cos, radians +from math import radians from mathutils import Vector, Matrix from blenderbim.bim.module.geometry.helper import Helper -from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat -from gpu_extras.batch import batch_for_shader +from blenderbim.bim.module.model.decorator import ProfileDecorator def calculate_quantities(usecase_path, ifc_file, settings): @@ -123,8 +114,8 @@ class DumbSlabGenerator: self.relating_type = relating_type def generate(self): - self.file = IfcStore.get_file() - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) + self.file = tool.Ifc.get() + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) thicknesses = [] for rel in self.relating_type.HasAssociations: if rel.is_a("IfcRelAssociatesMaterial"): @@ -268,7 +259,7 @@ class DumbSlabPlaner: def change_thickness(self, element, thickness): body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") - obj = IfcStore.get_element(element.id()) + obj = tool.Ifc.get_object(element) if not obj: return @@ -573,7 +564,7 @@ class DisableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator): return context.selected_objects def _execute(self, context): - DecorationsHandler.uninstall() + ProfileDecorator.uninstall() bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object @@ -620,7 +611,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator): tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position) bpy.ops.object.mode_set(mode="EDIT") - DecorationsHandler.install(context) + ProfileDecorator.install(context) if not bpy.app.background: bpy.ops.wm.tool_set_by_id(name="bim.cad_tool") return {"FINISHED"} @@ -633,7 +624,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - DecorationsHandler.uninstall() + ProfileDecorator.uninstall() bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object @@ -657,7 +648,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator): self.layout.label(text="INVALID PROFILE") bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") - DecorationsHandler.install(context) + ProfileDecorator.install(context) bpy.ops.object.mode_set(mode="EDIT") return @@ -749,245 +740,3 @@ class SetArcIndex(bpy.types.Operator): group.add(selected_vertices, 1, "REPLACE") bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} - - -class DecorationsHandler: - installed = None - - @classmethod - def install(cls, context): - if cls.installed: - cls.uninstall() - handler = cls() - cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW") - - @classmethod - def uninstall(cls): - try: - SpaceView3D.draw_handler_remove(cls.installed, "WINDOW") - except ValueError: - pass - cls.installed = None - - def __call__(self, context): - obj = context.active_object - if obj.mode != "EDIT": - return - - bgl.glLineWidth(2) - bgl.glPointSize(6) - bgl.glEnable(bgl.GL_BLEND) - bgl.glEnable(bgl.GL_LINE_SMOOTH) - - all_vertices = [] - error_vertices = [] - selected_vertices = [] - unselected_vertices = [] - special_vertices = [] - special_vertex_indices = {} - selected_edges = [] - unselected_edges = [] - special_edges = [] # edges that have a circle or an arc associated with them - - arc_groups = [] - circle_groups = [] - for i, group in enumerate(obj.vertex_groups): - if "IFCARCINDEX" in group.name: - arc_groups.append(i) - elif "IFCCIRCLE" in group.name: - circle_groups.append(i) - - arcs = {} - circles = {} - - bm = bmesh.from_edit_mesh(obj.data) - - # https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess - # This is how we access vertex groups via bmesh, apparently, it's not very intuitive - deform_layer = bm.verts.layers.deform.active - - for vertex in bm.verts: - co = tuple(obj.matrix_world @ vertex.co) - all_vertices.append(co) - if vertex.hide: - continue - - is_arc = False - for group_index in arc_groups: - if group_index in vertex[deform_layer]: - is_arc = True - break - if is_arc: - arcs.setdefault(group_index, []).append(vertex) - special_vertex_indices[vertex.index] = group_index - - is_circle = False - for group_index in circle_groups: - if group_index in vertex[deform_layer]: - is_circle = True - break - if is_circle: - circles.setdefault(group_index, []).append(vertex) - special_vertex_indices[vertex.index] = group_index - - if vertex.select: - selected_vertices.append(co) - else: - if len(vertex.link_edges) > 1 and is_circle: - error_vertices.append(co) - elif is_circle: - special_vertices.append(co) - elif len(vertex.link_edges) != 2: - error_vertices.append(co) - elif is_arc: - special_vertices.append(co) - else: - unselected_vertices.append(co) - - for edge in bm.edges: - edge_indices = [v.index for v in edge.verts] - if edge.hide: - continue - if edge.select: - selected_edges.append(edge_indices) - else: - i1, i2 = edge.verts[0].index, edge.verts[1].index - if i1 in special_vertex_indices and special_vertex_indices[i1] == special_vertex_indices.get(i2, None): - special_edges.append(edge_indices) - else: - unselected_edges.append(edge_indices) - indices = [[v.index for v in e.verts] for e in bm.edges] - - white = (1, 1, 1, 1) - green = (0.545, 0.863, 0, 1) - red = (1, 0.2, 0.322, 1) - blue = (0.157, 0.565, 1, 1) - grey = (0.2, 0.2, 0.2, 1) - - self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR") - - batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=unselected_edges) - self.shader.bind() - self.shader.uniform_float("color", white) - batch.draw(self.shader) - - batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=selected_edges) - self.shader.uniform_float("color", green) - batch.draw(self.shader) - - batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=special_edges) - self.shader.uniform_float("color", grey) - batch.draw(self.shader) - - batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices}) - self.shader.uniform_float("color", white) - batch.draw(self.shader) - - batch = batch_for_shader(self.shader, "POINTS", {"pos": error_vertices}) - self.shader.uniform_float("color", red) - batch.draw(self.shader) - - batch = batch_for_shader(self.shader, "POINTS", {"pos": special_vertices}) - self.shader.uniform_float("color", blue) - batch.draw(self.shader) - - batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices}) - self.shader.uniform_float("color", green) - batch.draw(self.shader) - - # Draw arcs - - arc_centroids = [] - arc_segments = [] - for arc in arcs.values(): - if len(arc) != 3: - continue - sorted_arc = [None, None, None] - for v1 in arc: - connections = 0 - for link_edge in v1.link_edges: - v2 = link_edge.other_vert(v1) - if v2 in arc: - connections += 1 - if connections == 2: # Midpoint - sorted_arc[1] = v1 - else: - sorted_arc[2 if sorted_arc[2] is None else 0] = v1 - points = [tuple(obj.matrix_world @ v.co) for v in sorted_arc] - centroid = tool.Cad.get_center_of_arc(points) - if centroid: - arc_centroids.append(tuple(centroid)) - arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True)) - - batch = batch_for_shader(self.shader, "POINTS", {"pos": arc_centroids}) - self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1)) - batch.draw(self.shader) - - for verts, edges in arc_segments: - batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges) - self.shader.uniform_float("color", (0.157, 0.565, 1, 1)) - batch.draw(self.shader) - - # Draw circles - - circle_centroids = [] - circle_segments = [] - for circle in circles.values(): - if len(circle) != 2: - continue - p1 = obj.matrix_world @ circle[0].co - p2 = obj.matrix_world @ circle[1].co - radius = (p2 - p1).length / 2 - centroid = p1.lerp(p2, 0.5) - circle_centroids.append(tuple(centroid)) - segments = self.create_circle_segments(360, 20, radius) - matrix = obj.matrix_world.copy() - matrix.col[3] = centroid.to_4d() - segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]] - circle_segments.append(segments) - - batch = batch_for_shader(self.shader, "POINTS", {"pos": circle_centroids}) - self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1)) - batch.draw(self.shader) - - for verts, edges in circle_segments: - batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges) - self.shader.uniform_float("color", (0.157, 0.565, 1, 1)) - batch.draw(self.shader) - - def create_matrix(self, p, x, y, z): - return Matrix([x, y, z, p]).to_4x4().transposed() - - # https://github.com/nortikin/sverchok/blob/master/nodes/generator/basic_3pt_arc.py - # This function is taken from Sverchok, licensed under GPL v2-or-later. - # This is a combination of the make_verts and make_edges function. - def create_circle_segments(self, Angle, Vertices, Radius): - if Angle < 360: - theta = Angle / (Vertices - 1) - else: - theta = Angle / Vertices - listVertX = [] - listVertY = [] - for i in range(Vertices): - listVertX.append(Radius * cos(radians(theta * i))) - listVertY.append(Radius * sin(radians(theta * i))) - - if Angle < 360 and self.mode_ == 0: - sigma = radians(Angle) - listVertX[-1] = Radius * cos(sigma) - listVertY[-1] = Radius * sin(sigma) - elif Angle < 360 and self.mode_ == 1: - listVertX.append(0.0) - listVertY.append(0.0) - - points = list((x, y, 0) for x, y in zip(listVertX, listVertY)) - - listEdg = [(i, i + 1) for i in range(Vertices - 1)] - - if Angle < 360 and self.mode_ == 1: - listEdg.append((0, Vertices)) - listEdg.append((Vertices - 1, Vertices)) - else: - listEdg.append((Vertices - 1, 0)) - - return points, listEdg diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index e7de90bdb5..9a8ed666de 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -83,17 +83,18 @@ class BimTool(WorkSpaceTool): ("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}), ("bim.hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}), ("bim.hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}), + ("bim.hotkey", {"type": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}), ("bim.hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}), ("bim.hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}), + ("bim.hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}), ("bim.hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}), - ("bim.hotkey", {"type": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}), ("bim.hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}), ("bim.hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}), ("bim.hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}), ("bim.hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}), ("bim.hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}), + ("bim.hotkey", {"type": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}), ("bim.hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}), - ("bim.hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}), ) def draw_settings(context, layout, ws_tool): @@ -253,6 +254,10 @@ class BimToolUI: row.label(text="", icon="EVENT_E") row.operator("bim.hotkey", text="Extend").hotkey = "S_E" row = cls.layout.row(align=True) + row.label(text="", icon="EVENT_ALT") + row.label(text="", icon="EVENT_E") + row.operator("bim.hotkey", text="Edit Axis").hotkey = "A_E" + row = cls.layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_T") row.operator("bim.hotkey", text="Butt").hotkey = "S_T" @@ -607,6 +612,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return bpy.ops.bim.select_decomposition() + def hotkey_A_E(self): + if not bpy.context.selected_objects: + return + if self.active_material_usage == "PROFILE": + bpy.ops.bim.enable_editing_extrusion_axis() + def hotkey_A_O(self): if not bpy.context.selected_objects: return diff --git a/src/blenderbim/blenderbim/bim/module/profile/data.py b/src/blenderbim/blenderbim/bim/module/profile/data.py index 3a1cd28a7d..698b0a1f5f 100644 --- a/src/blenderbim/blenderbim/bim/module/profile/data.py +++ b/src/blenderbim/blenderbim/bim/module/profile/data.py @@ -72,4 +72,4 @@ class ProfileData: @classmethod def is_editing_arbitrary_profile(cls): obj = bpy.context.active_object - return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile + return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "PROFILE" diff --git a/src/blenderbim/blenderbim/bim/module/profile/operator.py b/src/blenderbim/blenderbim/bim/module/profile/operator.py index f925609b60..929c2c3971 100644 --- a/src/blenderbim/blenderbim/bim/module/profile/operator.py +++ b/src/blenderbim/blenderbim/bim/module/profile/operator.py @@ -21,7 +21,7 @@ import ifcopenshell.api import blenderbim.bim.helper import blenderbim.tool as tool import blenderbim.bim.module.model.profile as model_profile -from blenderbim.bim.module.model.slab import DecorationsHandler +from blenderbim.bim.module.model.decorator import ProfileDecorator from blenderbim.bim.module.profile.prop import generate_thumbnail_for_active_profile @@ -138,7 +138,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): bpy.context.scene.collection.objects.link(obj) bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode="EDIT") - DecorationsHandler.install(context) + ProfileDecorator.install(context) bpy.ops.wm.tool_set_by_id(name="bim.cad_tool") @@ -149,8 +149,8 @@ class DisableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - if obj and obj.data and obj.data.BIMMeshProperties.is_profile: - DecorationsHandler.uninstall() + if obj and obj.data and obj.data.BIMMeshProperties.subshape_type == "PROFILE": + ProfileDecorator.uninstall() bpy.ops.object.mode_set(mode="OBJECT") bpy.data.objects.remove(obj) @@ -166,7 +166,7 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object - DecorationsHandler.uninstall() + ProfileDecorator.uninstall() bpy.ops.object.mode_set(mode="OBJECT") profile = tool.Model.export_profile(obj) @@ -176,7 +176,7 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): self.layout.label(text="INVALID PROFILE: " + indices[1]) bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") - DecorationsHandler.install(context) + ProfileDecorator.install(context) bpy.ops.object.mode_set(mode="EDIT") return diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py index 74490ccad3..77940a5e73 100644 --- a/src/blenderbim/blenderbim/bim/prop.py +++ b/src/blenderbim/blenderbim/bim/prop.py @@ -421,7 +421,7 @@ class BIMMeshProperties(PropertyGroup): is_native: BoolProperty(name="Is Native", default=False) is_swept_solid: BoolProperty(name="Is Swept Solid") is_parametric: BoolProperty(name="Is Parametric", default=False) - is_profile: BoolProperty(name="Is Profile", default=False) + subshape_type: StringProperty(name="Subshape Type") ifc_definition: StringProperty(name="IFC Definition") ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter) material_checksum: StringProperty(name="Material Checksum", default="[]") diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 11a7bd3654..96b8229ae4 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -140,6 +140,38 @@ class Model(blenderbim.core.tool.Model): else: break + @classmethod + def import_axis(cls, axis, obj=None, position=None): + cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + + if position is None: + position = Matrix() + + cls.vertices = [] + cls.edges = [] + cls.arcs = [] + cls.circles = [] + + if isinstance(axis, list): + cls.vertices.extend([ + position @ Vector(cls.convert_unit_to_si(axis[0])).to_3d(), + position @ Vector(cls.convert_unit_to_si(axis[1])).to_3d(), + ]) + cls.edges.append([0, 1]) + else: + cls.import_curve(obj, position, axis) + + mesh = bpy.data.meshes.new("Axis") + mesh.from_pydata(cls.vertices, cls.edges, []) + mesh.BIMMeshProperties.subshape_type = "AXIS" + + if obj is None: + obj = bpy.data.objects.new("Axis", mesh) + else: + obj.data = mesh + + return obj + @classmethod def import_profile(cls, profile, obj=None, position=None): cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) @@ -162,7 +194,7 @@ class Model(blenderbim.core.tool.Model): mesh = bpy.data.meshes.new("Profile") mesh.from_pydata(cls.vertices, cls.edges, []) - mesh.BIMMeshProperties.is_profile = True + mesh.BIMMeshProperties.subshape_type = "PROFILE" if obj is None: obj = bpy.data.objects.new("Profile", mesh) @@ -195,6 +227,7 @@ class Model(blenderbim.core.tool.Model): cls.edges[-1] = (len(cls.vertices) - 1, offset) # Close the loop elif curve.is_a("IfcIndexedPolyCurve"): is_arc = False + is_closed = False if curve.Segments: for segment in curve.Segments: if len(segment[0]) == 3: # IfcArcIndex @@ -213,15 +246,21 @@ class Model(blenderbim.core.tool.Model): if is_arc: cls.arcs[-1].append(len(cls.vertices) - 1) is_arc = False + + if curve.Segments[0][0][0] == curve.Segments[-1][0][-1]: + is_closed = True else: for local_point in curve.Points.CoordList: global_point = position @ Vector(cls.convert_unit_to_si(local_point)).to_3d() cls.vertices.append(global_point) - # Curves without segments are cls closing - del cls.vertices[-1] - 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 + if cls.vertices[offset] == cls.vertices[-1]: + is_closed = True + del cls.vertices[-1] + + cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices) - 1)]) + if is_closed: + cls.edges.append([len(cls.vertices) - 1, offset]) # Close the loop elif curve.is_a("IfcCircle"): center = cls.convert_unit_to_si( Matrix(ifcopenshell.util.placement.get_axis2placement(curve.Position).tolist()).col[3].to_3d()