From 02903c4929a8dae099215b505c5ee978c4485aa7 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 1 May 2022 17:25:03 +1000 Subject: [PATCH] #1153 New arc from 3 points command for 2D CAD --- .../blenderbim/bim/module/cad/__init__.py | 1 + .../blenderbim/bim/module/cad/operator.py | 84 ++++++++++++++++++- .../blenderbim/bim/module/cad/workspace.py | 5 ++ src/blenderbim/blenderbim/tool/cad.py | 32 ++++++- 4 files changed, 118 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/cad/__init__.py b/src/blenderbim/blenderbim/bim/module/cad/__init__.py index 179cf3005b..dd7efcb577 100644 --- a/src/blenderbim/blenderbim/bim/module/cad/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cad/__init__.py @@ -21,6 +21,7 @@ from . import operator, workspace classes = ( operator.CadArcFrom2Points, + operator.CadArcFrom3Points, operator.CadFillet, operator.CadMitre, operator.CadTrimExtend, diff --git a/src/blenderbim/blenderbim/bim/module/cad/operator.py b/src/blenderbim/blenderbim/bim/module/cad/operator.py index 8d1be13cbf..5f3a974a3e 100644 --- a/src/blenderbim/blenderbim/bim/module/cad/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cad/operator.py @@ -140,8 +140,9 @@ class CadFillet(bpy.types.Operator): layout.prop(self, prop) def execute(self, context): - if bpy.context.mode != "EDIT_MESH": - return {"CANCELLED"} + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.mode_set(mode="EDIT") + obj = bpy.context.active_object cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location mesh = obj.data @@ -205,6 +206,7 @@ class CadFillet(bpy.types.Operator): bmesh.update_edit_mesh(mesh) mesh.update() + bm.free() return {"FINISHED"} @@ -212,7 +214,6 @@ class CadArcFrom2Points(bpy.types.Operator): bl_idname = "bim.cad_arc_from_2_points" bl_label = "CAD Arc from 2 Points" bl_options = {"REGISTER", "UNDO"} - resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1) should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False) @@ -254,4 +255,81 @@ class CadArcFrom2Points(bpy.types.Operator): bmesh.ops.spin(bm, geom=[v], axis=axis, cent=cursor, steps=self.resolution * 4, angle=-angle) bmesh.update_edit_mesh(mesh) mesh.update() + bm.free() + return {"FINISHED"} + + +class CadArcFrom3Points(bpy.types.Operator): + bl_idname = "bim.cad_arc_from_3_points" + bl_label = "CAD Arc from 3 Points" + bl_options = {"REGISTER", "UNDO"} + resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1) + only_recalculate_center: bpy.props.BoolProperty(name="Only Recalculate Center", default=False) + + @classmethod + def poll(cls, context): + obj = context.active_object + return obj and obj.type == "MESH" and context.mode == "EDIT_MESH" + + def draw(self, context): + layout = self.layout + for prop in self.__class__.__annotations__.keys(): + layout.prop(self, prop) + + def execute(self, context): + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.mode_set(mode="EDIT") + + if bpy.context.mode != "EDIT_MESH": + return {"CANCELLED"} + + obj = bpy.context.edit_object + mesh = obj.data + bm = bmesh.from_edit_mesh(mesh) + + selected_verts = [v for v in bm.verts if v.select] + if len(selected_verts) != 3: + return {"CANCELLED"} + pts = [v.co for v in selected_verts] + center = tool.Cad.generate_3PT(pts, obj, self.resolution * 4) + if not center: + return {"CANCELLED"} + + bpy.context.scene.cursor.location = center + if self.only_recalculate_center: + bm.free() + return {"FINISHED"} + + def get_distance_to_other_points(vert): + other_verts = [v for v in selected_verts if v != vert] + total_vector = mathutils.Vector((0, 0, 0)) + for other_vert in other_verts: + total_vector += other_vert.co - vert.co + return total_vector.length + + # For three selected points that represent a start, end, and point on an + # arc, the start and end verts can be identified by being furthest away + # from the other 2 points. + arc_end_verts = sorted(selected_verts, key=get_distance_to_other_points)[-2:] + normal = mathutils.geometry.normal(pts) + + dir1 = (arc_end_verts[0].co - center).normalized() + dir2 = (arc_end_verts[1].co - center).normalized() + + # Let's get the matrix that represents the coordinate system of the arc. + # This matrix allows us to get 2D vectors for calculating the signed arc angle. + z = normal + x = (arc_end_verts[0].co - center).normalized() + y = z.cross(x) + arc_matrix = mathutils.Matrix([x, y, z]).transposed().to_4x4() + + dir1 = ((arc_matrix.inverted() @ arc_end_verts[0].co) - (arc_matrix.inverted() @ center)).normalized() + dir2 = ((arc_matrix.inverted() @ arc_end_verts[1].co) - (arc_matrix.inverted() @ center)).normalized() + angle = dir1.xy.angle_signed(dir2.xy) + + v = arc_end_verts[0] + bmesh.ops.spin(bm, geom=[v], axis=normal, cent=center, steps=self.resolution * 4, angle=-angle) + bmesh.update_edit_mesh(mesh) + mesh.update() + bm.free() return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cad/workspace.py b/src/blenderbim/blenderbim/bim/module/cad/workspace.py index df653ea33a..2203846f36 100644 --- a/src/blenderbim/blenderbim/bim/module/cad/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/cad/workspace.py @@ -41,6 +41,7 @@ class CadTool(WorkSpaceTool): # Enable Mesh Tools add-on to get this amazing tool ("mesh.offset_edges", {"type": "O", "value": "PRESS", "shift": True}, {"properties": []}), ("bim.cad_arc_from_2_points", {"type": "C", "value": "PRESS", "shift": True}, {"properties": []}), + ("bim.cad_arc_from_3_points", {"type": "V", "value": "PRESS", "shift": True}, {"properties": []}), ) def draw_settings(context, layout, tool): @@ -64,6 +65,10 @@ class CadTool(WorkSpaceTool): row.label(text="", icon="EVENT_SHIFT") row.label(text="2-Point Arc", icon="EVENT_C") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="3-Point Arc", icon="EVENT_V") + class CadHotkey(bpy.types.Operator): bl_idname = "bim.cad_hotkey" diff --git a/src/blenderbim/blenderbim/tool/cad.py b/src/blenderbim/blenderbim/tool/cad.py index 75d8c445dd..d668e0bff9 100644 --- a/src/blenderbim/blenderbim/tool/cad.py +++ b/src/blenderbim/blenderbim/tool/cad.py @@ -25,12 +25,15 @@ # # - Instead of AutoVTX, user explicitly chooses V, T, or X mode # - Instead of adding edges, existing edges are extended +# - An arc is reconstructed from 3 points instead of a full circle +# - You can now derive the center from an arc without generating geometry import sys import bpy +import math import bmesh -from mathutils import Vector, geometry +from mathutils import Vector, Matrix, geometry from mathutils.geometry import intersect_line_line as LineIntersect from mathutils.geometry import intersect_point_line as PtLineIntersect @@ -298,3 +301,30 @@ class Cad: elif mode == "V": bm = cls.perform_v(bm, point, edges[0], edges[1], (p1, p2, p3, p4), vertex_indices) return bm + + @classmethod + def generate_3PT(cls, pts, obj, nv, mode=1): + mw = obj.matrix_world + V = Vector + nv = max(3, nv) + + # construction + v1, v2, v3, v4 = V(pts[0]), V(pts[1]), V(pts[1]), V(pts[2]) + edge1_mid = v1.lerp(v2, 0.5) + edge2_mid = v3.lerp(v4, 0.5) + axis = geometry.normal(v1, v2, v4) + mat_rot = Matrix.Rotation(math.radians(90.0), 4, axis) + + # triangle edges + v1_ = ((v1 - edge1_mid) @ mat_rot) + edge1_mid + v2_ = ((v2 - edge1_mid) @ mat_rot) + edge1_mid + v3_ = ((v3 - edge2_mid) @ mat_rot) + edge2_mid + v4_ = ((v4 - edge2_mid) @ mat_rot) + edge2_mid + + r = geometry.intersect_line_line(v1_, v2_, v3_, v4_) + if r: + p1, _ = r + cp = mw @ p1 + return cp + else: + print("not on a circle")