312 lines
11 KiB
Python
312 lines
11 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
bl_info = {
|
|
"name": "Inset Straight Skeleton",
|
|
"author": "Howard Trickey",
|
|
"version": (1, 1),
|
|
"blender": (2, 80, 0),
|
|
"location": "3DView Operator",
|
|
"description": "Make an inset inside selection using straight skeleton algorithm.",
|
|
"warning": "",
|
|
"doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/inset_straight_skeleton.html",
|
|
"category": "Mesh",
|
|
}
|
|
|
|
|
|
if "bpy" in locals():
|
|
import importlib
|
|
else:
|
|
from . import (
|
|
geom,
|
|
model,
|
|
offset,
|
|
triquad,
|
|
)
|
|
|
|
import math
|
|
import bpy
|
|
import bmesh
|
|
import mathutils
|
|
from mathutils import Vector
|
|
from bpy_extras import view3d_utils
|
|
import gpu
|
|
from gpu_extras.batch import batch_for_shader
|
|
|
|
from bpy.props import (
|
|
BoolProperty,
|
|
EnumProperty,
|
|
FloatProperty,
|
|
)
|
|
|
|
SpaceView3D = bpy.types.SpaceView3D
|
|
|
|
INSET_VALUE = 0
|
|
HEIGHT_VALUE = 1
|
|
NUM_VALUES = 2
|
|
|
|
# TODO: make a dooted-line shader
|
|
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') if not bpy.app.background else None
|
|
|
|
class MESH_OT_InsetStraightSkeleton(bpy.types.Operator):
|
|
bl_idname = "mesh.insetstraightskeleton"
|
|
bl_label = "Inset Straight Skeleton"
|
|
bl_description = "Make an inset inside selection using straight skeleton algorithm"
|
|
bl_options = {'UNDO', 'REGISTER', 'GRAB_CURSOR', 'BLOCKING'}
|
|
|
|
inset_amount: FloatProperty(name="Amount",
|
|
description="Amount to move inset edges",
|
|
default=0.0,
|
|
min=0.0,
|
|
max=1000.0,
|
|
soft_min=0.0,
|
|
soft_max=100.0,
|
|
unit='LENGTH')
|
|
inset_height: FloatProperty(name="Height",
|
|
description="Amount to raise inset faces",
|
|
default=0.0,
|
|
min=-10000.0,
|
|
max=10000.0,
|
|
soft_min=-500.0,
|
|
soft_max=500.0,
|
|
unit='LENGTH')
|
|
region: BoolProperty(name="Region",
|
|
description="Inset selection as one region?",
|
|
default=True)
|
|
quadrangulate: BoolProperty(name="Quadrangulate",
|
|
description="Quadrangulate after inset?",
|
|
default=True)
|
|
|
|
@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
|
|
box = layout.box()
|
|
box.label(text="Inset Options:")
|
|
box.prop(self, "inset_amount")
|
|
box.prop(self, "inset_height")
|
|
box.prop(self, "region")
|
|
box.prop(self, "quadrangulate")
|
|
|
|
def invoke(self, context, event):
|
|
self.modal = True
|
|
# make backup bmesh from current mesh, after flushing editmode to mesh
|
|
bpy.context.object.update_from_editmode()
|
|
self.backup = bmesh.new()
|
|
self.backup.from_mesh(bpy.context.object.data)
|
|
self.inset_amount = 0.0
|
|
self.inset_height = 0.0
|
|
self.center, self.center3d = calc_select_center(context)
|
|
self.center_pixel_size = calc_pixel_size(context, self.center3d)
|
|
udpi = context.preferences.system.dpi
|
|
upixelsize = context.preferences.system.pixel_size
|
|
self.pixels_per_inch = udpi * upixelsize
|
|
self.value_mode = INSET_VALUE
|
|
self.initial_length = [-1.0, -1.0]
|
|
self.scale = [self.center_pixel_size] * NUM_VALUES
|
|
self.calc_initial_length(event, True)
|
|
self.mouse_cur = Vector((event.mouse_region_x, event.mouse_region_y))
|
|
col = context.preferences.themes["Default"].view_3d.view_overlay
|
|
self.line_color = (col.r, col.g, col.b, 1.0)
|
|
|
|
self.action(context)
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
self.draw_handle = SpaceView3D.draw_handler_add(draw_callback,
|
|
(self,), 'WINDOW', 'POST_PIXEL')
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def calc_initial_length(self, event, mode_changed):
|
|
mdiff = self.center - Vector((event.mouse_region_x, event.mouse_region_y))
|
|
mlen = mdiff.length;
|
|
vmode = self.value_mode
|
|
if mode_changed or self.initial_length[vmode] == -1:
|
|
if vmode == INSET_VALUE:
|
|
value = self.inset_amount
|
|
else:
|
|
value = self.inset_height
|
|
sc = self.scale[vmode]
|
|
if value != 0.0:
|
|
mlen = mlen - value / sc
|
|
self.initial_length[vmode] = mlen
|
|
|
|
def modal(self, context, event):
|
|
if event.type in ['LEFTMOUSE', 'RIGHTMOUSE', 'ESC']:
|
|
if self.modal:
|
|
self.backup.free()
|
|
if self.draw_handle:
|
|
SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW')
|
|
context.area.tag_redraw()
|
|
if event.type == 'LEFTMOUSE': # Confirm
|
|
return {'FINISHED'}
|
|
else: # Cancel
|
|
return {'CANCELLED'}
|
|
else:
|
|
# restore mesh to original state
|
|
bpy.ops.object.editmode_toggle()
|
|
self.backup.to_mesh(bpy.context.object.data)
|
|
bpy.ops.object.editmode_toggle()
|
|
if event.type == 'MOUSEMOVE':
|
|
if self.value_mode == INSET_VALUE and event.ctrl:
|
|
self.value_mode = HEIGHT_VALUE
|
|
self.calc_initial_length(event, True)
|
|
elif self.value_mode == HEIGHT_VALUE and not event.ctrl:
|
|
self.value_mode = INSET_VALUE
|
|
self.calc_initial_length(event, True)
|
|
self.mouse_cur = Vector((event.mouse_region_x, event.mouse_region_y))
|
|
vmode = self.value_mode
|
|
mdiff = self.center - self.mouse_cur
|
|
value = (mdiff.length - self.initial_length[vmode]) * self.scale[vmode]
|
|
if vmode == INSET_VALUE:
|
|
self.inset_amount = value
|
|
else:
|
|
self.inset_height = value
|
|
elif event.type == 'R' and event.value == 'PRESS':
|
|
self.region = not self.region
|
|
elif event.type == 'Q' and event.value == 'PRESS':
|
|
self.quadrangulate = not self.quadrangulate
|
|
self.action(context)
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def execute(self, context):
|
|
self.modal = False
|
|
self.action(context)
|
|
return {'FINISHED'}
|
|
|
|
def action(self, context):
|
|
obj = bpy.context.active_object
|
|
mesh = obj.data
|
|
do_inset(mesh, self.inset_amount, self.inset_height, self.region,
|
|
self.quadrangulate)
|
|
bpy.ops.object.editmode_toggle()
|
|
bpy.ops.object.editmode_toggle()
|
|
|
|
|
|
def draw_callback(op):
|
|
startpos = op.mouse_cur
|
|
endpos = op.center
|
|
coords = [startpos.to_tuple(), endpos.to_tuple()]
|
|
batch = batch_for_shader(shader, 'LINES', {"pos": coords})
|
|
|
|
try:
|
|
shader.bind()
|
|
shader.uniform_float("color", op.line_color)
|
|
batch.draw(shader)
|
|
except:
|
|
pass
|
|
|
|
def calc_pixel_size(context, co):
|
|
# returns size in blender units of a pixel at 3d coord co
|
|
# see C code in ED_view3d_pixel_size and ED_view3d_update_viewmat
|
|
m = context.region_data.perspective_matrix
|
|
v1 = m[0].to_3d()
|
|
v2 = m[1].to_3d()
|
|
ll = min(v1.length_squared, v2.length_squared)
|
|
len_pz = 2.0 / math.sqrt(ll)
|
|
len_sz = max(context.region.width, context.region.height)
|
|
rv3dpixsize = len_pz / len_sz
|
|
proj = m[3][0] * co[0] + m[3][1] * co[1] + m[3][2] * co[2] + m[3][3]
|
|
ups = context.preferences.system.pixel_size
|
|
return proj * rv3dpixsize * ups
|
|
|
|
def calc_select_center(context):
|
|
# returns region 2d coord and global 3d coord of selection center
|
|
ob = bpy.context.active_object
|
|
mesh = ob.data
|
|
center = Vector((0.0, 0.0, 0.0))
|
|
n = 0
|
|
for v in mesh.vertices:
|
|
if v.select:
|
|
center = center + Vector(v.co)
|
|
n += 1
|
|
if n > 0:
|
|
center = center / n
|
|
world_center = ob.matrix_world @ center
|
|
world_center_2d = view3d_utils.location_3d_to_region_2d( \
|
|
context.region, context.region_data, world_center)
|
|
return (world_center_2d, world_center)
|
|
|
|
def do_inset(mesh, amount, height, region, quadrangulate):
|
|
if amount <= 0.0:
|
|
return
|
|
pitch = math.atan(height / amount)
|
|
selfaces = []
|
|
selface_indices = []
|
|
bm = bmesh.from_edit_mesh(mesh)
|
|
for face in bm.faces:
|
|
if face.select:
|
|
selfaces.append(face)
|
|
selface_indices.append(face.index)
|
|
m = geom.Model()
|
|
# if add all mesh.vertices, coord indices will line up
|
|
# Note: not using Points.AddPoint which does dup elim
|
|
# because then would have to map vertices in and out
|
|
m.points.pos = [v.co.to_tuple() for v in bm.verts]
|
|
for f in selfaces:
|
|
m.faces.append([loop.vert.index for loop in f.loops])
|
|
m.face_data.append(f.index)
|
|
orig_numv = len(m.points.pos)
|
|
orig_numf = len(m.faces)
|
|
model.BevelSelectionInModel(m, amount, pitch, quadrangulate, region, False)
|
|
if len(m.faces) == orig_numf:
|
|
# something went wrong with Bevel - just treat as no-op
|
|
return
|
|
blender_faces = m.faces[orig_numf:len(m.faces)]
|
|
blender_old_face_index = m.face_data[orig_numf:len(m.faces)]
|
|
for i in range(orig_numv, len(m.points.pos)):
|
|
bvertnew = bm.verts.new(m.points.pos[i])
|
|
bm.verts.index_update()
|
|
bm.verts.ensure_lookup_table()
|
|
new_faces = []
|
|
start_faces = len(bm.faces)
|
|
for i, newf in enumerate(blender_faces):
|
|
vs = remove_dups([bm.verts[j] for j in newf])
|
|
if len(vs) < 3:
|
|
continue
|
|
# copy face attributes from old face that it was derived from
|
|
bfi = blender_old_face_index[i]
|
|
# sometimes, not sure why, this face already exists
|
|
# bmesh will give a value error in bm.faces.new() in that case
|
|
try:
|
|
if bfi and 0 <= bfi < start_faces:
|
|
bm.faces.ensure_lookup_table()
|
|
oldface = bm.faces[bfi]
|
|
bfacenew = bm.faces.new(vs, oldface)
|
|
# bfacenew.copy_from_face_interp(oldface)
|
|
else:
|
|
bfacenew = bm.faces.new(vs)
|
|
new_faces.append(bfacenew)
|
|
except ValueError:
|
|
# print("dup face with amount", amount)
|
|
|
|
# print([v.index for v in vs])
|
|
pass
|
|
# deselect original faces
|
|
for face in selfaces:
|
|
face.select_set(False)
|
|
# remove original faces
|
|
bmesh.ops.delete(bm, geom=selfaces, context='FACES')
|
|
# select all new faces (should only select inner faces, but that needs more surgery on rest of code)
|
|
for face in new_faces:
|
|
face.select_set(True)
|
|
bmesh.update_edit_mesh(mesh)
|
|
|
|
def remove_dups(vs):
|
|
seen = set()
|
|
return [x for x in vs if not (x in seen or seen.add(x))]
|
|
|
|
def menu(self, context):
|
|
self.layout.operator("mesh.insetstraightskeleton", text="Inset Straight Skeleton")
|
|
|
|
def register():
|
|
bpy.utils.register_class(MESH_OT_InsetStraightSkeleton)
|
|
bpy.types.VIEW3D_MT_edit_mesh_faces.append(menu)
|
|
|
|
def unregister():
|
|
bpy.utils.unregister_class(MESH_OT_InsetStraightSkeleton)
|
|
bpy.types.VIEW3D_MT_edit_mesh_faces.remove(menu)
|