mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
New 2D CAD offset operator
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user