1 # SPDX-License-Identifier: GPL-2.0-or-later
6 "name": "Inset Straight Skeleton",
7 "author": "Howard Trickey",
10 "location": "3DView Operator",
11 "description": "Make an inset inside selection using straight skeleton algorithm.",
13 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/inset_straight_skeleton.html",
32 from mathutils
import Vector
33 from bpy_extras
import view3d_utils
35 from gpu_extras
.batch
import batch_for_shader
37 from bpy
.props
import (
43 SpaceView3D
= bpy
.types
.SpaceView3D
49 # TODO: make a dooted-line shader
50 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR') if not bpy
.app
.background
else None
52 class MESH_OT_InsetStraightSkeleton(bpy
.types
.Operator
):
53 bl_idname
= "mesh.insetstraightskeleton"
54 bl_label
= "Inset Straight Skeleton"
55 bl_description
= "Make an inset inside selection using straight skeleton algorithm"
56 bl_options
= {'UNDO', 'REGISTER', 'GRAB_CURSOR', 'BLOCKING'}
58 inset_amount
: FloatProperty(name
="Amount",
59 description
="Amount to move inset edges",
66 inset_height
: FloatProperty(name
="Height",
67 description
="Amount to raise inset faces",
74 region
: BoolProperty(name
="Region",
75 description
="Inset selection as one region?",
77 quadrangulate
: BoolProperty(name
="Quadrangulate",
78 description
="Quadrangulate after inset?",
82 def poll(cls
, context
):
83 obj
= context
.active_object
84 return (obj
and obj
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
86 def draw(self
, context
):
89 box
.label(text
="Inset Options:")
90 box
.prop(self
, "inset_amount")
91 box
.prop(self
, "inset_height")
92 box
.prop(self
, "region")
93 box
.prop(self
, "quadrangulate")
95 def invoke(self
, context
, event
):
97 # make backup bmesh from current mesh, after flushing editmode to mesh
98 bpy
.context
.object.update_from_editmode()
99 self
.backup
= bmesh
.new()
100 self
.backup
.from_mesh(bpy
.context
.object.data
)
101 self
.inset_amount
= 0.0
102 self
.inset_height
= 0.0
103 self
.center
, self
.center3d
= calc_select_center(context
)
104 self
.center_pixel_size
= calc_pixel_size(context
, self
.center3d
)
105 udpi
= context
.preferences
.system
.dpi
106 upixelsize
= context
.preferences
.system
.pixel_size
107 self
.pixels_per_inch
= udpi
* upixelsize
108 self
.value_mode
= INSET_VALUE
109 self
.initial_length
= [-1.0, -1.0]
110 self
.scale
= [self
.center_pixel_size
] * NUM_VALUES
111 self
.calc_initial_length(event
, True)
112 self
.mouse_cur
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
113 col
= context
.preferences
.themes
["Default"].view_3d
.view_overlay
114 self
.line_color
= (col
.r
, col
.g
, col
.b
, 1.0)
118 context
.window_manager
.modal_handler_add(self
)
119 self
.draw_handle
= SpaceView3D
.draw_handler_add(draw_callback
,
120 (self
,), 'WINDOW', 'POST_PIXEL')
122 return {'RUNNING_MODAL'}
124 def calc_initial_length(self
, event
, mode_changed
):
125 mdiff
= self
.center
- Vector((event
.mouse_region_x
, event
.mouse_region_y
))
127 vmode
= self
.value_mode
128 if mode_changed
or self
.initial_length
[vmode
] == -1:
129 if vmode
== INSET_VALUE
:
130 value
= self
.inset_amount
132 value
= self
.inset_height
133 sc
= self
.scale
[vmode
]
135 mlen
= mlen
- value
/ sc
136 self
.initial_length
[vmode
] = mlen
138 def modal(self
, context
, event
):
139 if event
.type in ['LEFTMOUSE', 'RIGHTMOUSE', 'ESC']:
143 SpaceView3D
.draw_handler_remove(self
.draw_handle
, 'WINDOW')
144 context
.area
.tag_redraw()
145 if event
.type == 'LEFTMOUSE': # Confirm
150 # restore mesh to original state
151 bpy
.ops
.object.editmode_toggle()
152 self
.backup
.to_mesh(bpy
.context
.object.data
)
153 bpy
.ops
.object.editmode_toggle()
154 if event
.type == 'MOUSEMOVE':
155 if self
.value_mode
== INSET_VALUE
and event
.ctrl
:
156 self
.value_mode
= HEIGHT_VALUE
157 self
.calc_initial_length(event
, True)
158 elif self
.value_mode
== HEIGHT_VALUE
and not event
.ctrl
:
159 self
.value_mode
= INSET_VALUE
160 self
.calc_initial_length(event
, True)
161 self
.mouse_cur
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
162 vmode
= self
.value_mode
163 mdiff
= self
.center
- self
.mouse_cur
164 value
= (mdiff
.length
- self
.initial_length
[vmode
]) * self
.scale
[vmode
]
165 if vmode
== INSET_VALUE
:
166 self
.inset_amount
= value
168 self
.inset_height
= value
169 elif event
.type == 'R' and event
.value
== 'PRESS':
170 self
.region
= not self
.region
171 elif event
.type == 'Q' and event
.value
== 'PRESS':
172 self
.quadrangulate
= not self
.quadrangulate
175 return {'RUNNING_MODAL'}
177 def execute(self
, context
):
182 def action(self
, context
):
183 obj
= bpy
.context
.active_object
185 do_inset(mesh
, self
.inset_amount
, self
.inset_height
, self
.region
,
187 bpy
.ops
.object.editmode_toggle()
188 bpy
.ops
.object.editmode_toggle()
191 def draw_callback(op
):
192 startpos
= op
.mouse_cur
194 coords
= [startpos
.to_tuple(), endpos
.to_tuple()]
195 batch
= batch_for_shader(shader
, 'LINES', {"pos": coords
})
199 shader
.uniform_float("color", op
.line_color
)
204 def calc_pixel_size(context
, co
):
205 # returns size in blender units of a pixel at 3d coord co
206 # see C code in ED_view3d_pixel_size and ED_view3d_update_viewmat
207 m
= context
.region_data
.perspective_matrix
210 ll
= min(v1
.length_squared
, v2
.length_squared
)
211 len_pz
= 2.0 / math
.sqrt(ll
)
212 len_sz
= max(context
.region
.width
, context
.region
.height
)
213 rv3dpixsize
= len_pz
/ len_sz
214 proj
= m
[3][0] * co
[0] + m
[3][1] * co
[1] + m
[3][2] * co
[2] + m
[3][3]
215 ups
= context
.preferences
.system
.pixel_size
216 return proj
* rv3dpixsize
* ups
218 def calc_select_center(context
):
219 # returns region 2d coord and global 3d coord of selection center
220 ob
= bpy
.context
.active_object
222 center
= Vector((0.0, 0.0, 0.0))
224 for v
in mesh
.vertices
:
226 center
= center
+ Vector(v
.co
)
230 world_center
= ob
.matrix_world
@ center
231 world_center_2d
= view3d_utils
.location_3d_to_region_2d( \
232 context
.region
, context
.region_data
, world_center
)
233 return (world_center_2d
, world_center
)
235 def do_inset(mesh
, amount
, height
, region
, quadrangulate
):
238 pitch
= math
.atan(height
/ amount
)
241 bm
= bmesh
.from_edit_mesh(mesh
)
242 for face
in bm
.faces
:
244 selfaces
.append(face
)
245 selface_indices
.append(face
.index
)
247 # if add all mesh.vertices, coord indices will line up
248 # Note: not using Points.AddPoint which does dup elim
249 # because then would have to map vertices in and out
250 m
.points
.pos
= [v
.co
.to_tuple() for v
in bm
.verts
]
252 m
.faces
.append([loop
.vert
.index
for loop
in f
.loops
])
253 m
.face_data
.append(f
.index
)
254 orig_numv
= len(m
.points
.pos
)
255 orig_numf
= len(m
.faces
)
256 model
.BevelSelectionInModel(m
, amount
, pitch
, quadrangulate
, region
, False)
257 if len(m
.faces
) == orig_numf
:
258 # something went wrong with Bevel - just treat as no-op
260 blender_faces
= m
.faces
[orig_numf
:len(m
.faces
)]
261 blender_old_face_index
= m
.face_data
[orig_numf
:len(m
.faces
)]
262 for i
in range(orig_numv
, len(m
.points
.pos
)):
263 bvertnew
= bm
.verts
.new(m
.points
.pos
[i
])
264 bm
.verts
.index_update()
265 bm
.verts
.ensure_lookup_table()
267 start_faces
= len(bm
.faces
)
268 for i
, newf
in enumerate(blender_faces
):
269 vs
= remove_dups([bm
.verts
[j
] for j
in newf
])
272 # copy face attributes from old face that it was derived from
273 bfi
= blender_old_face_index
[i
]
274 # sometimes, not sure why, this face already exists
275 # bmesh will give a value error in bm.faces.new() in that case
277 if bfi
and 0 <= bfi
< start_faces
:
278 bm
.faces
.ensure_lookup_table()
279 oldface
= bm
.faces
[bfi
]
280 bfacenew
= bm
.faces
.new(vs
, oldface
)
281 # bfacenew.copy_from_face_interp(oldface)
283 bfacenew
= bm
.faces
.new(vs
)
284 new_faces
.append(bfacenew
)
286 # print("dup face with amount", amount)
288 # print([v.index for v in vs])
290 # deselect original faces
291 for face
in selfaces
:
292 face
.select_set(False)
293 # remove original faces
294 bmesh
.ops
.delete(bm
, geom
=selfaces
, context
='FACES')
295 # select all new faces (should only select inner faces, but that needs more surgery on rest of code)
296 for face
in new_faces
:
297 face
.select_set(True)
298 bmesh
.update_edit_mesh(mesh
)
302 return [x
for x
in vs
if not (x
in seen
or seen
.add(x
))]
304 def menu(self
, context
):
305 self
.layout
.operator("mesh.insetstraightskeleton", text
="Inset Straight Skeleton")
308 bpy
.utils
.register_class(MESH_OT_InsetStraightSkeleton
)
309 bpy
.types
.VIEW3D_MT_edit_mesh_faces
.append(menu
)
312 bpy
.utils
.unregister_class(MESH_OT_InsetStraightSkeleton
)
313 bpy
.types
.VIEW3D_MT_edit_mesh_faces
.remove(menu
)