New 2D CAD offset operator

This commit is contained in:
Dion Moult
2022-09-28 21:07:11 +10:00
parent eafad2862f
commit 90a788082d
3 changed files with 243 additions and 26 deletions
@@ -25,6 +25,7 @@ classes = (
operator.CadArcFrom3Points,
operator.CadFillet,
operator.CadMitre,
operator.CadOffset,
operator.CadTrimExtend,
workspace.CadHotkey,
)
@@ -22,6 +22,8 @@ import bmesh
import mathutils
import bpy_extras
import blenderbim.tool as tool
from mathutils import Vector, Matrix
from math import pi, radians, sin, cos, sqrt
messages = {
"SHARED_VERTEX": "Shared Vertex, no intersection possible",
@@ -40,7 +42,6 @@ class CadTrimExtend(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def cancel_message(self, msg):
print(msg)
self.report({"WARNING"}, msg)
return {"CANCELLED"}
@@ -73,7 +74,6 @@ class CadTrimExtend(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(me, loop_triangles=True)
return {"FINISHED"}
@@ -87,7 +87,6 @@ class CadMitre(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def cancel_message(self, msg):
print(msg)
self.report({"WARNING"}, msg)
return {"CANCELLED"}
@@ -120,7 +119,6 @@ class CadMitre(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(me, loop_triangles=True)
return {"FINISHED"}
@@ -257,7 +255,7 @@ class CadArcFrom2Points(bpy.types.Operator):
v = selected_verts[0]
bm.verts.remove(selected_verts[1])
axis = region_3d.view_rotation @ mathutils.Vector((0, 0, 1))
axis = region_3d.view_rotation @ Vector((0, 0, 1))
bmesh.ops.spin(
bm,
geom=[v],
@@ -334,6 +332,202 @@ class CadArcFrom3Points(bpy.types.Operator):
return {"FINISHED"}
class CadOffset(bpy.types.Operator):
bl_idname = "bim.cad_offset"
bl_label = "CAD Offset"
bl_options = {"REGISTER", "UNDO"}
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
@classmethod
def poll(self, context):
return context.mode == 'EDIT_MESH'
def draw(self, context):
layout = self.layout
for prop in self.__class__.__annotations__.keys():
layout.prop(self, prop)
def cancel_message(self, msg):
self.report({"WARNING"}, msg)
return {"CANCELLED"}
def execute(self, context):
# This is not the same as Mesh Tools or Sverchok. Mesh Tools is based on
# a 3D "rail" which allows fancy 3D edge offsets, but will get different
# results to traditional CAD, which expects offsets to occur on a
# defined 2D workplane. This CAD offset does a pure 2D operation, so it
# always looks correct from the desired "2D workplane". This also
# reduces the settings down to one parameter instead of the huge config
# dialog that Mesh Tools has. Sverchok is 2D based, but has a weird side
# effect of converting an unclosed loop into a closed loop which is also
# not what users expect.
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
obj = context.active_object
mw = obj.matrix_world
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
edges = [e for e in bm.edges if e.select and not e.hide]
if len(edges) < 1:
return self.cancel_message("NO_EDGES")
verts = set()
[verts.update(e.verts) for e in edges]
# Use the viewport angle to determine the offset direction
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
# Don't ask me, I don't know.
# view_matrix = area.spaces.active.region_3d.view_matrix
x = area.spaces.active.region_3d.view_rotation @ Vector((1,0,0))
y = area.spaces.active.region_3d.view_rotation @ Vector((0,1,0))
z = area.spaces.active.region_3d.view_rotation @ Vector((0,0,1))
wp = Matrix([x,y,z,Vector((0,0,0))]).to_4x4().transposed()
break
rotation = Matrix.Rotation(pi / 2, 2, 'Z')
rotation_i = Matrix.Rotation(- pi / 2, 2, 'Z')
# Create loops from edges
loop_edges = set(edges)
loops = []
while loop_edges:
edge = loop_edges.pop()
loop = [edge]
has_found_connected_edge = True
while has_found_connected_edge:
has_found_connected_edge = False
for edge in loop_edges.copy():
edge_verts = set(edge.verts)
if edge_verts & set(loop[0].verts):
loop.insert(0, edge)
loop_edges.remove(edge)
has_found_connected_edge = True
elif edge_verts & set(loop[-1].verts):
loop.append(edge)
loop_edges.remove(edge)
has_found_connected_edge = True
loops.append(loop)
for loop in loops:
all_verts = {v.index for e in loop for v in e.verts}
possible_v1s = []
is_closed = True
for edge in loop:
# If we have an open loop, start at either end instead
v = edge.verts[0]
if len(v.link_edges) == 1:
possible_v1s.append(v)
is_closed = False
else:
for link_edge in v.link_edges:
if link_edge.other_vert(v).index not in all_verts:
possible_v1s.append(v)
is_closed = False
break
v = edge.verts[1]
if len(v.link_edges) == 1:
possible_v1s.append(v)
is_closed = False
else:
for link_edge in v.link_edges:
if link_edge.other_vert(v).index not in all_verts:
possible_v1s.append(v)
is_closed = False
break
# Always start at the same vertex, so that when the operator is
# refreshed with new parameters the axes don't randomly get flipped.
if is_closed:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
start = bm.verts[min(all_verts)]
else:
start = possible_v1s[0] if possible_v1s[0].index < possible_v1s[1].index else possible_v1s[1]
v1 = start
new_verts = []
processed_verts = set()
# [v0] --> v1 --> v2
while v1:
if v1.index in processed_verts:
break
link_verts = []
if len(v1.link_edges) == 1:
v = v1.link_edges[0].other_vert(v1)
link_verts.append(v) if v.index in all_verts else None
else:
v = v1.link_edges[0].other_vert(v1)
link_verts.append(v) if v.index in all_verts else None
v = v1.link_edges[1].other_vert(v1)
link_verts.append(v) if v.index in all_verts else None
if len(link_verts) == 1:
v2 = link_verts[0]
v0 = None if v2.index not in processed_verts else v2
v2 = None if v2.index in processed_verts else v2
else:
v2 = link_verts[0]
v0 = link_verts[1]
if v2.index in processed_verts:
if is_closed and v0.index in processed_verts:
v3 = start
v0 = v0 if v2 == v3 else v2
v2 = v3
else:
v0, v2 = v2, v0
normals = []
v1co = (wp.inverted() @ mw @ v1.co).to_2d()
if v2:
v2co = (wp.inverted() @ mw @ v2.co).to_2d()
direction = (v2co - v1co).normalized()
local_direction = (rotation @ direction).normalized()
normals.append(local_direction)
if v0:
v0co = (wp.inverted() @ mw @ v0.co).to_2d()
direction = (v0co - v1co).normalized()
local_direction = (rotation_i @ direction).normalized()
normals.append(local_direction)
if len(normals) == 2:
# https://stackoverflow.com/a/54042831/9627415
new_normal = (normals[0].lerp(normals[1], 0.5)).normalized()
offset_length = self.distance / sqrt((1 + normals[0].dot(normals[1]))/2)
offset = mw.inverted().to_quaternion() @ (wp.to_quaternion() @ (new_normal * offset_length).to_3d())
new_vert = v1.co + offset
new_verts.append(bm.verts.new(new_vert))
else:
normal = (normals[0] * self.distance).to_3d()
offset = mw.inverted().to_quaternion() @ (wp.to_quaternion() @ normal)
new_vert = v1.co + offset
new_verts.append(bm.verts.new(new_vert))
processed_verts.add(v1.index)
if v2 in processed_verts:
break
v1 = v2
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
if is_closed:
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
return {"FINISHED"}
class AddIfcCircle(bpy.types.Operator):
bl_idname = "bim.add_ifccircle"
bl_label = "Add IfcCircle"
@@ -20,7 +20,6 @@ import os
import bpy
import blenderbim.bim.module.type.prop as type_prop
from bpy.types import WorkSpaceTool
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.data import AuthoringData
@@ -35,13 +34,12 @@ class CadTool(WorkSpaceTool):
bl_widget = None
# https://docs.blender.org/api/current/bpy.types.KeyMapItems.html
bl_keymap = (
("bim.cad_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
("bim.cad_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
("bim.cad_hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
("bim.cad_fillet", {"type": "F", "value": "PRESS", "shift": True}, {"properties": []}),
# Enable Mesh Tools add-on to get this amazing tool
("mesh.offset_edges", {"type": "O", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.cad_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
("bim.cad_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
("bim.cad_hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
("bim.cad_hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
("bim.cad_hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
("bim.cad_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
("bim.cad_hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
)
@@ -57,12 +55,12 @@ class CadTool(WorkSpaceTool):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Extend").hotkey = "S_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.hotkey", text="Mitre").hotkey = "S_T"
row.operator("bim.cad_hotkey", text="Mitre").hotkey = "S_T"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -73,25 +71,31 @@ class CadTool(WorkSpaceTool):
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_V")
row.operator("bim.set_arc_index", text="3-Point Arc")
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")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Extend").hotkey = "S_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.hotkey", text="Mitre").hotkey = "S_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.hotkey", text="Fillet").hotkey = "S_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="Offset", icon="EVENT_O")
row.label(text="", icon="EVENT_O")
row.operator("bim.cad_hotkey", text="Offset").hotkey = "S_O"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -99,7 +103,8 @@ class CadTool(WorkSpaceTool):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="3-Point Arc", icon="EVENT_V")
row.label(text="", icon="EVENT_V")
row.operator("bim.cad_hotkey", text="3-Point Arc").hotkey = "S_V"
class CadHotkey(bpy.types.Operator):
@@ -107,17 +112,28 @@ class CadHotkey(bpy.types.Operator):
bl_label = "CAD Hotkey"
bl_options = {"REGISTER", "UNDO"}
hotkey: bpy.props.StringProperty()
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
def execute(self, context):
self.props = context.scene.BIMModelProperties
self.has_ifc_class = True
try:
self.has_ifc_class = bool(self.props.ifc_class)
except:
pass
getattr(self, f"hotkey_{self.hotkey}")()
return {"FINISHED"}
def draw(self, context):
if self.hotkey == "S_V":
if not self.is_profile():
row = self.layout.row()
row.prop(self, "resolution")
elif self.hotkey == "S_F":
row = self.layout.row()
row.prop(self, "resolution")
row = self.layout.row()
row.prop(self, "radius")
elif self.hotkey == "S_O":
row = self.layout.row()
row.prop(self, "distance")
def hotkey_S_C(self):
if self.is_profile():
bpy.ops.bim.add_ifccircle()
@@ -127,6 +143,12 @@ class CadHotkey(bpy.types.Operator):
def hotkey_S_E(self):
bpy.ops.bim.cad_trim_extend()
def hotkey_S_F(self):
bpy.ops.bim.cad_fillet(resolution=self.resolution, radius=self.radius)
def hotkey_S_O(self):
bpy.ops.bim.cad_offset(distance=self.distance)
def hotkey_S_Q(self):
bpy.ops.bim.edit_extrusion_profile()
@@ -137,7 +159,7 @@ class CadHotkey(bpy.types.Operator):
if self.is_profile():
bpy.ops.bim.set_arc_index()
else:
bpy.ops.bim.cad_arc_from_3_points()
bpy.ops.bim.cad_arc_from_3_points(resolution=self.resolution)
def is_profile(self):
obj = bpy.context.active_object